No man's land: TurnResult.unowned + release/re-claim in applyTurnResult + distinct grey map rendering (#16 P3)

- shared/protocol.ts: TurnResult.unowned (keys released this turn) + the map
  presentation contract (regionFillStyle / unownedTooltipLabel, hue-less grey)
- shared/types.ts: required GameState.unowned (cumulative mid-game releases;
  [] at init, distinct from preset-unassigned regions)
- server/engine.ts: applyTurnResult applies releases first (sole-owner only,
  never fabricating no man's land from unassigned keys), then claims against
  the post-release snapshot (gain on an ownerless key = re-claim); lone
  release is isMeaningful; prompts + user payload carry the unowned field
- server/stub.ts: unchanged behavior (never releases; e2e stays stable)
- server/db.ts: legacy rows load with unowned defaulted to []
- server/index.ts: create-game sets []; jump reference table includes
  released keys so the model can name them for re-claim
- WorldMap/App: unowned prop -> visible grey + 'No man's land' tooltip
- unit tests: 105 -> 127 (release/re-claim suites, presentation contract,
  legacy-row normalization)
This commit is contained in:
2026-10-14 12:04:03 +00:00
parent cb426273a9
commit 764f24fc55
13 changed files with 567 additions and 53 deletions
+12
View File
@@ -23,6 +23,7 @@ function makeState(id: string): GameState {
events: [],
pending: [],
version: 1,
unowned: [],
};
}
@@ -73,6 +74,17 @@ describe('openDb', () => {
db.close();
});
it('(v0.22 #16 P3) a legacy row saved before the unowned field existed loads with it defaulted to []', () => {
const db = openDb(':memory:');
const legacy = makeState('legacy');
delete (legacy as { unowned?: string[] }).unowned; // a pre-v0.22 save (field absent)
db.save(legacy);
const loaded = db.load('legacy');
expect(loaded).not.toBeNull();
expect(loaded!.unowned).toEqual([]);
db.close();
});
it('delete removes a game by id and reports absence otherwise', () => {
const db = openDb(':memory:');
db.save(makeState('a'));