import { PRNG } from 'seedrandom'; import {EventEmitter} from 'events'; import { Board } from "./entities/Board"; import { PlayerMove } from "./entities/PlayerMove"; import { PlayerInterface } from "./entities/player/PlayerInterface"; import { Tile } from "./entities/Tile"; import { LoggingService } from "../common/LoggingService"; import { printBoard, printLine, uuid, whileNot } from '../common/utilities'; import { PlayerNotificationService } from '../server/services/PlayerNotificationService'; import { GameState } from './dto/GameState'; import { PlayerHuman } from './entities/player/PlayerHuman'; import { PlayerAI } from './entities/player/PlayerAI'; import { GameSummary } from './dto/GameSummary'; export class DominoesGame extends EventEmitter { private id: string; private seed: string | undefined; autoDeal: boolean = true; board: Board; currentPlayerIndex: number = 0; gameInProgress: boolean = false; gameOver: boolean = false; gameBlocked: boolean = false; gameTied: boolean = false; tileSelectionPhase: boolean = true; logger: LoggingService = new LoggingService(); blockedCount: number = 0; winner: PlayerInterface | null = null; rng: PRNG; handSize: number = 7; notificationService: PlayerNotificationService = new PlayerNotificationService(); lastMove: PlayerMove | null = null; forcedInitialPlayerIndex: number | null = null; canAskNextPlayerMove: boolean = true; clientsReady: string[] = []; constructor(public players: PlayerInterface[], seed: PRNG) { super(); this.id = uuid(); this.logger.info(`Game ID: ${this.id}`); this.rng = seed this.board = new Board(seed); this.initializeGame(); } get numHumanPlayers() { return this.players.filter(player => player instanceof PlayerHuman).length; } async initializeGame() { this.gameOver = false; this.gameBlocked = false; this.gameTied = false; this.board.boneyard = this.generateTiles(); } setForcedInitialPlayerIndex(index: number) { this.forcedInitialPlayerIndex = index; } reset() { this.board.reset(); this.initializeGame(); for (let player of this.players) { player.hand = []; } } generateTiles(): Tile[] { const tiles: Tile[] = []; for (let i = 6; i >= 0; i--) { for (let j = i; j >= 0; j--) { tiles.push(new Tile([i, j])); } } this.logger.debug('tiles :>> ' + tiles); return this.shuffle(tiles); } private shuffle(array: Tile[]): Tile[] { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(this.rng() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } nextPlayer() { this.logger.debug('Turn ended'); this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length; } isBlocked(): boolean { const freeEnds = this.board.getFreeEnds(); const tiles = [] for (let player of this.players) { tiles.push(...player.hand); } const canPlay = tiles.some(tile => tile.pips[0] === freeEnds[0] || tile.pips[1] === freeEnds[0] || tile.pips[0] === freeEnds[1] || tile.pips[1] === freeEnds[1]); return this.blockedCount >= 4 && !canPlay; } isGameOver(): boolean { const hasWinner: boolean = this.players.some(player => player.hand.length === 0); return hasWinner || this.gameBlocked; } getWinner(): PlayerInterface { const winnerNoTiles = this.players.find(player => player.hand.length === 0); if (winnerNoTiles !== undefined) { return winnerNoTiles; } const winnerMinPipsCount = this.players.reduce((acc, player) => { return player.pipsCount() < acc.pipsCount() ? player : acc; }); return winnerMinPipsCount; } getStartingPlayerIndex(): number { // Determine starting player let startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 6 && tile.pips[1] === 6)); if (startingIndex === -1) { startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 5 && tile.pips[1] === 5)); if (startingIndex === -1) { startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 4 && tile.pips[1] === 4)); if (startingIndex === -1) { startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 3 && tile.pips[1] === 3)); if (startingIndex === -1) { startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 2 && tile.pips[1] === 2)); if (startingIndex === -1) { startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 1 && tile.pips[1] === 1)); if (startingIndex === -1) { startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 0 && tile.pips[1] === 0)); } } } } } } this.logger.debug('Starting player index: ' + startingIndex); return startingIndex === -1 ? 0 : startingIndex; } playTurn() { try { const player = this.players[this.currentPlayerIndex]; this.notificationService.sendEventToPlayers('server:next-turn', this.players, this.getState()); this.logger.debug(`${player.name}'s turn (${player.hand.length} tiles)`); this.printPlayerHand(player); printBoard(this.board) player.askForMove(this.board); } catch (error) { this.logger.error(error, 'Error playing turn'); } } async checkAllClientsReadyToContinue() { try { const conditionFn = () => { this.logger.trace(`Clients ready: ${this.clientsReady.length}/${this.numHumanPlayers}`); return this.clientsReady.length === this.numHumanPlayers } await whileNot(conditionFn, 100); this.clientsReady = []; } catch (error) { this.logger.error(error, 'Error starting game'); throw new Error('Error starting game (checkAllClientsReadyToContinue)'); } } async finishTurn(playerMove: PlayerMove | null) { try { this.lastMove = playerMove; if (playerMove === null) { this.logger.info('Player cannot move'); this.blockedCount += 1; this.logger.trace(`Blocked count: ${this.blockedCount}`); this.gameBlocked = this.isBlocked(); this.notificationService.sendEventToPlayers('server:server-player-move', this.players, { move: playerMove }); if (this.gameBlocked) { this.gameEnded(); } else { this.nextPlayer(); await this.checkAllClientsReadyToContinue() this.playTurn(); } return; } const player = this.players[this.currentPlayerIndex]; const skipWaitForConfirmation = player instanceof PlayerAI && playerMove === null; this.blockedCount = 0; this.board.play(playerMove); player.hand = player.hand.filter(tile => tile !== playerMove.tile); this.notificationService.sendEventToPlayers('server:server-player-move', this.players, { move: playerMove }); this.canAskNextPlayerMove = false; // whileNotUndefined(() => this.canAskNextPlayerMove === true ? {} : undefined); this.gameOver = this.isGameOver(); if (!this.gameOver) { this.nextPlayer(); if (!skipWaitForConfirmation) { await this.checkAllClientsReadyToContinue() } this.playTurn(); } else { this.gameEnded(); } } catch (error) { this.logger.error(error, 'Error finishing move'); } } gameEnded() { this.gameInProgress = false; this.winner = this.getWinner(); this.setScores(); const summary: GameSummary = { gameId: this.id, isBlocked: this.gameBlocked, isTied: this.gameTied, winner: this.winner?.getState(true), score: this.players.map(player => ({id: player.id, name: player.name, score: player.score})), players: this.players.map(player => player.getState(true)), board: this.board.tiles.map(tile => tile.getState(true)), boneyard: this.board.boneyard.map(tile => tile.getState(true)), movements: this.board.movements } this.emit('game-over', summary); } resetPlayersScore() { for (let player of this.players) { player.score = 0; } } setCanAskNextPlayerMove(value: boolean) { this.canAskNextPlayerMove = value } private deal() { if (this.autoDeal) { this.autoDealTiles(); } else { // await this.tilesSelection(); } this.printPlayersHand(); } printPlayersHand() { for (let player of this.players) { this.printPlayerHand(player); } } printPlayerHand(player: PlayerInterface) { this.logger.debug(`${player.name}'s hand (${player.hand.length}): ${player.hand.map(tile => tile.toString())}`); } async start(): Promise { try { // Initalize game this.gameInProgress = false; this.resetPlayersScore(); this.tileSelectionPhase = true; this.deal(); const extractStates = (p: PlayerInterface) => ({ player: p.getState(true), gameState: this.getState() }); await this.notificationService.sendEventToPlayers('server:hand-dealt', this.players, extractStates); this.tileSelectionPhase = false; // Start game this.gameInProgress = true; this.currentPlayerIndex = (this.forcedInitialPlayerIndex !== null) ? this.forcedInitialPlayerIndex : this.getStartingPlayerIndex(); printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`); this.logger.debug("Before play turn") this.playTurn(); } catch (error) { this.logger.error(error, 'Error starting game'); throw new Error('Error starting game'); } } setScores() { const totalPips = this.players.reduce((acc, player) => acc + player.pipsCount(), 0) || 0; if (this.winner !== null) { const winner = this.winner; winner.score = totalPips; if (winner.teamedWith !== undefined) { const p = this.getPlayer(winner.teamedWith) if (p !== undefined) { p.score = totalPips; } } } } private getPlayer(userId: string) { return this.players.find(player => player.id === userId) || undefined; } private autoDealTiles() { for (let i = 0; i < this.handSize; i++) { for (let player of this.players) { const tile: Tile | undefined = this.board.boneyard.pop(); if (tile !== undefined) { tile.revealed = true; tile.playerId = player.id; player.hand.push(tile); } } } } private async tilesSelection() { while (this.board.boneyard.length > 0) { for (let player of this.players) { const choosen = await player.chooseTile(this.board); if (this.board.boneyard.length === 0) { break; } } } } getState(): GameState { const currentPlayer = this.players[this.currentPlayerIndex] const lastMove = this.lastMove?.getState() || null; const movements = this.board.movements.map(move => move.getState()); return { id: uuid(), lastMove, gameInProgress: this.gameInProgress, winner: this.winner, tileSelectionPhase: this.tileSelectionPhase, gameBlocked: this.gameBlocked, gameTied: this.gameTied, gameId: this.id, boneyard: this.board.boneyard.map(tile => tile.getState(false)), players: this.players.map(player => player.getState(false)), currentPlayer: currentPlayer.getState(), board: this.board.tiles.map(tile => tile.getState(true)), boardFreeEnds: this.board.getFreeEnds(), movements } } 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); } } }