362 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			362 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<void> {
 | 
						|
    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);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
 | 
						|
} |