LLM getNeighbours tool + contiguous region transfers (Stage 2 of #16 P2)
- llm.ts: LlmClient.chatWithTools — bounded OpenAI function-calling loop (default 6 tool rounds, then one forced no-tools request); shares the retry/backoff/reasoning-effort machinery with chat(); tools are only added to the request body when non-empty, so plain chat() bodies and their tests are unchanged; a tool-call response is a success (never a truncation); malformed tool arguments become an error tool-message. - engine.ts: GET_NEIGHBOURS_TOOL + makeNeighboursExecutor (degrades unknown tools/args/keys without throwing); playerFrontier (sorted, deduped, other-nation-owned only); buildSystemPrompt gains a tool variant (contiguity rules, frontier hint) used only when the tool is actually sent; buildUserPrompt emits a frontier field whenever a resolver is given; runTurn takes getNeighbours and uses chatWithTools when both the resolver and the client's capability are present, falling back to plain chat() otherwise. - applyTurnResult: snapshot-first sole-ownership algorithm — a region key transfers only with exactly one claimant and exactly one previous owner (different from the gainer); double claims and pre-existing double ownership are left untouched; lone losses are no-ops; every key has 0 or 2+ -> 1 owner post-turn, never orphaned or duplicated. - stub.ts: deterministic one-region contiguous conquest per turn from the prompt's frontier field (bypasses the tool loop; empty frontier -> no transfer, all existing assertions stay stable). - regions.ts: optional region-adjacency.json load; hasAdjacency() and neighboursOf(keys) (union, deduped, queried excluded, sorted). - index.ts: jump route passes the adjacency resolver when loaded. - tests: chatWithTools protocol (echo, executor args, bounded rounds, malformed args, retry), tool-path runTurn wiring + fallback, playerFrontier, executor, applyTurnResult invariants, neighboursOf/ hasAdjacency (105 unit tests, 14 e2e — all green).
This commit is contained in:
+5
-1
@@ -20,7 +20,7 @@ import { createLlmClient } from './llm';
|
||||
import { makeStubLlmClient } from './stub';
|
||||
import { askAdvisor, queueRoundItem, removeRoundItem, runTurn } from './engine';
|
||||
import { buildRegionMap } from './map';
|
||||
import { allTerritoryKeys, expandCountriesToKeys, loadRegions, referenceForKeys, validKeys } from './regions';
|
||||
import { allTerritoryKeys, expandCountriesToKeys, hasAdjacency, loadRegions, neighboursOf, referenceForKeys, validKeys } from './regions';
|
||||
import { createPresetStore } from './presets';
|
||||
import { openDb, type Db } from './db';
|
||||
|
||||
@@ -237,6 +237,10 @@ app.post('/api/games/:id/jump', async (req, res) => {
|
||||
items: game.pending,
|
||||
regionReference: referenceForKeys(allTerritoryKeys(game)),
|
||||
validRegionKeys: validKeys(),
|
||||
// v0.21 (#16 P2): when the region-adjacency manifest loaded, hand the
|
||||
// engine the resolver — the turn then runs with the LLM's getNeighbours
|
||||
// tool and the player-frontier prompt field (both switch off without it).
|
||||
getNeighbours: hasAdjacency() ? (keys) => neighboursOf(keys) : undefined,
|
||||
});
|
||||
db.save(state);
|
||||
broadcast(id, 'turn', { state, result });
|
||||
|
||||
Reference in New Issue
Block a user