Files
hitstar/src/server-deno/domain/models/GameState.ts

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;
}
}