reworked
This commit is contained in:
		@@ -4,6 +4,9 @@ import * as readline from 'readline';
 | 
			
		||||
import { Tile } from '../game/entities/Tile';
 | 
			
		||||
import chalk from 'chalk';
 | 
			
		||||
import { Board } from '../game/entities/Board';
 | 
			
		||||
import { LoggingService } from './LoggingService';
 | 
			
		||||
 | 
			
		||||
const logger = new LoggingService();
 | 
			
		||||
 | 
			
		||||
const rl = readline.createInterface({
 | 
			
		||||
  input: process.stdin,
 | 
			
		||||
@@ -29,6 +32,21 @@ export const whileNotUndefined = async (fn: Function, maxQueries: number = 20, m
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const whileNot = async (fn: Function, maxQueries: number = 20, millis: number = 500): Promise<void> => {
 | 
			
		||||
  return new Promise(async (resolve, reject) => {
 | 
			
		||||
    let result: boolean = false;
 | 
			
		||||
    while (result === false) {
 | 
			
		||||
      await wait(millis);
 | 
			
		||||
      result = fn()
 | 
			
		||||
      if (maxQueries-- < 0) {
 | 
			
		||||
        reject()
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    resolve();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function askQuestion(question: string): Promise<string> {
 | 
			
		||||
  return new Promise((resolve) => {
 | 
			
		||||
    // console.log(chalk.yellow(question));
 | 
			
		||||
@@ -46,7 +64,7 @@ export function getRandomSeed(): string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function printTiles(prefix:string, tiles: Tile[]): void {
 | 
			
		||||
  console.log(`${prefix}${tiles.join(' ')}`);
 | 
			
		||||
  logger.info(`${prefix}${tiles.join(' ')}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function printSelection(prefix:string, tiles: Tile[]): void {
 | 
			
		||||
@@ -55,22 +73,22 @@ export function printSelection(prefix:string, tiles: Tile[]): void {
 | 
			
		||||
    return `(${index > 9 ? `${index})`: `${index}) `} `
 | 
			
		||||
  }).join(' ');
 | 
			
		||||
  printTiles(prefix, tiles);
 | 
			
		||||
  console.log(`${Array(prefix.length).join((' '))} ${line}`);
 | 
			
		||||
  logger.info(`${Array(prefix.length).join((' '))} ${line}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function printBoard(board: Board, highlighted: boolean = false): void {
 | 
			
		||||
  if (highlighted)
 | 
			
		||||
    console.log(chalk.cyan(`Board: ${board.tiles.length > 0 ? board.tiles.join(' ') : '--empty--'}`));
 | 
			
		||||
    logger.info(chalk.cyan(`Board: ${board.tiles.length > 0 ? board.tiles.map(t => t.toString()).join(' ') : '--empty--'}`));
 | 
			
		||||
    else
 | 
			
		||||
  console.log(chalk.gray(`Board: ${board.tiles.length > 0 ? board.tiles.join(' ') : '--empty--'}`));
 | 
			
		||||
  logger.info(chalk.gray(`Board: ${board.tiles.length > 0 ? board.tiles.map(t => t.toString()).join(' ') : '--empty--'}`));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function printLine(msg: string): void {
 | 
			
		||||
  console.log(chalk.grey(msg));
 | 
			
		||||
  logger.info(chalk.grey(msg));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function printError(msg: string): void {
 | 
			
		||||
  console.log(chalk.red(msg));
 | 
			
		||||
  logger.info(chalk.red(msg));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function uuid() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
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, wait } from '../common/utilities';
 | 
			
		||||
import { GameSummary } from './dto/GameSummary';
 | 
			
		||||
import { printBoard, printLine, uuid, wait, whileNotUndefined } from '../common/utilities';
 | 
			
		||||
import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
 | 
			
		||||
import { GameState } from './dto/GameState';
 | 
			
		||||
 | 
			
		||||
export class DominoesGame {
 | 
			
		||||
export class DominoesGame extends EventEmitter {
 | 
			
		||||
  private id: string;
 | 
			
		||||
  private seed: string | undefined;
 | 
			
		||||
  autoDeal: boolean = true;
 | 
			
		||||
@@ -25,11 +25,13 @@ export class DominoesGame {
 | 
			
		||||
  winner: PlayerInterface | null = null;
 | 
			
		||||
  rng: PRNG;
 | 
			
		||||
  handSize: number = 7;
 | 
			
		||||
  notificationManager: PlayerNotificationService = new PlayerNotificationService();
 | 
			
		||||
  notificationService: PlayerNotificationService = new PlayerNotificationService();
 | 
			
		||||
  lastMove: PlayerMove | null = null;
 | 
			
		||||
  forcedInitialPlayerIndex: number | null = null;
 | 
			
		||||
  canAskNextPlayerMove: boolean = true;
 | 
			
		||||
 | 
			
		||||
  constructor(public players: PlayerInterface[], seed: PRNG) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.id = uuid();
 | 
			
		||||
    this.logger.info(`Game ID: ${this.id}`);
 | 
			
		||||
    this.rng = seed
 | 
			
		||||
@@ -76,6 +78,7 @@ export class DominoesGame {
 | 
			
		||||
  }  
 | 
			
		||||
 | 
			
		||||
  nextPlayer() {
 | 
			
		||||
    this.logger.debug('Turn ended');
 | 
			
		||||
    this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -88,10 +91,7 @@ export class DominoesGame {
 | 
			
		||||
    return hasWinner || this.gameBlocked;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getWinner(): PlayerInterface | null {
 | 
			
		||||
    if (!this.gameOver) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  getWinner(): PlayerInterface {
 | 
			
		||||
    const winnerNoTiles = this.players.find(player => player.hand.length === 0);
 | 
			
		||||
    if (winnerNoTiles !== undefined) {
 | 
			
		||||
      return winnerNoTiles;
 | 
			
		||||
@@ -123,35 +123,70 @@ export class DominoesGame {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.logger.debug('Starting player index: ' + startingIndex);
 | 
			
		||||
    return startingIndex === -1 ? 0 : startingIndex;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async playTurn(): Promise<void> {
 | 
			
		||||
    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();   
 | 
			
		||||
  playTurn() {
 | 
			
		||||
    try {
 | 
			
		||||
      const player = this.players[this.currentPlayerIndex];
 | 
			
		||||
      this.notificationService.sendEventToPlayers('server:next-turn', this.players, this.getGameState());
 | 
			
		||||
      this.logger.debug(`${player.name}'s turn (${player.hand.length} tiles)`);
 | 
			
		||||
      printBoard(this.board)
 | 
			
		||||
      player.askForMove(this.board);   
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.logger.error(error, 'Error playing turn');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  finishTurn(playerMove: PlayerMove | null) {
 | 
			
		||||
    try {
 | 
			
		||||
      this.lastMove = playerMove;
 | 
			
		||||
      if (playerMove === null) {
 | 
			
		||||
        console.log('Player cannot move');
 | 
			
		||||
        this.blockedCount += 1;
 | 
			
		||||
        
 | 
			
		||||
        this.gameBlocked = this.isBlocked();
 | 
			
		||||
        if (this.gameBlocked) {
 | 
			
		||||
          this.gameEnded();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        this.nextPlayer();     
 | 
			
		||||
        this.playTurn();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const player = this.players[this.currentPlayerIndex];
 | 
			
		||||
      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.printPlayersHand();
 | 
			
		||||
        this.nextPlayer();    
 | 
			
		||||
        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 = {
 | 
			
		||||
      gameId: this.id,
 | 
			
		||||
      isBlocked: this.gameBlocked,
 | 
			
		||||
      isTied: this.gameTied,
 | 
			
		||||
      winner: this.winner?.getState(),
 | 
			
		||||
      score: this.players.map(player => ({name: player.name, score: player.score}))
 | 
			
		||||
    }
 | 
			
		||||
    this.emit('game-over', summary);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  resetPlayersScore() {
 | 
			
		||||
@@ -160,63 +195,85 @@ export class DominoesGame {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async start(): Promise<GameSummary> {
 | 
			
		||||
    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
 | 
			
		||||
    };
 | 
			
		||||
  setCanAskNextPlayerMove(value: boolean) {
 | 
			
		||||
    this.canAskNextPlayerMove = value
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  dealTiles() {
 | 
			
		||||
  private deal() {
 | 
			
		||||
    if (this.autoDeal) {
 | 
			
		||||
      this.autoDealTiles();
 | 
			
		||||
    } else  {
 | 
			
		||||
      // await this.tilesSelection();
 | 
			
		||||
    }
 | 
			
		||||
    this.printPlayersHand();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  printPlayersHand() {
 | 
			
		||||
    for (let player of this.players) {
 | 
			
		||||
      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;
 | 
			
		||||
      // await this.notificationManager.notifyGameState(this);
 | 
			
		||||
      // await this.notificationManager.notifyPlayersState(this.players);
 | 
			
		||||
      this.deal();
 | 
			
		||||
      const extractStates =  (p: PlayerInterface) => {
 | 
			
		||||
        return p.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();
 | 
			
		||||
      // await this.notificationManager.notifyGameState(this);
 | 
			
		||||
      // await this.notificationManager.notifyPlayersState(this.players);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.logger.error(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 !== null) {
 | 
			
		||||
        winner.teamedWith.score = totalPips;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async tilesSelection() {
 | 
			
		||||
  private 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);
 | 
			
		||||
        // await this.notificationService.notifyGameState(this);
 | 
			
		||||
        // await this.notificationService.notifyPlayersState(this.players);
 | 
			
		||||
        if (this.board.boneyard.length === 0) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
@@ -240,6 +297,7 @@ export class DominoesGame {
 | 
			
		||||
      currentPlayer: currentPlayer.getState(),
 | 
			
		||||
      board: this.board.tiles.map(tile => ({
 | 
			
		||||
        id: tile.id,
 | 
			
		||||
        playerId: tile.playerId,
 | 
			
		||||
        pips: tile.pips
 | 
			
		||||
      })),
 | 
			
		||||
      boardFreeEnds: this.board.getFreeEnds(),
 | 
			
		||||
 
 | 
			
		||||
@@ -2,24 +2,30 @@ 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 } from "../common/utilities";
 | 
			
		||||
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 playerNotificationManager = new PlayerNotificationService();
 | 
			
		||||
  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';
 | 
			
		||||
@@ -35,7 +41,7 @@ export class MatchSession {
 | 
			
		||||
    this.seed = seed || getRandomSeed();
 | 
			
		||||
    this.id = uuid();
 | 
			
		||||
    this.name = name || `Game ${this.id}`;
 | 
			
		||||
    this.addPlayer(creator);    
 | 
			
		||||
    this.addPlayerToSession(creator);    
 | 
			
		||||
    this.creator = creator;
 | 
			
		||||
 | 
			
		||||
    this.logger.info(`Match session created by: ${creator.name}`);
 | 
			
		||||
@@ -43,7 +49,8 @@ export class MatchSession {
 | 
			
		||||
    this.logger.info(`Match session name: ${this.name}`);
 | 
			
		||||
    this.logger.info(`Points to win: ${this.pointsToWin}`);
 | 
			
		||||
    this.sessionInProgress = true;
 | 
			
		||||
    this.matchInProgress = false;
 | 
			
		||||
    this.waitingForPlayers = true;
 | 
			
		||||
    this.logger.info('Waiting for players to be ready');
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  get numPlayers() {
 | 
			
		||||
@@ -58,56 +65,137 @@ export class MatchSession {
 | 
			
		||||
    return this.players.filter(player => player instanceof PlayerHuman).length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async startMatch(seed: string) {
 | 
			
		||||
    this.rng = seedrandom(seed);
 | 
			
		||||
    const missingPlayers = this.maxPlayers - this.numPlayers;
 | 
			
		||||
    for (let i = 0; i < missingPlayers; i++) {
 | 
			
		||||
      this.addPlayer(this.createPlayerAI(i));
 | 
			
		||||
  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);
 | 
			
		||||
    }
 | 
			
		||||
    this.state = 'ready'
 | 
			
		||||
    this.resetScoreboard()
 | 
			
		||||
    let gameNumber: number = 0;
 | 
			
		||||
    this.matchInProgress = true
 | 
			
		||||
    this.playerNotificationManager.notifyMatchState(this);
 | 
			
		||||
    let winnerIndex: number | null = null;
 | 
			
		||||
    while (this.matchInProgress) {
 | 
			
		||||
      this.currentGame = new DominoesGame(this.players, this.rng);
 | 
			
		||||
      if (winnerIndex !== null) {
 | 
			
		||||
        this.currentGame.setForcedInitialPlayerIndex(winnerIndex);
 | 
			
		||||
      }
 | 
			
		||||
      gameNumber += 1;
 | 
			
		||||
      this.state = 'started'
 | 
			
		||||
      this.logger.info(`Game #${gameNumber} started`);
 | 
			
		||||
      // this.game.reset()
 | 
			
		||||
      const gameSummary = await this.currentGame.start();
 | 
			
		||||
      winnerIndex = this.players.findIndex(player => player.id === gameSummary.winner?.id);
 | 
			
		||||
      this.setScores();
 | 
			
		||||
      this.checkMatchWinner();
 | 
			
		||||
      this.resetPlayers();
 | 
			
		||||
      this.state = 'waiting'
 | 
			
		||||
      await this.playerNotificationManager.notifyMatchState(this);
 | 
			
		||||
      this.playerNotificationManager.sendEventToPlayers('game-finished', this.players);
 | 
			
		||||
      if (this.matchInProgress) {
 | 
			
		||||
        await this.checkHumanPlayersReady();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.state = 'end'
 | 
			
		||||
    // await this.game.start();
 | 
			
		||||
    return this.endGame();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
  async checkAllClientsReadyBeforeStart() {
 | 
			
		||||
    try {
 | 
			
		||||
      if (this.currentGame) {
 | 
			
		||||
        const conditionFn = () => {
 | 
			
		||||
          this.logger.trace(`Clients ready: ${this.clientsReady.length}/${this.numHumanPlayers}`);
 | 
			
		||||
          return this.clientsReady.length === this.numHumanPlayers   
 | 
			
		||||
        }
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    });
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -134,17 +222,19 @@ export class MatchSession {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setScores() {
 | 
			
		||||
    const totalPips = this.currentGame?.players.reduce((acc, player) => acc + player.pipsCount(), 0) || 0;
 | 
			
		||||
    if (this.currentGame && this.currentGame.winner !== null) {
 | 
			
		||||
      const winner = this.currentGame.winner;
 | 
			
		||||
      const currentPips = this.scoreboard.get(winner.name) || 0;
 | 
			
		||||
      this.logger.debug (`${winner.name} has ${currentPips} points`);
 | 
			
		||||
      this.scoreboard.set(winner.name, currentPips + totalPips);
 | 
			
		||||
      if (winner.teamedWith !== null) {
 | 
			
		||||
        this.scoreboard.set(winner.teamedWith.name, currentPips + totalPips);
 | 
			
		||||
      }
 | 
			
		||||
  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 {
 | 
			
		||||
@@ -155,13 +245,6 @@ export class MatchSession {
 | 
			
		||||
      this.getScore(this.currentGame);
 | 
			
		||||
      this.logger.info('Game ended');
 | 
			
		||||
      this.currentGame = null;
 | 
			
		||||
      this.playerNotificationManager.notifyMatchState(this);
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        gameBlocked,
 | 
			
		||||
        gameTied,
 | 
			
		||||
        winner
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -186,46 +269,23 @@ export class MatchSession {
 | 
			
		||||
 | 
			
		||||
  createPlayerAI(i: number) {
 | 
			
		||||
    const AInames = ["Alice (AI)", "Bob (AI)", "Charlie (AI)", "David (AI)"];
 | 
			
		||||
    const player = new PlayerAI(AInames[i], this.rng);
 | 
			
		||||
    const player = new PlayerAI(AInames[i], this.rng, this.id);
 | 
			
		||||
    player.ready = true;
 | 
			
		||||
    return player;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async start() {
 | 
			
		||||
    if (this.matchInProgress) {
 | 
			
		||||
      throw new Error("Game already in progress");
 | 
			
		||||
    }
 | 
			
		||||
    this.waitingForPlayers = true;
 | 
			
		||||
    this.logger.info('Waiting for players to be ready');
 | 
			
		||||
    while (this.numPlayers < this.maxPlayers) {
 | 
			
		||||
      this.waitingSeconds += 1;
 | 
			
		||||
      this.logger.info(`Waiting for players to join: ${this.waitingSeconds}`);
 | 
			
		||||
      await wait(1000);
 | 
			
		||||
    }
 | 
			
		||||
    this.waitingForPlayers = false;
 | 
			
		||||
    this.logger.info('All players joined');
 | 
			
		||||
    await this.startMatch(this.seed);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  addPlayer(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!`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setPlayerReady(userId: string) {
 | 
			
		||||
    this.logger.debug(userId)
 | 
			
		||||
    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.playerNotificationManager.notifyMatchState(this);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
    this.notificationService.notifyMatchState(this);
 | 
			
		||||
    if (this.matchInProgress && this.numPlayersReady === this.numHumanPlayers) {
 | 
			
		||||
      this.startGame();
 | 
			
		||||
    }
 | 
			
		||||
  }  
 | 
			
		||||
 | 
			
		||||
  toString() {
 | 
			
		||||
    return `GameSession:(${this.id} ${this.name})`;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
 | 
			
		||||
import { LoggingService } from "../common/LoggingService";
 | 
			
		||||
import { ServerEvents } from "./constants";
 | 
			
		||||
 | 
			
		||||
export class NetworkClientNotifier {
 | 
			
		||||
  static instance: NetworkClientNotifier;
 | 
			
		||||
@@ -17,33 +18,20 @@ export class NetworkClientNotifier {
 | 
			
		||||
    this.io = io;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async notifyPlayer(player: NetworkPlayer, event: string, data: any = {}, timeoutSecs: number = 900): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await this.io.to(player.socketId)
 | 
			
		||||
        .timeout(timeoutSecs * 1000)
 | 
			
		||||
        .emitWithAck(event, data);
 | 
			
		||||
      return response[0]
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.logger.error(error);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async sendEvent(player: NetworkPlayer, event: string, data?: any) {
 | 
			
		||||
    const eventData = { event, data };
 | 
			
		||||
    this.io.to(player.socketId).emit('game-event', eventData);
 | 
			
		||||
    this.io.to(player.socketId).emit(ServerEvents.SERVER_EVENT, eventData);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async sendEventWithAck(player: NetworkPlayer, event: string, data: any, timeoutSecs: number = 900) {
 | 
			
		||||
    const eventData = { event, data };
 | 
			
		||||
    const response = await this.io.to(player.socketId)
 | 
			
		||||
    .timeout(timeoutSecs * 1000).emitWithAck('game-event-ack', eventData);
 | 
			
		||||
    return response[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async broadcast(event: string, data: any) {
 | 
			
		||||
    const responses = await this.io.emit(event, data);
 | 
			
		||||
    this.logger.debug('responses :>> ', responses);
 | 
			
		||||
    return true;
 | 
			
		||||
    try {
 | 
			
		||||
      const eventData = { event, data };
 | 
			
		||||
      const response = await this.io.to(player.socketId)
 | 
			
		||||
      .timeout(timeoutSecs * 1000).emitWithAck(ServerEvents.SERVER_EVENT_WITH_ACK, eventData);
 | 
			
		||||
      return response[0];
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.logger.error(error, 'sendEventWithAck error');
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										120
									
								
								src/game/PlayerInteractionAI.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/game/PlayerInteractionAI.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
			
		||||
import { LoggingService } from "../common/LoggingService";
 | 
			
		||||
import { printLine, wait } from "../common/utilities";
 | 
			
		||||
import { InteractionService } from "../server/services/InteractionService";
 | 
			
		||||
import { PlayerMoveSide } from "./constants";
 | 
			
		||||
import { Board } from "./entities/Board";
 | 
			
		||||
import { PlayerAI } from "./entities/player/PlayerAI";
 | 
			
		||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
 | 
			
		||||
import { PlayerMove } from "./entities/PlayerMove";
 | 
			
		||||
import { Tile } from "./entities/Tile";
 | 
			
		||||
import { PlayerInteractionInterface } from "./PlayerInteractionInterface";
 | 
			
		||||
 | 
			
		||||
export class PlayerInteractionAI implements PlayerInteractionInterface {
 | 
			
		||||
  player: PlayerInterface;
 | 
			
		||||
  interactionService: InteractionService = new InteractionService();
 | 
			
		||||
  logger: LoggingService = new LoggingService();
 | 
			
		||||
 | 
			
		||||
  constructor(player: PlayerInterface) {
 | 
			
		||||
      this.player = player;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  askForMove(board: Board): void {
 | 
			
		||||
    this.logger.trace('Asking for move (AI)');
 | 
			
		||||
    let move: PlayerMove | null = null;
 | 
			
		||||
    if (board.tiles.length === 0) {
 | 
			
		||||
      printLine('playing the first tile');
 | 
			
		||||
      const highestPair = this.getHighestPair();
 | 
			
		||||
      if (highestPair !== null) {
 | 
			
		||||
        move = new PlayerMove(highestPair, PlayerMoveSide.BOTH, this.player.id);
 | 
			
		||||
      }
 | 
			
		||||
      const maxTile = this.getMaxTile();
 | 
			
		||||
      if (maxTile !== null) {
 | 
			
		||||
        move = new PlayerMove(maxTile, PlayerMoveSide.BOTH, this.player.id);        
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      move = this.chooseTileGreed(board);
 | 
			
		||||
    }
 | 
			
		||||
    const rndWait = Math.floor(Math.random() * 1500) + 2000;
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      this.interactionService.playerMove({
 | 
			
		||||
        sessionId: (<PlayerAI>this.player).sessionId,
 | 
			
		||||
        move
 | 
			
		||||
      });
 | 
			
		||||
      this.logger.trace('Move sent to server (AI');
 | 
			
		||||
    }, rndWait);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  async makeMove(board: Board): Promise<PlayerMove | null> {
 | 
			
		||||
    const rndWait = Math.floor(Math.random() * 1000) + 1000;
 | 
			
		||||
    await wait(rndWait); // Simulate thinking time
 | 
			
		||||
    if (board.tiles.length === 0) {
 | 
			
		||||
      printLine('playing the first tile');
 | 
			
		||||
      const highestPair = this.getHighestPair();
 | 
			
		||||
      if (highestPair !== null) {
 | 
			
		||||
        return new PlayerMove(highestPair, PlayerMoveSide.BOTH, this.player.id);
 | 
			
		||||
      }
 | 
			
		||||
      const maxTile = this.getMaxTile();
 | 
			
		||||
      if (maxTile !== null) {
 | 
			
		||||
        return new PlayerMove(maxTile, PlayerMoveSide.BOTH, this.player.id);        
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Analyze the game state
 | 
			
		||||
    // Return the best move based on strategy
 | 
			
		||||
    return this.chooseTileGreed(board);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  async chooseTile(board: Board): Promise<Tile> {
 | 
			
		||||
    const randomWait = Math.floor((Math.random() * 1000) + 500);
 | 
			
		||||
    await wait(randomWait); // Simulate thinking time
 | 
			
		||||
    const randomIndex = Math.floor((<PlayerAI>this.player).rng() * board.boneyard.length);
 | 
			
		||||
    const tile = board.boneyard.splice(randomIndex, 1)[0];
 | 
			
		||||
    this.player.hand.push(tile);
 | 
			
		||||
    printLine(`${this.player.name} has chosen a tile`);
 | 
			
		||||
    return tile;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getHighestPair(): Tile | null {
 | 
			
		||||
    if (this.player.hand.length === 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let highestPair: Tile | null = null;
 | 
			
		||||
    const pairs = this.player.hand.filter(tile => tile.pips[0] === tile.pips[1]);
 | 
			
		||||
    pairs.forEach(tile => {
 | 
			
		||||
      if (tile.count > (highestPair?.count ?? 0)) {
 | 
			
		||||
        highestPair = tile;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return highestPair;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private chooseTileGreed(board: Board): PlayerMove | null { // greed algorithm
 | 
			
		||||
    let bestMove: PlayerMove |null = null;
 | 
			
		||||
    let bestTileScore: number = -1;
 | 
			
		||||
    const validMoves: PlayerMove[] = board.getValidMoves(this.player);
 | 
			
		||||
 | 
			
		||||
    validMoves.forEach(move => {
 | 
			
		||||
      const { tile } = move;
 | 
			
		||||
      const tileScore = tile.pips[0] + tile.pips[1];
 | 
			
		||||
      if (tileScore > bestTileScore) {
 | 
			
		||||
        bestMove = move;
 | 
			
		||||
        bestTileScore = tileScore;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return bestMove;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMaxTile(): Tile | null {
 | 
			
		||||
    if (this.player.hand.length === 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let maxTile: Tile | null = null;
 | 
			
		||||
    this.player.hand.forEach(tile => {
 | 
			
		||||
      if (tile.count > (maxTile?.count ?? 0)) {
 | 
			
		||||
        maxTile = tile;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return maxTile;
 | 
			
		||||
  } 
 | 
			
		||||
}
 | 
			
		||||
@@ -5,14 +5,20 @@ import { PlayerMove } from "./entities/PlayerMove";
 | 
			
		||||
import { Tile } from "./entities/Tile";
 | 
			
		||||
import { PlayerMoveSide, PlayerMoveSideType } from "./constants";
 | 
			
		||||
import { PlayerInteractionInterface } from "./PlayerInteractionInterface";
 | 
			
		||||
import { InteractionService } from "../server/services/InteractionService";
 | 
			
		||||
 | 
			
		||||
export class PlayerInteractionConsole implements PlayerInteractionInterface {
 | 
			
		||||
  player: PlayerInterface;
 | 
			
		||||
  interactionService: InteractionService = new InteractionService();
 | 
			
		||||
  
 | 
			
		||||
  constructor(player: PlayerInterface) {
 | 
			
		||||
    this.player = player;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  askForMove(board: Board): void {
 | 
			
		||||
    wait(100)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async makeMove(board: Board): Promise<PlayerMove | null> {
 | 
			
		||||
    let move: PlayerMove | null = null;
 | 
			
		||||
    let tile: Tile;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { InteractionService } from "../server/services/InteractionService";
 | 
			
		||||
import { Board } from "./entities/Board";
 | 
			
		||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
 | 
			
		||||
import { PlayerMove } from "./entities/PlayerMove";
 | 
			
		||||
@@ -5,7 +6,9 @@ import { Tile } from "./entities/Tile";
 | 
			
		||||
 | 
			
		||||
export interface PlayerInteractionInterface {
 | 
			
		||||
    player: PlayerInterface;
 | 
			
		||||
    interactionService: InteractionService;
 | 
			
		||||
 | 
			
		||||
    askForMove(board: Board): void;
 | 
			
		||||
    makeMove(board: Board): Promise<PlayerMove | null>;
 | 
			
		||||
    chooseTile(board: Board): Promise<Tile>
 | 
			
		||||
}
 | 
			
		||||
@@ -7,20 +7,30 @@ import { NetworkClientNotifier } from './NetworkClientNotifier';
 | 
			
		||||
import { NetworkPlayer } from './entities/player/NetworkPlayer';
 | 
			
		||||
import { PlayerMoveSide, PlayerMoveSideType } from './constants';
 | 
			
		||||
import { SocketDisconnectedError } from '../common/errors/SocketDisconnectedError';
 | 
			
		||||
import { InteractionService } from '../server/services/InteractionService';
 | 
			
		||||
 | 
			
		||||
export class PlayerInteractionNetwork implements PlayerInteractionInterface {
 | 
			
		||||
    player: PlayerInterface;
 | 
			
		||||
    interactionService: InteractionService = new InteractionService();
 | 
			
		||||
    clientNotifier = new NetworkClientNotifier();
 | 
			
		||||
 | 
			
		||||
    constructor(player: PlayerInterface) {
 | 
			
		||||
        this.player = player;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    askForMove(board: Board): void {
 | 
			
		||||
      this.clientNotifier.sendEvent(this.player as NetworkPlayer, 'server:player-turn', {
 | 
			
		||||
        freeHands: board.getFreeEnds(),
 | 
			
		||||
        isFirstMove: board.tiles.length === 0
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    async makeMove(board: Board): Promise<PlayerMove | null> {
 | 
			
		||||
      let response = undefined;
 | 
			
		||||
      try {
 | 
			
		||||
        response = await this.clientNotifier.sendEventWithAck(this.player as NetworkPlayer, 'ask-client-for-move', {
 | 
			
		||||
          freeHands: board.getFreeEnds(),
 | 
			
		||||
          isFirstMove: board.tiles.length === 0
 | 
			
		||||
        });
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        throw new SocketDisconnectedError();
 | 
			
		||||
@@ -39,7 +49,7 @@ export class PlayerInteractionNetwork implements PlayerInteractionInterface {
 | 
			
		||||
 | 
			
		||||
    async chooseTile(board: Board): Promise<Tile> {
 | 
			
		||||
      const { player: { hand} } = this;
 | 
			
		||||
      const response: any = await this.clientNotifier.notifyPlayer(this.player as NetworkPlayer, 'chooseTile');
 | 
			
		||||
      const response: any = await this.clientNotifier.sendEventWithAck(this.player as NetworkPlayer, 'ask-client-for-tile', { boneyard: board.boneyard })
 | 
			
		||||
      const index: number = board.boneyard.findIndex(t => t.id === response.tileId);  
 | 
			
		||||
      const tile = board.boneyard.splice(index, 1)[0];
 | 
			
		||||
      tile.revealed = true;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,3 +13,20 @@ export const JointValue: { [key: string]: JointValueType } = {
 | 
			
		||||
  RIGHT: 1,
 | 
			
		||||
  NONE: 2
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ClientEvents = {
 | 
			
		||||
  CLIENT_EVENT: 'client:event',
 | 
			
		||||
  CLIENT_EVENT_WITH_ACK: 'client:event-with-ack'
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ServerEvents = {
 | 
			
		||||
  SERVER_EVENT: 'server:game-event',
 | 
			
		||||
  SERVER_EVENT_WITH_ACK: 'server:game-event-ack'  
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const EventActions = {
 | 
			
		||||
  START_SESSION: 'client:start-session',
 | 
			
		||||
  PLAYER_READY: 'client:set-player-ready',
 | 
			
		||||
  TILE_ANIMATION_ENDED: 'client:tile-animation-ended'
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import { PlayerInterface } from "../entities/player/PlayerInterface";
 | 
			
		||||
import { PlayerDto } from "./PlayerDto";
 | 
			
		||||
 | 
			
		||||
export interface GameSummary {
 | 
			
		||||
  gameId: string;
 | 
			
		||||
  isBlocked: boolean;
 | 
			
		||||
  isTied: boolean;
 | 
			
		||||
  winner: PlayerInterface | null;
 | 
			
		||||
  winner: PlayerDto;
 | 
			
		||||
  score: { name: string; score: number; }[]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								src/game/dto/MatchSummary.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/game/dto/MatchSummary.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
import { GameSummary } from "./GameSummary";
 | 
			
		||||
 | 
			
		||||
export interface MatchSummary {
 | 
			
		||||
  lastGameSummary: GameSummary;
 | 
			
		||||
  scoreboard: { player: string; score: number; }[];
 | 
			
		||||
}
 | 
			
		||||
@@ -3,10 +3,12 @@ import { PlayerMoveSideType, PlayerMoveSide, JointValue } from "../constants";
 | 
			
		||||
import { PlayerInterface } from "./player/PlayerInterface";
 | 
			
		||||
import { PlayerMove } from "./PlayerMove";
 | 
			
		||||
import { Tile } from "./Tile";
 | 
			
		||||
import { LoggingService } from "../../common/LoggingService";
 | 
			
		||||
 | 
			
		||||
export class Board {
 | 
			
		||||
  tiles: Tile[] = [];
 | 
			
		||||
  boneyard: Tile[] = [];
 | 
			
		||||
  logger = new LoggingService();
 | 
			
		||||
 | 
			
		||||
  constructor(private rng: PRNG) {}
 | 
			
		||||
 | 
			
		||||
@@ -52,6 +54,8 @@ export class Board {
 | 
			
		||||
 | 
			
		||||
  play(playerMove: PlayerMove): void {
 | 
			
		||||
    const { type, tile } = playerMove;
 | 
			
		||||
    
 | 
			
		||||
    const boneTile = this.boneyard.find(t => t.id === tile.id);
 | 
			
		||||
    tile.revealed = true;
 | 
			
		||||
    if (type === PlayerMoveSide.LEFT) {
 | 
			
		||||
      this.playTileLeft(tile);
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ export class Tile {
 | 
			
		||||
  pips: [number, number];
 | 
			
		||||
  revealed: boolean = true;
 | 
			
		||||
  flipped: boolean = false;
 | 
			
		||||
  playerId?: string;
 | 
			
		||||
 | 
			
		||||
  constructor(pips: [number, number]) {
 | 
			
		||||
    this.id = uuid();
 | 
			
		||||
 
 | 
			
		||||
@@ -19,11 +19,19 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
 | 
			
		||||
 | 
			
		||||
  constructor(public name: string) {
 | 
			
		||||
    super();
 | 
			
		||||
  }  
 | 
			
		||||
 | 
			
		||||
  askForMove(board: Board): void {
 | 
			
		||||
    this.playerInteraction.askForMove(board);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  abstract makeMove(board: Board): Promise<PlayerMove | null>;
 | 
			
		||||
  abstract chooseTile(board: Board): Promise<Tile>;
 | 
			
		||||
  
 | 
			
		||||
  async makeMove(board: Board): Promise<PlayerMove | null> {    
 | 
			
		||||
    return await this.playerInteraction.makeMove(board);   
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async chooseTile(board: Board): Promise<Tile> {
 | 
			
		||||
    return this.playerInteraction.chooseTile(board);
 | 
			
		||||
  }  
 | 
			
		||||
 | 
			
		||||
  async sendEventWithAck(event: string, data: any): Promise<void> {    
 | 
			
		||||
  }
 | 
			
		||||
@@ -41,20 +49,7 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
 | 
			
		||||
    return this.hand.reduce((acc, tile) => acc + tile.pips[0] + tile.pips[1], 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getHighestPair(): Tile | null {
 | 
			
		||||
    if (this.hand.length === 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let highestPair: Tile | null = null;
 | 
			
		||||
    const pairs = this.hand.filter(tile => tile.pips[0] === tile.pips[1]);
 | 
			
		||||
    pairs.forEach(tile => {
 | 
			
		||||
      if (tile.count > (highestPair?.count ?? 0)) {
 | 
			
		||||
        highestPair = tile;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return highestPair;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  getState(showPips: boolean = false): PlayerDto {
 | 
			
		||||
    return {
 | 
			
		||||
@@ -66,6 +61,7 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
 | 
			
		||||
          id: tile.id,
 | 
			
		||||
          pips: tile.pips,
 | 
			
		||||
          flipped: tile.revealed,
 | 
			
		||||
          playerId: tile.playerId,
 | 
			
		||||
        };
 | 
			
		||||
        if (showPips) {
 | 
			
		||||
          d.pips = tile.pips;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,71 +6,17 @@ import { PlayerMove } from "../PlayerMove";
 | 
			
		||||
import { SimulatedBoard } from "../../SimulatedBoard";
 | 
			
		||||
import { Tile } from "../Tile";
 | 
			
		||||
import { PRNG } from "seedrandom";
 | 
			
		||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
 | 
			
		||||
import { PlayerInteractionAI } from "../../PlayerInteractionAI";
 | 
			
		||||
import { InteractionService } from "../../../server/services/InteractionService";
 | 
			
		||||
 | 
			
		||||
export class PlayerAI extends AbstractPlayer {
 | 
			
		||||
  constructor(name: string, private rng: PRNG) {
 | 
			
		||||
  playerInteraction: PlayerInteractionInterface = new PlayerInteractionAI(this);
 | 
			
		||||
 | 
			
		||||
  constructor(name: string, public rng: PRNG, public sessionId: string) {
 | 
			
		||||
    super(name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async makeMove(board: Board): Promise<PlayerMove | null> {
 | 
			
		||||
    const rndWait = Math.floor(Math.random() * 1000) + 1000;
 | 
			
		||||
    await wait(rndWait); // Simulate thinking time
 | 
			
		||||
    if (board.tiles.length === 0) {
 | 
			
		||||
      printLine('playing the first tile');
 | 
			
		||||
      const highestPair = this.getHighestPair();
 | 
			
		||||
      if (highestPair !== null) {
 | 
			
		||||
        return new PlayerMove(highestPair, PlayerMoveSide.BOTH, this.id);
 | 
			
		||||
      }
 | 
			
		||||
      const maxTile = this.getMaxTile();
 | 
			
		||||
      if (maxTile !== null) {
 | 
			
		||||
        return new PlayerMove(maxTile, PlayerMoveSide.BOTH, this.id);        
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Analyze the game state
 | 
			
		||||
    // Return the best move based on strategy
 | 
			
		||||
    return this.chooseTileGreed(board);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async chooseTile(board: Board): Promise<Tile> {
 | 
			
		||||
    const randomWait = Math.floor((Math.random() * 1000) + 500);
 | 
			
		||||
    await wait(randomWait); // Simulate thinking time
 | 
			
		||||
    const randomIndex = Math.floor(this.rng() * board.boneyard.length);
 | 
			
		||||
    const tile = board.boneyard.splice(randomIndex, 1)[0];
 | 
			
		||||
    this.hand.push(tile);
 | 
			
		||||
    printLine(`${this.name} has chosen a tile`);
 | 
			
		||||
    return tile;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMaxTile(): Tile | null {
 | 
			
		||||
    if (this.hand.length === 0) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let maxTile: Tile | null = null;
 | 
			
		||||
    this.hand.forEach(tile => {
 | 
			
		||||
      if (tile.count > (maxTile?.count ?? 0)) {
 | 
			
		||||
        maxTile = tile;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return maxTile;
 | 
			
		||||
  } 
 | 
			
		||||
 | 
			
		||||
  chooseTileGreed(board: Board): PlayerMove | null { // greed algorithm
 | 
			
		||||
    let bestMove: PlayerMove |null = null;
 | 
			
		||||
    let bestTileScore: number = -1;
 | 
			
		||||
    const validMoves: PlayerMove[] = board.getValidMoves(this);
 | 
			
		||||
 | 
			
		||||
    validMoves.forEach(move => {
 | 
			
		||||
      const { tile } = move;
 | 
			
		||||
      const tileScore = tile.pips[0] + tile.pips[1];
 | 
			
		||||
      if (tileScore > bestTileScore) {
 | 
			
		||||
        bestMove = move;
 | 
			
		||||
        bestTileScore = tileScore;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return bestMove;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  chooseTileRandom(board: Board): Tile | null { // random algorithm
 | 
			
		||||
    const validTiles: Tile[] = this.hand.filter(tile => board.isValidMove(tile, null, this));
 | 
			
		||||
    return validTiles[Math.floor(this.rng() * validTiles.length)];
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,4 @@ export class PlayerHuman extends AbstractPlayer {
 | 
			
		||||
    super(name);
 | 
			
		||||
    this.id = id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async makeMove(board: Board): Promise<PlayerMove | null> {    
 | 
			
		||||
    return await this.playerInteraction.makeMove(board);   
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async chooseTile(board: Board): Promise<Tile> {
 | 
			
		||||
    return this.playerInteraction.chooseTile(board);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,8 +14,9 @@ export interface PlayerInterface {
 | 
			
		||||
  teamedWith: PlayerInterface | null;
 | 
			
		||||
  playerInteraction: PlayerInteractionInterface;
 | 
			
		||||
  ready: boolean;
 | 
			
		||||
 | 
			
		||||
  makeMove(gameState: Board): Promise<PlayerMove | null>;
 | 
			
		||||
  
 | 
			
		||||
  askForMove(board: Board): void;
 | 
			
		||||
  makeMove(board: Board): Promise<PlayerMove | null>;
 | 
			
		||||
  chooseTile(board: Board): Promise<Tile>;
 | 
			
		||||
  pipsCount(): number;
 | 
			
		||||
  reset(): void;
 | 
			
		||||
 
 | 
			
		||||
@@ -124,14 +124,14 @@ export class AuthController extends BaseController {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const tokenFromDb = await new ApiTokenMongoManager().getById(token._id.toString());
 | 
			
		||||
    const tokenFromDb: Token = await new ApiTokenMongoManager().getById(token._id.toString()) as Token;
 | 
			
		||||
 | 
			
		||||
    if (!tokenFromDb) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { roles } = tokenFromDb;
 | 
			
		||||
    const validRoles = rolesToCheck.filter((r: string) => roles.includes(r));
 | 
			
		||||
    const validRoles = rolesToCheck.filter((r: string) => roles?.includes(r) || false);
 | 
			
		||||
    return validRoles.length === rolesToCheck.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,22 +16,42 @@ export class GameController extends BaseController {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public joinMatch(req: Request, res: Response) {
 | 
			
		||||
  public async joinMatch(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const { user, body } = req;
 | 
			
		||||
      const { sessionId } = body;
 | 
			
		||||
      this.sessionService.joinSession(user, sessionId);
 | 
			
		||||
      const { user, params } = req;
 | 
			
		||||
      const { sessionId } = params;
 | 
			
		||||
      await this.sessionService.joinSession(user, sessionId);
 | 
			
		||||
      res.status(200).json({ status: 'ok' });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.handleError(res, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public listMatches(req: Request, res: Response) {
 | 
			
		||||
  public async listMatches(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      this.sessionService.listSessions().then((sessions) => {
 | 
			
		||||
        res.status(200).json(sessions);
 | 
			
		||||
      });
 | 
			
		||||
      const sessions = await this.sessionService.listJoinableSessions()
 | 
			
		||||
      res.status(200).json(sessions);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.handleError(res, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getMatch(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const { sessionId } = req.params;
 | 
			
		||||
      const session = this.sessionService.getSession(sessionId)
 | 
			
		||||
      res.status(200).json(session);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.handleError(res, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async deleteMatch(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const { sessionId } = req.params;
 | 
			
		||||
      await this.sessionService.deleteSession(sessionId);
 | 
			
		||||
      this.logger.info(`Session ${sessionId} deleted`);
 | 
			
		||||
      res.status(200).json({ status: 'ok' });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.handleError(res, error);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,10 @@ export interface User extends EntityMongo {
 | 
			
		||||
  namespace?: Namespace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DbMatchSessionUpdate extends EntityMongo {
 | 
			
		||||
  state?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DbMatchSession extends EntityMongo {
 | 
			
		||||
  id: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { ObjectId } from "mongodb";
 | 
			
		||||
import { mongoExecute } from "./mongoDBPool";
 | 
			
		||||
import { Entity } from "../../interfaces";
 | 
			
		||||
import { Entity, EntityMongo } from "../../interfaces";
 | 
			
		||||
import { LoggingService } from "../../../../common/LoggingService";
 | 
			
		||||
import toObjectId from "./mongoUtils";
 | 
			
		||||
 | 
			
		||||
@@ -9,7 +9,8 @@ export abstract class BaseMongoManager {
 | 
			
		||||
  protected abstract collection?: string;
 | 
			
		||||
  logger = new LoggingService().logger;
 | 
			
		||||
 | 
			
		||||
  create(data: Entity): Promise<ObjectId | undefined>{
 | 
			
		||||
  async create(data: Entity): Promise<ObjectId | undefined> {
 | 
			
		||||
    this.stampEntity(data);
 | 
			
		||||
    return mongoExecute(
 | 
			
		||||
      async ({ collection }) => {
 | 
			
		||||
        const result = await collection?.insertOne(data as any);
 | 
			
		||||
@@ -19,25 +20,27 @@ export abstract class BaseMongoManager {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  delete(id: string) {
 | 
			
		||||
  async delete(id: string): Promise<number> {
 | 
			
		||||
    return mongoExecute(
 | 
			
		||||
      async ({ collection }) => {
 | 
			
		||||
        await collection?.deleteOne({ _id: this.toObjectId(id) });
 | 
			
		||||
        const result = await collection?.deleteOne({ _id: this.toObjectId(id) });
 | 
			
		||||
        return result?.deletedCount || 0;
 | 
			
		||||
      },
 | 
			
		||||
      { colName: this.collection }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deleteByFilter(filter: any) {
 | 
			
		||||
  async deleteByFilter(filter: any): Promise<number> {
 | 
			
		||||
    return mongoExecute(
 | 
			
		||||
      async ({ collection }) => {
 | 
			
		||||
        await collection?.deleteOne(filter);
 | 
			
		||||
        const result = await collection?.deleteOne(filter);
 | 
			
		||||
        return result?.deletedCount || 0;
 | 
			
		||||
      },
 | 
			
		||||
      { colName: this.collection }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getById(id: string) {
 | 
			
		||||
  async getById(id: string): Promise<EntityMongo | null> {
 | 
			
		||||
    return mongoExecute(
 | 
			
		||||
      async ({ collection }) => {
 | 
			
		||||
        return await collection?.findOne({ _id: this.toObjectId(id) });
 | 
			
		||||
@@ -46,7 +49,7 @@ export abstract class BaseMongoManager {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getByFilter(filter: any) {
 | 
			
		||||
  async getByFilter(filter: any): Promise<EntityMongo | null> {
 | 
			
		||||
    return mongoExecute(
 | 
			
		||||
      async ({ collection }) => {
 | 
			
		||||
        return await collection?.findOne(filter);
 | 
			
		||||
@@ -55,51 +58,71 @@ export abstract class BaseMongoManager {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  list() {
 | 
			
		||||
  async list(sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise<EntityMongo[]> {
 | 
			
		||||
    return mongoExecute(
 | 
			
		||||
      async ({ collection }) => {
 | 
			
		||||
        return await collection?.find().toArray();
 | 
			
		||||
        const cursor = collection?.find();
 | 
			
		||||
        if (sortCriteria) {
 | 
			
		||||
          cursor?.sort(sortCriteria);
 | 
			
		||||
        }
 | 
			
		||||
        if (pagination) {
 | 
			
		||||
          cursor?.skip(pagination.pageSize * (pagination.page - 1)).limit(pagination.pageSize);
 | 
			
		||||
        }
 | 
			
		||||
        return await cursor?.toArray();
 | 
			
		||||
      },
 | 
			
		||||
      { colName: this.collection }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listByFilter(filter: any) {
 | 
			
		||||
  async listByFilter(filter: any, sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise<EntityMongo[]> {
 | 
			
		||||
    return mongoExecute(
 | 
			
		||||
      async ({ collection }) => {
 | 
			
		||||
        return await collection?.find(filter).toArray();
 | 
			
		||||
        const cursor = collection?.find(filter);
 | 
			
		||||
        if (sortCriteria) {
 | 
			
		||||
          cursor?.sort(sortCriteria);
 | 
			
		||||
        }
 | 
			
		||||
        if (pagination) {
 | 
			
		||||
          cursor?.skip(pagination.pageSize * (pagination.page - 1)).limit(pagination.pageSize);
 | 
			
		||||
        }
 | 
			
		||||
        return await cursor?.toArray();
 | 
			
		||||
      },
 | 
			
		||||
      { colName: this.collection }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  update(object: Entity) {
 | 
			
		||||
  async update(object: EntityMongo): Promise<number> {
 | 
			
		||||
    const data: any  = { ...object };
 | 
			
		||||
    const id = data._id;
 | 
			
		||||
    delete data._id;    
 | 
			
		||||
    this.stampEntity(data, false);
 | 
			
		||||
    delete data._id;
 | 
			
		||||
    return mongoExecute(async ({ collection }) => {
 | 
			
		||||
      return await collection?.updateOne(
 | 
			
		||||
      const result = await collection?.updateOne(
 | 
			
		||||
        { _id: this.toObjectId(id) },
 | 
			
		||||
        { $set: data }
 | 
			
		||||
      );
 | 
			
		||||
      return result?.modifiedCount || 0;
 | 
			
		||||
    },
 | 
			
		||||
    { colName: this.collection });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateMany(filter: any, data: Entity) {
 | 
			
		||||
  async updateMany(filter: any, data: Entity): Promise<number>{
 | 
			
		||||
    
 | 
			
		||||
    this.stampEntity(data, false);
 | 
			
		||||
    return mongoExecute(async ({ collection }) => {
 | 
			
		||||
      return await collection?.updateMany(filter, { $set: data as any });
 | 
			
		||||
      const result = await collection?.updateMany(filter, { $set: data as any });
 | 
			
		||||
      return result?.modifiedCount || 0;
 | 
			
		||||
    },
 | 
			
		||||
    { colName: this.collection });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  replaceOne(filter: any, object: Entity) {
 | 
			
		||||
  async replaceOne(filter: any, object: Entity): Promise<number> {
 | 
			
		||||
    return mongoExecute(async ({collection}) => {
 | 
			
		||||
      return await collection?.replaceOne(filter, object);
 | 
			
		||||
      const result = await collection?.replaceOne(filter, object);
 | 
			
		||||
      return result?.modifiedCount || 0;
 | 
			
		||||
    }, {colName: this.collection});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  aggregation(pipeline: any) {
 | 
			
		||||
  async aggregation(pipeline: any): Promise<EntityMongo[]> {
 | 
			
		||||
    return mongoExecute(
 | 
			
		||||
      async ({ collection }) => {
 | 
			
		||||
        return await collection?.aggregate(pipeline).toArray();
 | 
			
		||||
@@ -108,7 +131,7 @@ export abstract class BaseMongoManager {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  aggregationOne(pipeline: any) {
 | 
			
		||||
  async aggregationOne(pipeline: any): Promise<EntityMongo | null> {
 | 
			
		||||
    return mongoExecute(
 | 
			
		||||
      async ({ collection }) => {
 | 
			
		||||
        return await collection?.aggregate(pipeline).next();
 | 
			
		||||
@@ -120,4 +143,11 @@ export abstract class BaseMongoManager {
 | 
			
		||||
  protected toObjectId = (oid: string) => {
 | 
			
		||||
    return toObjectId(oid);    
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  protected stampEntity(entity: Entity, isCreate: boolean = true) {
 | 
			
		||||
    if (isCreate) {
 | 
			
		||||
      entity.createdAt = Date.now();
 | 
			
		||||
    }
 | 
			
		||||
    entity.modifiedAt = Date.now();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -28,8 +28,8 @@ export class SessionManager extends ManagerBase {
 | 
			
		||||
    SessionManager.sessions.set(session.id, session);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deleteSession(session: MatchSession) {
 | 
			
		||||
    SessionManager.sessions.delete(session.id);
 | 
			
		||||
  deleteSession(sessionId: string) {
 | 
			
		||||
    SessionManager.sessions.delete(sessionId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getSession(id: string) {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,10 @@ export default function(): Router {
 | 
			
		||||
  const { authenticate } = AuthController
 | 
			
		||||
 | 
			
		||||
  router.post('/match', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.createMatch(req, res));
 | 
			
		||||
  router.patch('/match/join', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.joinMatch(req, res));
 | 
			
		||||
  router.get('/match/list', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.listMatches(req, res)); 
 | 
			
		||||
  router.get('/match', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.listMatches(req, res)); 
 | 
			
		||||
  router.get('/match/:sessionId', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.getMatch(req, res)); 
 | 
			
		||||
  router.put('/match/:sessionId', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.joinMatch(req, res));
 | 
			
		||||
  router.delete('/match/:sessionId', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.deleteMatch(req, res)); 
 | 
			
		||||
 | 
			
		||||
  return router;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										110
									
								
								src/server/services/InteractionService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/server/services/InteractionService.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
import PubSub from "pubsub-js";
 | 
			
		||||
import { EventActions } from "../../game/constants";
 | 
			
		||||
import { MatchSession } from "../../game/MatchSession";
 | 
			
		||||
import { SessionManager } from "../managers/SessionManager";
 | 
			
		||||
import { PlayerNotificationService } from "./PlayerNotificationService";
 | 
			
		||||
import { ServiceBase } from "./ServiceBase";
 | 
			
		||||
import { PlayerMove } from "../../game/entities/PlayerMove";
 | 
			
		||||
 | 
			
		||||
export class InteractionService extends ServiceBase{
 | 
			
		||||
  private sessionManager: SessionManager = new SessionManager();  
 | 
			
		||||
  private notifyService = new PlayerNotificationService();  
 | 
			
		||||
 | 
			
		||||
  async handleClientEventWithAck(data: any): Promise<any> {
 | 
			
		||||
    const { event, data: eventData } = data;
 | 
			
		||||
    this.logger.trace(`Handling event: ${event} with ack`);
 | 
			
		||||
    switch(event) {
 | 
			
		||||
      case EventActions.START_SESSION:
 | 
			
		||||
        return this.onStartSession(eventData);
 | 
			
		||||
      default:
 | 
			
		||||
        // PubSub.publish(event, eventData);
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClientEvent(data: any): any {
 | 
			
		||||
    const { event, data: eventData } = data;
 | 
			
		||||
    this.logger.trace(`Handling event: ${event}`);
 | 
			
		||||
    switch(event) {
 | 
			
		||||
      case 'client:player-move':
 | 
			
		||||
        this.onClientMoveResponse(eventData);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'client:set-client-ready':
 | 
			
		||||
        this.onClientReady(eventData);
 | 
			
		||||
        break;
 | 
			
		||||
      case EventActions.PLAYER_READY:
 | 
			
		||||
        this.onPlayerReady(eventData);
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        PubSub.publish(event, eventData);
 | 
			
		||||
        break;
 | 
			
		||||
    } 
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private onStartSession(data: any): any {
 | 
			
		||||
    const sessionId: string = data.sessionId;
 | 
			
		||||
    const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
 | 
			
		||||
 | 
			
		||||
    if (session === undefined) {
 | 
			
		||||
      return ({
 | 
			
		||||
        status: 'error',
 | 
			
		||||
        message: 'Session not found'
 | 
			
		||||
      });
 | 
			
		||||
    } else if (session.matchInProgress) {
 | 
			
		||||
      return {
 | 
			
		||||
        status: 'error',
 | 
			
		||||
        message: 'Game already in progress'
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      const missingHumans = session.maxPlayers - session.numPlayers;
 | 
			
		||||
      for (let i = 0; i < missingHumans; i++) {
 | 
			
		||||
        session.addPlayerToSession(session.createPlayerAI(i));
 | 
			
		||||
      }
 | 
			
		||||
      this.notifyService.sendEventToPlayers('server:match-starting', session.players);
 | 
			
		||||
      session.start();      
 | 
			
		||||
      return {
 | 
			
		||||
        status: 'ok'
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public playerMove(data: any) {
 | 
			
		||||
    this.onClientMoveResponse(data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private onClientMoveResponse(data: any): any {
 | 
			
		||||
    const { sessionId, move }: { sessionId: string, move: PlayerMove } = data;
 | 
			
		||||
    const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
 | 
			
		||||
    if (session !== undefined) {
 | 
			
		||||
      session.playerMove(move);
 | 
			
		||||
      return {
 | 
			
		||||
        status: 'ok'
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private  onPlayerReady(data: any): any {
 | 
			
		||||
    const { userId, sessionId } = data;
 | 
			
		||||
    const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
 | 
			
		||||
    if (session !== undefined) {
 | 
			
		||||
      session.setPlayerReady(userId)
 | 
			
		||||
      this.notifyService.notifyMatchState(session);
 | 
			
		||||
      this.notifyService.notifyPlayersState(session.players);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private onClientReady(data: any): any {
 | 
			
		||||
    const { sessionId, userId } = data;
 | 
			
		||||
    const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
 | 
			
		||||
    session?.setClientReady(userId);
 | 
			
		||||
    return {
 | 
			
		||||
      status: 'ok'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public updateSocketId(sessionId: string, userId: string, socketId: string): any {
 | 
			
		||||
    return this.sessionManager.updateSocketId(sessionId, userId, socketId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
}
 | 
			
		||||
@@ -1,35 +1,42 @@
 | 
			
		||||
import { DominoesGame } from "../../game/DominoesGame";
 | 
			
		||||
import { MatchSession } from "../../game/MatchSession";
 | 
			
		||||
import { NetworkClientNotifier } from "../../game/NetworkClientNotifier";
 | 
			
		||||
import { GameState } from "../../game/dto/GameState";
 | 
			
		||||
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
 | 
			
		||||
import { PlayerInterface } from "../../game/entities/player/PlayerInterface";
 | 
			
		||||
import { ServiceBase } from "./ServiceBase";
 | 
			
		||||
 | 
			
		||||
export class PlayerNotificationService {
 | 
			
		||||
export class PlayerNotificationService extends ServiceBase {
 | 
			
		||||
  clientNotifier: NetworkClientNotifier = new NetworkClientNotifier();
 | 
			
		||||
 | 
			
		||||
  async notifyGameState(game: DominoesGame) {
 | 
			
		||||
  notifyGameState(game: DominoesGame) {
 | 
			
		||||
    const gameState: GameState = game.getGameState();
 | 
			
		||||
    const { players } = game;
 | 
			
		||||
    let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-game-state', gameState));
 | 
			
		||||
    return await Promise.all(promises);
 | 
			
		||||
    players.map(player => player.sendEvent('update-game-state', gameState));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async notifyPlayersState(players: PlayerInterface[]) {
 | 
			
		||||
    let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-player-state', player.getState()));
 | 
			
		||||
    return await Promise.all(promises);
 | 
			
		||||
  notifyPlayersState(players: PlayerInterface[]) {
 | 
			
		||||
    players.map(player => player.sendEvent('update-player-state', player.getState()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  async notifyMatchState(session: MatchSession) {
 | 
			
		||||
  notifyMatchState(session: MatchSession) {
 | 
			
		||||
    const { players } = session;
 | 
			
		||||
    let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-match-session-state', session.getState()));
 | 
			
		||||
    return await Promise.all(promises);
 | 
			
		||||
    players.map(player => player.sendEvent('update-match-session-state', session.getState()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async sendEventToPlayers(event: string, players: PlayerInterface[], data: any = {}) {
 | 
			
		||||
    let promises: Promise<void>[] = players.map(player => player.sendEvent(event, data));
 | 
			
		||||
    return await Promise.all(promises);
 | 
			
		||||
  async sendEventToPlayers(event: string, players: PlayerInterface[], data: Function | any = {}) {
 | 
			
		||||
    players.forEach((player) => {
 | 
			
		||||
      let dataTosend = data;
 | 
			
		||||
      if (typeof data === 'function') {
 | 
			
		||||
        dataTosend = data(player);
 | 
			
		||||
      }
 | 
			
		||||
      this.clientNotifier.sendEvent(player as NetworkPlayer, event, dataTosend);      
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async sendEvent(event: string, player: PlayerInterface, data: any = {}) {  
 | 
			
		||||
    player.sendEvent(event, data)
 | 
			
		||||
  sendEvent(event: string, player: PlayerInterface, data: any = {}) {  
 | 
			
		||||
    this.logger.debug(`Sending event '${event}' to player ${player.id}`);
 | 
			
		||||
    this.clientNotifier.sendEvent(player as NetworkPlayer, event, data);  
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +1,16 @@
 | 
			
		||||
import { SessionCreationError } from "../../common/errors/SessionCreationError";
 | 
			
		||||
import { SessionNotFoundError } from "../../common/errors/SessionNotFoundError";
 | 
			
		||||
import { wait, whileNotUndefined } from "../../common/utilities";
 | 
			
		||||
import { whileNotUndefined } from "../../common/utilities";
 | 
			
		||||
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
 | 
			
		||||
import { MatchSession } from "../../game/MatchSession";
 | 
			
		||||
import { PlayerNotificationService } from "./PlayerNotificationService";
 | 
			
		||||
import { matchSessionAdapter } from "../db/DbAdapter";
 | 
			
		||||
import { DbMatchSession } from "../db/interfaces";
 | 
			
		||||
import { DbMatchSession, DbMatchSessionUpdate } from "../db/interfaces";
 | 
			
		||||
import { MatchSessionMongoManager } from "../db/mongo/MatchSessionMongoManager";
 | 
			
		||||
import { SessionManager } from "../managers/SessionManager";
 | 
			
		||||
import { ServiceBase } from "./ServiceBase";
 | 
			
		||||
import { SocketIoService } from "./SocketIoService";
 | 
			
		||||
import toObjectId from "../db/mongo/common/mongoUtils";
 | 
			
		||||
 | 
			
		||||
export class SessionService extends ServiceBase{
 | 
			
		||||
  private dbManager: MatchSessionMongoManager = new MatchSessionMongoManager();
 | 
			
		||||
@@ -37,68 +38,48 @@ export class SessionService extends ServiceBase{
 | 
			
		||||
    this.sessionManager.setSession(session);    
 | 
			
		||||
    this.notifyService.notifyMatchState(session);
 | 
			
		||||
    this.notifyService.notifyPlayersState(session.players);
 | 
			
		||||
    this.logger.debug(`Session ${session.id} created`);
 | 
			
		||||
    return session.id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async joinSession(user: any, sessionId: string): Promise<void> {
 | 
			
		||||
  public async joinSession(user: any, sessionId: string): Promise<string> {
 | 
			
		||||
    const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
 | 
			
		||||
    if (session === undefined) {
 | 
			
		||||
      throw new SessionNotFoundError();
 | 
			
		||||
    }    let socketClient;
 | 
			
		||||
    }    
 | 
			
		||||
    let socketClient;
 | 
			
		||||
    try {
 | 
			
		||||
      socketClient = await whileNotUndefined(() => SocketIoService.getClient(user._id));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw new SessionCreationError();
 | 
			
		||||
    }
 | 
			
		||||
    const player =  new NetworkPlayer(user._id, user.name, socketClient.socketId);
 | 
			
		||||
    session.addPlayer(player);
 | 
			
		||||
    socketClient.sessionId = session.id;
 | 
			
		||||
    const player =  new NetworkPlayer(user._id, user.username, socketClient.socketId);
 | 
			
		||||
    this.dbManager.replaceOne({id: session.id}, matchSessionAdapter(session));
 | 
			
		||||
    session.addPlayerToSession(player);
 | 
			
		||||
    socketClient.sessionId = session.id;    
 | 
			
		||||
    this.notifyService.notifyMatchState(session);
 | 
			
		||||
    this.notifyService.notifyPlayersState(session.players);
 | 
			
		||||
    return sessionId
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public listSessions(): Promise<DbMatchSession[]> {
 | 
			
		||||
    return this.dbManager.listByFilter({});
 | 
			
		||||
  public async listJoinableSessions(): Promise<DbMatchSession[]> {
 | 
			
		||||
    return await this.dbManager.listByFilter(
 | 
			
		||||
      { state: 'created' }, 
 | 
			
		||||
      { createdAt: -1 }, 
 | 
			
		||||
      { page: 1, pageSize: 5 }) as DbMatchSession[];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public updateSocketId(sessionId: string, userId: string, socketId: string): any {
 | 
			
		||||
    this.sessionManager.updateSocketId(sessionId, userId, socketId);
 | 
			
		||||
  public async getSession(sessionId: string): Promise<DbMatchSession | undefined> {
 | 
			
		||||
    return await this.dbManager.getById(sessionId) as DbMatchSession | undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setPlayerReady(data: any): any {
 | 
			
		||||
    const { userId, sessionId } = data;
 | 
			
		||||
    const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
 | 
			
		||||
    if (session !== undefined) {
 | 
			
		||||
      session.setPlayerReady(userId)
 | 
			
		||||
      this.notifyService.notifyMatchState(session);
 | 
			
		||||
      this.notifyService.notifyPlayersState(session.players);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startSession(data: any): any {
 | 
			
		||||
    const sessionId: string = data.sessionId;
 | 
			
		||||
    const seed: string | undefined = data.seed;
 | 
			
		||||
    const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
 | 
			
		||||
 | 
			
		||||
    if (session === undefined) {
 | 
			
		||||
      return ({
 | 
			
		||||
        status: 'error',
 | 
			
		||||
        message: 'Session not found'
 | 
			
		||||
      });
 | 
			
		||||
    } else if (session.matchInProgress) {
 | 
			
		||||
      return {
 | 
			
		||||
        status: 'error',
 | 
			
		||||
        message: 'Game already in progress'
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      const missingHumans = session.maxPlayers - session.numPlayers;
 | 
			
		||||
      for (let i = 0; i < missingHumans; i++) {
 | 
			
		||||
        session.addPlayer(session.createPlayerAI(i));
 | 
			
		||||
      }
 | 
			
		||||
      session.start();
 | 
			
		||||
      return {
 | 
			
		||||
        status: 'ok'
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  public async deleteSession(sessionId: string): Promise<any> {
 | 
			
		||||
    this.sessionManager.deleteSession(sessionId);
 | 
			
		||||
    const session = {
 | 
			
		||||
      _id: toObjectId(sessionId),
 | 
			
		||||
      state: 'deleted'
 | 
			
		||||
    } as DbMatchSessionUpdate;
 | 
			
		||||
    return this.dbManager.update(session);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // public updateSession(session: MatchSession): any {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
import { Server as HttpServer } from "http";
 | 
			
		||||
import { ServiceBase } from "./ServiceBase";
 | 
			
		||||
import { Server } from "socket.io";
 | 
			
		||||
import { SessionManager } from "../managers/SessionManager";
 | 
			
		||||
import { SecurityManager } from "../managers/SecurityManager";
 | 
			
		||||
import { User } from "../db/interfaces";
 | 
			
		||||
import { Socket } from "socket.io";
 | 
			
		||||
import { SessionService } from "./SessionService";
 | 
			
		||||
import { InteractionService } from "./InteractionService";
 | 
			
		||||
import { ClientEvents } from "../../game/constants";
 | 
			
		||||
 | 
			
		||||
export class SocketIoService  extends ServiceBase{
 | 
			
		||||
  io: Server
 | 
			
		||||
  private static clients: Map<string, any> = new Map();
 | 
			
		||||
  private sessionService: SessionService = new SessionService();
 | 
			
		||||
  private interactionService: InteractionService = new InteractionService();
 | 
			
		||||
 | 
			
		||||
  static getClient(id: string) {
 | 
			
		||||
    return this.clients.get(id);
 | 
			
		||||
@@ -65,7 +65,7 @@ export class SocketIoService  extends ServiceBase{
 | 
			
		||||
            socket.join('room-general')
 | 
			
		||||
          } else {
 | 
			
		||||
            const client = SocketIoService.clients.get(userId);
 | 
			
		||||
            this.sessionService.updateSocketId(client.sessionId, userId, socketId);
 | 
			
		||||
            this.interactionService.updateSocketId(client.sessionId, userId, socketId);
 | 
			
		||||
            client.socketId = socketId;
 | 
			
		||||
            this.logger.debug(`User '${user.username}' already connected. Updating socketId to ${socketId}`);
 | 
			
		||||
            client.alive = true;
 | 
			
		||||
@@ -82,25 +82,14 @@ export class SocketIoService  extends ServiceBase{
 | 
			
		||||
          SocketIoService.clients.delete(id);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    
 | 
			
		||||
      // socket.on('createSession', (data, callback) => {
 | 
			
		||||
      //   const response = sessionController.createSession(data, socket.id);
 | 
			
		||||
      //   callback(response);
 | 
			
		||||
      // });
 | 
			
		||||
 | 
			
		||||
      socket.on('startSession', (data, callback) => {
 | 
			
		||||
        const response = this.sessionService.startSession(data);    
 | 
			
		||||
        callback(response);
 | 
			
		||||
      socket.on(ClientEvents.CLIENT_EVENT, (data) => {
 | 
			
		||||
        this.interactionService.handleClientEvent(data);
 | 
			
		||||
      });
 | 
			
		||||
    
 | 
			
		||||
      //  socket.on('joinSession', (data, callback) => {
 | 
			
		||||
      //   const response = sessionController.joinSession(data, socket.id);
 | 
			
		||||
      //   callback(response);
 | 
			
		||||
      // });
 | 
			
		||||
 | 
			
		||||
      socket.on('playerReady', (data, callback) => {
 | 
			
		||||
        const response = this.sessionService.setPlayerReady(data);
 | 
			
		||||
        callback(response);
 | 
			
		||||
      socket.on(ClientEvents.CLIENT_EVENT_WITH_ACK, (data, callback) => {
 | 
			
		||||
        const result = this.interactionService.handleClientEventWithAck(data);
 | 
			
		||||
        callback(result);
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      socket.on('pong', () => {
 | 
			
		||||
@@ -110,11 +99,42 @@ export class SocketIoService  extends ServiceBase{
 | 
			
		||||
          SocketIoService.clients.set(id, {...client,  alive: true });
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      socket.onAny((event, ...args) => {
 | 
			
		||||
        if (['pong'].includes(event)) return;
 | 
			
		||||
        let logStr = `Event received: ${event}`
 | 
			
		||||
        
 | 
			
		||||
        if (event.startsWith('client:') && args.length > 0) {
 | 
			
		||||
          logStr = `${logStr} (${args[0].event})`;
 | 
			
		||||
        }
 | 
			
		||||
        this.logger.debug(logStr);
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      this.pingClients()
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
      // // socket.on('createSession', (data, callback) => {
 | 
			
		||||
      // //   const response = sessionController.createSession(data, socket.id);
 | 
			
		||||
      // //   callback(response);
 | 
			
		||||
      // // });
 | 
			
		||||
 | 
			
		||||
      // socket.on('startSession', (data, callback) => {
 | 
			
		||||
      //   const response = this.sessionService.startSession(data);    
 | 
			
		||||
      //   callback(response);
 | 
			
		||||
      // });
 | 
			
		||||
 | 
			
		||||
      // socket.on('client:tile-animation-ended', (data) => {
 | 
			
		||||
      //   this.sessionService.onClientEndTileAnimation(data);
 | 
			
		||||
      // });
 | 
			
		||||
    
 | 
			
		||||
      //  socket.on('joinSession', (data, callback) => {
 | 
			
		||||
      //   const response = sessionController.joinSession(data, socket.id);
 | 
			
		||||
      //   callback(response);
 | 
			
		||||
      // });
 | 
			
		||||
 | 
			
		||||
      // socket.on('playerReady', (data, callback) => {
 | 
			
		||||
      //   const response = this.sessionService.setPlayerReady(data);
 | 
			
		||||
      //   callback(response);
 | 
			
		||||
      // });
 | 
			
		||||
    });    
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private pingClients() {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user