mirror of
https://github.com/tiennm99/ai-coding-workflow-labs.git
synced 2026-09-17 06:24:20 +00:00
feat: add shared game state manager with EventEmitter
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { Difficulty, GameStatus } from "../types";
|
||||
import { DIFFICULTY_CONFIGS } from "./constants";
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
interface GameState {
|
||||
status: GameStatus;
|
||||
difficulty: Difficulty | null;
|
||||
score: number;
|
||||
timerSeconds: number;
|
||||
hintsRemaining: number;
|
||||
shufflesRemaining: number;
|
||||
combo: number;
|
||||
lastMatchTime: number;
|
||||
}
|
||||
|
||||
export class GameStateManager {
|
||||
private state: GameState;
|
||||
private listeners: Map<string, Listener[]> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.state = {
|
||||
status: "menu",
|
||||
difficulty: null,
|
||||
score: 0,
|
||||
timerSeconds: 0,
|
||||
hintsRemaining: 0,
|
||||
shufflesRemaining: 0,
|
||||
combo: 1,
|
||||
lastMatchTime: 0,
|
||||
};
|
||||
}
|
||||
|
||||
getState(): Readonly<GameState> {
|
||||
return { ...this.state };
|
||||
}
|
||||
|
||||
on(event: string, listener: Listener): void {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event)!.push(listener);
|
||||
}
|
||||
|
||||
off(event: string, listener: Listener): void {
|
||||
const listeners = this.listeners.get(event);
|
||||
if (listeners) {
|
||||
const idx = listeners.indexOf(listener);
|
||||
if (idx !== -1) listeners.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
emit(event: string): void {
|
||||
const listeners = this.listeners.get(event);
|
||||
if (listeners) {
|
||||
for (const l of listeners) l();
|
||||
}
|
||||
}
|
||||
|
||||
startGame(difficulty: Difficulty): void {
|
||||
const config = DIFFICULTY_CONFIGS[difficulty];
|
||||
this.state = {
|
||||
status: "playing",
|
||||
difficulty,
|
||||
score: 0,
|
||||
timerSeconds: config.timerSeconds,
|
||||
hintsRemaining: config.hints,
|
||||
shufflesRemaining: config.shuffles,
|
||||
combo: 1,
|
||||
lastMatchTime: Date.now(),
|
||||
};
|
||||
this.emit("stateChange");
|
||||
}
|
||||
|
||||
addScore(points: number): void {
|
||||
this.state.score += points;
|
||||
this.emit("stateChange");
|
||||
}
|
||||
|
||||
useHint(): boolean {
|
||||
if (this.state.hintsRemaining <= 0) return false;
|
||||
this.state.hintsRemaining--;
|
||||
this.emit("stateChange");
|
||||
return true;
|
||||
}
|
||||
|
||||
useShuffle(): boolean {
|
||||
if (this.state.shufflesRemaining <= 0) return false;
|
||||
this.state.shufflesRemaining--;
|
||||
this.emit("stateChange");
|
||||
return true;
|
||||
}
|
||||
|
||||
tick(): void {
|
||||
if (this.state.status !== "playing") return;
|
||||
this.state.timerSeconds--;
|
||||
if (this.state.timerSeconds <= 0) {
|
||||
this.state.timerSeconds = 0;
|
||||
this.state.status = "lost";
|
||||
}
|
||||
this.emit("stateChange");
|
||||
}
|
||||
|
||||
incrementCombo(): void {
|
||||
this.state.combo++;
|
||||
this.emit("stateChange");
|
||||
}
|
||||
|
||||
resetCombo(): void {
|
||||
this.state.combo = 1;
|
||||
this.emit("stateChange");
|
||||
}
|
||||
|
||||
setStatus(status: GameStatus): void {
|
||||
this.state.status = status;
|
||||
this.emit("stateChange");
|
||||
}
|
||||
|
||||
setLastMatchTime(time: number): void {
|
||||
this.state.lastMatchTime = time;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { GameStateManager } from "../../src/game/state";
|
||||
|
||||
describe("GameStateManager", () => {
|
||||
let state: GameStateManager;
|
||||
|
||||
beforeEach(() => {
|
||||
state = new GameStateManager();
|
||||
});
|
||||
|
||||
it("initializes with menu status", () => {
|
||||
expect(state.getState().status).toBe("menu");
|
||||
});
|
||||
|
||||
it("startGame sets state for given difficulty", () => {
|
||||
state.startGame("easy");
|
||||
const s = state.getState();
|
||||
expect(s.status).toBe("playing");
|
||||
expect(s.score).toBe(0);
|
||||
expect(s.hintsRemaining).toBe(5);
|
||||
expect(s.shufflesRemaining).toBe(3);
|
||||
expect(s.timerSeconds).toBe(300);
|
||||
expect(s.difficulty).toBe("easy");
|
||||
expect(s.combo).toBe(1);
|
||||
});
|
||||
|
||||
it("addScore increases score", () => {
|
||||
state.startGame("easy");
|
||||
state.addScore(150);
|
||||
expect(state.getState().score).toBe(150);
|
||||
state.addScore(100);
|
||||
expect(state.getState().score).toBe(250);
|
||||
});
|
||||
|
||||
it("useHint decrements hints", () => {
|
||||
state.startGame("easy");
|
||||
expect(state.useHint()).toBe(true);
|
||||
expect(state.getState().hintsRemaining).toBe(4);
|
||||
});
|
||||
|
||||
it("useHint returns false when no hints left", () => {
|
||||
state.startGame("hard");
|
||||
expect(state.useHint()).toBe(true);
|
||||
expect(state.useHint()).toBe(false);
|
||||
expect(state.getState().hintsRemaining).toBe(0);
|
||||
});
|
||||
|
||||
it("useShuffle decrements shuffles", () => {
|
||||
state.startGame("easy");
|
||||
expect(state.useShuffle()).toBe(true);
|
||||
expect(state.getState().shufflesRemaining).toBe(2);
|
||||
});
|
||||
|
||||
it("useShuffle returns false when no shuffles left", () => {
|
||||
state.startGame("hard");
|
||||
expect(state.useShuffle()).toBe(true);
|
||||
expect(state.useShuffle()).toBe(false);
|
||||
});
|
||||
|
||||
it("tick decrements timer", () => {
|
||||
state.startGame("easy");
|
||||
state.tick();
|
||||
expect(state.getState().timerSeconds).toBe(299);
|
||||
});
|
||||
|
||||
it("tick sets status to lost when timer reaches 0", () => {
|
||||
state.startGame("easy");
|
||||
for (let i = 0; i < 300; i++) {
|
||||
state.tick();
|
||||
}
|
||||
expect(state.getState().status).toBe("lost");
|
||||
expect(state.getState().timerSeconds).toBe(0);
|
||||
});
|
||||
|
||||
it("emits events on state change", () => {
|
||||
const listener = vi.fn();
|
||||
state.on("stateChange", listener);
|
||||
state.startGame("easy");
|
||||
expect(listener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("incrementCombo and resetCombo work", () => {
|
||||
state.startGame("easy");
|
||||
state.incrementCombo();
|
||||
expect(state.getState().combo).toBe(2);
|
||||
state.incrementCombo();
|
||||
expect(state.getState().combo).toBe(3);
|
||||
state.resetCombo();
|
||||
expect(state.getState().combo).toBe(1);
|
||||
});
|
||||
|
||||
it("setStatus changes status and emits", () => {
|
||||
state.startGame("easy");
|
||||
const listener = vi.fn();
|
||||
state.on("stateChange", listener);
|
||||
state.setStatus("won");
|
||||
expect(state.getState().status).toBe("won");
|
||||
expect(listener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("pause and resume toggle status", () => {
|
||||
state.startGame("easy");
|
||||
state.setStatus("paused");
|
||||
expect(state.getState().status).toBe("paused");
|
||||
state.setStatus("playing");
|
||||
expect(state.getState().status).toBe("playing");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user