Each per-feature LLM ability is now a self-contained module in server/abilities/ with its own Ability<K> const (schema line, constraint bullet, validator, applier, isMeaningful, stub default, user-payload field). The ordered ABILITIES registry (index.ts) assembles them in the canonical order: takeovers → wars → hybrid → economy → industry → nuclear → leaders → ideology. All function bodies are moved VERBATIM from engine.ts. The engine is untouched in this commit — P4 will rewire it to use the registry.
89 lines
3.9 KiB
TypeScript
89 lines
3.9 KiB
TypeScript
// Shared, cross-ability `apply` primitives (v0.40.0, #67): the war-derived
|
|
// relation machinery that several abilities share. Two abilities (wars,
|
|
// nuclear) mutate `GameState.wars` and both need the same "which pairs are at
|
|
// war" primitive (`activeWarPairs` / `atWar`); the wars ability applies the
|
|
// turn's declare/end ops and then runs the war → relation sync
|
|
// (`syncWarRelations`), and the nuclear ability's `endWarsBetween` (which
|
|
// lives in ./nuclear.ts) reuses `activeWarPairs` to downgrade the relations a
|
|
// just-ended war no longer covers.
|
|
//
|
|
// The bodies here are moved VERBATIM from the pre-refactor engine.ts (v0.39.0)
|
|
// — `engine.ts` re-exports all three so existing callers (server/index.ts, the
|
|
// unit tests) keep importing them from the engine unchanged. All functions are
|
|
// pure or mutate their `GameState` argument in place and never throw.
|
|
|
|
import type { GameState, War } from '../../shared/types';
|
|
|
|
/**
|
|
* (v0.33.0, #56) The UNORDERED pairs of belligerents of a set of ACTIVE
|
|
* wars: a war with sides [a, b, c] contributes the pairs {a,b}, {a,c},
|
|
* {b,c} (any two distinct belligerents of the same active war are at war —
|
|
* see the War type's semantics). Pure; exported for the unit tests.
|
|
*/
|
|
export function activeWarPairs(wars: War[]): Set<string> {
|
|
const pairs = new Set<string>();
|
|
const key = (a: string, b: string) => (a < b ? `${a}\u0000${b}` : `${b}\u0000${a}`);
|
|
for (const w of wars) {
|
|
if (w.status !== 'active') continue;
|
|
for (let i = 0; i < w.sides.length; i++) {
|
|
for (let j = i + 1; j < w.sides.length; j++) {
|
|
pairs.add(key(w.sides[i], w.sides[j]));
|
|
}
|
|
}
|
|
}
|
|
return pairs;
|
|
}
|
|
|
|
/** True when nations `a` and `b` are both belligerents of ≥1 active war. */
|
|
export function atWar(wars: War[], a: string, b: string): boolean {
|
|
const key = a < b ? `${a}\u0000${b}` : `${b}\u0000${a}`;
|
|
return activeWarPairs(wars).has(key);
|
|
}
|
|
|
|
/**
|
|
* (v0.33.0, #56) Derive the `war` relation from the set of ACTIVE wars,
|
|
* MUTATING the (cloned) `next` state in place (the caller passes a clone —
|
|
* applyTurnResult does) and never touching `prev`. For every unordered pair
|
|
* of distinct existing nations:
|
|
*
|
|
* - the pair is on the sides of ≥1 ACTIVE war in `next` → BOTH directions
|
|
* of the pair's relation are set to `war` (the engine owns this relation
|
|
* while the war is active — a manual `relations` entry to a lesser status
|
|
* on a warring pair is overridden, so the two can never "make peace"
|
|
* without the war being ended);
|
|
* - the pair is NOT at war in `next` but WAS at war in `prev` (one of the
|
|
* wars that tied them ended this turn) AND the current relation is
|
|
* `war` → both directions become `hostile` (a war is the strongest
|
|
* hostility — when it stops, the nations stay hostile, not suddenly
|
|
* neutral);
|
|
* - otherwise (not at war now, and the relation is not `war`, or the pair
|
|
* was never tied by an active war) → untouched: the LLM owns
|
|
* ally/neutral/rival/hostile freely, and a **bare** `war` relation that
|
|
* no active war ever covered (a preset leftover) stays as-is — an
|
|
* implicit, unnamed war, never force-created into a War entity and never
|
|
* clobbered by the sync.
|
|
*
|
|
* Never throws.
|
|
*/
|
|
export function syncWarRelations(prev: GameState, next: GameState): void {
|
|
const prevAtWar = activeWarPairs(prev.wars ?? []);
|
|
const nextAtWar = activeWarPairs(next.wars ?? []);
|
|
const ids = Object.keys(next.nations);
|
|
for (let i = 0; i < ids.length; i++) {
|
|
for (let j = i + 1; j < ids.length; j++) {
|
|
const a = ids[i];
|
|
const b = ids[j];
|
|
const key = a < b ? `${a}\u0000${b}` : `${b}\u0000${a}`;
|
|
const na = next.nations[a];
|
|
const nb = next.nations[b];
|
|
if (nextAtWar.has(key)) {
|
|
na.relations[b] = 'war';
|
|
nb.relations[a] = 'war';
|
|
} else if (prevAtWar.has(key) && (na.relations[b] === 'war' || nb.relations[a] === 'war')) {
|
|
na.relations[b] = 'hostile';
|
|
nb.relations[a] = 'hostile';
|
|
}
|
|
}
|
|
}
|
|
}
|