319 lines
10 KiB
TypeScript
319 lines
10 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 { 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 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';
|
|
players: PlayerInterface[] = [];
|
|
pointsToWin: number = 50;
|
|
rng!: PRNG
|
|
scoreboard: Map<string, number> = new Map();
|
|
seed!: string
|
|
sessionInProgress: boolean = false;
|
|
state: string = 'created'
|
|
|
|
constructor(public creator: PlayerInterface, public name?: string, seed?: string) {
|
|
this.seed = seed || getRandomSeed();
|
|
this.id = uuid();
|
|
this.name = name || `Game ${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(`Points to win: ${this.pointsToWin}`);
|
|
this.sessionInProgress = true;
|
|
this.waitingForPlayers = true;
|
|
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);
|
|
}
|
|
}
|
|
|
|
async checkAllClientsReadyBeforeStart() {
|
|
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, 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 => {
|
|
player.reset()
|
|
});
|
|
}
|
|
|
|
checkMatchWinner() {
|
|
const scores = Array.from(this.scoreboard.values());
|
|
const maxScore = Math.max(...scores);
|
|
if (maxScore >= this.pointsToWin) {
|
|
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;
|
|
}
|
|
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(): MatchSessionState {
|
|
return {
|
|
id: this.id,
|
|
name: this.name!,
|
|
creator: this.creator.id,
|
|
players: this.players.map(player =>( {
|
|
id: player.id,
|
|
name: player.name,
|
|
ready: player.ready,
|
|
})),
|
|
playersReady: this.numPlayersReady,
|
|
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',
|
|
scoreboard: [...this.scoreboard.entries()],
|
|
matchWinner: this.matchWinner?.getState() || null,
|
|
matchInProgress: this.matchInProgress
|
|
};
|
|
}
|
|
} |