From 7741b07d60b434c60ee026f6e9316ea4c6d7f5d0 Mon Sep 17 00:00:00 2001 From: Jose Conde Date: Sun, 14 Jul 2024 21:41:38 +0200 Subject: [PATCH] working flow --- create.ts | 20 ++ secret.js | 22 ++ src/game/DominoesGame.ts | 97 ++++++--- src/game/MatchSession.ts | 37 ++-- src/game/PlayerInteractionAI.ts | 7 +- src/game/PlayerInteractionNetwork.ts | 3 + src/game/dto/GameSummary.ts | 3 +- src/game/dto/MatchSessionState.ts | 4 +- src/game/dto/PlayerDto.ts | 10 +- src/game/entities/Tile.ts | 10 + src/game/entities/player/AbstractPlayer.ts | 17 +- src/game/entities/player/PlayerInterface.ts | 4 +- src/server/controllers/ApiKeyController.ts | 8 +- src/server/controllers/UserController.ts | 2 +- src/server/db/DbAdapter.ts | 2 +- src/server/db/interfaces.ts | 37 ++-- src/server/db/mongo/ApiTokenMongoManager.ts | 7 - src/server/db/mongo/NamespacesMongoManager.ts | 13 -- .../db/mongo/common/BaseMongoManager.ts | 69 +++++- src/server/db/mongo/common/mongoUtils.ts | 7 +- src/server/services/InteractionService.ts | 37 +++- src/server/services/NamespacesService.ts | 9 +- src/server/services/SessionService.ts | 12 +- src/server/services/SocketIoService.ts | 2 +- src/server/services/UsersService.ts | 2 +- src/test.ts | 34 +-- text.txt | 206 ++++++++++++++++++ 27 files changed, 534 insertions(+), 147 deletions(-) create mode 100644 create.ts create mode 100644 secret.js create mode 100644 text.txt diff --git a/create.ts b/create.ts new file mode 100644 index 0000000..230989c --- /dev/null +++ b/create.ts @@ -0,0 +1,20 @@ +import { UsersService } from './src/server/services/UsersService'; + +const usersService = new UsersService(); +usersService.createUser({ + firstname: 'Test', + lastname: 'User 2', + username: 'test2', + password: 'qwerty123', + roles: ['user'], + email: '' +}); + +usersService.createUser({ + firstname: 'Test', + lastname: 'User 3', + username: 'test4', + password: 'qwerty123', + roles: ['user'], + email: '' +}); \ No newline at end of file diff --git a/secret.js b/secret.js new file mode 100644 index 0000000..48fd476 --- /dev/null +++ b/secret.js @@ -0,0 +1,22 @@ +const crypto = require('crypto'); + +/** + * Generate a random base32-encoded secret for authentication. + * @param {number} length - The length of the generated secret. + * @returns {string} - The base32-encoded secret. + */ +function generateRandomSecret(length = 32) { + const bytes = crypto.randomBytes(length); + const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // RFC 4648 Base32 alphabet + let secret = ''; + + for (let i = 0; i < bytes.length; i++) { + secret += base32Chars[bytes[i] % 32]; + } + + return secret; +} + +const secret = generateRandomSecret(); +console.log('Generated secret:', secret); +console.log(crypto.randomBytes(32).toString('hex')) \ No newline at end of file diff --git a/src/game/DominoesGame.ts b/src/game/DominoesGame.ts index 60ea9b1..67cb6dd 100644 --- a/src/game/DominoesGame.ts +++ b/src/game/DominoesGame.ts @@ -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 { 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); + } + } + + } \ No newline at end of file diff --git a/src/game/MatchSession.ts b/src/game/MatchSession.ts index 6959dff..bda4d56 100644 --- a/src/game/MatchSession.ts +++ b/src/game/MatchSession.ts @@ -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 = 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, }; } } \ No newline at end of file diff --git a/src/game/PlayerInteractionAI.ts b/src/game/PlayerInteractionAI.ts index f765fbe..78d1776 100644 --- a/src/game/PlayerInteractionAI.ts +++ b/src/game/PlayerInteractionAI.ts @@ -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: (this.player).sessionId, move }); - this.logger.trace('Move sent to server (AI'); + this.logger.trace('Move sent to server (AI)'); }, rndWait); } diff --git a/src/game/PlayerInteractionNetwork.ts b/src/game/PlayerInteractionNetwork.ts index 9f92009..7ee3d0e 100644 --- a/src/game/PlayerInteractionNetwork.ts +++ b/src/game/PlayerInteractionNetwork.ts @@ -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 diff --git a/src/game/dto/GameSummary.ts b/src/game/dto/GameSummary.ts index 36fdec3..5edf262 100644 --- a/src/game/dto/GameSummary.ts +++ b/src/game/dto/GameSummary.ts @@ -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[]; } \ No newline at end of file diff --git a/src/game/dto/MatchSessionState.ts b/src/game/dto/MatchSessionState.ts index 2fc8231..d762c4b 100644 --- a/src/game/dto/MatchSessionState.ts +++ b/src/game/dto/MatchSessionState.ts @@ -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[]; } \ No newline at end of file diff --git a/src/game/dto/PlayerDto.ts b/src/game/dto/PlayerDto.ts index 7ae0a80..c06b921 100644 --- a/src/game/dto/PlayerDto.ts +++ b/src/game/dto/PlayerDto.ts @@ -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; } \ No newline at end of file diff --git a/src/game/entities/Tile.ts b/src/game/entities/Tile.ts index 160e809..732773a 100644 --- a/src/game/entities/Tile.ts +++ b/src/game/entities/Tile.ts @@ -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 '[ | ]'; diff --git a/src/game/entities/player/AbstractPlayer.ts b/src/game/entities/player/AbstractPlayer.ts index 55dc10e..0aaedbd 100644 --- a/src/game/entities/player/AbstractPlayer.ts +++ b/src/game/entities/player/AbstractPlayer.ts @@ -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, }; } diff --git a/src/game/entities/player/PlayerInterface.ts b/src/game/entities/player/PlayerInterface.ts index 73376d4..524e7f0 100644 --- a/src/game/entities/player/PlayerInterface.ts +++ b/src/game/entities/player/PlayerInterface.ts @@ -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; sendEventWithAck(event: string, data: any): Promise; - getState(): PlayerDto; + getState(showPips?: boolean): PlayerDto; } \ No newline at end of file diff --git a/src/server/controllers/ApiKeyController.ts b/src/server/controllers/ApiKeyController.ts index 848762f..fdaadf3 100644 --- a/src/server/controllers/ApiKeyController.ts +++ b/src/server/controllers/ApiKeyController.ts @@ -22,7 +22,7 @@ export class ApiKeyController extends BaseController{ async createApiKey(req: Request, res: Response) { try { const token: Token = this._createTokenObject(req); - await this.apiTokenManager.addToken(token); + await this.apiTokenManager.create(token); res.status(201).end(); } catch (error) { this.handleError(res, error); @@ -62,7 +62,7 @@ export class ApiKeyController extends BaseController{ async createNamespaceApiKey(req: Request, res: Response) { try { const token = this._createTokenObject(req); - await this.apiTokenManager.addToken(token); + await this.apiTokenManager.create(token); res.status(201).end(); } catch (error) { this.handleError(res, error); @@ -81,9 +81,9 @@ export class ApiKeyController extends BaseController{ type }; if (type === 'namespace') { - newToken.namespaceId = toObjectId(namespaceId); + newToken.namespaceId = namespaceId; } else if (type === 'user') { - newToken.userId = toObjectId(userId); + newToken.userId = userId; } return newToken; } diff --git a/src/server/controllers/UserController.ts b/src/server/controllers/UserController.ts index cbeabe4..c99102c 100644 --- a/src/server/controllers/UserController.ts +++ b/src/server/controllers/UserController.ts @@ -142,7 +142,7 @@ export class UserController extends BaseController { } this.temporalTokenManager.deleteAllByUserAndType(userId.toString(), TemporalTokenMongoManager.Types.PASSWORD_RECOVERY); - this.temporalTokenManager.addToken(temporalToken); + this.temporalTokenManager.create(temporalToken); await this.mailService.sendRecoveryPasswordEmail(firstname, lastname, email, pin); res.status(200).end(); } catch (error: any) { diff --git a/src/server/db/DbAdapter.ts b/src/server/db/DbAdapter.ts index af0d2ab..3bed7e3 100644 --- a/src/server/db/DbAdapter.ts +++ b/src/server/db/DbAdapter.ts @@ -14,7 +14,7 @@ export function matchSessionAdapter(session: MatchSession) : DbMatchSession { numPlayers: session.numPlayers, scoreboard: Array.from(session.scoreboard.entries()).map(([player, score]) => ({ player, score })), matchWinner: session.matchWinner ? session.matchWinner.id : null, - state: session.state + status: session.status } } \ No newline at end of file diff --git a/src/server/db/interfaces.ts b/src/server/db/interfaces.ts index 20944e0..ea2c93f 100644 --- a/src/server/db/interfaces.ts +++ b/src/server/db/interfaces.ts @@ -1,14 +1,12 @@ -import { ObjectId } from "mongodb"; - export interface Entity { - createdAt?: number | null; - modifiedAt?: number | null; - createdBy?: ObjectId | null; - modifiedBy?: ObjectId | null; + createdAt?: number; + modifiedAt?: number; + createdBy?: string; + modifiedBy?: string; } export interface EntityMongo extends Entity { - _id?: ObjectId; + _id?: string; } export interface Score { @@ -21,14 +19,14 @@ export interface Namespace extends EntityMongo { description?: string; default: boolean; type: string | null; - ownerId?: ObjectId; + ownerId?: string; users?: any[]; } export interface User extends EntityMongo { id: string, username: string; - namespaceId: ObjectId; + namespaceId: string; hash?: string; roles: string[]; firstname?: string; @@ -55,13 +53,13 @@ export interface DbMatchSession extends EntityMongo { numPlayers: number; scoreboard: Score[]; matchWinner: string | null; - state: string; + status: string; } export interface DbUser extends EntityMongo { id: string, username: string; - namespaceId: ObjectId; + namespaceId: string; hash?: string; roles: string[]; firstname?: string; @@ -77,16 +75,16 @@ export interface DbNamespace extends EntityMongo { description?: string; default: boolean; type: string | null; - ownerId?: ObjectId; + ownerId?: string; } export interface Token extends EntityMongo { token: string; - userId?: ObjectId; + userId?: string; roles?: string[]; expiresAt?: number | null; type: string; - namespaceId?: ObjectId + namespaceId?: string description?: string; } @@ -100,3 +98,14 @@ export interface Role { permissions: string[]; } +export interface DbListResponse{ + pagination?: { + page: number; + next?: number; + size: number; + total: number; + totalPages: number; + } + sort?: any; + data: EntityMongo[]; +} \ No newline at end of file diff --git a/src/server/db/mongo/ApiTokenMongoManager.ts b/src/server/db/mongo/ApiTokenMongoManager.ts index ef261a9..564e97f 100644 --- a/src/server/db/mongo/ApiTokenMongoManager.ts +++ b/src/server/db/mongo/ApiTokenMongoManager.ts @@ -5,13 +5,6 @@ import { Token } from '../interfaces'; export class ApiTokenMongoManager extends BaseMongoManager{ collection = 'tokens'; - async addToken(token: Token) { - return await mongoExecute(async ({ collection }) => { - await collection?.insertOne(token); - return token; - }, { colName: this.collection }); - } - async getTokens(userId: string): Promise { return await mongoExecute(async ({ collection }) => { return await collection?.find({ userId: this.toObjectId(userId) }).toArray(); diff --git a/src/server/db/mongo/NamespacesMongoManager.ts b/src/server/db/mongo/NamespacesMongoManager.ts index 688958c..43a6cd4 100644 --- a/src/server/db/mongo/NamespacesMongoManager.ts +++ b/src/server/db/mongo/NamespacesMongoManager.ts @@ -11,19 +11,6 @@ export class NamespacesMongoManager extends BaseMongoManager{ super(); } - async createNamespace(namespace: Namespace): Promise { - return await mongoExecute(async ({collection}) => { - const now = new Date().getTime(); - delete namespace.users; - const result = await collection?.insertOne({ - ...namespace, - createdAt: now, - modifiedAt: now - }); - return result?.insertedId.toString() || ''; - }, {colName: this.collection}) - } - async updateNamespace(id: string, namespace: Namespace): Promise { return await mongoExecute(async ({collection}) => { const now = new Date().getTime(); diff --git a/src/server/db/mongo/common/BaseMongoManager.ts b/src/server/db/mongo/common/BaseMongoManager.ts index 72b676b..17af25a 100644 --- a/src/server/db/mongo/common/BaseMongoManager.ts +++ b/src/server/db/mongo/common/BaseMongoManager.ts @@ -1,6 +1,5 @@ -import { ObjectId } from "mongodb"; import { mongoExecute } from "./mongoDBPool"; -import { Entity, EntityMongo } from "../../interfaces"; +import { DbListResponse, Entity, EntityMongo } from "../../interfaces"; import { LoggingService } from "../../../../common/LoggingService"; import toObjectId from "./mongoUtils"; @@ -9,12 +8,12 @@ export abstract class BaseMongoManager { protected abstract collection?: string; logger = new LoggingService().logger; - async create(data: Entity): Promise { + async create(data: EntityMongo): Promise { this.stampEntity(data); return mongoExecute( async ({ collection }) => { const result = await collection?.insertOne(data as any); - return result?.insertedId; + return result?.insertedId.toString() || undefined; }, { colName: this.collection } ); @@ -58,7 +57,7 @@ export abstract class BaseMongoManager { ); } - async list(sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise { + async list(sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise { return mongoExecute( async ({ collection }) => { const cursor = collection?.find(); @@ -68,13 +67,39 @@ export abstract class BaseMongoManager { if (pagination) { cursor?.skip(pagination.pageSize * (pagination.page - 1)).limit(pagination.pageSize); } - return await cursor?.toArray(); + const cursorArray = (await cursor?.toArray()) || [] + const data = cursorArray.map((item: any) => { + item._id = item._id.toString(); + return item; + }) as EntityMongo[] + + const listResponse: DbListResponse = { + data + }; + + if (pagination) { + const total = await collection?.countDocuments() || 0; + const totalPages = Math.ceil(total / pagination.pageSize); + listResponse.pagination = { + page: pagination.page, + size: pagination.pageSize, + total, + totalPages, + next: totalPages > pagination.page ? pagination.page + 1 : undefined, + }; + } + + if (sortCriteria) { + listResponse.sort = sortCriteria; + } + + return listResponse; }, { colName: this.collection } ); } - async listByFilter(filter: any, sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise { + async listByFilter(filter: any, sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise { return mongoExecute( async ({ collection }) => { const cursor = collection?.find(filter); @@ -84,7 +109,35 @@ export abstract class BaseMongoManager { if (pagination) { cursor?.skip(pagination.pageSize * (pagination.page - 1)).limit(pagination.pageSize); } - return await cursor?.toArray(); + + const cursorArray = (await cursor?.toArray()) || [] + const data = cursorArray.map((item: any) => { + item._id = item._id.toString(); + return item; + }) as EntityMongo[] + + const listResponse: DbListResponse = { + data + }; + + if (pagination) { + const total = await collection?.countDocuments(filter) || 0; + const totalPages = Math.ceil(total / pagination.pageSize); + listResponse.pagination = { + page: pagination.page, + size: pagination.pageSize, + total, + totalPages, + next: totalPages > pagination.page ? pagination.page + 1 : undefined, + }; + + } + + if (sortCriteria) { + listResponse.sort = sortCriteria; + } + + return listResponse; }, { colName: this.collection } ); diff --git a/src/server/db/mongo/common/mongoUtils.ts b/src/server/db/mongo/common/mongoUtils.ts index 9bb5333..8d8446b 100644 --- a/src/server/db/mongo/common/mongoUtils.ts +++ b/src/server/db/mongo/common/mongoUtils.ts @@ -1,5 +1,8 @@ import { ObjectId } from "mongodb"; -export default function toObjectId(id: string) { - return ObjectId.createFromHexString(id); +export default function toObjectId(oid: string | ObjectId): ObjectId { + if (oid instanceof ObjectId) { + return oid; + } + return ObjectId.createFromHexString(oid); } \ No newline at end of file diff --git a/src/server/services/InteractionService.ts b/src/server/services/InteractionService.ts index 007d1aa..8010e44 100644 --- a/src/server/services/InteractionService.ts +++ b/src/server/services/InteractionService.ts @@ -27,14 +27,18 @@ export class InteractionService extends ServiceBase{ this.logger.trace(`Handling event: ${event}`); switch(event) { case 'client:player-move': - this.onClientMoveResponse(eventData); + this.playerMoveHuman(eventData); break; + case 'client:set-client-ready-for-next-game': case 'client:set-client-ready': this.onClientReady(eventData); break; case EventActions.PLAYER_READY: this.onPlayerReady(eventData); break; + case 'client:animation-ended': + this.onClientsAnimationEnded(eventData); + break; default: PubSub.publish(event, eventData); break; @@ -60,7 +64,9 @@ export class InteractionService extends ServiceBase{ for (let i = 0; i < missingHumans; i++) { session.addPlayerToSession(session.createPlayerAI(i)); } - this.notifyService.sendEventToPlayers('server:match-starting', session.players); + this.notifyService.sendEventToPlayers('server:match-starting', session.players, { + sessionState: session.getState() + }); session.start(); return { status: 'ok' @@ -68,15 +74,23 @@ export class InteractionService extends ServiceBase{ } } - public playerMove(data: any) { - this.onClientMoveResponse(data); + public playerMoveAI(data: any) { + this.onClientMoveResponse(data, false); } - private onClientMoveResponse(data: any): any { + public playerMoveHuman(data: any) { + this.onClientMoveResponse(data, true); + } + + private onClientMoveResponse(data: any, notifyClientReady: boolean = false): any { + this.logger.trace('Handling player move (onClientMoveResponse)'); const { sessionId, move }: { sessionId: string, move: PlayerMove } = data; const session: MatchSession | undefined = this.sessionManager.getSession(sessionId); if (session !== undefined) { - session.playerMove(move); + if (notifyClientReady) { + session.setCurrentGameClientReady(move.playerId); + } + session.playerMove(move); return { status: 'ok' }; @@ -100,8 +114,19 @@ export class InteractionService extends ServiceBase{ return { status: 'ok' } + } + + private onClientsAnimationEnded(data: any): any { + const { sessionId, userId } = data; + const session: MatchSession | undefined = this.sessionManager.getSession(sessionId); + session?.setCurrentGameClientReady(userId); + return { + status: 'ok' + } } + + public updateSocketId(sessionId: string, userId: string, socketId: string): any { return this.sessionManager.updateSocketId(sessionId, userId, socketId); } diff --git a/src/server/services/NamespacesService.ts b/src/server/services/NamespacesService.ts index f0201d6..2e5c92f 100644 --- a/src/server/services/NamespacesService.ts +++ b/src/server/services/NamespacesService.ts @@ -1,4 +1,5 @@ import { Namespace, User } from "../db/interfaces"; +import toObjectId from "../db/mongo/common/mongoUtils"; import { NamespacesMongoManager } from "../db/mongo/NamespacesMongoManager"; import { UsersService } from "./UsersService"; @@ -7,8 +8,12 @@ export class NamespacesService { usersService = new UsersService(); async createNamespace(namespace: Namespace, user: User) { - const insertedId = await this.namespacesManager.createNamespace({ ownerId: user._id, ...namespace, createdBy: user._id }); - await this._updateNamespaceUsers(namespace.users ?? [], insertedId); + const userId = user._id + if (userId === undefined) return undefined; + + const insertedId = await this.namespacesManager.create({ ownerId: userId, createdBy: userId, ...namespace }); + if (insertedId === undefined) return undefined; + await this._updateNamespaceUsers(namespace.users ?? [], insertedId.toString()); return insertedId; } diff --git a/src/server/services/SessionService.ts b/src/server/services/SessionService.ts index a3a2b07..e42413d 100644 --- a/src/server/services/SessionService.ts +++ b/src/server/services/SessionService.ts @@ -5,7 +5,7 @@ import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer"; import { MatchSession } from "../../game/MatchSession"; import { PlayerNotificationService } from "./PlayerNotificationService"; import { matchSessionAdapter } from "../db/DbAdapter"; -import { DbMatchSession, DbMatchSessionUpdate } from "../db/interfaces"; +import { DbListResponse, DbMatchSession, DbMatchSessionUpdate } from "../db/interfaces"; import { MatchSessionMongoManager } from "../db/mongo/MatchSessionMongoManager"; import { SessionManager } from "../managers/SessionManager"; import { ServiceBase } from "./ServiceBase"; @@ -62,11 +62,11 @@ export class SessionService extends ServiceBase{ return sessionId } - public async listJoinableSessions(): Promise { + public async listJoinableSessions(): Promise { return await this.dbManager.listByFilter( - { state: 'created' }, + { status: 'created' }, { createdAt: -1 }, - { page: 1, pageSize: 5 }) as DbMatchSession[]; + { page: 1, pageSize: 12 }) as DbListResponse; } public async getSession(sessionId: string): Promise { @@ -76,8 +76,8 @@ export class SessionService extends ServiceBase{ public async deleteSession(sessionId: string): Promise { this.sessionManager.deleteSession(sessionId); const session = { - _id: toObjectId(sessionId), - state: 'deleted' + _id: sessionId, + status: 'deleted' } as DbMatchSessionUpdate; return this.dbManager.update(session); } diff --git a/src/server/services/SocketIoService.ts b/src/server/services/SocketIoService.ts index 0fe7590..354945d 100644 --- a/src/server/services/SocketIoService.ts +++ b/src/server/services/SocketIoService.ts @@ -106,7 +106,7 @@ export class SocketIoService extends ServiceBase{ if (event.startsWith('client:') && args.length > 0) { logStr = `${logStr} (${args[0].event})`; } - this.logger.debug(logStr); + this.logger.trace(logStr); }); this.pingClients() diff --git a/src/server/services/UsersService.ts b/src/server/services/UsersService.ts index 1a4bd70..ef7abc2 100644 --- a/src/server/services/UsersService.ts +++ b/src/server/services/UsersService.ts @@ -69,7 +69,7 @@ export class UsersService extends ServiceBase { this.logger.info(`${password === undefined}`); if (_id !== undefined) { - user._id = toObjectId(_id); + user._id = _id; } if (password !== undefined && typeof password === 'string' && password.length > 0) { diff --git a/src/test.ts b/src/test.ts index cd7d73f..563332f 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,7 +1,7 @@ import { PlayerAI } from "./game/entities/player/PlayerAI"; import { PlayerHuman } from "./game/entities/player/PlayerHuman"; import {LoggingService} from "./common/LoggingService"; -import { GameSession } from "./game/GameSession"; +import { MatchSession } from "./game/MatchSession"; console.log('process.arg :>> ', process.argv); @@ -18,35 +18,35 @@ console.log('process.arg :>> ', process.argv); // } async function playSolo(seed?: string) { - const session = new GameSession(new PlayerHuman( "Jose"), "Test Game"); + const session = new MatchSession(new PlayerHuman( "Jose"), "Test Game"); console.log(`Session (${session.id}) created by: ${session.creator.name}`); - setTimeout(() => session.addPlayer(new PlayerAI("AI 2")), 1000); - setTimeout(() => session.addPlayer(new PlayerAI("AI 3")), 2000); - setTimeout(() => session.addPlayer(new PlayerAI("AI 4")), 3000); + setTimeout(() => session.addPlayerToSession(new PlayerAI("AI 2")), 1000); + setTimeout(() => session.addPlayerToSession(new PlayerAI("AI 3")), 2000); + setTimeout(() => session.addPlayerToSession(new PlayerAI("AI 4")), 3000); session.start(seed); } async function playHumans(seed?: string) { - const session = new GameSession(new PlayerHuman("Jose"), "Test Game"); - session.addPlayer(new PlayerHuman("Pepe")); - session.addPlayer(new PlayerHuman("Juan")); - session.addPlayer(new PlayerHuman("Luis")); + const session = new MatchSession(new PlayerHuman("Jose"), "Test Game"); + session.addPlayerToSession(new PlayerHuman("Pepe")); + session.addPlayerToSession(new PlayerHuman("Juan")); + session.addPlayerToSession(new PlayerHuman("Luis")); session.start(seed); } async function playAIs(seed?: string) { - const session = new GameSession(new PlayerAI("AI 1"), "Test Game"); - session.addPlayer(new PlayerAI("AI 2")); - session.addPlayer(new PlayerAI("AI 3")); - session.addPlayer(new PlayerAI("AI 4")); + const session = new MatchSession(new PlayerAI("AI 1"), "Test Game"); + session.addPlayerToSession(new PlayerAI("AI 2")); + session.addPlayerToSession(new PlayerAI("AI 3")); + session.addPlayerToSession(new PlayerAI("AI 4")); session.start(seed); } async function playTeams(seed?: string) { - const session = new GameSession(new PlayerHuman("Jose"), "Test Game"); - session.addPlayer(new PlayerAI("AI 1")); - session.addPlayer(new PlayerHuman("Juan")); - session.addPlayer(new PlayerAI("AI 2")); + const session = new MatchSession(new PlayerHuman("Jose"), "Test Game"); + session.addPlayerToSession(new PlayerAI("AI 1")); + session.addPlayerToSession(new PlayerHuman("Juan")); + session.addPlayerToSession(new PlayerAI("AI 2")); session.start(seed); } diff --git a/text.txt b/text.txt new file mode 100644 index 0000000..057bed1 --- /dev/null +++ b/text.txt @@ -0,0 +1,206 @@ +To add a player that uses a client connected by a socket in your dominoes game, you'll need to integrate WebSockets or Socket.io to enable real-time communication between the server and the client. This allows players to interact with the game from their web browsers or other client applications. + +Here’s a step-by-step guide to adding a socket-connected player to your game: + +### 1. **Setup Socket.io on the Server** + +Socket.io provides a straightforward way to handle real-time communication. + +#### **Install Dependencies** + +First, install the necessary dependencies: + +```bash +npm install express socket.io +``` + +#### **Server Setup** + +Create an Express server and integrate Socket.io. + +**server.ts**: +```typescript +import express from 'express'; +import http from 'http'; +import { Server } from 'socket.io'; +import { DominoesGame, Player, AIPlayer } from './dominoes-game'; // Adjust the path as needed + +const app = express(); +const server = http.createServer(app); +const io = new Server(server); + +const port = 3000; + +let game: DominoesGame; + +app.get('/', (req, res) => { + res.send('Dominoes Game Server'); +}); + +// Start the server +server.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); +}); +``` + +### 2. **Integrate Socket.io into the Game** + +Modify the `DominoesGame` class to support socket-connected players. + +**dominoes-game.ts**: +```typescript +import { Server } from 'socket.io'; + +interface Player { + id: string; + name: string; + hand: number[][]; + isAI: boolean; +} + +class DominoesGame { + players: Player[]; + board: number[][]; + remainingTiles: number[][]; + currentPlayerIndex: number; + gameOver: boolean; + io: Server; + + constructor(io: Server) { + this.players = []; + this.board = []; + this.remainingTiles = this.generateTiles(); + this.currentPlayerIndex = 0; + this.gameOver = false; + this.io = io; + } + + addPlayer(id: string, name: string, isAI: boolean = false) { + const player: Player = { id, name, hand: [], isAI }; + this.players.push(player); + this.dealTiles(player); + this.io.emit('playerJoined', player); + } + + // Other methods remain the same... + + generateTiles(): number[][] { + const tiles: number[][] = []; + for (let i = 0; i <= 6; i++) { + for (let j = i; j <= 6; j++) { + tiles.push([i, j]); + } + } + return this.shuffle(tiles); + } + + shuffle(array: any[]): any[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } + + dealTiles(player: Player): void { + const tilesPerPlayer = 7; + for (let i = 0; i < tilesPerPlayer; i++) { + player.hand.push(this.remainingTiles.pop() as number[]); + } + } +} + +export { DominoesGame, Player }; +``` + +### 3. **Handle Socket Connections** + +Update the server to handle connections and manage player interactions. + +**server.ts**: +```typescript +let game = new DominoesGame(io); + +io.on('connection', (socket) => { + console.log(`Player connected: ${socket.id}`); + + socket.on('joinGame', (name) => { + game.addPlayer(socket.id, name); + io.emit('updateGame', game); + }); + + socket.on('playTile', (tile) => { + const player = game.players.find(p => p.id === socket.id); + if (player) { + if (game.playTile(game.players.indexOf(player), tile)) { + io.emit('updateGame', game); + } else { + socket.emit('invalidMove', tile); + } + } + }); + + socket.on('disconnect', () => { + console.log(`Player disconnected: ${socket.id}`); + game.players = game.players.filter(p => p.id !== socket.id); + io.emit('updateGame', game); + }); +}); +``` + +### 4. **Client Implementation** + +Create a basic client to connect to the server and interact with the game. + +**client.html**: +```html + + + + Dominoes Game + + +

Dominoes Game

+
+ + + + + +``` + +### 5. **Testing and Debugging** + +1. **Run the Server**: Start your server with `node server.js`. +2. **Access the Game**: Open multiple browser windows and connect to `http://localhost:3000`. +3. **Join Game**: Each client can join the game and start interacting. +4. **Play Tiles**: Test playing tiles and ensure the game state updates correctly for all clients. + +### 6. **Enhancements** + +1. **Game State Persistence**: Implement game state persistence using a database. +2. **Authentication**: Add user authentication to track player progress. +3. **AI Integration**: Allow the game to mix human and AI players. + +By following these steps, you can effectively add a socket-connected player to your dominoes game, enabling real-time multiplayer interaction. \ No newline at end of file