domino-server/src/game/MatchSession.ts
Jose Conde 39060a4064 0.1.3
2024-07-18 19:42:32 +02:00

410 lines
13 KiB
TypeScript

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, whileNot } from "../common/utilities";
import { MatchSessionState } from "./dto/MatchSessionState";
import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
import seedrandom, { PRNG } from "seedrandom";
import { PlayerHuman } from "./entities/player/PlayerHuman";
import { GameSummary } from "./dto/GameSummary";
import { PlayerMove } from "./entities/PlayerMove";
import { SessionService } from "../server/services/SessionService";
import { Score } from "../server/db/interfaces";
import { MatchSessionOptions } from "./dto/MatchSessionOptions";
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 notificationService = new PlayerNotificationService();
private winnerIndex: number | null = null;
private clientsReady: string[] = [];
private gameSummaries: GameSummary[] = [];
private sessionService: SessionService = new SessionService();
id: string;
matchInProgress: boolean = false;
gameInProgress: boolean = false;
matchWinner?: PlayerInterface = undefined;
maxPlayers: number = 4;
mode: string = 'classic';
players: PlayerInterface[] = [];
// pointsToWin: number = 50;
rng!: PRNG
scoreboard: Map<string, number> = new Map();
seed!: string
sessionInProgress: boolean = false;
status: string = 'created'
name: string
constructor(public creator: PlayerInterface, private options: MatchSessionOptions) {
const { sessionName, seed, winType, winTarget } = options;
this.seed = seed || getRandomSeed();
this.id = uuid();
this.name = sessionName || `Match ${this.id}`;
this.addPlayerToSession(creator);
this.creator = creator;
this.logger.info(`Match session created by: ${creator.name}`);
this.logger.info(`Match session ID: ${this.id}`);
this.logger.info(`Match session name: ${this.name}`);
this.logger.info(`Win type: ${options.winType}`);
this.logger.info(`Win target: ${options.winTarget}`);
this.sessionInProgress = false;
this.waitingForPlayers = true;
this.status = 'created';
this.logger.info('Waiting for players to be ready');
}
get numPlayers() {
return this.players.length;
}
get numPlayersReady() {
return this.players.filter(player => player.ready).length;
}
get numHumanPlayers() {
return this.players.filter(player => player instanceof PlayerHuman).length;
}
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.logger.trace(`${this.clientsReady.length}`);
}
setCurrentGameClientReady(userId: string) {
if (this.currentGame) {
this.currentGame.setClientReady(userId);
}
}
async checkAllClientsReady() {
try {
if (this.currentGame) {
const conditionFn = () => {
this.logger.trace(`Clients ready: ${this.clientsReady.length}/${this.numHumanPlayers}`);
return this.clientsReady.length === this.numHumanPlayers
}
await whileNot(conditionFn, 50);
this.logger.info(`Game #${this.gameNumber} started`);
this.currentGame.start();
this.gameInProgress = true;
this.clientsReady = [];
}
} catch (error) {
this.logger.error(error, 'Error starting game');
throw new Error('Error starting game (checkAllClientsReadyBeforeStart)');
}
}
getPlayer(userId: string) {
return this.players.find(player => player.id === userId) || null;
}
playerMove(move: any) {
this.logger.trace('Handling player move (playerMove)');
this.logger.trace(`${this.clientsReady.length}`);
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, move.direction)
this.currentGame.finishTurn(newMove);
}
}
// This is the entry point for the game, method called by session host
async start(data: any) {
if (this.matchInProgress) {
throw new Error("Game already in progress");
}
this.waitingForPlayers = false;
this.sessionInProgress = true;
this.status = 'started'
this.sessionService.updateSession(this);
await this.startMatch(this.seed);
}
setTeams(data: any) {
if (data.teamedWith !== undefined) {
const creatorTeam = this.getPlayer(data.teamedWith)
if (!creatorTeam) {
throw new Error("Teamed player not found");
}
this.creator.teamedWith = data.teamedWith;
this.creator.team = 1
creatorTeam.teamedWith = this.creator.id;
creatorTeam.team = 1;
const others = this.players.filter(player => player.team === 0);
others[0].teamedWith = others[1].id;
others[0].team = 2;
others[1].teamedWith = others[0].id;
others[1].team = 2;
this.players = [this.creator, others[0], creatorTeam, others[1]];
}
}
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.gameSummaries.push(gameSummary);
this.winnerIndex = this.players.findIndex(player => player.id === gameSummary?.winner?.id);
this.setScores(gameSummary || undefined);
this.checkMatchWinner();
this.resetPlayers();
try {
if (!this.matchInProgress) {
this.status = 'end'
this.notificationService.sendEventToPlayers('server:match-finished', this.players, {
lastGame: gameSummary,
sessionState: this.getState(true),
});
} else {
this.status = 'waiting'
// await this.playerNotificationManager.notifyMatchState(this);
this.notificationService.sendEventToPlayers('server:game-finished', this.players, {
lastGame: gameSummary,
sessionState: this.getState(true)
});
this.waitingForPlayers = true;
this.startGame();
}
} finally {
this.sessionService.updateSession(this);
}
}
private startGame() {
try {
this.gameNumber += 1;
this.logger.info(`Game #${this.gameNumber} started`);
this.currentGame = new DominoesGame(this.players, this.rng);
if (this.winnerIndex !== null) {
this.currentGame.setForcedInitialPlayerIndex(this.winnerIndex);
}
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.checkAllClientsReady();
} catch (error: any) {
this.logger.error(error);
this.matchInProgress = false;
this.status = 'error'
this.notificationService.sendEventToPlayers('server:game-error', this.players, {
matchState: this.getState(),
error: error.message || error,
})
}
}
private async startMatch(seed: string) {
try {
this.status = '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.status = 'error'
}
}
resetPlayers() {
this.players.forEach(player => {
player.reset()
});
}
checkMatchWinner() {
const scores = Array.from(this.scoreboard.values());
const maxScore = Math.max(...scores);
if (this.options.winType === 'rounds') {
this.checkRoundsWinner(maxScore);
} else if (this.options.winType === 'points') {
this.checkPointsWinner(maxScore);
}
}
checkRoundsWinner(maxScore: number) {
if (maxScore >= this.options.winTarget) {
this.matchWinner = this.players.find(player => this.scoreboard.get(player.name) === maxScore);
if (!this.matchWinner) {
throw new Error('Match winner not found');
}
this.logger.info(`Match winner: ${this.matchWinner.name} with ${maxScore} points`);
this.matchInProgress = false;
}
}
checkPointsWinner(maxScore: number) {
if (maxScore >= this.options.winTarget) {
this.matchWinner = this.players.find(player => this.scoreboard.get(player.name) === maxScore);
if (!this.matchWinner) {
throw new Error('Match winner not found');
}
this.logger.info(`Match winner: ${this.matchWinner.name} with ${maxScore} points`);
this.matchInProgress = false;
}
}
resetScoreboard() {
this.scoreboard = new Map();
this.players.forEach(player => {
this.scoreboard.set(player.name, 0);
});
}
setScores(gameSummary?: GameSummary) {
if (!gameSummary) {
return;
}
if (this.options.winType === 'rounds') {
this.setScoresRounds(gameSummary);
} else if (this.options.winType === 'points') {
this.setScoresPoints(gameSummary);
}
}
setScoresRounds(gameSummary: GameSummary) {
const { winner } = gameSummary;
if (winner !== undefined) {
const currentScore = this.scoreboard.get(winner.name) ?? 0;
this.scoreboard.set(winner.name, 1 + currentScore);
}
}
setScoresPoints(gameSummary: GameSummary) {
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 {
if (this.currentGame !== null) {
const { gameBlocked, gameTied, winner } = this.currentGame;
gameBlocked ? this.logger.info('Game blocked!') : gameTied ? this.logger.info('Game tied!') : this.logger.info('Game over!');
this.logger.info('Winner: ' + winner?.name + ' with ' + winner?.pipsCount() + ' points');
this.getScore(this.currentGame);
this.logger.info('Game ended');
this.currentGame = null;
}
}
private getScore(game: DominoesGame) {
const pips = game.players
.sort((a,b) => (b.pipsCount() - a.pipsCount()))
.map(player => {
return `${player.name}: ${player.pipsCount()}`;
});
this.logger.info(`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}`;
});
this.logger.info(`Scores: ${scores.join(', ')}`);
}
createPlayerAI(i: number) {
const AInames = ["Alice (AI)", "Bob (AI)", "Charlie (AI)", "David (AI)"];
const player = new PlayerAI(AInames[i], this.rng, this.id);
player.ready = true;
return player;
}
setPlayerReady(userId: string) {
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.notificationService.notifyMatchState(this);
if (this.matchInProgress && this.numPlayersReady === this.numHumanPlayers) {
this.startGame();
}
}
toString() {
return `GameSession:(${this.id} ${this.name})`;
}
getState(showPips?: boolean): MatchSessionState {
return {
id: this.id,
name: this.name!,
creator: this.creator.id,
players: this.players.map(player => player.getState(showPips)),
playersReady: this.numPlayersReady,
sessionInProgress: this.sessionInProgress,
maxPlayers: this.maxPlayers,
numPlayers: this.numPlayers,
waitingForPlayers: this.waitingForPlayers,
waitingSeconds: this.waitingSeconds,
seed: this.seed,
mode: this.mode,
status: this.status,
scoreboard: this.getScoreBoardState(),
matchWinner: this.matchWinner?.getState(true) || null,
matchInProgress: this.matchInProgress,
gameSummaries: this.gameSummaries,
options: this.options,
};
}
getScoreBoardState(): Score[] {
return Array.from(this.scoreboard).map(([name, score]) => {
const player = this.players.find(player => player.name === name);
const id = player?.id || name;
return { id, name, score } as Score;
});
}
}