import { PRNG } from 'seedrandom'; 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, wait } from '../common/utilities'; import { GameSummary } from './dto/GameSummary'; import { PlayerNotificationService } from '../server/services/PlayerNotificationService'; import { GameState } from './dto/GameState'; export class DominoesGame { 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; notificationManager: PlayerNotificationService = new PlayerNotificationService(); lastMove: PlayerMove | null = null; forcedInitialPlayerIndex: number | null = null; constructor(public players: PlayerInterface[], seed: PRNG) { this.id = uuid(); this.logger.info(`Game ID: ${this.id}`); this.rng = seed this.board = new Board(seed); this.initializeGame(); } 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.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length; } isBlocked(): boolean { return this.blockedCount === this.players.length; } isGameOver(): boolean { const hasWinner: boolean = this.players.some(player => player.hand.length === 0); return hasWinner || this.gameBlocked; } getWinner(): PlayerInterface | null { if (!this.gameOver) { return null; } 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)); } } } } } } return startingIndex === -1 ? 0 : startingIndex; } async playTurn(): Promise { const player = this.players[this.currentPlayerIndex]; console.log(`${player.name}'s turn (${player.hand.length} tiles)`); printBoard(this.board); // let playerMove: PlayerMove | null = null; // while(playerMove === null) { // try { // playerMove = await player.makeMove(this.board); // } catch (error) { // this.logger.error(error, 'Error making move'); // } // } const playerMove = await player.makeMove(this.board); printBoard(this.board, true); this.lastMove = playerMove; if (playerMove === null) { console.log('Player cannot move'); this.blockedCount += 1; this.nextPlayer(); return; } this.blockedCount = 0; this.board.play(playerMove); player.hand = player.hand.filter(tile => tile !== playerMove.tile); this.nextPlayer(); } resetPlayersScore() { for (let player of this.players) { player.score = 0; } } async start(): Promise { this.resetPlayersScore(); this.gameInProgress = false; this.tileSelectionPhase = true; await this.notificationManager.notifyGameState(this); await this.notificationManager.notifyPlayersState(this.players); this.logger.debug('clients received boneyard :>> ' + this.board.boneyard); await wait(1000); if (this.autoDeal) { this.dealTiles(); await this.notificationManager.notifyGameState(this); await this.notificationManager.notifyPlayersState(this.players); } else { await this.tilesSelection(); } this.tileSelectionPhase = false; this.gameInProgress = true; this.currentPlayerIndex = (this.forcedInitialPlayerIndex !== null) ? this.forcedInitialPlayerIndex : this.getStartingPlayerIndex(); printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`); while (!this.gameOver) { await this.playTurn(); await this.notificationManager.notifyGameState(this); await this.notificationManager.notifyPlayersState(this.players); this.gameBlocked = this.isBlocked(); this.gameOver = this.isGameOver(); } this.gameInProgress = false; this.winner = this.getWinner(); return { gameId: this.id, isBlocked: this.gameBlocked, isTied: this.gameTied, winner: this.winner }; } dealTiles() { 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; player.hand.push(tile); } } } } async tilesSelection() { while (this.board.boneyard.length > 0) { for (let player of this.players) { const choosen = await player.chooseTile(this.board); await this.notificationManager.notifyGameState(this); await this.notificationManager.notifyPlayersState(this.players); if (this.board.boneyard.length === 0) { break; } } } } getGameState(): GameState { const currentPlayer = this.players[this.currentPlayerIndex] return { id: uuid(), lastMove: this.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 => ({ id: tile.id})), players: this.players.map(player => player.getState()), currentPlayer: currentPlayer.getState(), board: this.board.tiles.map(tile => ({ id: tile.id, pips: tile.pips })), boardFreeEnds: this.board.getFreeEnds(), } } }