import type { GamePhase, GameState, GameStatus, GuessResult, ID, TimelinePosition } from '../types.ts'; /** * Game state domain model * Encapsulates all game state and provides methods to manipulate it */ export class GameStateModel implements GameState { status: GameStatus; phase: GamePhase; turnOrder: ID[]; currentGuesser: ID | null; currentTrack: any; timeline: Record; tokens: Record; ready: Record; spectators: Record; lastResult: GuessResult | null; trackStartAt: number | null; paused: boolean; pausedPosSec: number; goal: number; playlist: string | null; titleArtistAwardedThisRound: Record; constructor(goal = 10) { this.status = 'lobby' as GameStatus; this.phase = 'guess' as GamePhase; this.turnOrder = []; this.currentGuesser = null; this.currentTrack = null; this.timeline = {}; this.tokens = {}; this.ready = {}; this.spectators = {}; this.lastResult = null; this.trackStartAt = null; this.paused = false; this.pausedPosSec = 0; this.goal = goal; this.playlist = null; this.titleArtistAwardedThisRound = {}; } /** * Initialize player in game state */ addPlayer(playerId: ID, isHost = false): void { this.ready[playerId] = isHost; // Host is ready by default this.timeline[playerId] = []; this.tokens[playerId] = 0; } /** * Remove player from game state */ removePlayer(playerId: ID): void { delete this.ready[playerId]; delete this.timeline[playerId]; delete this.tokens[playerId]; delete this.spectators[playerId]; this.turnOrder = this.turnOrder.filter((id) => id !== playerId); } /** * Set player ready status */ setReady(playerId: ID, ready: boolean): void { this.ready[playerId] = ready; } /** * Check if all players are ready */ areAllReady(): boolean { const playerIds = Object.keys(this.ready).filter((id) => !this.spectators[id]); return playerIds.length > 0 && playerIds.every((id) => this.ready[id]); } /** * Start the game */ startGame(playerIds: ID[]): void { this.status = 'playing' as GameStatus; this.turnOrder = this.shuffleArray([...playerIds.filter((id) => !this.spectators[id])]); this.currentGuesser = this.turnOrder[0] || null; } /** * End the game */ endGame(): void { this.status = 'ended' as GameStatus; this.currentTrack = null; this.currentGuesser = null; } /** * Move to next player's turn */ nextTurn(): ID | null { if (!this.currentGuesser || this.turnOrder.length === 0) { return this.turnOrder[0] || null; } const currentIndex = this.turnOrder.indexOf(this.currentGuesser); const nextIndex = (currentIndex + 1) % this.turnOrder.length; this.currentGuesser = this.turnOrder[nextIndex]; return this.currentGuesser; } /** * Award tokens to a player */ awardTokens(playerId: ID, amount: number): void { this.tokens[playerId] = (this.tokens[playerId] || 0) + amount; } /** * Add card to player's timeline */ addToTimeline(playerId: ID, year: number, position: number): void { if (!this.timeline[playerId]) { this.timeline[playerId] = []; } this.timeline[playerId].push({ year, position }); this.timeline[playerId].sort((a, b) => a.position - b.position); } /** * Check if player has won */ hasPlayerWon(playerId: ID): boolean { return (this.timeline[playerId]?.length || 0) >= this.goal; } /** * Get winner (if any) */ getWinner(): ID | null { for (const playerId of Object.keys(this.timeline)) { if (this.hasPlayerWon(playerId)) { return playerId; } } return null; } /** * Set spectator status */ setSpectator(playerId: ID, spectator: boolean): void { this.spectators[playerId] = spectator; if (spectator) { // Remove from turn order if spectating this.turnOrder = this.turnOrder.filter((id) => id !== playerId); } } /** * Reset round state (after track is complete) * Note: currentTrack and trackStartAt are set separately by the caller */ resetRound(): void { this.phase = 'guess' as GamePhase; this.lastResult = null; this.titleArtistAwardedThisRound = {}; } /** * Utility: Shuffle array */ private shuffleArray(array: T[]): T[] { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } }