v0.40.0 — Modular LLM-ability system (ordered registry) #75
@@ -158,9 +158,10 @@ export interface Ability<K extends AbilityField = AbilityField> {
|
||||
/**
|
||||
* The deterministic stub's value for this field. Every ability returns
|
||||
* `default()` (the stub never exercises an ability, so e2e is unaffected) —
|
||||
* kept as a method so the stub stays a uniform registry loop.
|
||||
* kept as a zero-arg method so the stub stays a uniform registry loop (the
|
||||
* stub has no real `GameState` / `AbilityCtx` to hand one; it needs none).
|
||||
*/
|
||||
stub(state: GameState, ctx: AbilityCtx): TurnResult[K];
|
||||
stub(): TurnResult[K];
|
||||
|
||||
/**
|
||||
* The keys this ability contributes to the user prompt payload, each included
|
||||
|
||||
+15
-11
@@ -4,6 +4,7 @@ import path from 'node:path';
|
||||
import Database from 'better-sqlite3';
|
||||
import { migrateRegionStats } from '../shared/protocol';
|
||||
import type { GameState } from '../shared/types';
|
||||
import { ABILITIES } from './abilities';
|
||||
|
||||
export interface Db {
|
||||
save(state: GameState): void;
|
||||
@@ -49,17 +50,20 @@ export function openDb(filePath: string): Db {
|
||||
if (state.pendingNationOps === undefined) state.pendingNationOps = [];
|
||||
// (v0.27, #44) Same for map markers: a legacy game has none placed.
|
||||
if (state.markers === undefined) state.markers = [];
|
||||
// (v0.33.0, #56) Same for named wars: a legacy game has none declared
|
||||
// (any pre-v0.33 `war` relation stays a bare/implicit relation — see
|
||||
// syncWarRelations, which never force-creates a War entity).
|
||||
if (state.wars === undefined) state.wars = [];
|
||||
// (v0.35.0, #58) Same for the trade & embargo economy layer: a
|
||||
// legacy game has no active trades or embargoes.
|
||||
if (state.trades === undefined) state.trades = [];
|
||||
if (state.embargoes === undefined) state.embargoes = [];
|
||||
// (v0.36, #59) Same for the industry layer: a legacy game has no
|
||||
// active industry projects.
|
||||
if (state.industry === undefined) state.industry = [];
|
||||
// (v0.33.0 #56, v0.35.0 #58, v0.36 #59, v0.40.0 #67) Same for every
|
||||
// ability-owned GameState field (wars / trades / embargoes / industry):
|
||||
// a legacy game has none of them. The ABILITIES registry is the single
|
||||
// source of truth for which fields the LLM abilities own — each is a
|
||||
// list, so the default is `[]`, applied only-if-undefined so a saved
|
||||
// value is never wiped. (Any pre-v0.33 `war` relation stays a
|
||||
// bare/implicit relation — see syncWarRelations, which never
|
||||
// force-creates a War entity.)
|
||||
for (const a of ABILITIES) {
|
||||
const fields = a.stateFields;
|
||||
if (!fields) continue;
|
||||
const row = state as unknown as Record<string, unknown>;
|
||||
for (const f of fields) if (row[f] === undefined) row[f] = [];
|
||||
}
|
||||
// (v0.28, #46) Rows saved before per-region stats existed are migrated
|
||||
// ONCE at load time (and never re-migrated afterwards — the field is
|
||||
// then present): each owned region is seeded with a copy of its owner's
|
||||
|
||||
+82
-1590
File diff suppressed because it is too large
Load Diff
+29
-9
@@ -4,6 +4,7 @@
|
||||
import { createLlmClient, type ChatMessage, type Transport } from './llm';
|
||||
import { addDays, clampStat, type TerritoryChange, type TurnEventSpec, type TurnResult } from '../shared/protocol';
|
||||
import type { RelationStatus, RoundItem } from '../shared/types';
|
||||
import { ABILITIES, type AbilityField } from './abilities';
|
||||
|
||||
export const ADVISOR_MARKER = '<<ADVISOR>>';
|
||||
|
||||
@@ -229,7 +230,18 @@ export function deterministicTurn(p: StubPayload): TurnResult {
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
// Core (non-ability) turn fields, in their historical position order.
|
||||
const pre: Pick<
|
||||
TurnResult,
|
||||
| 'date'
|
||||
| 'events'
|
||||
| 'stateChanges'
|
||||
| 'regionChanges'
|
||||
| 'relations'
|
||||
| 'territoryChanges'
|
||||
| 'unowned'
|
||||
| 'nationOps'
|
||||
> = {
|
||||
date: addDays(state?.date ?? '1939-09-01', 2),
|
||||
events,
|
||||
stateChanges: [{ nation: pid, stat: 'military', op: 'set', value: mil }],
|
||||
@@ -238,16 +250,24 @@ export function deterministicTurn(p: StubPayload): TurnResult {
|
||||
territoryChanges,
|
||||
unowned: [], // the stub never creates no man's land (see doc comment)
|
||||
nationOps: [], // the stub never proposes nation ops (see doc comment)
|
||||
takeovers: [], // the stub never performs a takeover (see doc comment)
|
||||
wars: { declare: [], end: [] }, // the stub never declares/ends wars (see doc comment)
|
||||
hybrid: { ops: [] }, // the stub never runs hybrid/covert ops (see doc comment)
|
||||
economy: { trade: [], embargo: [] }, // the stub never touches trades/embargoes (see doc comment)
|
||||
industry: { projects: [] }, // the stub never declares industry projects (see doc comment)
|
||||
nuclear: { arm: [], strikes: [] }, // the stub never arms or strikes (see doc comment)
|
||||
leaders: { changes: [] }, // the stub never changes leaders (see doc comment)
|
||||
ideology: { changes: [] }, // the stub never changes ideologies (see doc comment)
|
||||
};
|
||||
|
||||
// The 8 ability fields, filled by the registry in its canonical order. The
|
||||
// stub is ability-inert: every ability contributes its own no-op default
|
||||
// (each ability's `stub()` — see the per-ability doc comments above), so the
|
||||
// e2e assertions are unaffected. The insertion order here (ABILITIES) is the
|
||||
// historical key order pinned by the stub's deterministic output
|
||||
// (takeovers → wars → hybrid → economy → industry → nuclear → leaders →
|
||||
// ideology), which must stay byte-identical.
|
||||
const abilities = {} as Record<AbilityField, unknown>;
|
||||
for (const a of ABILITIES) abilities[a.field] = a.stub();
|
||||
|
||||
// The single field positioned after the abilities (markers last, as before).
|
||||
const post: Pick<TurnResult, 'markers'> = {
|
||||
markers: { add: [], remove: [] }, // the stub never touches markers (see doc comment)
|
||||
};
|
||||
|
||||
return { ...pre, ...abilities, ...post } as TurnResult;
|
||||
}
|
||||
|
||||
const CANNED_ADVISOR: string[] = [
|
||||
|
||||
Reference in New Issue
Block a user