initial commit
This commit is contained in:
		
							
								
								
									
										102
									
								
								src/common/LoggingService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/common/LoggingService.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
			
		||||
import pino, { BaseLogger } from 'pino';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
 | 
			
		||||
export class LoggingService {
 | 
			
		||||
  static instance: LoggingService;
 | 
			
		||||
  logsPath: string = path.join(process.cwd(), 'app', 'server', 'logs');
 | 
			
		||||
  logger!: BaseLogger;
 | 
			
		||||
  level: string = process.env.LOG_LEVEL || 'info';
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
  * ogger.fatal('fatal');
 | 
			
		||||
      logger.error('error');
 | 
			
		||||
      logger.warn('warn');
 | 
			
		||||
      logger.info('info');
 | 
			
		||||
      logger.debug('debug');
 | 
			
		||||
      logger.trace('trace');
 | 
			
		||||
  */ 
 | 
			
		||||
  constructor() {
 | 
			
		||||
    if ((!LoggingService.instance)) {
 | 
			
		||||
      LoggingService.instance = this;
 | 
			
		||||
      this.logger = pino({
 | 
			
		||||
        level: this.level,
 | 
			
		||||
        timestamp: pino.stdTimeFunctions.isoTime,
 | 
			
		||||
      }, this.transports);
 | 
			
		||||
    }
 | 
			
		||||
    return LoggingService.instance;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get commonRorationOptions() : any {
 | 
			
		||||
    return {
 | 
			
		||||
      interval: '1d',
 | 
			
		||||
      maxFiles: 10,
 | 
			
		||||
      path: this.logsPath,
 | 
			
		||||
      size: '10M',
 | 
			
		||||
      maxSize: '100M',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get transports() {
 | 
			
		||||
    return pino.transport({
 | 
			
		||||
      targets: [
 | 
			
		||||
        // {
 | 
			
		||||
        //   target: 'pino-rotating-file-stream',
 | 
			
		||||
        //   level: this.level,
 | 
			
		||||
        //   options: { 
 | 
			
		||||
        //     filename: 'app.log',
 | 
			
		||||
        //     ...this.commonRorationOptions
 | 
			
		||||
        //   },
 | 
			
		||||
        // },
 | 
			
		||||
        {
 | 
			
		||||
          target: 'pino-pretty',
 | 
			
		||||
          level: this.level,
 | 
			
		||||
          options: {
 | 
			
		||||
            sync: true,
 | 
			
		||||
            colorized: true,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      ]
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  debug(message: string, data?: any) {
 | 
			
		||||
    this.logger.debug(this._getMessageWidthObject(message, data));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  info(message: string, data?: any) {
 | 
			
		||||
    this.logger.info(this._getMessageWidthObject(message, data));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  warn(message: string, data?: any) {
 | 
			
		||||
    this.logger.warn(this._getMessageWidthObject(message, data));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  error(error: any, message?: string) {
 | 
			
		||||
    this.logger.error(error, message);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fatal(message: string, data?: any) {
 | 
			
		||||
    this.logger.fatal(this._getMessageWidthObject(message, data));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  trace(message: string, data?: any) {
 | 
			
		||||
    this.logger.trace(this._getMessageWidthObject(message, data));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  object(message: any) {
 | 
			
		||||
    this.logger.info(JSON.stringify(message, null, 2));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _getMessageWidthObject(message: string, data?: any) {
 | 
			
		||||
    if (!data) {
 | 
			
		||||
      return message;
 | 
			
		||||
    }
 | 
			
		||||
    return `${message}\n${this._getStringObject(data)}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _getStringObject(data: any) {
 | 
			
		||||
    return JSON.stringify(data, null, 2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/common/exceptions/ErrorBase.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/common/exceptions/ErrorBase.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
export class ErrorBase extends Error {
 | 
			
		||||
    constructor(message: string) {
 | 
			
		||||
        super(message);
 | 
			
		||||
        console.log('this.constructor.name :>> ', this.constructor.name);
 | 
			
		||||
        this.name = this.constructor.name;
 | 
			
		||||
        this.stack = (new Error()).stack;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/common/exceptions/SocketDisconnectedError.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/common/exceptions/SocketDisconnectedError.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import { ErrorBase } from "./ErrorBase";
 | 
			
		||||
 | 
			
		||||
export class SocketDisconnectedError extends ErrorBase {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('Socket disconnected');
 | 
			
		||||
    }
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										63
									
								
								src/common/utilities.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/common/utilities.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
 | 
			
		||||
import { randomBytes, randomUUID } from 'crypto';
 | 
			
		||||
import * as readline from 'readline';
 | 
			
		||||
import { Tile } from '../game/entities/Tile';
 | 
			
		||||
import chalk from 'chalk';
 | 
			
		||||
import { Board } from '../game/entities/Board';
 | 
			
		||||
 | 
			
		||||
const rl = readline.createInterface({
 | 
			
		||||
  input: process.stdin,
 | 
			
		||||
  output: process.stdout,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export async function wait(ms: number) {
 | 
			
		||||
  return new Promise(resolve => setTimeout(resolve, ms));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function askQuestion(question: string): Promise<string> {
 | 
			
		||||
  return new Promise((resolve) => {
 | 
			
		||||
    // console.log(chalk.yellow(question));
 | 
			
		||||
    rl.question(`${chalk.yellow(question + ' > ')}`, (answer) => {
 | 
			
		||||
      resolve(answer);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getRandomSeed(): string {
 | 
			
		||||
  const timestamp = Date.now();
 | 
			
		||||
  const randomPart = Math.random().toString(36).substring(2);
 | 
			
		||||
  const securePart = randomBytes(4).toString('hex');
 | 
			
		||||
  return `${timestamp}-${randomPart}-${securePart}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function printTiles(prefix:string, tiles: Tile[]): void {
 | 
			
		||||
  console.log(`${prefix}${tiles.join(' ')}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function printSelection(prefix:string, tiles: Tile[]): void {
 | 
			
		||||
  const line: string = tiles.map((t,i) => {
 | 
			
		||||
    const index = i + 1;
 | 
			
		||||
    return `(${index > 9 ? `${index})`: `${index}) `} `
 | 
			
		||||
  }).join(' ');
 | 
			
		||||
  printTiles(prefix, tiles);
 | 
			
		||||
  console.log(`${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--'}`));
 | 
			
		||||
    else
 | 
			
		||||
  console.log(chalk.gray(`Board: ${board.tiles.length > 0 ? board.tiles.join(' ') : '--empty--'}`));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function printLine(msg: string): void {
 | 
			
		||||
  console.log(chalk.grey(msg));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function printError(msg: string): void {
 | 
			
		||||
  console.log(chalk.red(msg));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function uuid() {
 | 
			
		||||
  return randomUUID();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										238
									
								
								src/game/DominoesGame.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								src/game/DominoesGame.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,238 @@
 | 
			
		||||
import { PRNG } from 'seedrandom';
 | 
			
		||||
import { Board } from "./entities/Board";
 | 
			
		||||
import { PlayerMove } from "./entities/PlayerMove";
 | 
			
		||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
 | 
			
		||||
import { Tile } from "./entities/Tile";
 | 
			
		||||
import { LoggingService } from "../common/LoggingService";
 | 
			
		||||
import { printBoard, printLine, uuid, wait } from '../common/utilities';
 | 
			
		||||
import { GameSummary } from './dto/GameSummary';
 | 
			
		||||
import { PlayerNotificationManager } from './PlayerNotificationManager';
 | 
			
		||||
import { GameState } from './dto/GameState';
 | 
			
		||||
 | 
			
		||||
export class DominoesGame {
 | 
			
		||||
  private id: string;
 | 
			
		||||
  private seed: string | undefined;
 | 
			
		||||
  autoDeal: boolean = true;
 | 
			
		||||
  board: Board;
 | 
			
		||||
  currentPlayerIndex: number = 0;
 | 
			
		||||
  gameInProgress: boolean = false;
 | 
			
		||||
  gameOver: boolean = false;
 | 
			
		||||
  gameBlocked: boolean = false;
 | 
			
		||||
  gameTied: boolean = false;
 | 
			
		||||
  tileSelectionPhase: boolean = true;
 | 
			
		||||
  logger: LoggingService = new LoggingService();
 | 
			
		||||
  blockedCount: number = 0;
 | 
			
		||||
  winner: PlayerInterface | null = null;
 | 
			
		||||
  rng: PRNG;
 | 
			
		||||
  handSize: number = 7;
 | 
			
		||||
  notificationManager: PlayerNotificationManager = new PlayerNotificationManager(this);
 | 
			
		||||
  lastMove: PlayerMove | null = null;
 | 
			
		||||
 | 
			
		||||
  constructor(public players: PlayerInterface[], seed: PRNG) {
 | 
			
		||||
    this.id = uuid();
 | 
			
		||||
    this.logger.info(`Game ID: ${this.id}`);
 | 
			
		||||
    this.logger.info(`Seed: ${this.seed}`);
 | 
			
		||||
    this.rng = seed
 | 
			
		||||
    this.board = new Board(seed);
 | 
			
		||||
    this.initializeGame();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async initializeGame() {
 | 
			
		||||
    this.gameOver = false;
 | 
			
		||||
    this.gameBlocked = false;
 | 
			
		||||
    this.gameTied = false;
 | 
			
		||||
    this.board.boneyard = this.generateTiles();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generateTiles(): Tile[] {
 | 
			
		||||
    const tiles: Tile[] = [];
 | 
			
		||||
    for (let i = 6; i >= 0; i--) {
 | 
			
		||||
      for (let j = i; j >= 0; j--) {
 | 
			
		||||
        tiles.push(new Tile([i, j]));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.logger.debug('tiles :>> ' + tiles);
 | 
			
		||||
    return this.shuffle(tiles);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private shuffle(array: Tile[]): Tile[] {
 | 
			
		||||
    for (let i = array.length - 1; i > 0; i--) {
 | 
			
		||||
      const j = Math.floor(this.rng() * (i + 1));
 | 
			
		||||
      [array[i], array[j]] = [array[j], array[i]];
 | 
			
		||||
    }
 | 
			
		||||
    return array;
 | 
			
		||||
  }  
 | 
			
		||||
 | 
			
		||||
  nextPlayer() {
 | 
			
		||||
    this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isBlocked(): boolean {
 | 
			
		||||
    return this.blockedCount === this.players.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isGameOver(): boolean {
 | 
			
		||||
    const hasWinner: boolean = this.players.some(player => player.hand.length === 0);
 | 
			
		||||
    return hasWinner || this.gameBlocked;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getWinner(): PlayerInterface | null {
 | 
			
		||||
    if (!this.gameOver) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    const winnerNoTiles = this.players.find(player => player.hand.length === 0);
 | 
			
		||||
    if (winnerNoTiles !== undefined) {
 | 
			
		||||
      return winnerNoTiles;
 | 
			
		||||
    }
 | 
			
		||||
    const winnerMinPipsCount = this.players.reduce((acc, player) => {
 | 
			
		||||
      return player.pipsCount() < acc.pipsCount() ? player : acc;
 | 
			
		||||
    });
 | 
			
		||||
    return winnerMinPipsCount;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStartingPlayerIndex(): number {
 | 
			
		||||
    // Determine starting player
 | 
			
		||||
    let startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 6 && tile.pips[1] === 6));
 | 
			
		||||
    if (startingIndex === -1) {
 | 
			
		||||
      startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 5 && tile.pips[1] === 5));
 | 
			
		||||
      if (startingIndex === -1) {
 | 
			
		||||
        startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 4 && tile.pips[1] === 4));
 | 
			
		||||
        if (startingIndex === -1) {
 | 
			
		||||
          startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 3 && tile.pips[1] === 3));
 | 
			
		||||
          if (startingIndex === -1) {
 | 
			
		||||
            startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 2 && tile.pips[1] === 2));
 | 
			
		||||
            if (startingIndex === -1) {
 | 
			
		||||
              startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 1 && tile.pips[1] === 1));
 | 
			
		||||
              if (startingIndex === -1) {
 | 
			
		||||
                startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 0 && tile.pips[1] === 0));
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return startingIndex === -1 ? 0 : startingIndex;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async playTurn(): Promise<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();   
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async start(): Promise<GameSummary> {
 | 
			
		||||
    this.gameInProgress = false;
 | 
			
		||||
    this.tileSelectionPhase = true;
 | 
			
		||||
    await this.notificationManager.notifyGameState();
 | 
			
		||||
    await this.notificationManager.notifyPlayersState();
 | 
			
		||||
    this.logger.debug('clients received boneyard :>> ' + this.board.boneyard);
 | 
			
		||||
    await wait(1000);
 | 
			
		||||
 | 
			
		||||
    if (this.autoDeal) {
 | 
			
		||||
      this.dealTiles();
 | 
			
		||||
      await this.notificationManager.notifyGameState();
 | 
			
		||||
      await this.notificationManager.notifyPlayersState();
 | 
			
		||||
    } else  {
 | 
			
		||||
      await this.tilesSelection();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.tileSelectionPhase = false;
 | 
			
		||||
    this.gameInProgress = true;
 | 
			
		||||
    this.currentPlayerIndex = this.getStartingPlayerIndex();
 | 
			
		||||
    printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`);
 | 
			
		||||
    while (!this.gameOver) {
 | 
			
		||||
      await this.playTurn();
 | 
			
		||||
      await this.notificationManager.notifyGameState();
 | 
			
		||||
      await this.notificationManager.notifyPlayersState();
 | 
			
		||||
      this.gameBlocked = this.isBlocked();
 | 
			
		||||
      this.gameOver = this.isGameOver();
 | 
			
		||||
    }
 | 
			
		||||
    this.gameInProgress = false;
 | 
			
		||||
    this.winner =  this.getWinner();
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      gameId: this.id,
 | 
			
		||||
      isBlocked: this.gameBlocked,
 | 
			
		||||
      isTied: this.gameTied,
 | 
			
		||||
      winner: this.winner
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  dealTiles() {
 | 
			
		||||
    for (let i = 0; i < this.handSize; i++) {
 | 
			
		||||
      for (let player of this.players) {
 | 
			
		||||
        const tile: Tile | undefined = this.board.boneyard.pop();
 | 
			
		||||
        if (tile !== undefined) {
 | 
			
		||||
          tile.revealed = true;
 | 
			
		||||
          player.hand.push(tile);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async tilesSelection() {
 | 
			
		||||
    while (this.board.boneyard.length > 0) {
 | 
			
		||||
      for (let player of this.players) {
 | 
			
		||||
        const choosen = await player.chooseTile(this.board);
 | 
			
		||||
        await this.notificationManager.notifyGameState();
 | 
			
		||||
        await this.notificationManager.notifyPlayersState();
 | 
			
		||||
        if (this.board.boneyard.length === 0) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getGameState(): GameState {
 | 
			
		||||
    const currentPlayer = this.players[this.currentPlayerIndex]
 | 
			
		||||
    return {
 | 
			
		||||
      id: uuid(),
 | 
			
		||||
      lastMove: this.lastMove,
 | 
			
		||||
      gameInProgress: this.gameInProgress,
 | 
			
		||||
      winner: this.winner,
 | 
			
		||||
      tileSelectionPhase: this.tileSelectionPhase, 
 | 
			
		||||
      gameBlocked: this.gameBlocked,
 | 
			
		||||
      gameTied: this.gameTied,
 | 
			
		||||
      gameId: this.id,
 | 
			
		||||
      boneyard: this.board.boneyard.map(tile => ({ id: tile.id})),
 | 
			
		||||
      players: this.players.map(player => ({
 | 
			
		||||
        id: player.id,
 | 
			
		||||
        name: player.name,
 | 
			
		||||
        score: player.score,
 | 
			
		||||
        hand: player.hand.map(tile => tile.id),
 | 
			
		||||
      })),
 | 
			
		||||
      currentPlayer: {
 | 
			
		||||
        id: currentPlayer.id,
 | 
			
		||||
        name: currentPlayer.name
 | 
			
		||||
      },
 | 
			
		||||
      board: this.board.tiles.map(tile => ({
 | 
			
		||||
        id: tile.id,
 | 
			
		||||
        pips: tile.pips
 | 
			
		||||
      })),
 | 
			
		||||
      boardFreeEnds: this.board.getFreeEnds(),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										149
									
								
								src/game/GameSession.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								src/game/GameSession.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,149 @@
 | 
			
		||||
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 { GameSessionState } from "./dto/GameSessionState";
 | 
			
		||||
import { PlayerNotificationManager } from './PlayerNotificationManager';
 | 
			
		||||
import seedrandom, { PRNG } from "seedrandom";
 | 
			
		||||
 | 
			
		||||
export class GameSession {
 | 
			
		||||
  private game: DominoesGame | null = null;
 | 
			
		||||
  private minHumanPlayers: number = 1;  
 | 
			
		||||
  private waitingForPlayers: boolean = true;
 | 
			
		||||
  private waitingSeconds: number = 0;
 | 
			
		||||
  private logger: LoggingService = new LoggingService();
 | 
			
		||||
  private mode: string = 'classic';
 | 
			
		||||
  private pointsToWin: number = 100;
 | 
			
		||||
  private playerNotificationManager: PlayerNotificationManager;
 | 
			
		||||
  id: string;
 | 
			
		||||
  players: PlayerInterface[] = [];
 | 
			
		||||
  sessionInProgress: boolean = false;
 | 
			
		||||
  maxPlayers: number = 4;
 | 
			
		||||
  seed!: string
 | 
			
		||||
  rng!: PRNG
 | 
			
		||||
 | 
			
		||||
  constructor(public creator: PlayerInterface, public name?: string) {
 | 
			
		||||
    this.playerNotificationManager = new PlayerNotificationManager(this);
 | 
			
		||||
    this.id = uuid();
 | 
			
		||||
    this.name = name || `Game ${this.id}`;
 | 
			
		||||
    this.addPlayer(creator);
 | 
			
		||||
    this.logger.info(`GameSession created by: ${creator.name}`);
 | 
			
		||||
    this.creator = creator;
 | 
			
		||||
    this.playerNotificationManager.notifySessionState();
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  get numPlayers() {
 | 
			
		||||
    return this.players.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async startGame(seed: string) {
 | 
			
		||||
    this.rng = seedrandom(seed);
 | 
			
		||||
    const missingPlayers = this.maxPlayers - this.numPlayers;
 | 
			
		||||
    for (let i = 0; i < missingPlayers; i++) {
 | 
			
		||||
      this.addPlayer(this.createPlayerAI(i));
 | 
			
		||||
    }
 | 
			
		||||
    this.game = new DominoesGame(this.players, this.rng);
 | 
			
		||||
    this.sessionInProgress = true;
 | 
			
		||||
    this.logger.info('Game started');
 | 
			
		||||
    this.playerNotificationManager.notifySessionState();
 | 
			
		||||
    await this.game.start();
 | 
			
		||||
    return this.endGame();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private endGame(): any {
 | 
			
		||||
    if (this.game !== null) {
 | 
			
		||||
      this.sessionInProgress = false;
 | 
			
		||||
      const { gameBlocked, gameTied, winner } = this.game;
 | 
			
		||||
      
 | 
			
		||||
      gameBlocked ? console.log('Game blocked!') : gameTied ? console.log('Game tied!') : console.log('Game over!');
 | 
			
		||||
      console.log('Winner: ' + winner?.name + ' with ' + winner?.pipsCount() + ' points');
 | 
			
		||||
      this.getScore(this.game);
 | 
			
		||||
      this.sessionInProgress = false;
 | 
			
		||||
      this.logger.info('Game ended');
 | 
			
		||||
      this.game = null;
 | 
			
		||||
      this.playerNotificationManager.notifySessionState();
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        gameBlocked,
 | 
			
		||||
        gameTied,
 | 
			
		||||
        winner
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getScore(game: DominoesGame) {
 | 
			
		||||
    const pips = game.players
 | 
			
		||||
      .sort((a,b) => (b.pipsCount() - a.pipsCount()))
 | 
			
		||||
      .map(player => {
 | 
			
		||||
        return `${player.name}: ${player.pipsCount()}`;
 | 
			
		||||
      });
 | 
			
		||||
    console.log(`Pips count: ${pips.join(', ')}`);
 | 
			
		||||
    const totalPoints = game.players.reduce((acc, player) => acc + player.pipsCount(), 0);
 | 
			
		||||
    if (game.winner !== null) {
 | 
			
		||||
      game.winner.score += totalPoints;
 | 
			
		||||
    }
 | 
			
		||||
    const scores = game.players
 | 
			
		||||
      .sort((a,b) => (b.score - a.score))
 | 
			
		||||
      .map(player => {
 | 
			
		||||
        return `${player.name}: ${player.score}`;
 | 
			
		||||
      });
 | 
			
		||||
    console.log(`Scores: ${scores.join(', ')}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createPlayerAI(i: number) {
 | 
			
		||||
    const AInames = ["Alice (AI)", "Bob (AI)", "Charlie (AI)", "David (AI)"];
 | 
			
		||||
    return new PlayerAI(AInames[i], this.rng);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async start(seed?: string) {
 | 
			
		||||
    this.seed = seed || getRandomSeed();
 | 
			
		||||
    console.log('seed :>> ', this.seed);
 | 
			
		||||
    if (this.sessionInProgress) {
 | 
			
		||||
      throw new Error("Game already in progress");
 | 
			
		||||
    }
 | 
			
		||||
    this.waitingForPlayers = true;
 | 
			
		||||
    this.logger.info('Waiting for players to join');
 | 
			
		||||
    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');
 | 
			
		||||
    this.startGame(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!`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toString() {
 | 
			
		||||
    return `GameSession:(${this.id} ${this.name})`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getState(): GameSessionState {
 | 
			
		||||
    return {
 | 
			
		||||
      id: this.id,
 | 
			
		||||
      name: this.name!,
 | 
			
		||||
      creator: this.creator.id,
 | 
			
		||||
      players: this.players.map(player =>( {
 | 
			
		||||
        id: player.id,
 | 
			
		||||
        name: player.name,
 | 
			
		||||
      })),
 | 
			
		||||
      sessionInProgress: this.sessionInProgress,
 | 
			
		||||
      maxPlayers: this.maxPlayers,
 | 
			
		||||
      numPlayers: this.numPlayers,
 | 
			
		||||
      waitingForPlayers: this.waitingForPlayers,
 | 
			
		||||
      waitingSeconds: this.waitingSeconds,
 | 
			
		||||
      seed: this.seed,
 | 
			
		||||
      mode: this.mode,
 | 
			
		||||
      pointsToWin: this.pointsToWin,
 | 
			
		||||
      status: this.sessionInProgress ? 'in progress' : 'waiting'
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								src/game/NetworkClientNotifier.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/game/NetworkClientNotifier.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
 | 
			
		||||
import { LoggingService } from "../common/LoggingService";
 | 
			
		||||
 | 
			
		||||
export class NetworkClientNotifier {
 | 
			
		||||
  static instance: NetworkClientNotifier;
 | 
			
		||||
  io: any;
 | 
			
		||||
  logger: LoggingService = new LoggingService();
 | 
			
		||||
  constructor() {
 | 
			
		||||
    if (!NetworkClientNotifier.instance) {
 | 
			
		||||
      NetworkClientNotifier.instance = this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return NetworkClientNotifier.instance
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setSocket(io: any) {
 | 
			
		||||
    this.io = io;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async notifyPlayer(player: NetworkPlayer, event: string, data: any = {}, timeoutSecs: number = 300): 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 broadcast(event: string, data: any) {
 | 
			
		||||
    const responses = await this.io.emit(event, data);
 | 
			
		||||
    this.logger.debug('responses :>> ', responses);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								src/game/PlayerInteractionConsole.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/game/PlayerInteractionConsole.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
import { Board } from "./entities/Board";
 | 
			
		||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
 | 
			
		||||
import { askQuestion, printError, printSelection, printTiles, wait } from "../common/utilities";
 | 
			
		||||
import { PlayerMove } from "./entities/PlayerMove";
 | 
			
		||||
import { Tile } from "./entities/Tile";
 | 
			
		||||
import { PlayerMoveSide, PlayerMoveSideType } from "./constants";
 | 
			
		||||
import { PlayerInteractionInterface } from "./PlayerInteractionInterface";
 | 
			
		||||
 | 
			
		||||
export class PlayerInteractionConsole implements PlayerInteractionInterface {
 | 
			
		||||
  player: PlayerInterface;
 | 
			
		||||
  
 | 
			
		||||
  constructor(player: PlayerInterface) {
 | 
			
		||||
    this.player = player;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async makeMove(board: Board): Promise<PlayerMove | null> {
 | 
			
		||||
    let move: PlayerMove | null = null;
 | 
			
		||||
    let tile: Tile;
 | 
			
		||||
    let side: PlayerMoveSideType | null = null;
 | 
			
		||||
    const { player } = this;
 | 
			
		||||
 | 
			
		||||
    printSelection('Hand: ', player.hand);
 | 
			
		||||
    while (move === null) {
 | 
			
		||||
      const answer = await askQuestion('Enter your move (tile index side is L or R, e.g. 0L, <Enter> to pass');
 | 
			
		||||
      const char0 = answer.charAt(0);
 | 
			
		||||
      const char1 = answer.charAt(1);
 | 
			
		||||
      if (answer === '' || answer === '') {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
    
 | 
			
		||||
      if (char1 === 'L' || char1 === 'R' || char1 === 'l' || char1 === 'r') {
 | 
			
		||||
        side = char1 === 'L' || char1 === 'l' ? PlayerMoveSide.LEFT : PlayerMoveSide.RIGHT;
 | 
			
		||||
      }
 | 
			
		||||
    
 | 
			
		||||
      if (char0 > '0' && char0 <= String(player.hand.length)) {
 | 
			
		||||
        const tileIndex = parseInt(char0) - 1;
 | 
			
		||||
        tile = player.hand[tileIndex];
 | 
			
		||||
        move = board.isValidMove(tile, side, player);
 | 
			
		||||
        if (move === null) {
 | 
			
		||||
          printError('Invalid move');
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return move;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async chooseTile(board: Board): Promise<Tile> {
 | 
			
		||||
    const { player: { hand} } = this;
 | 
			
		||||
    let index: number = -1;
 | 
			
		||||
    while (index < 0 || index >= board.boneyard.length) {
 | 
			
		||||
      printTiles('Hand: ', hand);
 | 
			
		||||
      printSelection('Boneyard: ', board.boneyard);
 | 
			
		||||
      const answer = await askQuestion('Choose a tile from the boneyard');
 | 
			
		||||
      if (answer === '') {
 | 
			
		||||
        index = 0;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      if (answer < '0' || answer > '9') {
 | 
			
		||||
        printError('Invalid selection');
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      index = parseInt(answer) - 1;
 | 
			
		||||
    }    
 | 
			
		||||
    const tile = board.boneyard.splice(index, 1)[0];
 | 
			
		||||
    tile.revealed = true;
 | 
			
		||||
    hand.push(tile);
 | 
			
		||||
    return tile;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								src/game/PlayerInteractionInterface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/game/PlayerInteractionInterface.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
import { Board } from "./entities/Board";
 | 
			
		||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
 | 
			
		||||
import { PlayerMove } from "./entities/PlayerMove";
 | 
			
		||||
import { Tile } from "./entities/Tile";
 | 
			
		||||
 | 
			
		||||
export interface PlayerInteractionInterface {
 | 
			
		||||
    player: PlayerInterface;
 | 
			
		||||
 | 
			
		||||
    makeMove(board: Board): Promise<PlayerMove | null>;
 | 
			
		||||
    chooseTile(board: Board): Promise<Tile>
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								src/game/PlayerInteractionNetwork.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/game/PlayerInteractionNetwork.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
import { PlayerInteractionInterface } from './PlayerInteractionInterface';
 | 
			
		||||
import { Board } from './entities/Board';
 | 
			
		||||
import { PlayerInterface } from './entities/player/PlayerInterface';
 | 
			
		||||
import { PlayerMove } from './entities/PlayerMove';
 | 
			
		||||
import { Tile } from './entities/Tile';
 | 
			
		||||
import { NetworkClientNotifier } from './NetworkClientNotifier';
 | 
			
		||||
import { NetworkPlayer } from './entities/player/NetworkPlayer';
 | 
			
		||||
import { PlayerMoveSide, PlayerMoveSideType } from './constants';
 | 
			
		||||
import { SocketDisconnectedError } from '../common/exceptions/SocketDisconnectedError';
 | 
			
		||||
 | 
			
		||||
export class PlayerInteractionNetwork implements PlayerInteractionInterface {
 | 
			
		||||
    player: PlayerInterface;
 | 
			
		||||
    clientNotifier = new NetworkClientNotifier();
 | 
			
		||||
 | 
			
		||||
    constructor(player: PlayerInterface) {
 | 
			
		||||
        this.player = player;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    async makeMove(board: Board): Promise<PlayerMove | null> {
 | 
			
		||||
      let response = undefined;
 | 
			
		||||
      try {
 | 
			
		||||
        response = await this.clientNotifier.notifyPlayer(this.player as NetworkPlayer, 'makeMove', {
 | 
			
		||||
          freeHands: board.getFreeEnds(),
 | 
			
		||||
        });
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        throw new SocketDisconnectedError();
 | 
			
		||||
      }
 | 
			
		||||
      const { tile: tilePlayed, type, direction } = response;
 | 
			
		||||
      if (type === 'pass') {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
      const { player: { hand} } = this;
 | 
			
		||||
      const index: number = hand.findIndex(t => t.id === tilePlayed.id); 
 | 
			
		||||
      const side: PlayerMoveSideType =  type === 'left' ? PlayerMoveSide.LEFT : PlayerMoveSide.RIGHT;
 | 
			
		||||
      const tile = hand.splice(index, 1)[0];
 | 
			
		||||
      tile.revealed = true;      
 | 
			
		||||
      return board.isValidMove(tile, side, this.player, direction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async chooseTile(board: Board): Promise<Tile> {
 | 
			
		||||
      const { player: { hand} } = this;
 | 
			
		||||
      const response: any = await this.clientNotifier.notifyPlayer(this.player as NetworkPlayer, 'chooseTile');
 | 
			
		||||
      const index: number = board.boneyard.findIndex(t => t.id === response.tileId);  
 | 
			
		||||
      const tile = board.boneyard.splice(index, 1)[0];
 | 
			
		||||
      tile.revealed = true;
 | 
			
		||||
      hand.push(tile);
 | 
			
		||||
      return tile;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								src/game/PlayerNotificationManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/game/PlayerNotificationManager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
import { DominoesGame } from "./DominoesGame";
 | 
			
		||||
import { GameSession } from "./GameSession";
 | 
			
		||||
import { GameState } from "./dto/GameState";
 | 
			
		||||
 | 
			
		||||
export class PlayerNotificationManager {
 | 
			
		||||
  game!: DominoesGame;
 | 
			
		||||
  session!: GameSession;
 | 
			
		||||
 | 
			
		||||
  constructor(game: DominoesGame | GameSession) {
 | 
			
		||||
    if (game instanceof GameSession) {
 | 
			
		||||
      this.session = game;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.game = game; 
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async notifyGameState() {
 | 
			
		||||
    if(!this.game) throw new Error('Game not initialized');
 | 
			
		||||
    const gameState: GameState = this.game.getGameState();
 | 
			
		||||
    const { players } = this.game;
 | 
			
		||||
    let promises: Promise<void>[] = players.map(player => player.notifyGameState(gameState));
 | 
			
		||||
    return await Promise.all(promises);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async notifyPlayersState() {
 | 
			
		||||
    if(!this.game) throw new Error('Game not initialized');
 | 
			
		||||
    const { players } = this.game;
 | 
			
		||||
    let promises: Promise<void>[] = players.map(player => player.notifyPlayerState(player.getState()));
 | 
			
		||||
    return await Promise.all(promises);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  async notifySessionState() {
 | 
			
		||||
    if(!this.session) throw new Error('Session not initialized');
 | 
			
		||||
    const { players } = this.session;
 | 
			
		||||
    let promises: Promise<void>[] = players.map(player => player.notifySessionState(this.session.getState()));
 | 
			
		||||
    return await Promise.all(promises);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/game/SimulatedBoard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/game/SimulatedBoard.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import { PRNG } from "seedrandom";
 | 
			
		||||
import { Board } from "./entities/Board";
 | 
			
		||||
import { Tile } from "./entities/Tile";
 | 
			
		||||
 | 
			
		||||
export class SimulatedBoard extends Board {
 | 
			
		||||
  constructor(tiles: Tile[] = [], rng: PRNG) {
 | 
			
		||||
    super(rng);
 | 
			
		||||
    this.tiles = tiles;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  evaluate(): number {
 | 
			
		||||
    return this.tiles.length;
 | 
			
		||||
    //return this.tiles.reduce((acc, tile) => acc + tile.count, 0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/game/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/game/constants.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
export type PlayerType = 'AI' | 'Human';
 | 
			
		||||
export type PlayerMoveSideType = 'left' | 'right' | 'both'; 
 | 
			
		||||
export type JointValueType = 0 | 1 | 2;
 | 
			
		||||
 | 
			
		||||
export const PlayerMoveSide: { [key: string]: PlayerMoveSideType } = {
 | 
			
		||||
  LEFT: 'left',
 | 
			
		||||
  RIGHT: 'right',
 | 
			
		||||
  BOTH: 'both'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const JointValue: { [key: string]: JointValueType } = {
 | 
			
		||||
  LEFT: 0,
 | 
			
		||||
  RIGHT: 1,
 | 
			
		||||
  NONE: 2
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										0
									
								
								src/game/dto/Game.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/game/dto/Game.ts
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										17
									
								
								src/game/dto/GameSessionState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/game/dto/GameSessionState.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import { PlayerDto } from "./PlayerDto";
 | 
			
		||||
 | 
			
		||||
export interface GameSessionState {
 | 
			
		||||
  id: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  creator: string;
 | 
			
		||||
  players: PlayerDto[];
 | 
			
		||||
  seed: string;
 | 
			
		||||
  waitingForPlayers: boolean;
 | 
			
		||||
  mode: string;
 | 
			
		||||
  pointsToWin: number;
 | 
			
		||||
  sessionInProgress: boolean;
 | 
			
		||||
  status: string;
 | 
			
		||||
  maxPlayers: number;
 | 
			
		||||
  numPlayers: number;
 | 
			
		||||
  waitingSeconds: number;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								src/game/dto/GameState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/game/dto/GameState.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import { PlayerMove } from "../entities/PlayerMove";
 | 
			
		||||
import { PlayerDto } from "./PlayerDto";
 | 
			
		||||
 | 
			
		||||
export interface GameState {
 | 
			
		||||
  id: string;
 | 
			
		||||
  players: PlayerDto[];
 | 
			
		||||
  boneyard: any[];
 | 
			
		||||
  currentPlayer: PlayerDto | null;
 | 
			
		||||
  board: any[];
 | 
			
		||||
  gameInProgress: boolean;
 | 
			
		||||
  winner?: any;
 | 
			
		||||
  gameBlocked: boolean;
 | 
			
		||||
  gameTied: boolean;
 | 
			
		||||
  gameId: string;
 | 
			
		||||
  tileSelectionPhase: boolean;
 | 
			
		||||
  boardFreeEnds: number[];
 | 
			
		||||
  lastMove: PlayerMove | null;
 | 
			
		||||
}  
 | 
			
		||||
							
								
								
									
										8
									
								
								src/game/dto/GameSummary.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/game/dto/GameSummary.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
import { PlayerInterface } from "../entities/player/PlayerInterface";
 | 
			
		||||
 | 
			
		||||
export interface GameSummary {
 | 
			
		||||
  gameId: string;
 | 
			
		||||
  isBlocked: boolean;
 | 
			
		||||
  isTied: boolean;
 | 
			
		||||
  winner: PlayerInterface | null;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								src/game/dto/PlayerDto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/game/dto/PlayerDto.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
export interface PlayerDto {
 | 
			
		||||
  id: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  score?: number;
 | 
			
		||||
  hand?: string[];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/game/dto/PlayerState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/game/dto/PlayerState.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
export interface PlayerState {
 | 
			
		||||
  id: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  score: number;
 | 
			
		||||
  hand: any[];
 | 
			
		||||
  teamedWith: string | undefined;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										125
									
								
								src/game/entities/Board.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/game/entities/Board.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,125 @@
 | 
			
		||||
import { PRNG } from "seedrandom";
 | 
			
		||||
import { PlayerMoveSideType, PlayerMoveSide, JointValue } from "../constants";
 | 
			
		||||
import { PlayerInterface } from "./player/PlayerInterface";
 | 
			
		||||
import { PlayerMove } from "./PlayerMove";
 | 
			
		||||
import { Tile } from "./Tile";
 | 
			
		||||
 | 
			
		||||
export class Board {
 | 
			
		||||
  tiles: Tile[] = [];
 | 
			
		||||
  boneyard: Tile[] = [];
 | 
			
		||||
 | 
			
		||||
  constructor(private rng: PRNG) {}
 | 
			
		||||
 | 
			
		||||
  get isGameOver(): boolean {
 | 
			
		||||
    return this.tiles.length === 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get playedPipsCount() {
 | 
			
		||||
    return this.tiles.reduce((acc, tile) => acc + tile.count, 0);    
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get count () {
 | 
			
		||||
    return this.tiles.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get leftEnd() {
 | 
			
		||||
    return this.tiles[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get rightEnd() {
 | 
			
		||||
    return this.tiles[this.tiles.length - 1];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get leftFreeEnd() {
 | 
			
		||||
    return this.leftEnd?.flippedPips[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get rightFreeEnd() {
 | 
			
		||||
    return this.rightEnd?.flippedPips[1];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getFreeEnds() {
 | 
			
		||||
    if(this.count === 0) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    return [this.leftEnd.flippedPips[0], this.rightEnd.flippedPips[1]];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  play(playerMove: PlayerMove): void {
 | 
			
		||||
    const { type, tile } = playerMove;
 | 
			
		||||
    tile.revealed = true;
 | 
			
		||||
    if (type === PlayerMoveSide.LEFT) {
 | 
			
		||||
      this.playTileLeft(tile);
 | 
			
		||||
      // printLine(`${tile} -- left`);       
 | 
			
		||||
    } else {
 | 
			
		||||
      this.playTileRight(tile);
 | 
			
		||||
      // printLine(`${tile} -- right`);       
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  playTileLeft(tile: Tile) {
 | 
			
		||||
    if (tile.flippedPips[1] !== this.leftFreeEnd) {
 | 
			
		||||
      tile.flip();
 | 
			
		||||
    }
 | 
			
		||||
    this.tiles.unshift(tile);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  playTileRight(tile: Tile) {
 | 
			
		||||
    if (tile.flippedPips[0] !== this.rightFreeEnd) {
 | 
			
		||||
      tile.flip();
 | 
			
		||||
    }
 | 
			
		||||
    this.tiles.push(tile);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  matchesFreeEnd(tile: Tile, freeEnd: number): boolean {
 | 
			
		||||
    return tile.pips[0] === freeEnd || tile.pips[1] === freeEnd;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  isValidMove(tile: Tile, side: PlayerMoveSideType | null, player: PlayerInterface, direction?: string): PlayerMove | null {
 | 
			
		||||
    if (this.count === 0) {
 | 
			
		||||
      return new PlayerMove(tile, PlayerMoveSide.BOTH, player.id, direction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const freeEnds = this.getFreeEnds();
 | 
			
		||||
    const leftEnd = freeEnds[0];
 | 
			
		||||
    const rightEnd = freeEnds[1];   
 | 
			
		||||
 | 
			
		||||
    if (side !== null) {
 | 
			
		||||
      if (side === PlayerMoveSide.LEFT) {
 | 
			
		||||
        if (this.matchesFreeEnd(tile, leftEnd)) {
 | 
			
		||||
          return new PlayerMove(tile, side, player.id, direction);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        if (this.matchesFreeEnd(tile, rightEnd)) {
 | 
			
		||||
          return new PlayerMove(tile, side, player.id, direction);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ((this.matchesFreeEnd(tile, leftEnd) && this.matchesFreeEnd(tile, rightEnd))) {
 | 
			
		||||
      const side = this.rng() < 0.5 ? PlayerMoveSide.LEFT : PlayerMoveSide.RIGHT;
 | 
			
		||||
      return new PlayerMove(tile, side, player.id, direction);
 | 
			
		||||
    } else if (this.matchesFreeEnd(tile, leftEnd)) {
 | 
			
		||||
      return new PlayerMove(tile, PlayerMoveSide.LEFT, player.id, direction);
 | 
			
		||||
    } else if (this.matchesFreeEnd(tile, rightEnd)) {
 | 
			
		||||
      return new PlayerMove(tile, PlayerMoveSide.RIGHT, player.id, direction);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getValidMoves(player: PlayerInterface): PlayerMove[] {
 | 
			
		||||
 | 
			
		||||
    return player.hand.reduce((acc, tile) => {
 | 
			
		||||
      const validMove = this.isValidMove(tile, null, player);
 | 
			
		||||
      if (validMove !== null) {
 | 
			
		||||
        acc.push(validMove);
 | 
			
		||||
      }
 | 
			
		||||
      return acc;4
 | 
			
		||||
    }, [] as PlayerMove[]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toString(): string {
 | 
			
		||||
    return this.tiles.map(tile => tile.toString()).join(' ');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								src/game/entities/PlayerMove.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/game/entities/PlayerMove.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
import { uuid } from "../../common/utilities";
 | 
			
		||||
import { PlayerMoveSideType } from "../constants";
 | 
			
		||||
import { Tile } from "./Tile";
 | 
			
		||||
 | 
			
		||||
export class PlayerMove {
 | 
			
		||||
  id: string = uuid();
 | 
			
		||||
  constructor(public tile: Tile, public type: PlayerMoveSideType | null, public playerId: string, direction?: string) {}
 | 
			
		||||
 | 
			
		||||
  toString() {
 | 
			
		||||
    return `PlayerMove:([${this.tile.pips[0]}|${this.tile.pips[1]}] ${this.type})`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								src/game/entities/Tile.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/game/entities/Tile.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import { uuid } from "../../common/utilities";
 | 
			
		||||
 | 
			
		||||
export class Tile {
 | 
			
		||||
  id: string;
 | 
			
		||||
  pips: [number, number];
 | 
			
		||||
  revealed: boolean = true;
 | 
			
		||||
  flipped: boolean = false;
 | 
			
		||||
 | 
			
		||||
  constructor(pips: [number, number]) {
 | 
			
		||||
    this.id = uuid();
 | 
			
		||||
    this.pips = pips;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  get count() {
 | 
			
		||||
    return this.pips[0] + this.pips[1];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get isPair(): boolean {
 | 
			
		||||
    return this.pips[0] === this.pips[1];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get flippedPips(): [number, number] {
 | 
			
		||||
    return this.flipped ? [this.pips[1], this.pips[0]] : this.pips;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  flip() {
 | 
			
		||||
    this.flipped = !this.flipped;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toString(): string {
 | 
			
		||||
    if (!this.revealed) {
 | 
			
		||||
      return '[ | ]';
 | 
			
		||||
    } else {
 | 
			
		||||
      return `[${this.flippedPips[0]}|${this.flippedPips[1]}]`;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								src/game/entities/player/AbstractPlayer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/game/entities/player/AbstractPlayer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
import { Board } from "../Board";
 | 
			
		||||
import { PlayerInterface } from "./PlayerInterface";
 | 
			
		||||
import { PlayerMove } from "../PlayerMove";
 | 
			
		||||
import { Tile } from "../Tile";
 | 
			
		||||
import { LoggingService } from "../../../common/LoggingService";
 | 
			
		||||
import { EventEmitter } from "stream";
 | 
			
		||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
 | 
			
		||||
import { uuid } from "../../../common/utilities";
 | 
			
		||||
import { GameState } from "../../dto/GameState";
 | 
			
		||||
import { PlayerState } from "../../dto/PlayerState";
 | 
			
		||||
import { GameSessionState } from "../../dto/GameSessionState";
 | 
			
		||||
 | 
			
		||||
export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface {
 | 
			
		||||
  hand: Tile[] = [];
 | 
			
		||||
  score: number = 0;
 | 
			
		||||
  logger: LoggingService = new LoggingService();
 | 
			
		||||
  teamedWith: PlayerInterface | null = null;
 | 
			
		||||
  playerInteraction: PlayerInteractionInterface = undefined as any;
 | 
			
		||||
  id: string = uuid();
 | 
			
		||||
 | 
			
		||||
  constructor(public name: string) {
 | 
			
		||||
    super();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  abstract makeMove(board: Board): Promise<PlayerMove | null>;
 | 
			
		||||
  abstract chooseTile(board: Board): Promise<Tile>;
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  async notifyGameState(state: GameState): Promise<void> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async notifyPlayerState(state: PlayerState): Promise<void> {
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  async notifySessionState(state: GameSessionState): Promise<void> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pipsCount(): number {
 | 
			
		||||
    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(): PlayerState {
 | 
			
		||||
    return {
 | 
			
		||||
      id: this.id,
 | 
			
		||||
      name: this.name,
 | 
			
		||||
      score: this.score,
 | 
			
		||||
      hand: this.hand.map(tile => ({ 
 | 
			
		||||
        id: tile.id,
 | 
			
		||||
        pips: tile.pips,
 | 
			
		||||
        flipped: tile.revealed,
 | 
			
		||||
      })),
 | 
			
		||||
      teamedWith: this.teamedWith?.id,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								src/game/entities/player/NetworkPlayer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/game/entities/player/NetworkPlayer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
 | 
			
		||||
import { PlayerInteractionNetwork } from "../../PlayerInteractionNetwork";
 | 
			
		||||
import { PlayerHuman } from "./PlayerHuman";
 | 
			
		||||
import { NetworkClientNotifier } from "../../NetworkClientNotifier";
 | 
			
		||||
import { Tile } from "../Tile";
 | 
			
		||||
import { Board } from "../Board";
 | 
			
		||||
import { GameState } from "../../dto/GameState";
 | 
			
		||||
import { PlayerState } from "../../dto/PlayerState";
 | 
			
		||||
import { GameSessionState } from "../../dto/GameSessionState";
 | 
			
		||||
import { SocketDisconnectedError } from "../../../common/exceptions/SocketDisconnectedError";
 | 
			
		||||
 | 
			
		||||
export class NetworkPlayer extends PlayerHuman {
 | 
			
		||||
  socketId: string;
 | 
			
		||||
  playerInteraction: PlayerInteractionInterface = new PlayerInteractionNetwork(this);
 | 
			
		||||
  clientNotifier: NetworkClientNotifier = new NetworkClientNotifier();
 | 
			
		||||
  
 | 
			
		||||
  constructor(name: string, socketId: string) {
 | 
			
		||||
    super(name);
 | 
			
		||||
    this.socketId = socketId;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async notifyGameState(state: GameState): Promise<void> {
 | 
			
		||||
    const response = await this.clientNotifier.notifyPlayer(this, 'gameState', state);
 | 
			
		||||
    console.log('game state notified :>> ', response);
 | 
			
		||||
    if (response === undefined || response.status !== 'ok' ) {
 | 
			
		||||
      throw new SocketDisconnectedError();
 | 
			
		||||
    }
 | 
			
		||||
  } 
 | 
			
		||||
 | 
			
		||||
  async notifyPlayerState(state: PlayerState): Promise<void> {
 | 
			
		||||
    const response = await this.clientNotifier.notifyPlayer(this, 'playerState', state);
 | 
			
		||||
    console.log('player state notified :>> ', response);
 | 
			
		||||
    if (response === undefined || response.status !== 'ok' ) {
 | 
			
		||||
      throw new SocketDisconnectedError();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async notifySessionState(state: GameSessionState): Promise<void> {
 | 
			
		||||
    const response = await this.clientNotifier.notifyPlayer(this, 'sessionState', state);
 | 
			
		||||
    console.log('session state notified :>> ', response);
 | 
			
		||||
    if (response === undefined || response.status !== 'ok' ) {
 | 
			
		||||
      throw new SocketDisconnectedError();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  async chooseTile(board: Board): Promise<Tile> {
 | 
			
		||||
    return await this.playerInteraction.chooseTile(board);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										145
									
								
								src/game/entities/player/PlayerAI.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/game/entities/player/PlayerAI.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,145 @@
 | 
			
		||||
import { PlayerMoveSide } from "../../constants";
 | 
			
		||||
import { printLine, wait } from "../../../common/utilities";
 | 
			
		||||
import { AbstractPlayer } from "../../entities/player/AbstractPlayer";
 | 
			
		||||
import { Board } from "../Board";
 | 
			
		||||
import { PlayerMove } from "../PlayerMove";
 | 
			
		||||
import { SimulatedBoard } from "../../SimulatedBoard";
 | 
			
		||||
import { Tile } from "../Tile";
 | 
			
		||||
import { PRNG } from "seedrandom";
 | 
			
		||||
 | 
			
		||||
export class PlayerAI extends AbstractPlayer {
 | 
			
		||||
  constructor(name: string, private rng: PRNG) {
 | 
			
		||||
    super(name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async makeMove(board: Board): Promise<PlayerMove | null> {
 | 
			
		||||
    await wait(500); // 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)];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  chooseTileMinMax(board: Board, depth: number = 3): Tile | null { // minmax algorithm
 | 
			
		||||
    const bestMove: Tile | null = null;
 | 
			
		||||
    let bestScore: number = -Infinity;
 | 
			
		||||
    const validMoves: PlayerMove[] = board.getValidMoves(this);
 | 
			
		||||
 | 
			
		||||
    validMoves.forEach(move => {
 | 
			
		||||
      const simulatedBoard = new SimulatedBoard([ ...board.tiles ], this.rng);
 | 
			
		||||
      simulatedBoard.play(move);
 | 
			
		||||
      const score = this.minmax(simulatedBoard, depth - 1, false);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  minmax(simulatedBoard: SimulatedBoard, depth: number, isMaximizing: boolean): number {
 | 
			
		||||
    if (depth === 0 || simulatedBoard.isGameOver) {
 | 
			
		||||
      return simulatedBoard.evaluate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isMaximizing) {
 | 
			
		||||
      let maxEval = -Infinity;
 | 
			
		||||
      const validMoves = simulatedBoard.getValidMoves(this);
 | 
			
		||||
      validMoves.forEach((move: PlayerMove) => {
 | 
			
		||||
        const newSimulatedBoard = new SimulatedBoard([ ...simulatedBoard.tiles ], this.rng);
 | 
			
		||||
        newSimulatedBoard.play(move);
 | 
			
		||||
        const evaluation: number = this.minmax(newSimulatedBoard, depth - 1, false);
 | 
			
		||||
        maxEval = Math.max(maxEval, evaluation);
 | 
			
		||||
      });
 | 
			
		||||
      return maxEval;
 | 
			
		||||
    }
 | 
			
		||||
     else {
 | 
			
		||||
      let minEval = Infinity;
 | 
			
		||||
      const validMoves = simulatedBoard.getValidMoves(this);
 | 
			
		||||
      validMoves.forEach((move: PlayerMove) => {
 | 
			
		||||
        const newSimulatedBoard = new SimulatedBoard([ ...simulatedBoard.tiles ], this.rng);
 | 
			
		||||
        newSimulatedBoard.play(move);
 | 
			
		||||
        const evaluation: number = this.minmax(newSimulatedBoard, depth - 1, true);
 | 
			
		||||
        minEval = Math.min(minEval, evaluation);
 | 
			
		||||
      });
 | 
			
		||||
      return minEval;
 | 
			
		||||
     }
 | 
			
		||||
 | 
			
		||||
    // if (isMaximizing) {
 | 
			
		||||
    //   let bestScore = -Infinity;
 | 
			
		||||
    //   boardTiles.forEach(tile => {
 | 
			
		||||
    //     const newBoardTiles = [ ...boardTiles ];
 | 
			
		||||
    //     newBoardTiles.push(tile);
 | 
			
		||||
    //     const score = this.minmax(newBoardTiles, depth - 1, false);
 | 
			
		||||
    //     bestScore = Math.max(score, bestScore);
 | 
			
		||||
    //   });
 | 
			
		||||
    //   return bestScore;
 | 
			
		||||
    // } else {
 | 
			
		||||
    //   let bestScore = Infinity;
 | 
			
		||||
    //   boardTiles.forEach(tile => {
 | 
			
		||||
    //     const newBoardTiles = [ ...boardTiles ];
 | 
			
		||||
    //     newBoardTiles.push(tile);
 | 
			
		||||
    //     const score = this.minmax(newBoardTiles, depth - 1, true);
 | 
			
		||||
    //     bestScore = Math.min(score, bestScore);
 | 
			
		||||
    //   });
 | 
			
		||||
      // return bestScore;
 | 
			
		||||
    // }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  evaluateBoard(board: Board): number {
 | 
			
		||||
    // Custom heuristic to evaluate the board state
 | 
			
		||||
    return board.tiles.length; // Simplistic example
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/game/entities/player/PlayerHuman.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/game/entities/player/PlayerHuman.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
import { PlayerMoveSide, PlayerMoveSideType } from '../../constants';
 | 
			
		||||
import { AbstractPlayer } from './AbstractPlayer';
 | 
			
		||||
import { Board } from '../Board';
 | 
			
		||||
import { PlayerMove } from '../PlayerMove';
 | 
			
		||||
import { Tile } from '../Tile';
 | 
			
		||||
import { PlayerInteractionConsole } from '../../PlayerInteractionConsole';
 | 
			
		||||
import { PlayerInteractionInterface } from '../../PlayerInteractionInterface';
 | 
			
		||||
 | 
			
		||||
export class PlayerHuman extends AbstractPlayer {
 | 
			
		||||
  playerInteraction: PlayerInteractionInterface = new PlayerInteractionConsole(this);
 | 
			
		||||
  constructor(name: string) {
 | 
			
		||||
    super(name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async makeMove(board: Board): Promise<PlayerMove | null> {    
 | 
			
		||||
    return await this.playerInteraction.makeMove(board);   
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async chooseTile(board: Board): Promise<Tile> {
 | 
			
		||||
    return this.playerInteraction.chooseTile(board);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								src/game/entities/player/PlayerInterface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/game/entities/player/PlayerInterface.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
 | 
			
		||||
import { Board } from "../Board";
 | 
			
		||||
import { GameState } from "../../dto/GameState";
 | 
			
		||||
import { PlayerMove } from "../PlayerMove";
 | 
			
		||||
import { PlayerState } from "../../dto/PlayerState";
 | 
			
		||||
import { Tile } from "../Tile";
 | 
			
		||||
import { GameSessionState } from "../../dto/GameSessionState";
 | 
			
		||||
 | 
			
		||||
export interface PlayerInterface {
 | 
			
		||||
  id: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  score: number;
 | 
			
		||||
  hand: Tile[];
 | 
			
		||||
  teamedWith: PlayerInterface | null;
 | 
			
		||||
  playerInteraction: PlayerInteractionInterface;
 | 
			
		||||
 | 
			
		||||
  makeMove(gameState: Board): Promise<PlayerMove | null>;
 | 
			
		||||
  chooseTile(board: Board): Promise<Tile>;
 | 
			
		||||
  pipsCount(): number;
 | 
			
		||||
  notifyGameState(state: GameState): Promise<void>;
 | 
			
		||||
  notifyPlayerState(state: PlayerState): Promise<void>;
 | 
			
		||||
  notifySessionState(state: GameSessionState): Promise<void>;
 | 
			
		||||
  getState(): PlayerState;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								src/server/controllers/ControllerBase.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/server/controllers/ControllerBase.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
import { LoggingService } from "../../common/LoggingService";
 | 
			
		||||
 | 
			
		||||
export class ControllerBase {
 | 
			
		||||
  protected logger = new LoggingService();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										77
									
								
								src/server/controllers/SessionController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/server/controllers/SessionController.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
import { LoggingService } from "../../common/LoggingService";
 | 
			
		||||
import { GameSession } from "../../game/GameSession";
 | 
			
		||||
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
 | 
			
		||||
 | 
			
		||||
import { ControllerBase } from "./ControllerBase";
 | 
			
		||||
 | 
			
		||||
export class SessionController extends ControllerBase{
 | 
			
		||||
  private static sessions: any = {};
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super();
 | 
			
		||||
    this.logger.info('SessionController created');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createSession(data: any, socketId: string): any {
 | 
			
		||||
    const { user, sessionName } = data;
 | 
			
		||||
    const player =  new NetworkPlayer(user, socketId);
 | 
			
		||||
    const session = new GameSession(player, sessionName);
 | 
			
		||||
    SessionController.sessions[session.id] = session;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      status: 'ok',
 | 
			
		||||
      sessionId: session.id,
 | 
			
		||||
      playerId: player.id
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  joinSession(data: any, socketId: string): any {
 | 
			
		||||
    this.logger.debug('joinSession data :>> ')
 | 
			
		||||
    this.logger.object(data);
 | 
			
		||||
    const { user, sessionId } = data;
 | 
			
		||||
    const session = SessionController.sessions[sessionId];
 | 
			
		||||
    const player =  new NetworkPlayer(user, socketId);
 | 
			
		||||
    session.addPlayer(player);
 | 
			
		||||
    return {
 | 
			
		||||
      status: 'ok',
 | 
			
		||||
      sessionId: session.id,
 | 
			
		||||
      playerId: player.id
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startSession(data: any): any {
 | 
			
		||||
    const sessionId: string = data.sessionId;
 | 
			
		||||
    const seed: string | undefined = data.seed;
 | 
			
		||||
    const session = SessionController.sessions[sessionId];
 | 
			
		||||
 | 
			
		||||
    if (!session) {
 | 
			
		||||
      return ({
 | 
			
		||||
        status: 'error',
 | 
			
		||||
        message: 'Session not found'
 | 
			
		||||
      });
 | 
			
		||||
    } else if (session.gameInProgress) {
 | 
			
		||||
      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(seed);
 | 
			
		||||
      return {
 | 
			
		||||
        status: 'ok'
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  getSession(id: string) {
 | 
			
		||||
    return SessionController.sessions[id];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deleteSession(id: string) {
 | 
			
		||||
    delete SessionController.sessions[id];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								src/server/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/server/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
 | 
			
		||||
    <title>Socket.IO chat</title>
 | 
			
		||||
    <style>
 | 
			
		||||
      textarea {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 60px;
 | 
			
		||||
        resize: none;
 | 
			
		||||
      }
 | 
			
		||||
      #response {
 | 
			
		||||
        height: 200px;
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <ul id="messages"></ul>
 | 
			
		||||
    <form id="form" action="">
 | 
			
		||||
      <p>
 | 
			
		||||
        <select id="event" autocomplete="off">
 | 
			
		||||
          <option value="">Select event</option>
 | 
			
		||||
        </select>
 | 
			
		||||
      </p>
 | 
			
		||||
      <!-- <p><input id="room" autocomplete="off" /></p> -->
 | 
			
		||||
      <p><textarea id="message" autocomplete="off" placeholder="Data"></textarea></p>
 | 
			
		||||
      <p><button>Send</button></p>
 | 
			
		||||
      <p><textarea id="response" autocomplete="off" placeholder="Response"></textarea></p>
 | 
			
		||||
    </form>
 | 
			
		||||
 | 
			
		||||
    <script src="/socket.io/socket.io.js"></script>
 | 
			
		||||
    <script>
 | 
			
		||||
        const socket = io();
 | 
			
		||||
 | 
			
		||||
        const form = document.getElementById("form");
 | 
			
		||||
        const event = document.getElementById("event");
 | 
			
		||||
        // const room = document.getElementById("room");
 | 
			
		||||
        const message = document.getElementById("message");
 | 
			
		||||
        const responseEl = document.getElementById("response");
 | 
			
		||||
        const messages = document.getElementById("messages");
 | 
			
		||||
 | 
			
		||||
        const options = [
 | 
			
		||||
          { value: "createSession", default: '{"user": "arhuako"}' },
 | 
			
		||||
          { value: "startSession", default: '{"sessionId": "arhuako"}' },
 | 
			
		||||
          { value: "joinSession", default: '{"user": "pepe", "sessionId": "arhuako"}' },
 | 
			
		||||
          { value: "leaveSession", default: '{"user": "pepe", "sessionId": "arhuako"}' },          
 | 
			
		||||
          { value: "chat message", default: "chat message" }
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //`<option value="${option.value}">${option.value}</option>`
 | 
			
		||||
        options.forEach((option) => {
 | 
			
		||||
          const opt = document.createElement("option");
 | 
			
		||||
          opt.value = option.value;
 | 
			
		||||
          opt.textContent = option.value;
 | 
			
		||||
          event.appendChild(opt);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        event.addEventListener("change", (e) => {
 | 
			
		||||
          const option = options.find((option) => option.value === e.target.value);
 | 
			
		||||
          message.value = option.default;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const getMessage = (msg) => {
 | 
			
		||||
          if (msg.startsWith("{") && msg.endsWith("}")) return JSON.parse(msg);
 | 
			
		||||
          return msg;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        form.addEventListener("submit", async (e) => {
 | 
			
		||||
          e.preventDefault();
 | 
			
		||||
          if (event.value.trim() && message.value.trim()) {
 | 
			
		||||
            const response = await socket.emitWithAck(event.value.trim(), getMessage(message.value.trim()));
 | 
			
		||||
            console.log('response :>> ', response);
 | 
			
		||||
            message.value = "";
 | 
			
		||||
            const responseStr =  JSON.stringify(response, null, 2);
 | 
			
		||||
            responseEl.value = !responseEl.value ? responseStr : responseEl.value + '\n---\n ' + responseStr;
 | 
			
		||||
            event.selectedIndex = 0;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        socket.onAny((eventName, msg) => {
 | 
			
		||||
          const item = document.createElement("li");
 | 
			
		||||
          item.textContent = `${eventName}: ${msg}`;
 | 
			
		||||
          messages.appendChild(item);
 | 
			
		||||
          window.scrollTo(0, document.body.scrollHeight);
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										25
									
								
								src/server/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/server/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import express from 'express';
 | 
			
		||||
import http from 'http';
 | 
			
		||||
import cors from 'cors';
 | 
			
		||||
import { join } from 'path';
 | 
			
		||||
 | 
			
		||||
import { NetworkClientNotifier } from '../game/NetworkClientNotifier';
 | 
			
		||||
import { SocketIoService } from './services/SocketIoService';
 | 
			
		||||
 | 
			
		||||
const clientNotifier = new NetworkClientNotifier();
 | 
			
		||||
const app = express();
 | 
			
		||||
const httpServer = http.createServer(app);
 | 
			
		||||
const socketIoService = new SocketIoService(httpServer);
 | 
			
		||||
clientNotifier.setSocket(socketIoService.getServer());
 | 
			
		||||
 | 
			
		||||
const PORT = process.env.PORT || 3000;
 | 
			
		||||
console.log('__dirname :>> ', __dirname);
 | 
			
		||||
 | 
			
		||||
app.use(cors());
 | 
			
		||||
app.get('/', (req, res) => {
 | 
			
		||||
  res.sendFile(join(__dirname, 'index.html'));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
httpServer.listen(PORT, () => {
 | 
			
		||||
  console.log(`listening on *:${PORT}`);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										5
									
								
								src/server/services/ServiceBase.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/server/services/ServiceBase.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
import { LoggingService } from "../../common/LoggingService";
 | 
			
		||||
 | 
			
		||||
export class ServiceBase {
 | 
			
		||||
  protected logger = new LoggingService();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										75
									
								
								src/server/services/SocketIoService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/server/services/SocketIoService.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
import { Server as HttpServer } from "http";
 | 
			
		||||
import { ServiceBase } from "./ServiceBase";
 | 
			
		||||
import { Server } from "socket.io";
 | 
			
		||||
import { SessionController } from "../controllers/SessionController";
 | 
			
		||||
 | 
			
		||||
export class SocketIoService  extends ServiceBase{
 | 
			
		||||
  io: Server
 | 
			
		||||
  constructor(private httpServer: HttpServer) {
 | 
			
		||||
    super()
 | 
			
		||||
    this.io = this.socketIo(httpServer);   
 | 
			
		||||
    this.initListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getServer(): Server {
 | 
			
		||||
    return this.io;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private initListeners() {
 | 
			
		||||
    const sessionController = new SessionController();
 | 
			
		||||
    this.io.on('connection', (socket) => {
 | 
			
		||||
      console.log(`connect ${socket.id}`);
 | 
			
		||||
      if (socket.recovered) {
 | 
			
		||||
        // recovery was successful: socket.id, socket.rooms and socket.data were restored
 | 
			
		||||
        console.log("recovered!");
 | 
			
		||||
        console.log("socket.rooms:", socket.rooms);
 | 
			
		||||
        console.log("socket.data:", socket.data);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log("new connection");
 | 
			
		||||
        socket.join('room-general')
 | 
			
		||||
        socket.data.foo = "bar";
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    
 | 
			
		||||
      socket.on('disconnect', () => {
 | 
			
		||||
        console.log('user disconnected');
 | 
			
		||||
      });
 | 
			
		||||
    
 | 
			
		||||
      socket.on('createSession', (data, callback) => {
 | 
			
		||||
        const response = sessionController.createSession(data, socket.id);
 | 
			
		||||
        callback(response);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      socket.on('startSession', (data, callback) => {
 | 
			
		||||
        const response = sessionController.startSession(data);    
 | 
			
		||||
        callback(response);
 | 
			
		||||
      });
 | 
			
		||||
    
 | 
			
		||||
       socket.on('joinSession', (data, callback) => {
 | 
			
		||||
        const response = sessionController.joinSession(data, socket.id);
 | 
			
		||||
        callback(response);
 | 
			
		||||
      });
 | 
			
		||||
    
 | 
			
		||||
      // socket.on('chat message', (msg, callback) => {
 | 
			
		||||
      //   io.emit('chat message', msg);
 | 
			
		||||
      //   callback({
 | 
			
		||||
      //     status: 'ok',
 | 
			
		||||
      //     message: 'Message received',
 | 
			
		||||
      //   })
 | 
			
		||||
      // });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  private socketIo(httpServer: HttpServer): Server {
 | 
			
		||||
    return new Server(httpServer, {
 | 
			
		||||
      cors: {
 | 
			
		||||
        origin: '*',
 | 
			
		||||
      },
 | 
			
		||||
      connectionStateRecovery: {
 | 
			
		||||
        maxDisconnectionDuration: 15 * 60 * 1000,
 | 
			
		||||
        skipMiddlewares: true,
 | 
			
		||||
      },
 | 
			
		||||
      connectTimeout: 15 * 60 * 1000,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}   
 | 
			
		||||
							
								
								
									
										56
									
								
								src/test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
import { PlayerAI } from "./game/entities/player/PlayerAI";
 | 
			
		||||
import { PlayerHuman } from "./game/entities/player/PlayerHuman";
 | 
			
		||||
import {LoggingService} from "./common/LoggingService";
 | 
			
		||||
import { GameSession } from "./game/GameSession";
 | 
			
		||||
 | 
			
		||||
console.log('process.arg :>> ', process.argv);
 | 
			
		||||
 | 
			
		||||
// const game = new DominoesGame([
 | 
			
		||||
//   new PlayerAI("1", "Player 1"),
 | 
			
		||||
//   new PlayerAI("2", "Player 2"),
 | 
			
		||||
//   new PlayerAI("3", "Player 3"),
 | 
			
		||||
//   new PlayerAI("4", "Player 4"),
 | 
			
		||||
// ]);
 | 
			
		||||
// const logger = new LoggingService();
 | 
			
		||||
 | 
			
		||||
// async function wait(ms: number) {s
 | 
			
		||||
//   return new Promise(resolve => setTimeout(resolve, ms));
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
async function playSolo(seed?: string) {
 | 
			
		||||
  const session = new GameSession(new PlayerHuman( "Jose"), "Test Game");
 | 
			
		||||
  console.log(`Session (${session.id}) created by: ${session.creator.name}`);
 | 
			
		||||
  setTimeout(() => session.addPlayer(new PlayerAI("AI 2")), 1000);
 | 
			
		||||
  setTimeout(() => session.addPlayer(new PlayerAI("AI 3")), 2000);
 | 
			
		||||
  setTimeout(() => session.addPlayer(new PlayerAI("AI 4")), 3000);
 | 
			
		||||
  session.start(seed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function playHumans(seed?: string) {
 | 
			
		||||
  const session = new GameSession(new PlayerHuman("Jose"), "Test Game");
 | 
			
		||||
  session.addPlayer(new PlayerHuman("Pepe"));
 | 
			
		||||
  session.addPlayer(new PlayerHuman("Juan"));
 | 
			
		||||
  session.addPlayer(new PlayerHuman("Luis"));
 | 
			
		||||
  session.start(seed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function playAIs(seed?: string) {
 | 
			
		||||
  const session = new GameSession(new PlayerAI("AI 1"), "Test Game");
 | 
			
		||||
  session.addPlayer(new PlayerAI("AI 2"));
 | 
			
		||||
  session.addPlayer(new PlayerAI("AI 3"));
 | 
			
		||||
  session.addPlayer(new PlayerAI("AI 4"));
 | 
			
		||||
  session.start(seed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function playTeams(seed?: string) {
 | 
			
		||||
  const session = new GameSession(new PlayerHuman("Jose"), "Test Game");
 | 
			
		||||
  session.addPlayer(new PlayerAI("AI 1"));
 | 
			
		||||
  session.addPlayer(new PlayerHuman("Juan"));
 | 
			
		||||
  session.addPlayer(new PlayerAI("AI 2"));
 | 
			
		||||
  session.start(seed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const blockedSeed = '1719236688462-ytwrwzfzoi-01aad98f';
 | 
			
		||||
const seed2 = '1719237652000-09vddd3hsth7-adbc1842';
 | 
			
		||||
 | 
			
		||||
playSolo('1719248315701-itmcciws3oi-e5dd2024');
 | 
			
		||||
		Reference in New Issue
	
	Block a user