feat(04-01): implement GameStateManager class with transition validation

- Add transition validation with validTransitions mapping
- Implement transitionTo() method with validation and event emission
- Add canSelectTile() helper method for input blocking
- Add reset() method for restart functionality
- Update tests to cover all state machine behavior (7 test cases)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-03-11 08:16:08 +00:00
co-authored by Claude
parent 83cba85f04
commit d70271878f
2 changed files with 125 additions and 30 deletions
+50 -18
View File
@@ -1,7 +1,8 @@
// src/__tests__/GameStateManager.test.ts - Unit tests for GameStateManager class
import { describe, test, expect, beforeEach, vi } from 'vitest';
import { GameStateManager } from '../state/GameStateManager';
import { GameStateManager, GameState } from '../state/GameStateManager';
import { TypedEventEmitter } from '../game/EventEmitter';
import type { GameEvents } from '../types';
// Mock TypedEventEmitter for testing
vi.mock('../game/EventEmitter', () => ({
@@ -9,64 +10,95 @@ vi.mock('../game/EventEmitter', () => ({
on = vi.fn();
emit = vi.fn();
off = vi.fn();
removeAllListeners = vi.fn();
},
}));
describe('GameStateManager', () => {
let mockEventEmitter: TypedEventEmitter<any>;
let mockEventEmitter: TypedEventEmitter<GameEvents>;
let stateManager: GameStateManager;
beforeEach(() => {
// Create fresh mock event emitter for each test
mockEventEmitter = new TypedEventEmitter() as any;
stateManager = new GameStateManager(mockEventEmitter);
vi.clearAllMocks();
});
describe('initialization', () => {
test('should initialize in IDLE state', () => {
// TODO: Implement test
expect(true).toBe(true);
expect(stateManager.getState()).toBe(GameState.IDLE);
});
});
describe('state transitions', () => {
test('should validate state transitions correctly', () => {
// TODO: Implement test
expect(true).toBe(true);
// Valid transition: IDLE → SELECTING
expect(stateManager.transitionTo(GameState.SELECTING)).toBe(true);
expect(stateManager.getState()).toBe(GameState.SELECTING);
// Valid transition: SELECTING → MATCHING
expect(stateManager.transitionTo(GameState.MATCHING)).toBe(true);
expect(stateManager.getState()).toBe(GameState.MATCHING);
});
test('should emit state change events on valid transition', () => {
// TODO: Implement test
expect(true).toBe(true);
stateManager.transitionTo(GameState.SELECTING);
expect(mockEventEmitter.emit).toHaveBeenCalledWith('game:stateChange', {
from: GameState.IDLE,
to: GameState.SELECTING
});
});
test('should return false for invalid transitions', () => {
// TODO: Implement test
expect(true).toBe(true);
// Invalid: IDLE → GAME_OVER (must go through MATCHING first)
expect(stateManager.transitionTo(GameState.GAME_OVER)).toBe(false);
expect(stateManager.getState()).toBe(GameState.IDLE);
// Invalid: SELECTING → IDLE is valid
expect(stateManager.transitionTo(GameState.SELECTING)).toBe(true);
expect(stateManager.transitionTo(GameState.IDLE)).toBe(true);
});
});
describe('tile selection by state', () => {
test('should allow tile selection in IDLE state', () => {
// TODO: Implement test
expect(true).toBe(true);
expect(stateManager.canSelectTile()).toBe(true);
});
test('should allow tile selection in SELECTING state', () => {
stateManager.transitionTo(GameState.SELECTING);
expect(stateManager.canSelectTile()).toBe(true);
});
test('should block tile selection in MATCHING state', () => {
// TODO: Implement test
expect(true).toBe(true);
stateManager.transitionTo(GameState.SELECTING);
stateManager.transitionTo(GameState.MATCHING);
expect(stateManager.canSelectTile()).toBe(false);
});
test('should block tile selection in GAME_OVER state', () => {
// TODO: Implement test
expect(true).toBe(true);
stateManager.transitionTo(GameState.SELECTING);
stateManager.transitionTo(GameState.MATCHING);
stateManager.transitionTo(GameState.GAME_OVER);
expect(stateManager.canSelectTile()).toBe(false);
});
});
describe('game reset', () => {
test('should reset from GAME_OVER to IDLE', () => {
// TODO: Implement test
expect(true).toBe(true);
stateManager.transitionTo(GameState.SELECTING);
stateManager.transitionTo(GameState.MATCHING);
stateManager.transitionTo(GameState.GAME_OVER);
stateManager.reset();
expect(stateManager.getState()).toBe(GameState.IDLE);
expect(mockEventEmitter.emit).toHaveBeenCalledWith('game:stateChange', {
from: GameState.GAME_OVER,
to: GameState.IDLE
});
});
});
});
+75 -12
View File
@@ -1,35 +1,98 @@
// src/state/GameStateManager.ts - Stub implementation for TDD
// This will be implemented in Plan 04-01
// src/state/GameStateManager.ts - Game state machine with transition validation
import { TypedEventEmitter } from '../game/EventEmitter';
import type { GameEvents } from '../types';
/**
* Represents a game state in the state machine
* String enum for better debugging and logging
*/
export enum GameState {
/** Waiting for player input */
IDLE = 'IDLE',
/** One tile selected, waiting for second tile */
SELECTING = 'SELECTING',
/** Processing match, input blocked */
MATCHING = 'MATCHING',
/** Game ended (win or no moves) */
GAME_OVER = 'GAME_OVER',
}
/**
* Manages game state transitions with validation and event emission
* Enforces valid state transitions and provides helper methods for state-based logic
*/
export class GameStateManager {
private currentState: GameState = GameState.IDLE;
private currentState: GameState;
private readonly events: TypedEventEmitter<GameEvents>;
constructor(private events: TypedEventEmitter<any>) {}
/**
* Valid state transitions
* Maps each state to the list of states it can transition to
*/
private readonly validTransitions: Record<GameState, GameState[]> = {
[GameState.IDLE]: [GameState.SELECTING],
[GameState.SELECTING]: [GameState.IDLE, GameState.MATCHING],
[GameState.MATCHING]: [GameState.IDLE, GameState.GAME_OVER],
[GameState.GAME_OVER]: [GameState.IDLE], // restart only
};
getCurrentState(): GameState {
constructor(events: TypedEventEmitter<GameEvents>) {
this.events = events;
this.currentState = GameState.IDLE;
}
/**
* Transition to a new state if valid
* @param newState - The state to transition to
* @returns true if transition succeeded, false if invalid
*/
transitionTo(newState: GameState): boolean {
// Validate transition
const allowedStates = this.validTransitions[this.currentState];
if (!allowedStates.includes(newState)) {
return false;
}
// Perform transition
const previousState = this.currentState;
this.currentState = newState;
// Emit event for other components to react
this.events.emit('game:stateChange', {
from: previousState,
to: newState
});
return true;
}
/**
* Get the current game state
* @returns The current GameState
*/
getState(): GameState {
return this.currentState;
}
/**
* Check if tiles can be selected in the current state
* @returns true if selection is allowed, false otherwise
*/
canSelectTile(): boolean {
return this.currentState === GameState.IDLE || this.currentState === GameState.SELECTING;
}
transitionTo(newState: GameState): boolean {
// TODO: Implement state transition validation
this.currentState = newState;
return true;
}
/**
* Reset the state machine to IDLE (for restart functionality)
* Emits a state change event from current state to IDLE
*/
reset(): void {
const previousState = this.currentState;
this.currentState = GameState.IDLE;
this.events.emit('game:stateChange', {
from: previousState,
to: GameState.IDLE
});
}
}