import { DominoesGame } from "./DominoesGame"; import { PlayerAI } from "./entities/player/PlayerAI"; import { PlayerInterface } from "./entities/player/PlayerInterface"; import { LoggingService } from "../common/LoggingService"; import { getRandomSeed, uuid, wait, whileNot } from "../common/utilities"; import { MatchSessionState } from "./dto/MatchSessionState"; import { PlayerNotificationService } from '../server/services/PlayerNotificationService'; import seedrandom, { PRNG } from "seedrandom"; import { NetworkPlayer } from "./entities/player/NetworkPlayer"; import { PlayerHuman } from "./entities/player/PlayerHuman"; import { GameSummary } from "./dto/GameSummary"; import { PlayerMove } from "./entities/PlayerMove"; export class MatchSession { private currentGame: DominoesGame | null = null; private minHumanPlayers: number = 1; private gameNumber: number = 0; private waitingForPlayers: boolean = true; private waitingSeconds: number = 0; private logger: LoggingService = new LoggingService(); private notificationService = new PlayerNotificationService(); private winnerIndex: number | null = null; private clientsReady: string[] = []; id: string; matchInProgress: boolean = false; gameInProgress: boolean = false; matchWinner?: PlayerInterface = undefined; maxPlayers: number = 4; mode: string = 'classic'; players: PlayerInterface[] = []; pointsToWin: number = 50; rng!: PRNG scoreboard: Map = new Map(); seed!: string sessionInProgress: boolean = false; state: string = 'created' constructor(public creator: PlayerInterface, public name?: string, seed?: string) { this.seed = seed || getRandomSeed(); this.id = uuid(); this.name = name || `Game ${this.id}`; this.addPlayerToSession(creator); this.creator = creator; this.logger.info(`Match session created by: ${creator.name}`); this.logger.info(`Match session ID: ${this.id}`); this.logger.info(`Match session name: ${this.name}`); this.logger.info(`Points to win: ${this.pointsToWin}`); this.sessionInProgress = true; this.waitingForPlayers = true; this.logger.info('Waiting for players to be ready'); } get numPlayers() { return this.players.length; } get numPlayersReady() { return this.players.filter(player => player.ready).length; } get numHumanPlayers() { return this.players.filter(player => player instanceof PlayerHuman).length; } setClientReady(userId: string) { this.logger.trace(`${userId} - ${this.clientsReady}`); if (!this.clientsReady.includes(userId)) { this.logger.trace(`Client ${userId} is ready`) this.clientsReady.push(userId); } } async checkAllClientsReadyBeforeStart() { try { if (this.currentGame) { const conditionFn = () => { this.logger.trace(`Clients ready: ${this.clientsReady.length}/${this.numHumanPlayers}`); return this.clientsReady.length === this.numHumanPlayers } await whileNot(conditionFn, 10); this.logger.info(`Game #${this.gameNumber} started`); this.currentGame.start(); this.gameInProgress = true; } } catch (error) { this.logger.error(error, 'Error starting game'); } } getPlayer(userId: string) { return this.players.find(player => player.id === userId); } playerMove(move: any) { if (this.currentGame) { if ((move === null) || (move === undefined) || move.type === 'pass') { this.currentGame.finishTurn(null); return; } const player = this.getPlayer(move.playerId); if (!player) { throw new Error("Player not found"); } const tile = player.hand.find(tile => tile.id === move.tile.id); if (!tile) { throw new Error("Tile not found"); } const newMove = new PlayerMove(tile, move.type, move. playerId) this.currentGame.finishTurn(newMove); } } // This is the entry point for the game, method called by session host async start() { if (this.matchInProgress) { throw new Error("Game already in progress"); } this.waitingForPlayers = false; await this.startMatch(this.seed); } addPlayerToSession(player: PlayerInterface) { if (this.numPlayers >= this.maxPlayers) { throw new Error("GameSession is full"); } this.players.push(player); this.logger.info(`${player.name} joined the game!`); } private continueMatch(gameSummary: GameSummary) { this.winnerIndex = this.players.findIndex(player => player.id === gameSummary?.winner?.id); if (this.winnerIndex !== null) { this.currentGame?.setForcedInitialPlayerIndex(this.winnerIndex); } this.setScores(gameSummary || undefined); this.checkMatchWinner(); this.resetPlayers(); if (!this.matchInProgress) { this.state = 'end' this.notificationService.sendEventToPlayers('server:match-finished', this.players, { lastGame: gameSummary, sessionState: this.getState(), }); } else { this.state = 'waiting' // await this.playerNotificationManager.notifyMatchState(this); this.notificationService.sendEventToPlayers('server:game-finished', this.players, { lastGame: gameSummary, sessionState: this.getState() }); this.waitingForPlayers = true; } } private startGame() { this.gameNumber += 1; this.logger.info(`Game #${this.gameNumber} started`); this.currentGame = new DominoesGame(this.players, this.rng); this.currentGame.on('game-over', (gameSummary: GameSummary) => { this.gameInProgress = false; this.continueMatch(gameSummary); }); this.logger.info(`Waiting for ${this.numHumanPlayers} clients to be ready`); this.checkAllClientsReadyBeforeStart(); } private async startMatch(seed: string) { try { this.state = 'in-game' this.rng = seedrandom(seed); this.resetScoreboard() this.gameNumber = 0; this.matchInProgress = true // this.playerNotificationManager.notifyMatchState(this); this.winnerIndex = null; this.startGame(); } catch (error) { this.logger.error(error); this.matchInProgress = false; this.state = 'error' } } // async checkHumanPlayersReady() { // this.logger.info('Waiting for human players to be ready'); // return new Promise((resolve) => { // const interval = setInterval(() => { // this.logger.debug(`Human players ready: ${this.numPlayersReady}/${this.numHumanPlayers}`) // if (this.numPlayersReady === this.numHumanPlayers) { // clearInterval(interval); // resolve(true); // } // }, 1000); // }); // } resetPlayers() { this.players.forEach(player => { player.reset() }); } checkMatchWinner() { const scores = Array.from(this.scoreboard.values()); const maxScore = Math.max(...scores); if (maxScore >= this.pointsToWin) { this.matchWinner = this.players.find(player => this.scoreboard.get(player.name) === maxScore); if (!this.matchWinner) { throw new Error('Match winner not found'); } this.logger.info(`Match winner: ${this.matchWinner.name} with ${maxScore} points`); this.matchInProgress = false; } } resetScoreboard() { this.scoreboard = new Map(); this.players.forEach(player => { this.scoreboard.set(player.name, 0); }); } setScores(gameSummary?: GameSummary) { if (!gameSummary) { return; } const { score } = gameSummary; score.forEach(playerScore => { const currentScore = this.scoreboard.get(playerScore.name) ?? 0; this.scoreboard.set(playerScore.name, currentScore + playerScore.score); }); } afterTileAnimation(data: any) { this.currentGame?.setCanAskNextPlayerMove(true); } private endGame(): any { if (this.currentGame !== null) { const { gameBlocked, gameTied, winner } = this.currentGame; gameBlocked ? this.logger.info('Game blocked!') : gameTied ? this.logger.info('Game tied!') : this.logger.info('Game over!'); this.logger.info('Winner: ' + winner?.name + ' with ' + winner?.pipsCount() + ' points'); this.getScore(this.currentGame); this.logger.info('Game ended'); this.currentGame = null; } } private getScore(game: DominoesGame) { const pips = game.players .sort((a,b) => (b.pipsCount() - a.pipsCount())) .map(player => { return `${player.name}: ${player.pipsCount()}`; }); this.logger.info(`Pips count: ${pips.join(', ')}`); const totalPoints = game.players.reduce((acc, player) => acc + player.pipsCount(), 0); if (game.winner !== null) { game.winner.score += totalPoints; } const scores = game.players .sort((a,b) => (b.score - a.score)) .map(player => { return `${player.name}: ${player.score}`; }); this.logger.info(`Scores: ${scores.join(', ')}`); } createPlayerAI(i: number) { const AInames = ["Alice (AI)", "Bob (AI)", "Charlie (AI)", "David (AI)"]; const player = new PlayerAI(AInames[i], this.rng, this.id); player.ready = true; return player; } setPlayerReady(userId: string) { const player = this.players.find(player => player.id === userId); if (!player) { throw new Error("Player not found"); } player.ready = true; this.logger.info(`${player.name} is ready!`); this.notificationService.notifyMatchState(this); if (this.matchInProgress && this.numPlayersReady === this.numHumanPlayers) { this.startGame(); } } toString() { return `GameSession:(${this.id} ${this.name})`; } getState(): MatchSessionState { return { id: this.id, name: this.name!, creator: this.creator.id, players: this.players.map(player =>( { id: player.id, name: player.name, ready: player.ready, })), playersReady: this.numPlayersReady, sessionInProgress: this.sessionInProgress, maxPlayers: this.maxPlayers, numPlayers: this.numPlayers, waitingForPlayers: this.waitingForPlayers, waitingSeconds: this.waitingSeconds, seed: this.seed, mode: this.mode, pointsToWin: this.pointsToWin, status: this.sessionInProgress ? 'in progress' : 'waiting', scoreboard: [...this.scoreboard.entries()], matchWinner: this.matchWinner?.getState() || null, matchInProgress: this.matchInProgress }; } }