reworked
This commit is contained in:
parent
ca0f1466c2
commit
5f117667a4
13
package-lock.json
generated
13
package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"pino-http": "^10.2.0",
|
"pino-http": "^10.2.0",
|
||||||
"pino-pretty": "^11.2.1",
|
"pino-pretty": "^11.2.1",
|
||||||
"pino-rotating-file-stream": "^0.0.2",
|
"pino-rotating-file-stream": "^0.0.2",
|
||||||
|
"pubsub-js": "^1.9.4",
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
"socket.io": "^4.7.5"
|
"socket.io": "^4.7.5"
|
||||||
},
|
},
|
||||||
@ -32,6 +33,7 @@
|
|||||||
"@types/node": "^20.14.8",
|
"@types/node": "^20.14.8",
|
||||||
"@types/nodemailer": "^6.4.15",
|
"@types/nodemailer": "^6.4.15",
|
||||||
"@types/nodemailer-express-handlebars": "^4.0.5",
|
"@types/nodemailer-express-handlebars": "^4.0.5",
|
||||||
|
"@types/pubsub-js": "^1.8.6",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.5.2"
|
"typescript": "^5.5.2"
|
||||||
@ -257,6 +259,12 @@
|
|||||||
"@types/nodemailer": "*"
|
"@types/nodemailer": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pubsub-js": {
|
||||||
|
"version": "1.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pubsub-js/-/pubsub-js-1.8.6.tgz",
|
||||||
|
"integrity": "sha512-Kwug5cwV0paUDm/NfwDx1sp9xI0bGIvmWJjJWCU8NngkCCMt3EIC7oPDvb6fV7BR8kPpFyyBu4D11bda/2MdPA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.15",
|
"version": "6.9.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
|
||||||
@ -1696,6 +1704,11 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pubsub-js": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-hJYpaDvPH4w8ZX/0Fdf9ma1AwRgU353GfbaVfPjfJQf1KxZ2iHaHl3fAUw1qlJIR5dr4F3RzjGaWohYUEyoh7A=="
|
||||||
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"pino-http": "^10.2.0",
|
"pino-http": "^10.2.0",
|
||||||
"pino-pretty": "^11.2.1",
|
"pino-pretty": "^11.2.1",
|
||||||
"pino-rotating-file-stream": "^0.0.2",
|
"pino-rotating-file-stream": "^0.0.2",
|
||||||
|
"pubsub-js": "^1.9.4",
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
"socket.io": "^4.7.5"
|
"socket.io": "^4.7.5"
|
||||||
},
|
},
|
||||||
@ -46,6 +47,7 @@
|
|||||||
"@types/node": "^20.14.8",
|
"@types/node": "^20.14.8",
|
||||||
"@types/nodemailer": "^6.4.15",
|
"@types/nodemailer": "^6.4.15",
|
||||||
"@types/nodemailer-express-handlebars": "^4.0.5",
|
"@types/nodemailer-express-handlebars": "^4.0.5",
|
||||||
|
"@types/pubsub-js": "^1.8.6",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.5.2"
|
"typescript": "^5.5.2"
|
||||||
|
@ -4,6 +4,9 @@ import * as readline from 'readline';
|
|||||||
import { Tile } from '../game/entities/Tile';
|
import { Tile } from '../game/entities/Tile';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { Board } from '../game/entities/Board';
|
import { Board } from '../game/entities/Board';
|
||||||
|
import { LoggingService } from './LoggingService';
|
||||||
|
|
||||||
|
const logger = new LoggingService();
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
@ -29,6 +32,21 @@ export const whileNotUndefined = async (fn: Function, maxQueries: number = 20, m
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const whileNot = async (fn: Function, maxQueries: number = 20, millis: number = 500): Promise<void> => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let result: boolean = false;
|
||||||
|
while (result === false) {
|
||||||
|
await wait(millis);
|
||||||
|
result = fn()
|
||||||
|
if (maxQueries-- < 0) {
|
||||||
|
reject()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function askQuestion(question: string): Promise<string> {
|
export function askQuestion(question: string): Promise<string> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// console.log(chalk.yellow(question));
|
// console.log(chalk.yellow(question));
|
||||||
@ -46,7 +64,7 @@ export function getRandomSeed(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function printTiles(prefix:string, tiles: Tile[]): void {
|
export function printTiles(prefix:string, tiles: Tile[]): void {
|
||||||
console.log(`${prefix}${tiles.join(' ')}`);
|
logger.info(`${prefix}${tiles.join(' ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printSelection(prefix:string, tiles: Tile[]): void {
|
export function printSelection(prefix:string, tiles: Tile[]): void {
|
||||||
@ -55,22 +73,22 @@ export function printSelection(prefix:string, tiles: Tile[]): void {
|
|||||||
return `(${index > 9 ? `${index})`: `${index}) `} `
|
return `(${index > 9 ? `${index})`: `${index}) `} `
|
||||||
}).join(' ');
|
}).join(' ');
|
||||||
printTiles(prefix, tiles);
|
printTiles(prefix, tiles);
|
||||||
console.log(`${Array(prefix.length).join((' '))} ${line}`);
|
logger.info(`${Array(prefix.length).join((' '))} ${line}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printBoard(board: Board, highlighted: boolean = false): void {
|
export function printBoard(board: Board, highlighted: boolean = false): void {
|
||||||
if (highlighted)
|
if (highlighted)
|
||||||
console.log(chalk.cyan(`Board: ${board.tiles.length > 0 ? board.tiles.join(' ') : '--empty--'}`));
|
logger.info(chalk.cyan(`Board: ${board.tiles.length > 0 ? board.tiles.map(t => t.toString()).join(' ') : '--empty--'}`));
|
||||||
else
|
else
|
||||||
console.log(chalk.gray(`Board: ${board.tiles.length > 0 ? board.tiles.join(' ') : '--empty--'}`));
|
logger.info(chalk.gray(`Board: ${board.tiles.length > 0 ? board.tiles.map(t => t.toString()).join(' ') : '--empty--'}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printLine(msg: string): void {
|
export function printLine(msg: string): void {
|
||||||
console.log(chalk.grey(msg));
|
logger.info(chalk.grey(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printError(msg: string): void {
|
export function printError(msg: string): void {
|
||||||
console.log(chalk.red(msg));
|
logger.info(chalk.red(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uuid() {
|
export function uuid() {
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { PRNG } from 'seedrandom';
|
import { PRNG } from 'seedrandom';
|
||||||
|
import {EventEmitter} from 'events';
|
||||||
import { Board } from "./entities/Board";
|
import { Board } from "./entities/Board";
|
||||||
import { PlayerMove } from "./entities/PlayerMove";
|
import { PlayerMove } from "./entities/PlayerMove";
|
||||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||||
import { Tile } from "./entities/Tile";
|
import { Tile } from "./entities/Tile";
|
||||||
import { LoggingService } from "../common/LoggingService";
|
import { LoggingService } from "../common/LoggingService";
|
||||||
import { printBoard, printLine, uuid, wait } from '../common/utilities';
|
import { printBoard, printLine, uuid, wait, whileNotUndefined } from '../common/utilities';
|
||||||
import { GameSummary } from './dto/GameSummary';
|
|
||||||
import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
|
import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
|
||||||
import { GameState } from './dto/GameState';
|
import { GameState } from './dto/GameState';
|
||||||
|
|
||||||
export class DominoesGame {
|
export class DominoesGame extends EventEmitter {
|
||||||
private id: string;
|
private id: string;
|
||||||
private seed: string | undefined;
|
private seed: string | undefined;
|
||||||
autoDeal: boolean = true;
|
autoDeal: boolean = true;
|
||||||
@ -25,11 +25,13 @@ export class DominoesGame {
|
|||||||
winner: PlayerInterface | null = null;
|
winner: PlayerInterface | null = null;
|
||||||
rng: PRNG;
|
rng: PRNG;
|
||||||
handSize: number = 7;
|
handSize: number = 7;
|
||||||
notificationManager: PlayerNotificationService = new PlayerNotificationService();
|
notificationService: PlayerNotificationService = new PlayerNotificationService();
|
||||||
lastMove: PlayerMove | null = null;
|
lastMove: PlayerMove | null = null;
|
||||||
forcedInitialPlayerIndex: number | null = null;
|
forcedInitialPlayerIndex: number | null = null;
|
||||||
|
canAskNextPlayerMove: boolean = true;
|
||||||
|
|
||||||
constructor(public players: PlayerInterface[], seed: PRNG) {
|
constructor(public players: PlayerInterface[], seed: PRNG) {
|
||||||
|
super();
|
||||||
this.id = uuid();
|
this.id = uuid();
|
||||||
this.logger.info(`Game ID: ${this.id}`);
|
this.logger.info(`Game ID: ${this.id}`);
|
||||||
this.rng = seed
|
this.rng = seed
|
||||||
@ -76,6 +78,7 @@ export class DominoesGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextPlayer() {
|
nextPlayer() {
|
||||||
|
this.logger.debug('Turn ended');
|
||||||
this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length;
|
this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,10 +91,7 @@ export class DominoesGame {
|
|||||||
return hasWinner || this.gameBlocked;
|
return hasWinner || this.gameBlocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
getWinner(): PlayerInterface | null {
|
getWinner(): PlayerInterface {
|
||||||
if (!this.gameOver) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const winnerNoTiles = this.players.find(player => player.hand.length === 0);
|
const winnerNoTiles = this.players.find(player => player.hand.length === 0);
|
||||||
if (winnerNoTiles !== undefined) {
|
if (winnerNoTiles !== undefined) {
|
||||||
return winnerNoTiles;
|
return winnerNoTiles;
|
||||||
@ -123,35 +123,70 @@ export class DominoesGame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.logger.debug('Starting player index: ' + startingIndex);
|
||||||
return startingIndex === -1 ? 0 : startingIndex;
|
return startingIndex === -1 ? 0 : startingIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
async playTurn(): Promise<void> {
|
playTurn() {
|
||||||
const player = this.players[this.currentPlayerIndex];
|
try {
|
||||||
console.log(`${player.name}'s turn (${player.hand.length} tiles)`);
|
const player = this.players[this.currentPlayerIndex];
|
||||||
printBoard(this.board);
|
this.notificationService.sendEventToPlayers('server:next-turn', this.players, this.getGameState());
|
||||||
|
this.logger.debug(`${player.name}'s turn (${player.hand.length} tiles)`);
|
||||||
// let playerMove: PlayerMove | null = null;
|
printBoard(this.board)
|
||||||
// while(playerMove === null) {
|
player.askForMove(this.board);
|
||||||
// try {
|
} catch (error) {
|
||||||
// playerMove = await player.makeMove(this.board);
|
this.logger.error(error, 'Error playing turn');
|
||||||
// } 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);
|
finishTurn(playerMove: PlayerMove | null) {
|
||||||
this.nextPlayer();
|
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() {
|
resetPlayersScore() {
|
||||||
@ -160,63 +195,85 @@ export class DominoesGame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<GameSummary> {
|
setCanAskNextPlayerMove(value: boolean) {
|
||||||
this.resetPlayersScore();
|
this.canAskNextPlayerMove = value
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 i = 0; i < this.handSize; i++) {
|
||||||
for (let player of this.players) {
|
for (let player of this.players) {
|
||||||
const tile: Tile | undefined = this.board.boneyard.pop();
|
const tile: Tile | undefined = this.board.boneyard.pop();
|
||||||
if (tile !== undefined) {
|
if (tile !== undefined) {
|
||||||
tile.revealed = true;
|
tile.revealed = true;
|
||||||
|
tile.playerId = player.id;
|
||||||
player.hand.push(tile);
|
player.hand.push(tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async tilesSelection() {
|
private async tilesSelection() {
|
||||||
while (this.board.boneyard.length > 0) {
|
while (this.board.boneyard.length > 0) {
|
||||||
for (let player of this.players) {
|
for (let player of this.players) {
|
||||||
const choosen = await player.chooseTile(this.board);
|
const choosen = await player.chooseTile(this.board);
|
||||||
await this.notificationManager.notifyGameState(this);
|
// await this.notificationService.notifyGameState(this);
|
||||||
await this.notificationManager.notifyPlayersState(this.players);
|
// await this.notificationService.notifyPlayersState(this.players);
|
||||||
if (this.board.boneyard.length === 0) {
|
if (this.board.boneyard.length === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -240,6 +297,7 @@ export class DominoesGame {
|
|||||||
currentPlayer: currentPlayer.getState(),
|
currentPlayer: currentPlayer.getState(),
|
||||||
board: this.board.tiles.map(tile => ({
|
board: this.board.tiles.map(tile => ({
|
||||||
id: tile.id,
|
id: tile.id,
|
||||||
|
playerId: tile.playerId,
|
||||||
pips: tile.pips
|
pips: tile.pips
|
||||||
})),
|
})),
|
||||||
boardFreeEnds: this.board.getFreeEnds(),
|
boardFreeEnds: this.board.getFreeEnds(),
|
||||||
|
@ -2,24 +2,30 @@ import { DominoesGame } from "./DominoesGame";
|
|||||||
import { PlayerAI } from "./entities/player/PlayerAI";
|
import { PlayerAI } from "./entities/player/PlayerAI";
|
||||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||||
import { LoggingService } from "../common/LoggingService";
|
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 { MatchSessionState } from "./dto/MatchSessionState";
|
||||||
import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
|
import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
|
||||||
import seedrandom, { PRNG } from "seedrandom";
|
import seedrandom, { PRNG } from "seedrandom";
|
||||||
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
|
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
|
||||||
import { PlayerHuman } from "./entities/player/PlayerHuman";
|
import { PlayerHuman } from "./entities/player/PlayerHuman";
|
||||||
|
import { GameSummary } from "./dto/GameSummary";
|
||||||
|
import { PlayerMove } from "./entities/PlayerMove";
|
||||||
|
|
||||||
|
|
||||||
export class MatchSession {
|
export class MatchSession {
|
||||||
private currentGame: DominoesGame | null = null;
|
private currentGame: DominoesGame | null = null;
|
||||||
private minHumanPlayers: number = 1;
|
private minHumanPlayers: number = 1;
|
||||||
|
private gameNumber: number = 0;
|
||||||
private waitingForPlayers: boolean = true;
|
private waitingForPlayers: boolean = true;
|
||||||
private waitingSeconds: number = 0;
|
private waitingSeconds: number = 0;
|
||||||
private logger: LoggingService = new LoggingService();
|
private logger: LoggingService = new LoggingService();
|
||||||
private playerNotificationManager = new PlayerNotificationService();
|
private notificationService = new PlayerNotificationService();
|
||||||
|
private winnerIndex: number | null = null;
|
||||||
|
private clientsReady: string[] = [];
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
matchInProgress: boolean = false;
|
matchInProgress: boolean = false;
|
||||||
|
gameInProgress: boolean = false;
|
||||||
matchWinner?: PlayerInterface = undefined;
|
matchWinner?: PlayerInterface = undefined;
|
||||||
maxPlayers: number = 4;
|
maxPlayers: number = 4;
|
||||||
mode: string = 'classic';
|
mode: string = 'classic';
|
||||||
@ -35,7 +41,7 @@ export class MatchSession {
|
|||||||
this.seed = seed || getRandomSeed();
|
this.seed = seed || getRandomSeed();
|
||||||
this.id = uuid();
|
this.id = uuid();
|
||||||
this.name = name || `Game ${this.id}`;
|
this.name = name || `Game ${this.id}`;
|
||||||
this.addPlayer(creator);
|
this.addPlayerToSession(creator);
|
||||||
this.creator = creator;
|
this.creator = creator;
|
||||||
|
|
||||||
this.logger.info(`Match session created by: ${creator.name}`);
|
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(`Match session name: ${this.name}`);
|
||||||
this.logger.info(`Points to win: ${this.pointsToWin}`);
|
this.logger.info(`Points to win: ${this.pointsToWin}`);
|
||||||
this.sessionInProgress = true;
|
this.sessionInProgress = true;
|
||||||
this.matchInProgress = false;
|
this.waitingForPlayers = true;
|
||||||
|
this.logger.info('Waiting for players to be ready');
|
||||||
}
|
}
|
||||||
|
|
||||||
get numPlayers() {
|
get numPlayers() {
|
||||||
@ -58,57 +65,138 @@ export class MatchSession {
|
|||||||
return this.players.filter(player => player instanceof PlayerHuman).length;
|
return this.players.filter(player => player instanceof PlayerHuman).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async startMatch(seed: string) {
|
setClientReady(userId: string) {
|
||||||
this.rng = seedrandom(seed);
|
this.logger.trace(`${userId} - ${this.clientsReady}`);
|
||||||
const missingPlayers = this.maxPlayers - this.numPlayers;
|
if (!this.clientsReady.includes(userId)) {
|
||||||
for (let i = 0; i < missingPlayers; i++) {
|
this.logger.trace(`Client ${userId} is ready`)
|
||||||
this.addPlayer(this.createPlayerAI(i));
|
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() {
|
async checkAllClientsReadyBeforeStart() {
|
||||||
this.logger.info('Waiting for human players to be ready');
|
try {
|
||||||
return new Promise((resolve) => {
|
if (this.currentGame) {
|
||||||
const interval = setInterval(() => {
|
const conditionFn = () => {
|
||||||
this.logger.debug(`Human players ready: ${this.numPlayersReady}/${this.numHumanPlayers}`)
|
this.logger.trace(`Clients ready: ${this.clientsReady.length}/${this.numHumanPlayers}`);
|
||||||
if (this.numPlayersReady === this.numHumanPlayers) {
|
return this.clientsReady.length === this.numHumanPlayers
|
||||||
clearInterval(interval);
|
|
||||||
resolve(true);
|
|
||||||
}
|
}
|
||||||
}, 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() {
|
resetPlayers() {
|
||||||
this.players.forEach(player => {
|
this.players.forEach(player => {
|
||||||
player.reset()
|
player.reset()
|
||||||
@ -134,17 +222,19 @@ export class MatchSession {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setScores() {
|
setScores(gameSummary?: GameSummary) {
|
||||||
const totalPips = this.currentGame?.players.reduce((acc, player) => acc + player.pipsCount(), 0) || 0;
|
if (!gameSummary) {
|
||||||
if (this.currentGame && this.currentGame.winner !== null) {
|
return;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 {
|
private endGame(): any {
|
||||||
@ -155,13 +245,6 @@ export class MatchSession {
|
|||||||
this.getScore(this.currentGame);
|
this.getScore(this.currentGame);
|
||||||
this.logger.info('Game ended');
|
this.logger.info('Game ended');
|
||||||
this.currentGame = null;
|
this.currentGame = null;
|
||||||
this.playerNotificationManager.notifyMatchState(this);
|
|
||||||
|
|
||||||
return {
|
|
||||||
gameBlocked,
|
|
||||||
gameTied,
|
|
||||||
winner
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,47 +269,24 @@ export class MatchSession {
|
|||||||
|
|
||||||
createPlayerAI(i: number) {
|
createPlayerAI(i: number) {
|
||||||
const AInames = ["Alice (AI)", "Bob (AI)", "Charlie (AI)", "David (AI)"];
|
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;
|
player.ready = true;
|
||||||
return player;
|
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) {
|
setPlayerReady(userId: string) {
|
||||||
this.logger.debug(userId)
|
|
||||||
const player = this.players.find(player => player.id === userId);
|
const player = this.players.find(player => player.id === userId);
|
||||||
if (!player) {
|
if (!player) {
|
||||||
throw new Error("Player not found");
|
throw new Error("Player not found");
|
||||||
}
|
}
|
||||||
player.ready = true;
|
player.ready = true;
|
||||||
this.logger.info(`${player.name} is ready!`);
|
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() {
|
toString() {
|
||||||
return `GameSession:(${this.id} ${this.name})`;
|
return `GameSession:(${this.id} ${this.name})`;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
|
import { NetworkPlayer } from "./entities/player/NetworkPlayer";
|
||||||
import { LoggingService } from "../common/LoggingService";
|
import { LoggingService } from "../common/LoggingService";
|
||||||
|
import { ServerEvents } from "./constants";
|
||||||
|
|
||||||
export class NetworkClientNotifier {
|
export class NetworkClientNotifier {
|
||||||
static instance: NetworkClientNotifier;
|
static instance: NetworkClientNotifier;
|
||||||
@ -17,33 +18,20 @@ export class NetworkClientNotifier {
|
|||||||
this.io = io;
|
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) {
|
async sendEvent(player: NetworkPlayer, event: string, data?: any) {
|
||||||
const eventData = { event, data };
|
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) {
|
async sendEventWithAck(player: NetworkPlayer, event: string, data: any, timeoutSecs: number = 900) {
|
||||||
const eventData = { event, data };
|
try {
|
||||||
const response = await this.io.to(player.socketId)
|
const eventData = { event, data };
|
||||||
.timeout(timeoutSecs * 1000).emitWithAck('game-event-ack', eventData);
|
const response = await this.io.to(player.socketId)
|
||||||
return response[0];
|
.timeout(timeoutSecs * 1000).emitWithAck(ServerEvents.SERVER_EVENT_WITH_ACK, eventData);
|
||||||
}
|
return response[0];
|
||||||
|
} catch (error) {
|
||||||
async broadcast(event: string, data: any) {
|
this.logger.error(error, 'sendEventWithAck error');
|
||||||
const responses = await this.io.emit(event, data);
|
return null;
|
||||||
this.logger.debug('responses :>> ', responses);
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 { Tile } from "./entities/Tile";
|
||||||
import { PlayerMoveSide, PlayerMoveSideType } from "./constants";
|
import { PlayerMoveSide, PlayerMoveSideType } from "./constants";
|
||||||
import { PlayerInteractionInterface } from "./PlayerInteractionInterface";
|
import { PlayerInteractionInterface } from "./PlayerInteractionInterface";
|
||||||
|
import { InteractionService } from "../server/services/InteractionService";
|
||||||
|
|
||||||
export class PlayerInteractionConsole implements PlayerInteractionInterface {
|
export class PlayerInteractionConsole implements PlayerInteractionInterface {
|
||||||
player: PlayerInterface;
|
player: PlayerInterface;
|
||||||
|
interactionService: InteractionService = new InteractionService();
|
||||||
|
|
||||||
constructor(player: PlayerInterface) {
|
constructor(player: PlayerInterface) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
askForMove(board: Board): void {
|
||||||
|
wait(100)
|
||||||
|
}
|
||||||
|
|
||||||
async makeMove(board: Board): Promise<PlayerMove | null> {
|
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||||
let move: PlayerMove | null = null;
|
let move: PlayerMove | null = null;
|
||||||
let tile: Tile;
|
let tile: Tile;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { InteractionService } from "../server/services/InteractionService";
|
||||||
import { Board } from "./entities/Board";
|
import { Board } from "./entities/Board";
|
||||||
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
import { PlayerInterface } from "./entities/player/PlayerInterface";
|
||||||
import { PlayerMove } from "./entities/PlayerMove";
|
import { PlayerMove } from "./entities/PlayerMove";
|
||||||
@ -5,7 +6,9 @@ import { Tile } from "./entities/Tile";
|
|||||||
|
|
||||||
export interface PlayerInteractionInterface {
|
export interface PlayerInteractionInterface {
|
||||||
player: PlayerInterface;
|
player: PlayerInterface;
|
||||||
|
interactionService: InteractionService;
|
||||||
|
|
||||||
|
askForMove(board: Board): void;
|
||||||
makeMove(board: Board): Promise<PlayerMove | null>;
|
makeMove(board: Board): Promise<PlayerMove | null>;
|
||||||
chooseTile(board: Board): Promise<Tile>
|
chooseTile(board: Board): Promise<Tile>
|
||||||
}
|
}
|
@ -7,20 +7,30 @@ import { NetworkClientNotifier } from './NetworkClientNotifier';
|
|||||||
import { NetworkPlayer } from './entities/player/NetworkPlayer';
|
import { NetworkPlayer } from './entities/player/NetworkPlayer';
|
||||||
import { PlayerMoveSide, PlayerMoveSideType } from './constants';
|
import { PlayerMoveSide, PlayerMoveSideType } from './constants';
|
||||||
import { SocketDisconnectedError } from '../common/errors/SocketDisconnectedError';
|
import { SocketDisconnectedError } from '../common/errors/SocketDisconnectedError';
|
||||||
|
import { InteractionService } from '../server/services/InteractionService';
|
||||||
|
|
||||||
export class PlayerInteractionNetwork implements PlayerInteractionInterface {
|
export class PlayerInteractionNetwork implements PlayerInteractionInterface {
|
||||||
player: PlayerInterface;
|
player: PlayerInterface;
|
||||||
|
interactionService: InteractionService = new InteractionService();
|
||||||
clientNotifier = new NetworkClientNotifier();
|
clientNotifier = new NetworkClientNotifier();
|
||||||
|
|
||||||
constructor(player: PlayerInterface) {
|
constructor(player: PlayerInterface) {
|
||||||
this.player = player;
|
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> {
|
async makeMove(board: Board): Promise<PlayerMove | null> {
|
||||||
let response = undefined;
|
let response = undefined;
|
||||||
try {
|
try {
|
||||||
response = await this.clientNotifier.sendEventWithAck(this.player as NetworkPlayer, 'ask-client-for-move', {
|
response = await this.clientNotifier.sendEventWithAck(this.player as NetworkPlayer, 'ask-client-for-move', {
|
||||||
freeHands: board.getFreeEnds(),
|
freeHands: board.getFreeEnds(),
|
||||||
|
isFirstMove: board.tiles.length === 0
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new SocketDisconnectedError();
|
throw new SocketDisconnectedError();
|
||||||
@ -39,7 +49,7 @@ export class PlayerInteractionNetwork implements PlayerInteractionInterface {
|
|||||||
|
|
||||||
async chooseTile(board: Board): Promise<Tile> {
|
async chooseTile(board: Board): Promise<Tile> {
|
||||||
const { player: { hand} } = this;
|
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 index: number = board.boneyard.findIndex(t => t.id === response.tileId);
|
||||||
const tile = board.boneyard.splice(index, 1)[0];
|
const tile = board.boneyard.splice(index, 1)[0];
|
||||||
tile.revealed = true;
|
tile.revealed = true;
|
||||||
|
@ -13,3 +13,20 @@ export const JointValue: { [key: string]: JointValueType } = {
|
|||||||
RIGHT: 1,
|
RIGHT: 1,
|
||||||
NONE: 2
|
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 {
|
export interface GameSummary {
|
||||||
gameId: string;
|
gameId: string;
|
||||||
isBlocked: boolean;
|
isBlocked: boolean;
|
||||||
isTied: 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 { PlayerInterface } from "./player/PlayerInterface";
|
||||||
import { PlayerMove } from "./PlayerMove";
|
import { PlayerMove } from "./PlayerMove";
|
||||||
import { Tile } from "./Tile";
|
import { Tile } from "./Tile";
|
||||||
|
import { LoggingService } from "../../common/LoggingService";
|
||||||
|
|
||||||
export class Board {
|
export class Board {
|
||||||
tiles: Tile[] = [];
|
tiles: Tile[] = [];
|
||||||
boneyard: Tile[] = [];
|
boneyard: Tile[] = [];
|
||||||
|
logger = new LoggingService();
|
||||||
|
|
||||||
constructor(private rng: PRNG) {}
|
constructor(private rng: PRNG) {}
|
||||||
|
|
||||||
@ -52,6 +54,8 @@ export class Board {
|
|||||||
|
|
||||||
play(playerMove: PlayerMove): void {
|
play(playerMove: PlayerMove): void {
|
||||||
const { type, tile } = playerMove;
|
const { type, tile } = playerMove;
|
||||||
|
|
||||||
|
const boneTile = this.boneyard.find(t => t.id === tile.id);
|
||||||
tile.revealed = true;
|
tile.revealed = true;
|
||||||
if (type === PlayerMoveSide.LEFT) {
|
if (type === PlayerMoveSide.LEFT) {
|
||||||
this.playTileLeft(tile);
|
this.playTileLeft(tile);
|
||||||
|
@ -5,6 +5,7 @@ export class Tile {
|
|||||||
pips: [number, number];
|
pips: [number, number];
|
||||||
revealed: boolean = true;
|
revealed: boolean = true;
|
||||||
flipped: boolean = false;
|
flipped: boolean = false;
|
||||||
|
playerId?: string;
|
||||||
|
|
||||||
constructor(pips: [number, number]) {
|
constructor(pips: [number, number]) {
|
||||||
this.id = uuid();
|
this.id = uuid();
|
||||||
|
@ -21,9 +21,17 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract makeMove(board: Board): Promise<PlayerMove | null>;
|
askForMove(board: Board): void {
|
||||||
abstract chooseTile(board: Board): Promise<Tile>;
|
this.playerInteraction.askForMove(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
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);
|
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 {
|
getState(showPips: boolean = false): PlayerDto {
|
||||||
return {
|
return {
|
||||||
@ -66,6 +61,7 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
|
|||||||
id: tile.id,
|
id: tile.id,
|
||||||
pips: tile.pips,
|
pips: tile.pips,
|
||||||
flipped: tile.revealed,
|
flipped: tile.revealed,
|
||||||
|
playerId: tile.playerId,
|
||||||
};
|
};
|
||||||
if (showPips) {
|
if (showPips) {
|
||||||
d.pips = tile.pips;
|
d.pips = tile.pips;
|
||||||
|
@ -6,71 +6,17 @@ import { PlayerMove } from "../PlayerMove";
|
|||||||
import { SimulatedBoard } from "../../SimulatedBoard";
|
import { SimulatedBoard } from "../../SimulatedBoard";
|
||||||
import { Tile } from "../Tile";
|
import { Tile } from "../Tile";
|
||||||
import { PRNG } from "seedrandom";
|
import { PRNG } from "seedrandom";
|
||||||
|
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
|
||||||
|
import { PlayerInteractionAI } from "../../PlayerInteractionAI";
|
||||||
|
import { InteractionService } from "../../../server/services/InteractionService";
|
||||||
|
|
||||||
export class PlayerAI extends AbstractPlayer {
|
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);
|
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
|
chooseTileRandom(board: Board): Tile | null { // random algorithm
|
||||||
const validTiles: Tile[] = this.hand.filter(tile => board.isValidMove(tile, null, this));
|
const validTiles: Tile[] = this.hand.filter(tile => board.isValidMove(tile, null, this));
|
||||||
return validTiles[Math.floor(this.rng() * validTiles.length)];
|
return validTiles[Math.floor(this.rng() * validTiles.length)];
|
||||||
|
@ -12,12 +12,4 @@ export class PlayerHuman extends AbstractPlayer {
|
|||||||
super(name);
|
super(name);
|
||||||
this.id = id;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -15,7 +15,8 @@ export interface PlayerInterface {
|
|||||||
playerInteraction: PlayerInteractionInterface;
|
playerInteraction: PlayerInteractionInterface;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
|
||||||
makeMove(gameState: Board): Promise<PlayerMove | null>;
|
askForMove(board: Board): void;
|
||||||
|
makeMove(board: Board): Promise<PlayerMove | null>;
|
||||||
chooseTile(board: Board): Promise<Tile>;
|
chooseTile(board: Board): Promise<Tile>;
|
||||||
pipsCount(): number;
|
pipsCount(): number;
|
||||||
reset(): void;
|
reset(): void;
|
||||||
|
@ -124,14 +124,14 @@ export class AuthController extends BaseController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenFromDb = await new ApiTokenMongoManager().getById(token._id.toString());
|
const tokenFromDb: Token = await new ApiTokenMongoManager().getById(token._id.toString()) as Token;
|
||||||
|
|
||||||
if (!tokenFromDb) {
|
if (!tokenFromDb) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { roles } = tokenFromDb;
|
const { roles } = tokenFromDb;
|
||||||
const validRoles = rolesToCheck.filter((r: string) => roles.includes(r));
|
const validRoles = rolesToCheck.filter((r: string) => roles?.includes(r) || false);
|
||||||
return validRoles.length === rolesToCheck.length;
|
return validRoles.length === rolesToCheck.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,22 +16,42 @@ export class GameController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public joinMatch(req: Request, res: Response) {
|
public async joinMatch(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { user, body } = req;
|
const { user, params } = req;
|
||||||
const { sessionId } = body;
|
const { sessionId } = params;
|
||||||
this.sessionService.joinSession(user, sessionId);
|
await this.sessionService.joinSession(user, sessionId);
|
||||||
res.status(200).json({ status: 'ok' });
|
res.status(200).json({ status: 'ok' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(res, error);
|
this.handleError(res, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public listMatches(req: Request, res: Response) {
|
public async listMatches(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
this.sessionService.listSessions().then((sessions) => {
|
const sessions = await this.sessionService.listJoinableSessions()
|
||||||
res.status(200).json(sessions);
|
res.status(200).json(sessions);
|
||||||
});
|
} catch (error) {
|
||||||
|
this.handleError(res, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getMatch(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { sessionId } = req.params;
|
||||||
|
const session = this.sessionService.getSession(sessionId)
|
||||||
|
res.status(200).json(session);
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(res, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteMatch(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { sessionId } = req.params;
|
||||||
|
await this.sessionService.deleteSession(sessionId);
|
||||||
|
this.logger.info(`Session ${sessionId} deleted`);
|
||||||
|
res.status(200).json({ status: 'ok' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(res, error);
|
this.handleError(res, error);
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,10 @@ export interface User extends EntityMongo {
|
|||||||
namespace?: Namespace;
|
namespace?: Namespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DbMatchSessionUpdate extends EntityMongo {
|
||||||
|
state?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DbMatchSession extends EntityMongo {
|
export interface DbMatchSession extends EntityMongo {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import { mongoExecute } from "./mongoDBPool";
|
import { mongoExecute } from "./mongoDBPool";
|
||||||
import { Entity } from "../../interfaces";
|
import { Entity, EntityMongo } from "../../interfaces";
|
||||||
import { LoggingService } from "../../../../common/LoggingService";
|
import { LoggingService } from "../../../../common/LoggingService";
|
||||||
import toObjectId from "./mongoUtils";
|
import toObjectId from "./mongoUtils";
|
||||||
|
|
||||||
@ -9,7 +9,8 @@ export abstract class BaseMongoManager {
|
|||||||
protected abstract collection?: string;
|
protected abstract collection?: string;
|
||||||
logger = new LoggingService().logger;
|
logger = new LoggingService().logger;
|
||||||
|
|
||||||
create(data: Entity): Promise<ObjectId | undefined>{
|
async create(data: Entity): Promise<ObjectId | undefined> {
|
||||||
|
this.stampEntity(data);
|
||||||
return mongoExecute(
|
return mongoExecute(
|
||||||
async ({ collection }) => {
|
async ({ collection }) => {
|
||||||
const result = await collection?.insertOne(data as any);
|
const result = await collection?.insertOne(data as any);
|
||||||
@ -19,25 +20,27 @@ export abstract class BaseMongoManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(id: string) {
|
async delete(id: string): Promise<number> {
|
||||||
return mongoExecute(
|
return mongoExecute(
|
||||||
async ({ collection }) => {
|
async ({ collection }) => {
|
||||||
await collection?.deleteOne({ _id: this.toObjectId(id) });
|
const result = await collection?.deleteOne({ _id: this.toObjectId(id) });
|
||||||
|
return result?.deletedCount || 0;
|
||||||
},
|
},
|
||||||
{ colName: this.collection }
|
{ colName: this.collection }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteByFilter(filter: any) {
|
async deleteByFilter(filter: any): Promise<number> {
|
||||||
return mongoExecute(
|
return mongoExecute(
|
||||||
async ({ collection }) => {
|
async ({ collection }) => {
|
||||||
await collection?.deleteOne(filter);
|
const result = await collection?.deleteOne(filter);
|
||||||
|
return result?.deletedCount || 0;
|
||||||
},
|
},
|
||||||
{ colName: this.collection }
|
{ colName: this.collection }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getById(id: string) {
|
async getById(id: string): Promise<EntityMongo | null> {
|
||||||
return mongoExecute(
|
return mongoExecute(
|
||||||
async ({ collection }) => {
|
async ({ collection }) => {
|
||||||
return await collection?.findOne({ _id: this.toObjectId(id) });
|
return await collection?.findOne({ _id: this.toObjectId(id) });
|
||||||
@ -46,7 +49,7 @@ export abstract class BaseMongoManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByFilter(filter: any) {
|
async getByFilter(filter: any): Promise<EntityMongo | null> {
|
||||||
return mongoExecute(
|
return mongoExecute(
|
||||||
async ({ collection }) => {
|
async ({ collection }) => {
|
||||||
return await collection?.findOne(filter);
|
return await collection?.findOne(filter);
|
||||||
@ -55,51 +58,71 @@ export abstract class BaseMongoManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
list() {
|
async list(sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise<EntityMongo[]> {
|
||||||
return mongoExecute(
|
return mongoExecute(
|
||||||
async ({ collection }) => {
|
async ({ collection }) => {
|
||||||
return await collection?.find().toArray();
|
const cursor = collection?.find();
|
||||||
|
if (sortCriteria) {
|
||||||
|
cursor?.sort(sortCriteria);
|
||||||
|
}
|
||||||
|
if (pagination) {
|
||||||
|
cursor?.skip(pagination.pageSize * (pagination.page - 1)).limit(pagination.pageSize);
|
||||||
|
}
|
||||||
|
return await cursor?.toArray();
|
||||||
},
|
},
|
||||||
{ colName: this.collection }
|
{ colName: this.collection }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
listByFilter(filter: any) {
|
async listByFilter(filter: any, sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise<EntityMongo[]> {
|
||||||
return mongoExecute(
|
return mongoExecute(
|
||||||
async ({ collection }) => {
|
async ({ collection }) => {
|
||||||
return await collection?.find(filter).toArray();
|
const cursor = collection?.find(filter);
|
||||||
|
if (sortCriteria) {
|
||||||
|
cursor?.sort(sortCriteria);
|
||||||
|
}
|
||||||
|
if (pagination) {
|
||||||
|
cursor?.skip(pagination.pageSize * (pagination.page - 1)).limit(pagination.pageSize);
|
||||||
|
}
|
||||||
|
return await cursor?.toArray();
|
||||||
},
|
},
|
||||||
{ colName: this.collection }
|
{ colName: this.collection }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(object: Entity) {
|
async update(object: EntityMongo): Promise<number> {
|
||||||
const data: any = { ...object };
|
const data: any = { ...object };
|
||||||
const id = data._id;
|
const id = data._id;
|
||||||
|
this.stampEntity(data, false);
|
||||||
delete data._id;
|
delete data._id;
|
||||||
return mongoExecute(async ({ collection }) => {
|
return mongoExecute(async ({ collection }) => {
|
||||||
return await collection?.updateOne(
|
const result = await collection?.updateOne(
|
||||||
{ _id: this.toObjectId(id) },
|
{ _id: this.toObjectId(id) },
|
||||||
{ $set: data }
|
{ $set: data }
|
||||||
);
|
);
|
||||||
|
return result?.modifiedCount || 0;
|
||||||
},
|
},
|
||||||
{ colName: this.collection });
|
{ colName: this.collection });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMany(filter: any, data: Entity) {
|
async updateMany(filter: any, data: Entity): Promise<number>{
|
||||||
|
|
||||||
|
this.stampEntity(data, false);
|
||||||
return mongoExecute(async ({ collection }) => {
|
return mongoExecute(async ({ collection }) => {
|
||||||
return await collection?.updateMany(filter, { $set: data as any });
|
const result = await collection?.updateMany(filter, { $set: data as any });
|
||||||
|
return result?.modifiedCount || 0;
|
||||||
},
|
},
|
||||||
{ colName: this.collection });
|
{ colName: this.collection });
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceOne(filter: any, object: Entity) {
|
async replaceOne(filter: any, object: Entity): Promise<number> {
|
||||||
return mongoExecute(async ({collection}) => {
|
return mongoExecute(async ({collection}) => {
|
||||||
return await collection?.replaceOne(filter, object);
|
const result = await collection?.replaceOne(filter, object);
|
||||||
|
return result?.modifiedCount || 0;
|
||||||
}, {colName: this.collection});
|
}, {colName: this.collection});
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregation(pipeline: any) {
|
async aggregation(pipeline: any): Promise<EntityMongo[]> {
|
||||||
return mongoExecute(
|
return mongoExecute(
|
||||||
async ({ collection }) => {
|
async ({ collection }) => {
|
||||||
return await collection?.aggregate(pipeline).toArray();
|
return await collection?.aggregate(pipeline).toArray();
|
||||||
@ -108,7 +131,7 @@ export abstract class BaseMongoManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregationOne(pipeline: any) {
|
async aggregationOne(pipeline: any): Promise<EntityMongo | null> {
|
||||||
return mongoExecute(
|
return mongoExecute(
|
||||||
async ({ collection }) => {
|
async ({ collection }) => {
|
||||||
return await collection?.aggregate(pipeline).next();
|
return await collection?.aggregate(pipeline).next();
|
||||||
@ -120,4 +143,11 @@ export abstract class BaseMongoManager {
|
|||||||
protected toObjectId = (oid: string) => {
|
protected toObjectId = (oid: string) => {
|
||||||
return toObjectId(oid);
|
return toObjectId(oid);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
protected stampEntity(entity: Entity, isCreate: boolean = true) {
|
||||||
|
if (isCreate) {
|
||||||
|
entity.createdAt = Date.now();
|
||||||
|
}
|
||||||
|
entity.modifiedAt = Date.now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,8 @@ export class SessionManager extends ManagerBase {
|
|||||||
SessionManager.sessions.set(session.id, session);
|
SessionManager.sessions.set(session.id, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteSession(session: MatchSession) {
|
deleteSession(sessionId: string) {
|
||||||
SessionManager.sessions.delete(session.id);
|
SessionManager.sessions.delete(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSession(id: string) {
|
getSession(id: string) {
|
||||||
|
@ -10,8 +10,10 @@ export default function(): Router {
|
|||||||
const { authenticate } = AuthController
|
const { authenticate } = AuthController
|
||||||
|
|
||||||
router.post('/match', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.createMatch(req, res));
|
router.post('/match', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.createMatch(req, res));
|
||||||
router.patch('/match/join', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.joinMatch(req, res));
|
router.get('/match', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.listMatches(req, res));
|
||||||
router.get('/match/list', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.listMatches(req, res));
|
router.get('/match/:sessionId', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.getMatch(req, res));
|
||||||
|
router.put('/match/:sessionId', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.joinMatch(req, res));
|
||||||
|
router.delete('/match/:sessionId', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.deleteMatch(req, res));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
110
src/server/services/InteractionService.ts
Normal file
110
src/server/services/InteractionService.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import PubSub from "pubsub-js";
|
||||||
|
import { EventActions } from "../../game/constants";
|
||||||
|
import { MatchSession } from "../../game/MatchSession";
|
||||||
|
import { SessionManager } from "../managers/SessionManager";
|
||||||
|
import { PlayerNotificationService } from "./PlayerNotificationService";
|
||||||
|
import { ServiceBase } from "./ServiceBase";
|
||||||
|
import { PlayerMove } from "../../game/entities/PlayerMove";
|
||||||
|
|
||||||
|
export class InteractionService extends ServiceBase{
|
||||||
|
private sessionManager: SessionManager = new SessionManager();
|
||||||
|
private notifyService = new PlayerNotificationService();
|
||||||
|
|
||||||
|
async handleClientEventWithAck(data: any): Promise<any> {
|
||||||
|
const { event, data: eventData } = data;
|
||||||
|
this.logger.trace(`Handling event: ${event} with ack`);
|
||||||
|
switch(event) {
|
||||||
|
case EventActions.START_SESSION:
|
||||||
|
return this.onStartSession(eventData);
|
||||||
|
default:
|
||||||
|
// PubSub.publish(event, eventData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClientEvent(data: any): any {
|
||||||
|
const { event, data: eventData } = data;
|
||||||
|
this.logger.trace(`Handling event: ${event}`);
|
||||||
|
switch(event) {
|
||||||
|
case 'client:player-move':
|
||||||
|
this.onClientMoveResponse(eventData);
|
||||||
|
break;
|
||||||
|
case 'client:set-client-ready':
|
||||||
|
this.onClientReady(eventData);
|
||||||
|
break;
|
||||||
|
case EventActions.PLAYER_READY:
|
||||||
|
this.onPlayerReady(eventData);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
PubSub.publish(event, eventData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onStartSession(data: any): any {
|
||||||
|
const sessionId: string = data.sessionId;
|
||||||
|
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
||||||
|
|
||||||
|
if (session === undefined) {
|
||||||
|
return ({
|
||||||
|
status: 'error',
|
||||||
|
message: 'Session not found'
|
||||||
|
});
|
||||||
|
} else if (session.matchInProgress) {
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: 'Game already in progress'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const missingHumans = session.maxPlayers - session.numPlayers;
|
||||||
|
for (let i = 0; i < missingHumans; i++) {
|
||||||
|
session.addPlayerToSession(session.createPlayerAI(i));
|
||||||
|
}
|
||||||
|
this.notifyService.sendEventToPlayers('server:match-starting', session.players);
|
||||||
|
session.start();
|
||||||
|
return {
|
||||||
|
status: 'ok'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public playerMove(data: any) {
|
||||||
|
this.onClientMoveResponse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClientMoveResponse(data: any): any {
|
||||||
|
const { sessionId, move }: { sessionId: string, move: PlayerMove } = data;
|
||||||
|
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
||||||
|
if (session !== undefined) {
|
||||||
|
session.playerMove(move);
|
||||||
|
return {
|
||||||
|
status: 'ok'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPlayerReady(data: any): any {
|
||||||
|
const { userId, sessionId } = data;
|
||||||
|
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
||||||
|
if (session !== undefined) {
|
||||||
|
session.setPlayerReady(userId)
|
||||||
|
this.notifyService.notifyMatchState(session);
|
||||||
|
this.notifyService.notifyPlayersState(session.players);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClientReady(data: any): any {
|
||||||
|
const { sessionId, userId } = data;
|
||||||
|
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
||||||
|
session?.setClientReady(userId);
|
||||||
|
return {
|
||||||
|
status: 'ok'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateSocketId(sessionId: string, userId: string, socketId: string): any {
|
||||||
|
return this.sessionManager.updateSocketId(sessionId, userId, socketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,35 +1,42 @@
|
|||||||
import { DominoesGame } from "../../game/DominoesGame";
|
import { DominoesGame } from "../../game/DominoesGame";
|
||||||
import { MatchSession } from "../../game/MatchSession";
|
import { MatchSession } from "../../game/MatchSession";
|
||||||
|
import { NetworkClientNotifier } from "../../game/NetworkClientNotifier";
|
||||||
import { GameState } from "../../game/dto/GameState";
|
import { GameState } from "../../game/dto/GameState";
|
||||||
|
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
|
||||||
import { PlayerInterface } from "../../game/entities/player/PlayerInterface";
|
import { PlayerInterface } from "../../game/entities/player/PlayerInterface";
|
||||||
|
import { ServiceBase } from "./ServiceBase";
|
||||||
|
|
||||||
export class PlayerNotificationService {
|
export class PlayerNotificationService extends ServiceBase {
|
||||||
|
clientNotifier: NetworkClientNotifier = new NetworkClientNotifier();
|
||||||
|
|
||||||
async notifyGameState(game: DominoesGame) {
|
notifyGameState(game: DominoesGame) {
|
||||||
const gameState: GameState = game.getGameState();
|
const gameState: GameState = game.getGameState();
|
||||||
const { players } = game;
|
const { players } = game;
|
||||||
let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-game-state', gameState));
|
players.map(player => player.sendEvent('update-game-state', gameState));
|
||||||
return await Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async notifyPlayersState(players: PlayerInterface[]) {
|
notifyPlayersState(players: PlayerInterface[]) {
|
||||||
let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-player-state', player.getState()));
|
players.map(player => player.sendEvent('update-player-state', player.getState()));
|
||||||
return await Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async notifyMatchState(session: MatchSession) {
|
notifyMatchState(session: MatchSession) {
|
||||||
const { players } = session;
|
const { players } = session;
|
||||||
let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-match-session-state', session.getState()));
|
players.map(player => player.sendEvent('update-match-session-state', session.getState()));
|
||||||
return await Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendEventToPlayers(event: string, players: PlayerInterface[], data: any = {}) {
|
async sendEventToPlayers(event: string, players: PlayerInterface[], data: Function | any = {}) {
|
||||||
let promises: Promise<void>[] = players.map(player => player.sendEvent(event, data));
|
players.forEach((player) => {
|
||||||
return await Promise.all(promises);
|
let dataTosend = data;
|
||||||
|
if (typeof data === 'function') {
|
||||||
|
dataTosend = data(player);
|
||||||
|
}
|
||||||
|
this.clientNotifier.sendEvent(player as NetworkPlayer, event, dataTosend);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendEvent(event: string, player: PlayerInterface, data: any = {}) {
|
sendEvent(event: string, player: PlayerInterface, data: any = {}) {
|
||||||
player.sendEvent(event, data)
|
this.logger.debug(`Sending event '${event}' to player ${player.id}`);
|
||||||
|
this.clientNotifier.sendEvent(player as NetworkPlayer, event, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,15 +1,16 @@
|
|||||||
import { SessionCreationError } from "../../common/errors/SessionCreationError";
|
import { SessionCreationError } from "../../common/errors/SessionCreationError";
|
||||||
import { SessionNotFoundError } from "../../common/errors/SessionNotFoundError";
|
import { SessionNotFoundError } from "../../common/errors/SessionNotFoundError";
|
||||||
import { wait, whileNotUndefined } from "../../common/utilities";
|
import { whileNotUndefined } from "../../common/utilities";
|
||||||
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
|
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
|
||||||
import { MatchSession } from "../../game/MatchSession";
|
import { MatchSession } from "../../game/MatchSession";
|
||||||
import { PlayerNotificationService } from "./PlayerNotificationService";
|
import { PlayerNotificationService } from "./PlayerNotificationService";
|
||||||
import { matchSessionAdapter } from "../db/DbAdapter";
|
import { matchSessionAdapter } from "../db/DbAdapter";
|
||||||
import { DbMatchSession } from "../db/interfaces";
|
import { DbMatchSession, DbMatchSessionUpdate } from "../db/interfaces";
|
||||||
import { MatchSessionMongoManager } from "../db/mongo/MatchSessionMongoManager";
|
import { MatchSessionMongoManager } from "../db/mongo/MatchSessionMongoManager";
|
||||||
import { SessionManager } from "../managers/SessionManager";
|
import { SessionManager } from "../managers/SessionManager";
|
||||||
import { ServiceBase } from "./ServiceBase";
|
import { ServiceBase } from "./ServiceBase";
|
||||||
import { SocketIoService } from "./SocketIoService";
|
import { SocketIoService } from "./SocketIoService";
|
||||||
|
import toObjectId from "../db/mongo/common/mongoUtils";
|
||||||
|
|
||||||
export class SessionService extends ServiceBase{
|
export class SessionService extends ServiceBase{
|
||||||
private dbManager: MatchSessionMongoManager = new MatchSessionMongoManager();
|
private dbManager: MatchSessionMongoManager = new MatchSessionMongoManager();
|
||||||
@ -37,68 +38,48 @@ export class SessionService extends ServiceBase{
|
|||||||
this.sessionManager.setSession(session);
|
this.sessionManager.setSession(session);
|
||||||
this.notifyService.notifyMatchState(session);
|
this.notifyService.notifyMatchState(session);
|
||||||
this.notifyService.notifyPlayersState(session.players);
|
this.notifyService.notifyPlayersState(session.players);
|
||||||
|
this.logger.debug(`Session ${session.id} created`);
|
||||||
return session.id;
|
return session.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async joinSession(user: any, sessionId: string): Promise<void> {
|
public async joinSession(user: any, sessionId: string): Promise<string> {
|
||||||
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
||||||
if (session === undefined) {
|
if (session === undefined) {
|
||||||
throw new SessionNotFoundError();
|
throw new SessionNotFoundError();
|
||||||
} let socketClient;
|
}
|
||||||
|
let socketClient;
|
||||||
try {
|
try {
|
||||||
socketClient = await whileNotUndefined(() => SocketIoService.getClient(user._id));
|
socketClient = await whileNotUndefined(() => SocketIoService.getClient(user._id));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new SessionCreationError();
|
throw new SessionCreationError();
|
||||||
}
|
}
|
||||||
const player = new NetworkPlayer(user._id, user.name, socketClient.socketId);
|
const player = new NetworkPlayer(user._id, user.username, socketClient.socketId);
|
||||||
session.addPlayer(player);
|
|
||||||
socketClient.sessionId = session.id;
|
|
||||||
this.dbManager.replaceOne({id: session.id}, matchSessionAdapter(session));
|
this.dbManager.replaceOne({id: session.id}, matchSessionAdapter(session));
|
||||||
|
session.addPlayerToSession(player);
|
||||||
|
socketClient.sessionId = session.id;
|
||||||
|
this.notifyService.notifyMatchState(session);
|
||||||
|
this.notifyService.notifyPlayersState(session.players);
|
||||||
|
return sessionId
|
||||||
}
|
}
|
||||||
|
|
||||||
public listSessions(): Promise<DbMatchSession[]> {
|
public async listJoinableSessions(): Promise<DbMatchSession[]> {
|
||||||
return this.dbManager.listByFilter({});
|
return await this.dbManager.listByFilter(
|
||||||
|
{ state: 'created' },
|
||||||
|
{ createdAt: -1 },
|
||||||
|
{ page: 1, pageSize: 5 }) as DbMatchSession[];
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateSocketId(sessionId: string, userId: string, socketId: string): any {
|
public async getSession(sessionId: string): Promise<DbMatchSession | undefined> {
|
||||||
this.sessionManager.updateSocketId(sessionId, userId, socketId);
|
return await this.dbManager.getById(sessionId) as DbMatchSession | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlayerReady(data: any): any {
|
public async deleteSession(sessionId: string): Promise<any> {
|
||||||
const { userId, sessionId } = data;
|
this.sessionManager.deleteSession(sessionId);
|
||||||
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
const session = {
|
||||||
if (session !== undefined) {
|
_id: toObjectId(sessionId),
|
||||||
session.setPlayerReady(userId)
|
state: 'deleted'
|
||||||
this.notifyService.notifyMatchState(session);
|
} as DbMatchSessionUpdate;
|
||||||
this.notifyService.notifyPlayersState(session.players);
|
return this.dbManager.update(session);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startSession(data: any): any {
|
|
||||||
const sessionId: string = data.sessionId;
|
|
||||||
const seed: string | undefined = data.seed;
|
|
||||||
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
|
||||||
|
|
||||||
if (session === undefined) {
|
|
||||||
return ({
|
|
||||||
status: 'error',
|
|
||||||
message: 'Session not found'
|
|
||||||
});
|
|
||||||
} else if (session.matchInProgress) {
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: 'Game already in progress'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const missingHumans = session.maxPlayers - session.numPlayers;
|
|
||||||
for (let i = 0; i < missingHumans; i++) {
|
|
||||||
session.addPlayer(session.createPlayerAI(i));
|
|
||||||
}
|
|
||||||
session.start();
|
|
||||||
return {
|
|
||||||
status: 'ok'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// public updateSession(session: MatchSession): any {
|
// public updateSession(session: MatchSession): any {
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { Server as HttpServer } from "http";
|
import { Server as HttpServer } from "http";
|
||||||
import { ServiceBase } from "./ServiceBase";
|
import { ServiceBase } from "./ServiceBase";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import { SessionManager } from "../managers/SessionManager";
|
|
||||||
import { SecurityManager } from "../managers/SecurityManager";
|
import { SecurityManager } from "../managers/SecurityManager";
|
||||||
import { User } from "../db/interfaces";
|
import { User } from "../db/interfaces";
|
||||||
import { Socket } from "socket.io";
|
import { Socket } from "socket.io";
|
||||||
import { SessionService } from "./SessionService";
|
import { InteractionService } from "./InteractionService";
|
||||||
|
import { ClientEvents } from "../../game/constants";
|
||||||
|
|
||||||
export class SocketIoService extends ServiceBase{
|
export class SocketIoService extends ServiceBase{
|
||||||
io: Server
|
io: Server
|
||||||
private static clients: Map<string, any> = new Map();
|
private static clients: Map<string, any> = new Map();
|
||||||
private sessionService: SessionService = new SessionService();
|
private interactionService: InteractionService = new InteractionService();
|
||||||
|
|
||||||
static getClient(id: string) {
|
static getClient(id: string) {
|
||||||
return this.clients.get(id);
|
return this.clients.get(id);
|
||||||
@ -65,7 +65,7 @@ export class SocketIoService extends ServiceBase{
|
|||||||
socket.join('room-general')
|
socket.join('room-general')
|
||||||
} else {
|
} else {
|
||||||
const client = SocketIoService.clients.get(userId);
|
const client = SocketIoService.clients.get(userId);
|
||||||
this.sessionService.updateSocketId(client.sessionId, userId, socketId);
|
this.interactionService.updateSocketId(client.sessionId, userId, socketId);
|
||||||
client.socketId = socketId;
|
client.socketId = socketId;
|
||||||
this.logger.debug(`User '${user.username}' already connected. Updating socketId to ${socketId}`);
|
this.logger.debug(`User '${user.username}' already connected. Updating socketId to ${socketId}`);
|
||||||
client.alive = true;
|
client.alive = true;
|
||||||
@ -83,24 +83,13 @@ export class SocketIoService extends ServiceBase{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// socket.on('createSession', (data, callback) => {
|
socket.on(ClientEvents.CLIENT_EVENT, (data) => {
|
||||||
// const response = sessionController.createSession(data, socket.id);
|
this.interactionService.handleClientEvent(data);
|
||||||
// callback(response);
|
|
||||||
// });
|
|
||||||
|
|
||||||
socket.on('startSession', (data, callback) => {
|
|
||||||
const response = this.sessionService.startSession(data);
|
|
||||||
callback(response);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// socket.on('joinSession', (data, callback) => {
|
socket.on(ClientEvents.CLIENT_EVENT_WITH_ACK, (data, callback) => {
|
||||||
// const response = sessionController.joinSession(data, socket.id);
|
const result = this.interactionService.handleClientEventWithAck(data);
|
||||||
// callback(response);
|
callback(result);
|
||||||
// });
|
|
||||||
|
|
||||||
socket.on('playerReady', (data, callback) => {
|
|
||||||
const response = this.sessionService.setPlayerReady(data);
|
|
||||||
callback(response);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('pong', () => {
|
socket.on('pong', () => {
|
||||||
@ -110,11 +99,42 @@ export class SocketIoService extends ServiceBase{
|
|||||||
SocketIoService.clients.set(id, {...client, alive: true });
|
SocketIoService.clients.set(id, {...client, alive: true });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
socket.onAny((event, ...args) => {
|
||||||
|
if (['pong'].includes(event)) return;
|
||||||
|
let logStr = `Event received: ${event}`
|
||||||
|
|
||||||
|
if (event.startsWith('client:') && args.length > 0) {
|
||||||
|
logStr = `${logStr} (${args[0].event})`;
|
||||||
|
}
|
||||||
|
this.logger.debug(logStr);
|
||||||
|
});
|
||||||
|
|
||||||
this.pingClients()
|
this.pingClients()
|
||||||
|
|
||||||
|
// // socket.on('createSession', (data, callback) => {
|
||||||
|
// // const response = sessionController.createSession(data, socket.id);
|
||||||
|
// // callback(response);
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// socket.on('startSession', (data, callback) => {
|
||||||
|
// const response = this.sessionService.startSession(data);
|
||||||
|
// callback(response);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// socket.on('client:tile-animation-ended', (data) => {
|
||||||
|
// this.sessionService.onClientEndTileAnimation(data);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// socket.on('joinSession', (data, callback) => {
|
||||||
|
// const response = sessionController.joinSession(data, socket.id);
|
||||||
|
// callback(response);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// socket.on('playerReady', (data, callback) => {
|
||||||
|
// const response = this.sessionService.setPlayerReady(data);
|
||||||
|
// callback(response);
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private pingClients() {
|
private pingClients() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user