reworked
This commit is contained in:
@ -1,15 +1,15 @@
|
||||
import { PRNG } from 'seedrandom';
|
||||
import {EventEmitter} from 'events';
|
||||
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 { printBoard, printLine, uuid, wait, whileNotUndefined } from '../common/utilities';
|
||||
import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
|
||||
import { GameState } from './dto/GameState';
|
||||
|
||||
export class DominoesGame {
|
||||
export class DominoesGame extends EventEmitter {
|
||||
private id: string;
|
||||
private seed: string | undefined;
|
||||
autoDeal: boolean = true;
|
||||
@ -25,11 +25,13 @@ export class DominoesGame {
|
||||
winner: PlayerInterface | null = null;
|
||||
rng: PRNG;
|
||||
handSize: number = 7;
|
||||
notificationManager: PlayerNotificationService = new PlayerNotificationService();
|
||||
notificationService: PlayerNotificationService = new PlayerNotificationService();
|
||||
lastMove: PlayerMove | null = null;
|
||||
forcedInitialPlayerIndex: number | null = null;
|
||||
canAskNextPlayerMove: boolean = true;
|
||||
|
||||
constructor(public players: PlayerInterface[], seed: PRNG) {
|
||||
super();
|
||||
this.id = uuid();
|
||||
this.logger.info(`Game ID: ${this.id}`);
|
||||
this.rng = seed
|
||||
@ -76,6 +78,7 @@ export class DominoesGame {
|
||||
}
|
||||
|
||||
nextPlayer() {
|
||||
this.logger.debug('Turn ended');
|
||||
this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length;
|
||||
}
|
||||
|
||||
@ -88,10 +91,7 @@ export class DominoesGame {
|
||||
return hasWinner || this.gameBlocked;
|
||||
}
|
||||
|
||||
getWinner(): PlayerInterface | null {
|
||||
if (!this.gameOver) {
|
||||
return null;
|
||||
}
|
||||
getWinner(): PlayerInterface {
|
||||
const winnerNoTiles = this.players.find(player => player.hand.length === 0);
|
||||
if (winnerNoTiles !== undefined) {
|
||||
return winnerNoTiles;
|
||||
@ -123,35 +123,70 @@ export class DominoesGame {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.logger.debug('Starting player index: ' + startingIndex);
|
||||
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();
|
||||
playTurn() {
|
||||
try {
|
||||
const player = this.players[this.currentPlayerIndex];
|
||||
this.notificationService.sendEventToPlayers('server:next-turn', this.players, this.getGameState());
|
||||
this.logger.debug(`${player.name}'s turn (${player.hand.length} tiles)`);
|
||||
printBoard(this.board)
|
||||
player.askForMove(this.board);
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'Error playing turn');
|
||||
}
|
||||
}
|
||||
|
||||
finishTurn(playerMove: PlayerMove | null) {
|
||||
try {
|
||||
this.lastMove = playerMove;
|
||||
if (playerMove === null) {
|
||||
console.log('Player cannot move');
|
||||
this.blockedCount += 1;
|
||||
|
||||
this.gameBlocked = this.isBlocked();
|
||||
if (this.gameBlocked) {
|
||||
this.gameEnded();
|
||||
return;
|
||||
}
|
||||
this.nextPlayer();
|
||||
this.playTurn();
|
||||
return;
|
||||
}
|
||||
const player = this.players[this.currentPlayerIndex];
|
||||
this.blockedCount = 0;
|
||||
this.board.play(playerMove);
|
||||
player.hand = player.hand.filter(tile => tile !== playerMove.tile);
|
||||
this.notificationService.sendEventToPlayers('server:server-player-move', this.players, { move: playerMove });
|
||||
this.canAskNextPlayerMove = false;
|
||||
// whileNotUndefined(() => this.canAskNextPlayerMove === true ? {} : undefined);
|
||||
this.gameOver = this.isGameOver();
|
||||
if (!this.gameOver) {
|
||||
this.printPlayersHand();
|
||||
this.nextPlayer();
|
||||
this.playTurn();
|
||||
} else {
|
||||
this.gameEnded();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'Error finishing move');
|
||||
}
|
||||
}
|
||||
|
||||
gameEnded() {
|
||||
this.gameInProgress = false;
|
||||
this.winner = this.getWinner();
|
||||
this.setScores();
|
||||
const summary = {
|
||||
gameId: this.id,
|
||||
isBlocked: this.gameBlocked,
|
||||
isTied: this.gameTied,
|
||||
winner: this.winner?.getState(),
|
||||
score: this.players.map(player => ({name: player.name, score: player.score}))
|
||||
}
|
||||
this.emit('game-over', summary);
|
||||
}
|
||||
|
||||
resetPlayersScore() {
|
||||
@ -160,63 +195,85 @@ export class DominoesGame {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
setCanAskNextPlayerMove(value: boolean) {
|
||||
this.canAskNextPlayerMove = value
|
||||
}
|
||||
|
||||
dealTiles() {
|
||||
private deal() {
|
||||
if (this.autoDeal) {
|
||||
this.autoDealTiles();
|
||||
} else {
|
||||
// await this.tilesSelection();
|
||||
}
|
||||
this.printPlayersHand();
|
||||
}
|
||||
|
||||
printPlayersHand() {
|
||||
for (let player of this.players) {
|
||||
this.logger.debug(`${player.name}'s hand (${player.hand.length}): ${player.hand.map(tile => tile.toString())}`);
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
// Initalize game
|
||||
this.gameInProgress = false;
|
||||
this.resetPlayersScore();
|
||||
this.tileSelectionPhase = true;
|
||||
// await this.notificationManager.notifyGameState(this);
|
||||
// await this.notificationManager.notifyPlayersState(this.players);
|
||||
this.deal();
|
||||
const extractStates = (p: PlayerInterface) => {
|
||||
return p.getState()
|
||||
};
|
||||
await this.notificationService.sendEventToPlayers('server:hand-dealt', this.players, extractStates);
|
||||
|
||||
this.tileSelectionPhase = false;
|
||||
|
||||
// Start game
|
||||
this.gameInProgress = true;
|
||||
this.currentPlayerIndex = (this.forcedInitialPlayerIndex !== null) ? this.forcedInitialPlayerIndex : this.getStartingPlayerIndex();
|
||||
|
||||
printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`);
|
||||
this.logger.debug("Before play turn")
|
||||
this.playTurn();
|
||||
// await this.notificationManager.notifyGameState(this);
|
||||
// await this.notificationManager.notifyPlayersState(this.players);
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'Error starting game');
|
||||
}
|
||||
}
|
||||
|
||||
setScores() {
|
||||
const totalPips = this.players.reduce((acc, player) => acc + player.pipsCount(), 0) || 0;
|
||||
if (this.winner !== null) {
|
||||
const winner = this.winner;
|
||||
winner.score = totalPips;
|
||||
if (winner.teamedWith !== null) {
|
||||
winner.teamedWith.score = totalPips;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private autoDealTiles() {
|
||||
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;
|
||||
tile.playerId = player.id;
|
||||
player.hand.push(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async tilesSelection() {
|
||||
private 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);
|
||||
// await this.notificationService.notifyGameState(this);
|
||||
// await this.notificationService.notifyPlayersState(this.players);
|
||||
if (this.board.boneyard.length === 0) {
|
||||
break;
|
||||
}
|
||||
@ -240,6 +297,7 @@ export class DominoesGame {
|
||||
currentPlayer: currentPlayer.getState(),
|
||||
board: this.board.tiles.map(tile => ({
|
||||
id: tile.id,
|
||||
playerId: tile.playerId,
|
||||
pips: tile.pips
|
||||
})),
|
||||
boardFreeEnds: this.board.getFreeEnds(),
|
||||
|
@ -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})`;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
|
||||
import { LoggingService } from "../common/LoggingService";
|
||||
import { ServerEvents } from "./constants";
|
||||
|
||||
export class NetworkClientNotifier {
|
||||
static instance: NetworkClientNotifier;
|
||||
@ -17,33 +18,20 @@ export class NetworkClientNotifier {
|
||||
this.io = io;
|
||||
}
|
||||
|
||||
async notifyPlayer(player: NetworkPlayer, event: string, data: any = {}, timeoutSecs: number = 900): 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 sendEvent(player: NetworkPlayer, event: string, data?: any) {
|
||||
const eventData = { event, data };
|
||||
this.io.to(player.socketId).emit('game-event', eventData);
|
||||
this.io.to(player.socketId).emit(ServerEvents.SERVER_EVENT, eventData);
|
||||
}
|
||||
|
||||
async sendEventWithAck(player: NetworkPlayer, event: string, data: any, timeoutSecs: number = 900) {
|
||||
const eventData = { event, data };
|
||||
const response = await this.io.to(player.socketId)
|
||||
.timeout(timeoutSecs * 1000).emitWithAck('game-event-ack', eventData);
|
||||
return response[0];
|
||||
}
|
||||
|
||||
async broadcast(event: string, data: any) {
|
||||
const responses = await this.io.emit(event, data);
|
||||
this.logger.debug('responses :>> ', responses);
|
||||
return true;
|
||||
try {
|
||||
const eventData = { event, data };
|
||||
const response = await this.io.to(player.socketId)
|
||||
.timeout(timeoutSecs * 1000).emitWithAck(ServerEvents.SERVER_EVENT_WITH_ACK, eventData);
|
||||
return response[0];
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'sendEventWithAck error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
120
src/game/PlayerInteractionAI.ts
Normal file
120
src/game/PlayerInteractionAI.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { LoggingService } from "../common/LoggingService";
|
||||
import { printLine, wait } from "../common/utilities";
|
||||
import { InteractionService } from "../server/services/InteractionService";
|
||||
import { PlayerMoveSide } from "./constants";
|
||||
import { Board } from "./entities/Board";
|
||||
import { PlayerAI } from "./entities/player/PlayerAI";
|
||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||
import { PlayerMove } from "./entities/PlayerMove";
|
||||
import { Tile } from "./entities/Tile";
|
||||
import { PlayerInteractionInterface } from "./PlayerInteractionInterface";
|
||||
|
||||
export class PlayerInteractionAI implements PlayerInteractionInterface {
|
||||
player: PlayerInterface;
|
||||
interactionService: InteractionService = new InteractionService();
|
||||
logger: LoggingService = new LoggingService();
|
||||
|
||||
constructor(player: PlayerInterface) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
askForMove(board: Board): void {
|
||||
this.logger.trace('Asking for move (AI)');
|
||||
let move: PlayerMove | null = null;
|
||||
if (board.tiles.length === 0) {
|
||||
printLine('playing the first tile');
|
||||
const highestPair = this.getHighestPair();
|
||||
if (highestPair !== null) {
|
||||
move = new PlayerMove(highestPair, PlayerMoveSide.BOTH, this.player.id);
|
||||
}
|
||||
const maxTile = this.getMaxTile();
|
||||
if (maxTile !== null) {
|
||||
move = new PlayerMove(maxTile, PlayerMoveSide.BOTH, this.player.id);
|
||||
}
|
||||
} else {
|
||||
move = this.chooseTileGreed(board);
|
||||
}
|
||||
const rndWait = Math.floor(Math.random() * 1500) + 2000;
|
||||
setTimeout(() => {
|
||||
this.interactionService.playerMove({
|
||||
sessionId: (<PlayerAI>this.player).sessionId,
|
||||
move
|
||||
});
|
||||
this.logger.trace('Move sent to server (AI');
|
||||
}, rndWait);
|
||||
}
|
||||
|
||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||
const rndWait = Math.floor(Math.random() * 1000) + 1000;
|
||||
await wait(rndWait); // 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.player.id);
|
||||
}
|
||||
const maxTile = this.getMaxTile();
|
||||
if (maxTile !== null) {
|
||||
return new PlayerMove(maxTile, PlayerMoveSide.BOTH, this.player.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((<PlayerAI>this.player).rng() * board.boneyard.length);
|
||||
const tile = board.boneyard.splice(randomIndex, 1)[0];
|
||||
this.player.hand.push(tile);
|
||||
printLine(`${this.player.name} has chosen a tile`);
|
||||
return tile;
|
||||
}
|
||||
|
||||
private getHighestPair(): Tile | null {
|
||||
if (this.player.hand.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let highestPair: Tile | null = null;
|
||||
const pairs = this.player.hand.filter(tile => tile.pips[0] === tile.pips[1]);
|
||||
pairs.forEach(tile => {
|
||||
if (tile.count > (highestPair?.count ?? 0)) {
|
||||
highestPair = tile;
|
||||
}
|
||||
});
|
||||
return highestPair;
|
||||
}
|
||||
|
||||
private chooseTileGreed(board: Board): PlayerMove | null { // greed algorithm
|
||||
let bestMove: PlayerMove |null = null;
|
||||
let bestTileScore: number = -1;
|
||||
const validMoves: PlayerMove[] = board.getValidMoves(this.player);
|
||||
|
||||
validMoves.forEach(move => {
|
||||
const { tile } = move;
|
||||
const tileScore = tile.pips[0] + tile.pips[1];
|
||||
if (tileScore > bestTileScore) {
|
||||
bestMove = move;
|
||||
bestTileScore = tileScore;
|
||||
}
|
||||
});
|
||||
return bestMove;
|
||||
}
|
||||
|
||||
getMaxTile(): Tile | null {
|
||||
if (this.player.hand.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let maxTile: Tile | null = null;
|
||||
this.player.hand.forEach(tile => {
|
||||
if (tile.count > (maxTile?.count ?? 0)) {
|
||||
maxTile = tile;
|
||||
}
|
||||
});
|
||||
return maxTile;
|
||||
}
|
||||
}
|
@ -5,14 +5,20 @@ import { PlayerMove } from "./entities/PlayerMove";
|
||||
import { Tile } from "./entities/Tile";
|
||||
import { PlayerMoveSide, PlayerMoveSideType } from "./constants";
|
||||
import { PlayerInteractionInterface } from "./PlayerInteractionInterface";
|
||||
import { InteractionService } from "../server/services/InteractionService";
|
||||
|
||||
export class PlayerInteractionConsole implements PlayerInteractionInterface {
|
||||
player: PlayerInterface;
|
||||
interactionService: InteractionService = new InteractionService();
|
||||
|
||||
constructor(player: PlayerInterface) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
askForMove(board: Board): void {
|
||||
wait(100)
|
||||
}
|
||||
|
||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||
let move: PlayerMove | null = null;
|
||||
let tile: Tile;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { InteractionService } from "../server/services/InteractionService";
|
||||
import { Board } from "./entities/Board";
|
||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||
import { PlayerMove } from "./entities/PlayerMove";
|
||||
@ -5,7 +6,9 @@ import { Tile } from "./entities/Tile";
|
||||
|
||||
export interface PlayerInteractionInterface {
|
||||
player: PlayerInterface;
|
||||
interactionService: InteractionService;
|
||||
|
||||
askForMove(board: Board): void;
|
||||
makeMove(board: Board): Promise<PlayerMove | null>;
|
||||
chooseTile(board: Board): Promise<Tile>
|
||||
}
|
@ -7,20 +7,30 @@ import { NetworkClientNotifier } from './NetworkClientNotifier';
|
||||
import { NetworkPlayer } from './entities/player/NetworkPlayer';
|
||||
import { PlayerMoveSide, PlayerMoveSideType } from './constants';
|
||||
import { SocketDisconnectedError } from '../common/errors/SocketDisconnectedError';
|
||||
import { InteractionService } from '../server/services/InteractionService';
|
||||
|
||||
export class PlayerInteractionNetwork implements PlayerInteractionInterface {
|
||||
player: PlayerInterface;
|
||||
interactionService: InteractionService = new InteractionService();
|
||||
clientNotifier = new NetworkClientNotifier();
|
||||
|
||||
constructor(player: PlayerInterface) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
askForMove(board: Board): void {
|
||||
this.clientNotifier.sendEvent(this.player as NetworkPlayer, 'server:player-turn', {
|
||||
freeHands: board.getFreeEnds(),
|
||||
isFirstMove: board.tiles.length === 0
|
||||
});
|
||||
}
|
||||
|
||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||
let response = undefined;
|
||||
try {
|
||||
response = await this.clientNotifier.sendEventWithAck(this.player as NetworkPlayer, 'ask-client-for-move', {
|
||||
freeHands: board.getFreeEnds(),
|
||||
isFirstMove: board.tiles.length === 0
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SocketDisconnectedError();
|
||||
@ -39,7 +49,7 @@ export class PlayerInteractionNetwork implements PlayerInteractionInterface {
|
||||
|
||||
async chooseTile(board: Board): Promise<Tile> {
|
||||
const { player: { hand} } = this;
|
||||
const response: any = await this.clientNotifier.notifyPlayer(this.player as NetworkPlayer, 'chooseTile');
|
||||
const response: any = await this.clientNotifier.sendEventWithAck(this.player as NetworkPlayer, 'ask-client-for-tile', { boneyard: board.boneyard })
|
||||
const index: number = board.boneyard.findIndex(t => t.id === response.tileId);
|
||||
const tile = board.boneyard.splice(index, 1)[0];
|
||||
tile.revealed = true;
|
||||
|
@ -13,3 +13,20 @@ export const JointValue: { [key: string]: JointValueType } = {
|
||||
RIGHT: 1,
|
||||
NONE: 2
|
||||
};
|
||||
|
||||
export const ClientEvents = {
|
||||
CLIENT_EVENT: 'client:event',
|
||||
CLIENT_EVENT_WITH_ACK: 'client:event-with-ack'
|
||||
|
||||
};
|
||||
|
||||
export const ServerEvents = {
|
||||
SERVER_EVENT: 'server:game-event',
|
||||
SERVER_EVENT_WITH_ACK: 'server:game-event-ack'
|
||||
};
|
||||
|
||||
export const EventActions = {
|
||||
START_SESSION: 'client:start-session',
|
||||
PLAYER_READY: 'client:set-player-ready',
|
||||
TILE_ANIMATION_ENDED: 'client:tile-animation-ended'
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { PlayerInterface } from "../entities/player/PlayerInterface";
|
||||
import { PlayerDto } from "./PlayerDto";
|
||||
|
||||
export interface GameSummary {
|
||||
gameId: string;
|
||||
isBlocked: boolean;
|
||||
isTied: boolean;
|
||||
winner: PlayerInterface | null;
|
||||
winner: PlayerDto;
|
||||
score: { name: string; score: number; }[]
|
||||
}
|
6
src/game/dto/MatchSummary.ts
Normal file
6
src/game/dto/MatchSummary.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { GameSummary } from "./GameSummary";
|
||||
|
||||
export interface MatchSummary {
|
||||
lastGameSummary: GameSummary;
|
||||
scoreboard: { player: string; score: number; }[];
|
||||
}
|
@ -3,10 +3,12 @@ import { PlayerMoveSideType, PlayerMoveSide, JointValue } from "../constants";
|
||||
import { PlayerInterface } from "./player/PlayerInterface";
|
||||
import { PlayerMove } from "./PlayerMove";
|
||||
import { Tile } from "./Tile";
|
||||
import { LoggingService } from "../../common/LoggingService";
|
||||
|
||||
export class Board {
|
||||
tiles: Tile[] = [];
|
||||
boneyard: Tile[] = [];
|
||||
logger = new LoggingService();
|
||||
|
||||
constructor(private rng: PRNG) {}
|
||||
|
||||
@ -52,6 +54,8 @@ export class Board {
|
||||
|
||||
play(playerMove: PlayerMove): void {
|
||||
const { type, tile } = playerMove;
|
||||
|
||||
const boneTile = this.boneyard.find(t => t.id === tile.id);
|
||||
tile.revealed = true;
|
||||
if (type === PlayerMoveSide.LEFT) {
|
||||
this.playTileLeft(tile);
|
||||
|
@ -5,6 +5,7 @@ export class Tile {
|
||||
pips: [number, number];
|
||||
revealed: boolean = true;
|
||||
flipped: boolean = false;
|
||||
playerId?: string;
|
||||
|
||||
constructor(pips: [number, number]) {
|
||||
this.id = uuid();
|
||||
|
@ -19,11 +19,19 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
||||
|
||||
constructor(public name: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
askForMove(board: Board): void {
|
||||
this.playerInteraction.askForMove(board);
|
||||
}
|
||||
|
||||
abstract makeMove(board: Board): Promise<PlayerMove | null>;
|
||||
abstract chooseTile(board: Board): Promise<Tile>;
|
||||
|
||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||
return await this.playerInteraction.makeMove(board);
|
||||
}
|
||||
|
||||
async chooseTile(board: Board): Promise<Tile> {
|
||||
return this.playerInteraction.chooseTile(board);
|
||||
}
|
||||
|
||||
async sendEventWithAck(event: string, data: any): Promise<void> {
|
||||
}
|
||||
@ -41,20 +49,7 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
||||
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(showPips: boolean = false): PlayerDto {
|
||||
return {
|
||||
@ -66,6 +61,7 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
||||
id: tile.id,
|
||||
pips: tile.pips,
|
||||
flipped: tile.revealed,
|
||||
playerId: tile.playerId,
|
||||
};
|
||||
if (showPips) {
|
||||
d.pips = tile.pips;
|
||||
|
@ -6,71 +6,17 @@ import { PlayerMove } from "../PlayerMove";
|
||||
import { SimulatedBoard } from "../../SimulatedBoard";
|
||||
import { Tile } from "../Tile";
|
||||
import { PRNG } from "seedrandom";
|
||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
||||
import { PlayerInteractionAI } from "../../PlayerInteractionAI";
|
||||
import { InteractionService } from "../../../server/services/InteractionService";
|
||||
|
||||
export class PlayerAI extends AbstractPlayer {
|
||||
constructor(name: string, private rng: PRNG) {
|
||||
playerInteraction: PlayerInteractionInterface = new PlayerInteractionAI(this);
|
||||
|
||||
constructor(name: string, public rng: PRNG, public sessionId: string) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||
const rndWait = Math.floor(Math.random() * 1000) + 1000;
|
||||
await wait(rndWait); // 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)];
|
||||
|
@ -12,12 +12,4 @@ export class PlayerHuman extends AbstractPlayer {
|
||||
super(name);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||
return await this.playerInteraction.makeMove(board);
|
||||
}
|
||||
|
||||
async chooseTile(board: Board): Promise<Tile> {
|
||||
return this.playerInteraction.chooseTile(board);
|
||||
}
|
||||
}
|
@ -14,8 +14,9 @@ export interface PlayerInterface {
|
||||
teamedWith: PlayerInterface | null;
|
||||
playerInteraction: PlayerInteractionInterface;
|
||||
ready: boolean;
|
||||
|
||||
makeMove(gameState: Board): Promise<PlayerMove | null>;
|
||||
|
||||
askForMove(board: Board): void;
|
||||
makeMove(board: Board): Promise<PlayerMove | null>;
|
||||
chooseTile(board: Board): Promise<Tile>;
|
||||
pipsCount(): number;
|
||||
reset(): void;
|
||||
|
Reference in New Issue
Block a user