domino-server/src/game/DominoesGame.ts
Jose Conde ca0f1466c2 flow
2024-07-08 10:21:56 +02:00

249 lines
8.0 KiB
TypeScript

import { PRNG } from 'seedrandom';
import { Board } from "./entities/Board";
import { PlayerMove } from "./entities/PlayerMove";
import { PlayerInterface } from "./entities/player/PlayerInterface";
import { Tile } from "./entities/Tile";
import { LoggingService } from "../common/LoggingService";
import { printBoard, printLine, uuid, wait } from '../common/utilities';
import { GameSummary } from './dto/GameSummary';
import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
import { GameState } from './dto/GameState';
export class DominoesGame {
private id: string;
private seed: string | undefined;
autoDeal: boolean = true;
board: Board;
currentPlayerIndex: number = 0;
gameInProgress: boolean = false;
gameOver: boolean = false;
gameBlocked: boolean = false;
gameTied: boolean = false;
tileSelectionPhase: boolean = true;
logger: LoggingService = new LoggingService();
blockedCount: number = 0;
winner: PlayerInterface | null = null;
rng: PRNG;
handSize: number = 7;
notificationManager: PlayerNotificationService = new PlayerNotificationService();
lastMove: PlayerMove | null = null;
forcedInitialPlayerIndex: number | null = null;
constructor(public players: PlayerInterface[], seed: PRNG) {
this.id = uuid();
this.logger.info(`Game ID: ${this.id}`);
this.rng = seed
this.board = new Board(seed);
this.initializeGame();
}
async initializeGame() {
this.gameOver = false;
this.gameBlocked = false;
this.gameTied = false;
this.board.boneyard = this.generateTiles();
}
setForcedInitialPlayerIndex(index: number) {
this.forcedInitialPlayerIndex = index;
}
reset() {
this.board.reset();
this.initializeGame();
for (let player of this.players) {
player.hand = [];
}
}
generateTiles(): Tile[] {
const tiles: Tile[] = [];
for (let i = 6; i >= 0; i--) {
for (let j = i; j >= 0; j--) {
tiles.push(new Tile([i, j]));
}
}
this.logger.debug('tiles :>> ' + tiles);
return this.shuffle(tiles);
}
private shuffle(array: Tile[]): Tile[] {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(this.rng() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
nextPlayer() {
this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length;
}
isBlocked(): boolean {
return this.blockedCount === this.players.length;
}
isGameOver(): boolean {
const hasWinner: boolean = this.players.some(player => player.hand.length === 0);
return hasWinner || this.gameBlocked;
}
getWinner(): PlayerInterface | null {
if (!this.gameOver) {
return null;
}
const winnerNoTiles = this.players.find(player => player.hand.length === 0);
if (winnerNoTiles !== undefined) {
return winnerNoTiles;
}
const winnerMinPipsCount = this.players.reduce((acc, player) => {
return player.pipsCount() < acc.pipsCount() ? player : acc;
});
return winnerMinPipsCount;
}
getStartingPlayerIndex(): number {
// Determine starting player
let startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 6 && tile.pips[1] === 6));
if (startingIndex === -1) {
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 5 && tile.pips[1] === 5));
if (startingIndex === -1) {
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 4 && tile.pips[1] === 4));
if (startingIndex === -1) {
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 3 && tile.pips[1] === 3));
if (startingIndex === -1) {
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 2 && tile.pips[1] === 2));
if (startingIndex === -1) {
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 1 && tile.pips[1] === 1));
if (startingIndex === -1) {
startingIndex = this.players.findIndex(player => player.hand.some(tile => tile.pips[0] === 0 && tile.pips[1] === 0));
}
}
}
}
}
}
return startingIndex === -1 ? 0 : startingIndex;
}
async playTurn(): Promise<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();
}
resetPlayersScore() {
for (let player of this.players) {
player.score = 0;
}
}
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
};
}
dealTiles() {
for (let i = 0; i < this.handSize; i++) {
for (let player of this.players) {
const tile: Tile | undefined = this.board.boneyard.pop();
if (tile !== undefined) {
tile.revealed = true;
player.hand.push(tile);
}
}
}
}
async tilesSelection() {
while (this.board.boneyard.length > 0) {
for (let player of this.players) {
const choosen = await player.chooseTile(this.board);
await this.notificationManager.notifyGameState(this);
await this.notificationManager.notifyPlayersState(this.players);
if (this.board.boneyard.length === 0) {
break;
}
}
}
}
getGameState(): GameState {
const currentPlayer = this.players[this.currentPlayerIndex]
return {
id: uuid(),
lastMove: this.lastMove,
gameInProgress: this.gameInProgress,
winner: this.winner,
tileSelectionPhase: this.tileSelectionPhase,
gameBlocked: this.gameBlocked,
gameTied: this.gameTied,
gameId: this.id,
boneyard: this.board.boneyard.map(tile => ({ id: tile.id})),
players: this.players.map(player => player.getState()),
currentPlayer: currentPlayer.getState(),
board: this.board.tiles.map(tile => ({
id: tile.id,
pips: tile.pips
})),
boardFreeEnds: this.board.getFreeEnds(),
}
}
}