working flow
This commit is contained in:
@ -5,9 +5,12 @@ 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, whileNotUndefined } from '../common/utilities';
|
||||
import { printBoard, printLine, uuid, wait, whileNot, whileNotUndefined } from '../common/utilities';
|
||||
import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
|
||||
import { GameState } from './dto/GameState';
|
||||
import { PlayerHuman } from './entities/player/PlayerHuman';
|
||||
import { PlayerAI } from './entities/player/PlayerAI';
|
||||
import { GameSummary } from './dto/GameSummary';
|
||||
|
||||
export class DominoesGame extends EventEmitter {
|
||||
private id: string;
|
||||
@ -29,6 +32,7 @@ export class DominoesGame extends EventEmitter {
|
||||
lastMove: PlayerMove | null = null;
|
||||
forcedInitialPlayerIndex: number | null = null;
|
||||
canAskNextPlayerMove: boolean = true;
|
||||
clientsReady: string[] = [];
|
||||
|
||||
constructor(public players: PlayerInterface[], seed: PRNG) {
|
||||
super();
|
||||
@ -39,6 +43,10 @@ export class DominoesGame extends EventEmitter {
|
||||
this.initializeGame();
|
||||
}
|
||||
|
||||
get numHumanPlayers() {
|
||||
return this.players.filter(player => player instanceof PlayerHuman).length;
|
||||
}
|
||||
|
||||
async initializeGame() {
|
||||
this.gameOver = false;
|
||||
this.gameBlocked = false;
|
||||
@ -83,7 +91,13 @@ export class DominoesGame extends EventEmitter {
|
||||
}
|
||||
|
||||
isBlocked(): boolean {
|
||||
return this.blockedCount === this.players.length;
|
||||
const freeEnds = this.board.getFreeEnds();
|
||||
const tiles = []
|
||||
for (let player of this.players) {
|
||||
tiles.push(...player.hand);
|
||||
}
|
||||
const canPlay = tiles.some(tile => tile.pips[0] === freeEnds[0] || tile.pips[1] === freeEnds[0] || tile.pips[0] === freeEnds[1] || tile.pips[1] === freeEnds[1]);
|
||||
return !canPlay;
|
||||
}
|
||||
|
||||
isGameOver(): boolean {
|
||||
@ -132,6 +146,7 @@ export class DominoesGame extends EventEmitter {
|
||||
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)`);
|
||||
this.printPlayerHand(player);
|
||||
printBoard(this.board)
|
||||
player.askForMove(this.board);
|
||||
} catch (error) {
|
||||
@ -139,23 +154,40 @@ export class DominoesGame extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
finishTurn(playerMove: PlayerMove | null) {
|
||||
async checkAllClientsReadyToContinue() {
|
||||
try {
|
||||
const conditionFn = () => {
|
||||
this.logger.trace(`Clients ready: ${this.clientsReady.length}/${this.numHumanPlayers}`);
|
||||
return this.clientsReady.length === this.numHumanPlayers
|
||||
}
|
||||
await whileNot(conditionFn, 100);
|
||||
this.clientsReady = [];
|
||||
} catch (error) {
|
||||
this.logger.error(error, 'Error starting game');
|
||||
throw new Error('Error starting game (checkAllClientsReadyToContinue)');
|
||||
}
|
||||
}
|
||||
|
||||
async finishTurn(playerMove: PlayerMove | null) {
|
||||
try {
|
||||
this.lastMove = playerMove;
|
||||
if (playerMove === null) {
|
||||
console.log('Player cannot move');
|
||||
this.logger.info('Player cannot move');
|
||||
this.blockedCount += 1;
|
||||
|
||||
this.logger.trace(`Blocked count: ${this.blockedCount}`);
|
||||
this.gameBlocked = this.isBlocked();
|
||||
this.notificationService.sendEventToPlayers('server:server-player-move', this.players, { move: playerMove });
|
||||
if (this.gameBlocked) {
|
||||
this.gameEnded();
|
||||
return;
|
||||
} else {
|
||||
this.nextPlayer();
|
||||
await this.checkAllClientsReadyToContinue()
|
||||
this.playTurn();
|
||||
}
|
||||
this.nextPlayer();
|
||||
this.playTurn();
|
||||
return;
|
||||
}
|
||||
const player = this.players[this.currentPlayerIndex];
|
||||
const skipWaitForConfirmation = player instanceof PlayerAI && playerMove === null;
|
||||
this.blockedCount = 0;
|
||||
this.board.play(playerMove);
|
||||
player.hand = player.hand.filter(tile => tile !== playerMove.tile);
|
||||
@ -164,8 +196,10 @@ export class DominoesGame extends EventEmitter {
|
||||
// whileNotUndefined(() => this.canAskNextPlayerMove === true ? {} : undefined);
|
||||
this.gameOver = this.isGameOver();
|
||||
if (!this.gameOver) {
|
||||
this.printPlayersHand();
|
||||
this.nextPlayer();
|
||||
this.nextPlayer();
|
||||
if (!skipWaitForConfirmation) {
|
||||
await this.checkAllClientsReadyToContinue()
|
||||
}
|
||||
this.playTurn();
|
||||
} else {
|
||||
this.gameEnded();
|
||||
@ -179,12 +213,13 @@ export class DominoesGame extends EventEmitter {
|
||||
this.gameInProgress = false;
|
||||
this.winner = this.getWinner();
|
||||
this.setScores();
|
||||
const summary = {
|
||||
const summary: GameSummary = {
|
||||
gameId: this.id,
|
||||
isBlocked: this.gameBlocked,
|
||||
isTied: this.gameTied,
|
||||
winner: this.winner?.getState(),
|
||||
score: this.players.map(player => ({name: player.name, score: player.score}))
|
||||
score: this.players.map(player => ({id: player.id, name: player.name, score: player.score})),
|
||||
players: this.players.map(player => player.getState())
|
||||
}
|
||||
this.emit('game-over', summary);
|
||||
}
|
||||
@ -210,22 +245,25 @@ export class DominoesGame extends EventEmitter {
|
||||
|
||||
printPlayersHand() {
|
||||
for (let player of this.players) {
|
||||
this.logger.debug(`${player.name}'s hand (${player.hand.length}): ${player.hand.map(tile => tile.toString())}`);
|
||||
this.printPlayerHand(player);
|
||||
}
|
||||
}
|
||||
|
||||
printPlayerHand(player: PlayerInterface) {
|
||||
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()
|
||||
};
|
||||
const extractStates = (p: PlayerInterface) => ({
|
||||
player: p.getState(true),
|
||||
gameState: this.getGameState()
|
||||
});
|
||||
await this.notificationService.sendEventToPlayers('server:hand-dealt', this.players, extractStates);
|
||||
|
||||
this.tileSelectionPhase = false;
|
||||
@ -237,10 +275,9 @@ export class DominoesGame extends EventEmitter {
|
||||
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');
|
||||
throw new Error('Error starting game');
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,8 +309,6 @@ export class DominoesGame extends EventEmitter {
|
||||
while (this.board.boneyard.length > 0) {
|
||||
for (let player of this.players) {
|
||||
const choosen = await player.chooseTile(this.board);
|
||||
// await this.notificationService.notifyGameState(this);
|
||||
// await this.notificationService.notifyPlayersState(this.players);
|
||||
if (this.board.boneyard.length === 0) {
|
||||
break;
|
||||
}
|
||||
@ -292,16 +327,22 @@ export class DominoesGame extends EventEmitter {
|
||||
gameBlocked: this.gameBlocked,
|
||||
gameTied: this.gameTied,
|
||||
gameId: this.id,
|
||||
boneyard: this.board.boneyard.map(tile => ({ id: tile.id})),
|
||||
boneyard: this.board.boneyard.map(tile => tile.getState(false)),
|
||||
players: this.players.map(player => player.getState()),
|
||||
currentPlayer: currentPlayer.getState(),
|
||||
board: this.board.tiles.map(tile => ({
|
||||
id: tile.id,
|
||||
playerId: tile.playerId,
|
||||
pips: tile.pips
|
||||
})),
|
||||
board: this.board.tiles.map(tile => tile.getState(true)),
|
||||
boardFreeEnds: this.board.getFreeEnds(),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -22,6 +22,7 @@ export class MatchSession {
|
||||
private notificationService = new PlayerNotificationService();
|
||||
private winnerIndex: number | null = null;
|
||||
private clientsReady: string[] = [];
|
||||
private gameSummaries: GameSummary[] = [];
|
||||
|
||||
id: string;
|
||||
matchInProgress: boolean = false;
|
||||
@ -35,7 +36,7 @@ export class MatchSession {
|
||||
scoreboard: Map<string, number> = new Map();
|
||||
seed!: string
|
||||
sessionInProgress: boolean = false;
|
||||
state: string = 'created'
|
||||
status: string = 'created'
|
||||
|
||||
constructor(public creator: PlayerInterface, public name?: string, seed?: string) {
|
||||
this.seed = seed || getRandomSeed();
|
||||
@ -71,6 +72,13 @@ export class MatchSession {
|
||||
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 checkAllClientsReadyBeforeStart() {
|
||||
@ -80,13 +88,15 @@ export class MatchSession {
|
||||
this.logger.trace(`Clients ready: ${this.clientsReady.length}/${this.numHumanPlayers}`);
|
||||
return this.clientsReady.length === this.numHumanPlayers
|
||||
}
|
||||
await whileNot(conditionFn, 10);
|
||||
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)');
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,6 +105,8 @@ export class MatchSession {
|
||||
}
|
||||
|
||||
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);
|
||||
@ -131,6 +143,7 @@ export class MatchSession {
|
||||
}
|
||||
|
||||
private continueMatch(gameSummary: GameSummary) {
|
||||
this.gameSummaries.push(gameSummary);
|
||||
this.winnerIndex = this.players.findIndex(player => player.id === gameSummary?.winner?.id);
|
||||
if (this.winnerIndex !== null) {
|
||||
this.currentGame?.setForcedInitialPlayerIndex(this.winnerIndex);
|
||||
@ -139,19 +152,20 @@ export class MatchSession {
|
||||
this.checkMatchWinner();
|
||||
this.resetPlayers();
|
||||
if (!this.matchInProgress) {
|
||||
this.state = 'end'
|
||||
this.status = 'end'
|
||||
this.notificationService.sendEventToPlayers('server:match-finished', this.players, {
|
||||
lastGame: gameSummary,
|
||||
sessionState: this.getState(),
|
||||
});
|
||||
} else {
|
||||
this.state = 'waiting'
|
||||
this.status = 'waiting'
|
||||
// await this.playerNotificationManager.notifyMatchState(this);
|
||||
this.notificationService.sendEventToPlayers('server:game-finished', this.players, {
|
||||
lastGame: gameSummary,
|
||||
sessionState: this.getState()
|
||||
});
|
||||
this.waitingForPlayers = true;
|
||||
this.waitingForPlayers = true;
|
||||
this.startGame();
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,7 +183,7 @@ export class MatchSession {
|
||||
|
||||
private async startMatch(seed: string) {
|
||||
try {
|
||||
this.state = 'in-game'
|
||||
this.status = 'in-game'
|
||||
this.rng = seedrandom(seed);
|
||||
this.resetScoreboard()
|
||||
this.gameNumber = 0;
|
||||
@ -180,7 +194,7 @@ export class MatchSession {
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
this.matchInProgress = false;
|
||||
this.state = 'error'
|
||||
this.status = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
@ -296,11 +310,7 @@ export class MatchSession {
|
||||
id: this.id,
|
||||
name: this.name!,
|
||||
creator: this.creator.id,
|
||||
players: this.players.map(player =>( {
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
ready: player.ready,
|
||||
})),
|
||||
players: this.players.map(player => player.getState()),
|
||||
playersReady: this.numPlayersReady,
|
||||
sessionInProgress: this.sessionInProgress,
|
||||
maxPlayers: this.maxPlayers,
|
||||
@ -313,7 +323,8 @@ export class MatchSession {
|
||||
status: this.sessionInProgress ? 'in progress' : 'waiting',
|
||||
scoreboard: [...this.scoreboard.entries()],
|
||||
matchWinner: this.matchWinner?.getState() || null,
|
||||
matchInProgress: this.matchInProgress
|
||||
matchInProgress: this.matchInProgress,
|
||||
gameSummaries: this.gameSummaries,
|
||||
};
|
||||
}
|
||||
}
|
@ -34,13 +34,14 @@ export class PlayerInteractionAI implements PlayerInteractionInterface {
|
||||
} else {
|
||||
move = this.chooseTileGreed(board);
|
||||
}
|
||||
const rndWait = Math.floor(Math.random() * 1500) + 2000;
|
||||
this.logger.debug(`AI move: ${move?.tile.pips}`);
|
||||
const rndWait = Math.floor(Math.random() * 1000) + 1000;
|
||||
setTimeout(() => {
|
||||
this.interactionService.playerMove({
|
||||
this.interactionService.playerMoveAI({
|
||||
sessionId: (<PlayerAI>this.player).sessionId,
|
||||
move
|
||||
});
|
||||
this.logger.trace('Move sent to server (AI');
|
||||
this.logger.trace('Move sent to server (AI)');
|
||||
}, rndWait);
|
||||
}
|
||||
|
||||
|
@ -8,17 +8,20 @@ import { NetworkPlayer } from './entities/player/NetworkPlayer';
|
||||
import { PlayerMoveSide, PlayerMoveSideType } from './constants';
|
||||
import { SocketDisconnectedError } from '../common/errors/SocketDisconnectedError';
|
||||
import { InteractionService } from '../server/services/InteractionService';
|
||||
import { LoggingService } from '../common/LoggingService';
|
||||
|
||||
export class PlayerInteractionNetwork implements PlayerInteractionInterface {
|
||||
player: PlayerInterface;
|
||||
interactionService: InteractionService = new InteractionService();
|
||||
clientNotifier = new NetworkClientNotifier();
|
||||
logger: LoggingService = new LoggingService();
|
||||
|
||||
constructor(player: PlayerInterface) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
askForMove(board: Board): void {
|
||||
this.logger.trace('Asking for move (Player)');
|
||||
this.clientNotifier.sendEvent(this.player as NetworkPlayer, 'server:player-turn', {
|
||||
freeHands: board.getFreeEnds(),
|
||||
isFirstMove: board.tiles.length === 0
|
||||
|
@ -5,5 +5,6 @@ export interface GameSummary {
|
||||
isBlocked: boolean;
|
||||
isTied: boolean;
|
||||
winner: PlayerDto;
|
||||
score: { name: string; score: number; }[]
|
||||
score: { id: string, name: string; score: number; }[],
|
||||
players?: PlayerDto[];
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { GameSummary } from "./GameSummary";
|
||||
import { PlayerDto } from "./PlayerDto";
|
||||
|
||||
export interface MatchSessionState {
|
||||
@ -17,5 +18,6 @@ export interface MatchSessionState {
|
||||
scoreboard: [string, number][];
|
||||
matchWinner: PlayerDto | null;
|
||||
matchInProgress: boolean;
|
||||
playersReady: number
|
||||
playersReady: number,
|
||||
gameSummaries: GameSummary[];
|
||||
}
|
@ -1,8 +1,16 @@
|
||||
export interface TileDto {
|
||||
id: string;
|
||||
pips?: number[];
|
||||
flipped: boolean;
|
||||
revealed: boolean;
|
||||
playerId: string | undefined;
|
||||
}
|
||||
|
||||
export interface PlayerDto {
|
||||
id: string;
|
||||
name: string;
|
||||
score?: number;
|
||||
hand?: any[];
|
||||
hand?: TileDto[];
|
||||
teamedWith?: PlayerDto | null;
|
||||
ready: boolean;
|
||||
}
|
@ -28,6 +28,16 @@ export class Tile {
|
||||
this.flipped = !this.flipped;
|
||||
}
|
||||
|
||||
getState(showPips: boolean = false) {
|
||||
return {
|
||||
id: this.id,
|
||||
pips: showPips ? this.pips : undefined,
|
||||
flipped: this.flipped,
|
||||
revealed: this.revealed,
|
||||
playerId: this.playerId,
|
||||
};
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (!this.revealed) {
|
||||
return '[ | ]';
|
||||
|
@ -6,7 +6,7 @@ import { LoggingService } from "../../../common/LoggingService";
|
||||
import { EventEmitter } from "stream";
|
||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
||||
import { uuid } from "../../../common/utilities";
|
||||
import { PlayerDto } from "../../dto/PlayerDto";
|
||||
import { PlayerDto, TileDto } from "../../dto/PlayerDto";
|
||||
|
||||
export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface {
|
||||
hand: Tile[] = [];
|
||||
@ -56,19 +56,8 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
score: this.score,
|
||||
hand: this.hand.map(tile => {
|
||||
const d = {
|
||||
id: tile.id,
|
||||
pips: tile.pips,
|
||||
flipped: tile.revealed,
|
||||
playerId: tile.playerId,
|
||||
};
|
||||
if (showPips) {
|
||||
d.pips = tile.pips;
|
||||
}
|
||||
return d;
|
||||
}),
|
||||
teamedWith: this.teamedWith?.getState() ?? null,
|
||||
hand: this.hand.map(tile => tile.getState(showPips)),
|
||||
teamedWith: this.teamedWith?.getState(showPips) ?? null,
|
||||
ready: this.ready,
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
||||
import { Board } from "../Board";
|
||||
import { GameState } from "../../dto/GameState";
|
||||
import { PlayerMove } from "../PlayerMove";
|
||||
import { Tile } from "../Tile";
|
||||
import { MatchSessionState } from "../../dto/MatchSessionState";
|
||||
import { PlayerDto } from "../../dto/PlayerDto";
|
||||
|
||||
export interface PlayerInterface {
|
||||
@ -24,5 +22,5 @@ export interface PlayerInterface {
|
||||
sendEvent(event: string, data: any): Promise<void>;
|
||||
sendEventWithAck(event: string, data: any): Promise<any>;
|
||||
|
||||
getState(): PlayerDto;
|
||||
getState(showPips?: boolean): PlayerDto;
|
||||
}
|
Reference in New Issue
Block a user