Move the 5 shared reference/id primitives (resolveNationRef, resolveRegionKey, freshNationId, freshWarId, NEW_NATION_STATS) from engine.ts into the new server/abilities/shared.ts (breaking the engine<->ability import cycle), and add server/abilities/types.ts with the Ability<K> interface + AbilityCtx / UserPromptCtx types that the 8 per-feature ability modules will implement. engine.ts imports the helpers and re-exports them so existing callers (server/index.ts, the unit tests) are untouched. Behavior is identical; all 511 unit tests, lint and typecheck stay green.
97 lines
3.8 KiB
TypeScript
97 lines
3.8 KiB
TypeScript
// Shared reference-resolution / id-generation primitives used across the
|
|
// modular LLM abilities (and re-exported by the core engine). Living here —
|
|
// and in `./types.ts` rather than in `engine.ts` — breaks the engine <->
|
|
// ability import cycle: the ability modules import from here, `engine.ts`
|
|
// imports + re-exports them for the existing callers (`server/index.ts`, the
|
|
// unit tests), and no ability imports the engine. Pure and dependency-free.
|
|
|
|
import type { GameState, NationStats } from '../../shared/types';
|
|
|
|
/**
|
|
* Resolve a nation reference (the `nationRef` of a NationOp, or the `target` /
|
|
* `from` / `sides` of an ability op — the LLM may write either the nation's
|
|
* stable `id` or its human-readable `name`) to a concrete nation id: an exact
|
|
* id match wins, then a unique case-insensitive name match. Returns `null` when
|
|
* the reference is empty, unknown, or ambiguous (two or more nations share that
|
|
* name) — every caller treats `null` as "no-op" and nothing here ever throws.
|
|
*/
|
|
export function resolveNationRef(state: GameState, ref: string): string | null {
|
|
const r = (ref ?? '').trim();
|
|
if (!r) return null;
|
|
if (state.nations[r]) return r; // exact id first
|
|
const matches = Object.values(state.nations).filter(
|
|
(n) => n.name.trim().toLowerCase() === r.toLowerCase(),
|
|
);
|
|
return matches.length === 1 ? matches[0].id : null;
|
|
}
|
|
|
|
/**
|
|
* (v0.29, #46 PR 2) Resolve a **region** reference (a region op's `region` /
|
|
* `regionKey` — the LLM may write either the region's stable **key** or its
|
|
* human-readable **name**) to a concrete region key: an exact key match wins,
|
|
* then a unique case-insensitive name match (via `nameOf`). Returns `null` when
|
|
* the reference is empty, unknown, or ambiguous (two or more regions share that
|
|
* name) — every caller treats `null` as "drop this change" and nothing here
|
|
* ever throws. Mirrors {@link resolveNationRef}. `validKeys` is the set of all
|
|
* region keys and `nameOf` maps a key to its human name (both supplied by
|
|
* server/regions.ts).
|
|
*/
|
|
export function resolveRegionKey(
|
|
ref: string,
|
|
validKeys: ReadonlySet<string>,
|
|
nameOf: (key: string) => string | undefined,
|
|
): string | null {
|
|
const r = (ref ?? '').trim();
|
|
if (!r) return null;
|
|
if (validKeys.has(r)) return r; // exact key first
|
|
const lower = r.toLowerCase();
|
|
let match: string | null = null;
|
|
for (const k of validKeys) {
|
|
const name = nameOf(k);
|
|
if (name && name.trim().toLowerCase() === lower) {
|
|
if (match !== null) return null; // ambiguous: two or more regions share the name
|
|
match = k;
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
|
|
/**
|
|
* Generate a fresh, unused nation id for a newly created nation (split parts,
|
|
* v0.24 #16 P4b): the first free of `n1`, `n2`, ... The caller adds each id it
|
|
* creates to `taken` as it goes, so ids stay unique within an op and are never
|
|
* reused from a removed nation.
|
|
*/
|
|
export function freshNationId(taken: ReadonlySet<string>): string {
|
|
for (let i = 1; ; i++) {
|
|
const id = `n${i}`;
|
|
if (!taken.has(id)) return id;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a fresh, unused war id for a newly declared {@link War}
|
|
* (v0.33.0, #56): the first free of `w1`, `w2`, ... The caller adds each id it
|
|
* creates to `taken` as it goes, so ids stay unique within a turn and are never
|
|
* reused from an ended war.
|
|
*/
|
|
export function freshWarId(taken: ReadonlySet<string>): string {
|
|
for (let i = 1; ; i++) {
|
|
const id = `w${i}`;
|
|
if (!taken.has(id)) return id;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The baseline stats a freshly created nation starts with (split parts,
|
|
* v0.24 #16 P4b) — a neutral middle-of-the-scale profile, deliberately NOT a
|
|
* copy of the parent's stats (a small rump state is not the parent's military
|
|
* power).
|
|
*/
|
|
export const NEW_NATION_STATS: NationStats = {
|
|
military: 50,
|
|
economy: 50,
|
|
stability: 50,
|
|
population: 50,
|
|
};
|