This commit is contained in:
Jose Conde
2024-07-12 16:27:52 +02:00
parent ca0f1466c2
commit 5f117667a4
29 changed files with 823 additions and 407 deletions

View File

@ -2,24 +2,30 @@ import { DominoesGame } from "./DominoesGame";
import { PlayerAI } from "./entities/player/PlayerAI";
import { PlayerInterface } from "./entities/player/PlayerInterface";
import { LoggingService } from "../common/LoggingService";
import { getRandomSeed, uuid, wait } from "../common/utilities";
import { getRandomSeed, uuid, wait, whileNot } from "../common/utilities";
import { MatchSessionState } from "./dto/MatchSessionState";
import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
import seedrandom, { PRNG } from "seedrandom";
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
import { PlayerHuman } from "./entities/player/PlayerHuman";
import { GameSummary } from "./dto/GameSummary";
import { PlayerMove } from "./entities/PlayerMove";
export class MatchSession {
private currentGame: DominoesGame | null = null;
private minHumanPlayers: number = 1;
private gameNumber: number = 0;
private waitingForPlayers: boolean = true;
private waitingSeconds: number = 0;
private logger: LoggingService = new LoggingService();
private playerNotificationManager = new PlayerNotificationService();
private notificationService = new PlayerNotificationService();
private winnerIndex: number | null = null;
private clientsReady: string[] = [];
id: string;
matchInProgress: boolean = false;
gameInProgress: boolean = false;
matchWinner?: PlayerInterface = undefined;
maxPlayers: number = 4;
mode: string = 'classic';
@ -35,7 +41,7 @@ export class MatchSession {
this.seed = seed || getRandomSeed();
this.id = uuid();
this.name = name || `Game ${this.id}`;
this.addPlayer(creator);
this.addPlayerToSession(creator);
this.creator = creator;
this.logger.info(`Match session created by: ${creator.name}`);
@ -43,7 +49,8 @@ export class MatchSession {
this.logger.info(`Match session name: ${this.name}`);
this.logger.info(`Points to win: ${this.pointsToWin}`);
this.sessionInProgress = true;
this.matchInProgress = false;
this.waitingForPlayers = true;
this.logger.info('Waiting for players to be ready');
}
get numPlayers() {
@ -58,56 +65,137 @@ export class MatchSession {
return this.players.filter(player => player instanceof PlayerHuman).length;
}
private async startMatch(seed: string) {
this.rng = seedrandom(seed);
const missingPlayers = this.maxPlayers - this.numPlayers;
for (let i = 0; i < missingPlayers; i++) {
this.addPlayer(this.createPlayerAI(i));
setClientReady(userId: string) {
this.logger.trace(`${userId} - ${this.clientsReady}`);
if (!this.clientsReady.includes(userId)) {
this.logger.trace(`Client ${userId} is ready`)
this.clientsReady.push(userId);
}
this.state = 'ready'
this.resetScoreboard()
let gameNumber: number = 0;
this.matchInProgress = true
this.playerNotificationManager.notifyMatchState(this);
let winnerIndex: number | null = null;
while (this.matchInProgress) {
this.currentGame = new DominoesGame(this.players, this.rng);
if (winnerIndex !== null) {
this.currentGame.setForcedInitialPlayerIndex(winnerIndex);
}
gameNumber += 1;
this.state = 'started'
this.logger.info(`Game #${gameNumber} started`);
// this.game.reset()
const gameSummary = await this.currentGame.start();
winnerIndex = this.players.findIndex(player => player.id === gameSummary.winner?.id);
this.setScores();
this.checkMatchWinner();
this.resetPlayers();
this.state = 'waiting'
await this.playerNotificationManager.notifyMatchState(this);
this.playerNotificationManager.sendEventToPlayers('game-finished', this.players);
if (this.matchInProgress) {
await this.checkHumanPlayersReady();
}
}
this.state = 'end'
// await this.game.start();
return this.endGame();
}
async checkHumanPlayersReady() {
this.logger.info('Waiting for human players to be ready');
return new Promise((resolve) => {
const interval = setInterval(() => {
this.logger.debug(`Human players ready: ${this.numPlayersReady}/${this.numHumanPlayers}`)
if (this.numPlayersReady === this.numHumanPlayers) {
clearInterval(interval);
resolve(true);
async checkAllClientsReadyBeforeStart() {
try {
if (this.currentGame) {
const conditionFn = () => {
this.logger.trace(`Clients ready: ${this.clientsReady.length}/${this.numHumanPlayers}`);
return this.clientsReady.length === this.numHumanPlayers
}
}, 1000);
});
await whileNot(conditionFn, 10);
this.logger.info(`Game #${this.gameNumber} started`);
this.currentGame.start();
this.gameInProgress = true;
}
} catch (error) {
this.logger.error(error, 'Error starting game');
}
}
getPlayer(userId: string) {
return this.players.find(player => player.id === userId);
}
playerMove(move: any) {
if (this.currentGame) {
if ((move === null) || (move === undefined) || move.type === 'pass') {
this.currentGame.finishTurn(null);
return;
}
const player = this.getPlayer(move.playerId);
if (!player) {
throw new Error("Player not found");
}
const tile = player.hand.find(tile => tile.id === move.tile.id);
if (!tile) {
throw new Error("Tile not found");
}
const newMove = new PlayerMove(tile, move.type, move. playerId)
this.currentGame.finishTurn(newMove);
}
}
// This is the entry point for the game, method called by session host
async start() {
if (this.matchInProgress) {
throw new Error("Game already in progress");
}
this.waitingForPlayers = false;
await this.startMatch(this.seed);
}
addPlayerToSession(player: PlayerInterface) {
if (this.numPlayers >= this.maxPlayers) {
throw new Error("GameSession is full");
}
this.players.push(player);
this.logger.info(`${player.name} joined the game!`);
}
private continueMatch(gameSummary: GameSummary) {
this.winnerIndex = this.players.findIndex(player => player.id === gameSummary?.winner?.id);
if (this.winnerIndex !== null) {
this.currentGame?.setForcedInitialPlayerIndex(this.winnerIndex);
}
this.setScores(gameSummary || undefined);
this.checkMatchWinner();
this.resetPlayers();
if (!this.matchInProgress) {
this.state = 'end'
this.notificationService.sendEventToPlayers('server:match-finished', this.players, {
lastGame: gameSummary,
sessionState: this.getState(),
});
} else {
this.state = 'waiting'
// await this.playerNotificationManager.notifyMatchState(this);
this.notificationService.sendEventToPlayers('server:game-finished', this.players, {
lastGame: gameSummary,
sessionState: this.getState()
});
this.waitingForPlayers = true;
}
}
private startGame() {
this.gameNumber += 1;
this.logger.info(`Game #${this.gameNumber} started`);
this.currentGame = new DominoesGame(this.players, this.rng);
this.currentGame.on('game-over', (gameSummary: GameSummary) => {
this.gameInProgress = false;
this.continueMatch(gameSummary);
});
this.logger.info(`Waiting for ${this.numHumanPlayers} clients to be ready`);
this.checkAllClientsReadyBeforeStart();
}
private async startMatch(seed: string) {
try {
this.state = 'in-game'
this.rng = seedrandom(seed);
this.resetScoreboard()
this.gameNumber = 0;
this.matchInProgress = true
// this.playerNotificationManager.notifyMatchState(this);
this.winnerIndex = null;
this.startGame();
} catch (error) {
this.logger.error(error);
this.matchInProgress = false;
this.state = 'error'
}
}
// async checkHumanPlayersReady() {
// this.logger.info('Waiting for human players to be ready');
// return new Promise((resolve) => {
// const interval = setInterval(() => {
// this.logger.debug(`Human players ready: ${this.numPlayersReady}/${this.numHumanPlayers}`)
// if (this.numPlayersReady === this.numHumanPlayers) {
// clearInterval(interval);
// resolve(true);
// }
// }, 1000);
// });
// }
resetPlayers() {
this.players.forEach(player => {
@ -134,17 +222,19 @@ export class MatchSession {
});
}
setScores() {
const totalPips = this.currentGame?.players.reduce((acc, player) => acc + player.pipsCount(), 0) || 0;
if (this.currentGame && this.currentGame.winner !== null) {
const winner = this.currentGame.winner;
const currentPips = this.scoreboard.get(winner.name) || 0;
this.logger.debug (`${winner.name} has ${currentPips} points`);
this.scoreboard.set(winner.name, currentPips + totalPips);
if (winner.teamedWith !== null) {
this.scoreboard.set(winner.teamedWith.name, currentPips + totalPips);
}
setScores(gameSummary?: GameSummary) {
if (!gameSummary) {
return;
}
const { score } = gameSummary;
score.forEach(playerScore => {
const currentScore = this.scoreboard.get(playerScore.name) ?? 0;
this.scoreboard.set(playerScore.name, currentScore + playerScore.score);
});
}
afterTileAnimation(data: any) {
this.currentGame?.setCanAskNextPlayerMove(true);
}
private endGame(): any {
@ -155,13 +245,6 @@ export class MatchSession {
this.getScore(this.currentGame);
this.logger.info('Game ended');
this.currentGame = null;
this.playerNotificationManager.notifyMatchState(this);
return {
gameBlocked,
gameTied,
winner
};
}
}
@ -186,46 +269,23 @@ export class MatchSession {
createPlayerAI(i: number) {
const AInames = ["Alice (AI)", "Bob (AI)", "Charlie (AI)", "David (AI)"];
const player = new PlayerAI(AInames[i], this.rng);
const player = new PlayerAI(AInames[i], this.rng, this.id);
player.ready = true;
return player;
}
async start() {
if (this.matchInProgress) {
throw new Error("Game already in progress");
}
this.waitingForPlayers = true;
this.logger.info('Waiting for players to be ready');
while (this.numPlayers < this.maxPlayers) {
this.waitingSeconds += 1;
this.logger.info(`Waiting for players to join: ${this.waitingSeconds}`);
await wait(1000);
}
this.waitingForPlayers = false;
this.logger.info('All players joined');
await this.startMatch(this.seed);
}
addPlayer(player: PlayerInterface) {
if (this.numPlayers >= this.maxPlayers) {
throw new Error("GameSession is full");
}
this.players.push(player);
this.logger.info(`${player.name} joined the game!`);
}
setPlayerReady(userId: string) {
this.logger.debug(userId)
const player = this.players.find(player => player.id === userId);
if (!player) {
throw new Error("Player not found");
}
player.ready = true;
this.logger.info(`${player.name} is ready!`);
this.playerNotificationManager.notifyMatchState(this);
}
this.notificationService.notifyMatchState(this);
if (this.matchInProgress && this.numPlayersReady === this.numHumanPlayers) {
this.startGame();
}
}
toString() {
return `GameSession:(${this.id} ${this.name})`;