refactor(#74) P2: extract shared helpers to server/abilities + define the Ability type surface
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.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
// 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,
|
||||
};
|
||||
Reference in New Issue
Block a user