180 lines
4.6 KiB
TypeScript
180 lines
4.6 KiB
TypeScript
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<ID, TimelinePosition[]>;
|
|
tokens: Record<ID, number>;
|
|
ready: Record<ID, boolean>;
|
|
spectators: Record<ID, boolean>;
|
|
lastResult: GuessResult | null;
|
|
trackStartAt: number | null;
|
|
paused: boolean;
|
|
pausedPosSec: number;
|
|
goal: number;
|
|
playlist: string | null;
|
|
titleArtistAwardedThisRound: Record<ID, boolean>;
|
|
|
|
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<T>(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;
|
|
}
|
|
}
|