Skip to content

Commit 5190880

Browse files
author
Sorra
committed
Merge wl-CG-0MMM3EX2E0VD02N0-easy-mode-fix: Fix Easy mode stuck in DayStart phase
2 parents 3e26280 + a73a906 commit 5190880

2 files changed

Lines changed: 143 additions & 2 deletions

File tree

example-games/main-street/scenes/MainStreetScene.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,12 @@ export class MainStreetScene extends CardGameScene {
385385
difficulty: this.selectedDifficulty,
386386
unlockedCardIds: this.campaign.unlockedCardIds,
387387
});
388-
this.refreshAll();
388+
// Must call startDayPhase() (not just refreshAll) so the new
389+
// state transitions from DayStart -> MarketPhase and the UI
390+
// phase is synchronised. Without this, the engine stays in
391+
// DayStart while the UI shows market controls, blocking all
392+
// player actions and causing End Turn to hang.
393+
this.startDayPhase();
389394
}
390395
}).catch(() => {
391396
// If load fails, continue with defaults (already set up above)
@@ -426,7 +431,19 @@ export class MainStreetScene extends CardGameScene {
426431
this.refreshActionButtons();
427432

428433
// Process end-of-turn phases (events, income, night, end check)
429-
const result: TurnResult = processEndOfTurn(this.state);
434+
let result: TurnResult;
435+
try {
436+
result = processEndOfTurn(this.state);
437+
} catch (e) {
438+
// Defensive: if processEndOfTurn throws (e.g. phase mismatch from
439+
// async state replacement), recover gracefully instead of hanging
440+
// with a permanent "Processing end of turn..." message.
441+
console.error('[MainStreet] endTurn failed:', e);
442+
this.uiPhase = 'market';
443+
this.instructionText.setText(`Error: ${(e as Error).message}`);
444+
this.refreshAll();
445+
return;
446+
}
430447

431448
// Brief delay then show result / advance
432449
this.time.delayedCall(400, () => {
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* Main Street: Easy Mode Phase Bug Regression Tests
3+
*
4+
* Verifies that the async campaign load race condition (CG-0MMM3EX2E0VD02N0)
5+
* is properly handled: when a new game state is created (simulating the async
6+
* campaign reload), executeDayStart must be called on the new state before the
7+
* player can interact.
8+
*
9+
* Work items: CG-0MMM3EX2E0VD02N0, CG-0MMM3VJNQ1O43G56, CG-0MMM3VQIS039HTA5
10+
*/
11+
import { describe, it, expect } from 'vitest';
12+
13+
import {
14+
setupMainStreetGame,
15+
type MainStreetState,
16+
} from '../../example-games/main-street/MainStreetState';
17+
import {
18+
executeDayStart,
19+
processEndOfTurn,
20+
executeAction,
21+
type PlayerAction,
22+
} from '../../example-games/main-street/MainStreetEngine';
23+
import type { DifficultyName } from '../../example-games/main-street/MainStreetDifficulty';
24+
25+
// ── Helpers ─────────────────────────────────────────────────
26+
27+
function createState(
28+
seed: string,
29+
difficulty: DifficultyName = 'Easy',
30+
): MainStreetState {
31+
return setupMainStreetGame({ seed, difficulty });
32+
}
33+
34+
// ── Tests ───────────────────────────────────────────────────
35+
36+
describe('Easy mode: round-1 market phase reachability', () => {
37+
it('should start in DayStart phase', () => {
38+
const state = createState('easy-phase-1');
39+
expect(state.phase).toBe('DayStart');
40+
expect(state.turn).toBe(1);
41+
});
42+
43+
it('should transition to MarketPhase after executeDayStart', () => {
44+
const state = createState('easy-phase-2');
45+
executeDayStart(state);
46+
expect(state.phase).toBe('MarketPhase');
47+
expect(state.turn).toBe(1);
48+
});
49+
50+
it('should allow End Turn action after executeDayStart on Easy', () => {
51+
const state = createState('easy-phase-3');
52+
executeDayStart(state);
53+
expect(state.phase).toBe('MarketPhase');
54+
55+
// processEndOfTurn should not throw
56+
const result = processEndOfTurn(state);
57+
expect(result).toBeDefined();
58+
expect(['playing', 'win', 'loss']).toContain(result.gameResult);
59+
});
60+
61+
it('should reject End Turn when still in DayStart (the bug scenario)', () => {
62+
const state = createState('easy-phase-4');
63+
// Do NOT call executeDayStart -- simulating the race condition
64+
expect(state.phase).toBe('DayStart');
65+
66+
expect(() => processEndOfTurn(state)).toThrow(
67+
/Cannot end turn during DayStart/,
68+
);
69+
});
70+
71+
it('should reject buy-business action when still in DayStart', () => {
72+
const state = createState('easy-phase-5');
73+
// Do NOT call executeDayStart
74+
expect(state.phase).toBe('DayStart');
75+
76+
const action: PlayerAction = {
77+
type: 'buy-business',
78+
cardId: 'any-card',
79+
slotIndex: 0,
80+
};
81+
expect(() => executeAction(state, action)).toThrow(
82+
/Cannot perform buy-business during DayStart/,
83+
);
84+
});
85+
});
86+
87+
describe('Async state replacement race condition (regression)', () => {
88+
it('replacing state after executeDayStart leaves new state in DayStart', () => {
89+
// Simulate the exact sequence from the bug:
90+
// 1. Create state (DayStart) -> executeDayStart -> MarketPhase
91+
// 2. Replace state with a new one (simulating async campaign load)
92+
// 3. New state is back in DayStart
93+
94+
const state1 = createState('race-1');
95+
executeDayStart(state1);
96+
expect(state1.phase).toBe('MarketPhase');
97+
98+
// Simulate async callback replacing the state
99+
const state2 = createState('race-2');
100+
expect(state2.phase).toBe('DayStart');
101+
102+
// The fix: calling executeDayStart on the new state
103+
executeDayStart(state2);
104+
expect(state2.phase).toBe('MarketPhase');
105+
106+
// Now processEndOfTurn should work on the new state
107+
const result = processEndOfTurn(state2);
108+
expect(result).toBeDefined();
109+
});
110+
111+
it('works for all difficulty levels', () => {
112+
for (const difficulty of ['Easy', 'Medium', 'Hard'] as DifficultyName[]) {
113+
const state = createState(`phase-${difficulty}`, difficulty);
114+
expect(state.phase).toBe('DayStart');
115+
116+
executeDayStart(state);
117+
expect(state.phase).toBe('MarketPhase');
118+
119+
const result = processEndOfTurn(state);
120+
expect(result).toBeDefined();
121+
expect(['playing', 'win', 'loss']).toContain(result.gameResult);
122+
}
123+
});
124+
});

0 commit comments

Comments
 (0)