mirror of
https://github.com/tiennm99/gsd-framework.git
synced 2026-08-18 10:23:29 +00:00
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:
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user