Skip to content

Commit c66e3a8

Browse files
emp3thyclaude
andcommitted
docs+test: pin alive-first ordering in planAi human guard
BugBot review on PR #4 flagged that the !me.alive early-return preempts the isHuman throw, "undermining the fail-fast guard." This is intentional design, documented in the Task 5 plan ("dead humans return [] — same as dead AI"), but not previously visible in the code. Add a 5-line comment to ai/index.ts explaining the ordering rationale, plus a regression test asserting the dead- human path returns [] (locks the ordering against future reorderings). No behaviour change. 161/161 tests; typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9d0319d commit c66e3a8

2 files changed

Lines changed: 20 additions & 1 deletion

File tree

src/engine/ai/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ const DIFFICULTY_RANDOM_PCT: Record<Difficulty, number> = {
1313

1414
export function planAi(state: GameState, leaderId: LeaderId, difficulty?: Difficulty): Order[] {
1515
const me = state.leaders[leaderId];
16+
// Alive-check first by design: dead leaders (AI or human) return [] — same shape,
17+
// same caller contract. The human throw below catches misroutes for ALIVE humans
18+
// (Phase 3 UI-orchestrator bug), not eliminated humans whose lifecycle handling
19+
// should mirror eliminated AI. Reordering would create asymmetric semantics
20+
// (dead AI = []; dead human = throw) for no benefit.
1621
if (!me || !me.alive) return [];
1722
if (isHuman(leaderId)) {
1823
throw new Error(

tests/engine/ai/dispatcher.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,26 @@ describe('planAi dispatcher', () => {
6060
}
6161
});
6262

63-
it('throws when called for a human player slot', () => {
63+
it('throws when called for an alive human player slot', () => {
6464
const s = initialState({
6565
cast: ['player1', 'chump'],
6666
difficulty: 'normal',
6767
seed: 'planAi-human',
6868
});
6969
expect(() => planAi(s, 'player1')).toThrow(/planAi.*human/i);
7070
});
71+
72+
it('returns [] (does not throw) when called for an eliminated human player slot', () => {
73+
// Dead AI returns []; dead human must mirror that — Phase 3 callers iterate
74+
// the cast and rely on planAi(state, deadId) === [] without special-casing.
75+
// The "throw on human" guard is for ALIVE-human routing bugs only.
76+
const s = initialState({
77+
cast: ['player1', 'chump'],
78+
difficulty: 'normal',
79+
seed: 'planAi-dead-human',
80+
});
81+
s.leaders.player1.alive = false;
82+
s.leaders.player1.population = 0;
83+
expect(planAi(s, 'player1')).toEqual([]);
84+
});
7185
});

0 commit comments

Comments
 (0)