Files
History/server/abilities/shared.ts
T
kamilkrzywda 5cfbce861b 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.
2026-10-16 07:23:11 +00:00

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,
};