working flow

This commit is contained in:
Jose Conde 2024-07-14 21:41:38 +02:00
parent 5f117667a4
commit 7741b07d60
27 changed files with 534 additions and 147 deletions

20
create.ts Normal file
View File

@ -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: ''
});

22
secret.js Normal file
View File

@ -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'))

View File

@ -5,9 +5,12 @@ 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, whileNotUndefined } from '../common/utilities'; import { printBoard, printLine, uuid, wait, whileNot, whileNotUndefined } from '../common/utilities';
import { PlayerNotificationService } from '../server/services/PlayerNotificationService'; import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
import { GameState } from './dto/GameState'; 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 { export class DominoesGame extends EventEmitter {
private id: string; private id: string;
@ -29,6 +32,7 @@ export class DominoesGame extends EventEmitter {
lastMove: PlayerMove | null = null; lastMove: PlayerMove | null = null;
forcedInitialPlayerIndex: number | null = null; forcedInitialPlayerIndex: number | null = null;
canAskNextPlayerMove: boolean = true; canAskNextPlayerMove: boolean = true;
clientsReady: string[] = [];
constructor(public players: PlayerInterface[], seed: PRNG) { constructor(public players: PlayerInterface[], seed: PRNG) {
super(); super();
@ -39,6 +43,10 @@ export class DominoesGame extends EventEmitter {
this.initializeGame(); this.initializeGame();
} }
get numHumanPlayers() {
return this.players.filter(player => player instanceof PlayerHuman).length;
}
async initializeGame() { async initializeGame() {
this.gameOver = false; this.gameOver = false;
this.gameBlocked = false; this.gameBlocked = false;
@ -83,7 +91,13 @@ export class DominoesGame extends EventEmitter {
} }
isBlocked(): boolean { 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 { isGameOver(): boolean {
@ -132,6 +146,7 @@ export class DominoesGame extends EventEmitter {
const player = this.players[this.currentPlayerIndex]; const player = this.players[this.currentPlayerIndex];
this.notificationService.sendEventToPlayers('server:next-turn', this.players, this.getGameState()); this.notificationService.sendEventToPlayers('server:next-turn', this.players, this.getGameState());
this.logger.debug(`${player.name}'s turn (${player.hand.length} tiles)`); this.logger.debug(`${player.name}'s turn (${player.hand.length} tiles)`);
this.printPlayerHand(player);
printBoard(this.board) printBoard(this.board)
player.askForMove(this.board); player.askForMove(this.board);
} catch (error) { } 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 { try {
this.lastMove = playerMove; this.lastMove = playerMove;
if (playerMove === null) { if (playerMove === null) {
console.log('Player cannot move'); this.logger.info('Player cannot move');
this.blockedCount += 1; this.blockedCount += 1;
this.logger.trace(`Blocked count: ${this.blockedCount}`);
this.gameBlocked = this.isBlocked(); this.gameBlocked = this.isBlocked();
this.notificationService.sendEventToPlayers('server:server-player-move', this.players, { move: playerMove });
if (this.gameBlocked) { if (this.gameBlocked) {
this.gameEnded(); this.gameEnded();
return; } else {
this.nextPlayer();
await this.checkAllClientsReadyToContinue()
this.playTurn();
} }
this.nextPlayer();
this.playTurn();
return; return;
} }
const player = this.players[this.currentPlayerIndex]; const player = this.players[this.currentPlayerIndex];
const skipWaitForConfirmation = player instanceof PlayerAI && playerMove === null;
this.blockedCount = 0; this.blockedCount = 0;
this.board.play(playerMove); this.board.play(playerMove);
player.hand = player.hand.filter(tile => tile !== playerMove.tile); player.hand = player.hand.filter(tile => tile !== playerMove.tile);
@ -164,8 +196,10 @@ export class DominoesGame extends EventEmitter {
// whileNotUndefined(() => this.canAskNextPlayerMove === true ? {} : undefined); // whileNotUndefined(() => this.canAskNextPlayerMove === true ? {} : undefined);
this.gameOver = this.isGameOver(); this.gameOver = this.isGameOver();
if (!this.gameOver) { if (!this.gameOver) {
this.printPlayersHand(); this.nextPlayer();
this.nextPlayer(); if (!skipWaitForConfirmation) {
await this.checkAllClientsReadyToContinue()
}
this.playTurn(); this.playTurn();
} else { } else {
this.gameEnded(); this.gameEnded();
@ -179,12 +213,13 @@ export class DominoesGame extends EventEmitter {
this.gameInProgress = false; this.gameInProgress = false;
this.winner = this.getWinner(); this.winner = this.getWinner();
this.setScores(); this.setScores();
const summary = { const summary: GameSummary = {
gameId: this.id, gameId: this.id,
isBlocked: this.gameBlocked, isBlocked: this.gameBlocked,
isTied: this.gameTied, isTied: this.gameTied,
winner: this.winner?.getState(), 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); this.emit('game-over', summary);
} }
@ -210,22 +245,25 @@ export class DominoesGame extends EventEmitter {
printPlayersHand() { printPlayersHand() {
for (let player of this.players) { for (let player of this.players) {
this.logger.debug(`${player.name}'s hand (${player.hand.length}): ${player.hand.map(tile => tile.toString())}`); this.printPlayerHand(player);
} }
} }
printPlayerHand(player: PlayerInterface) {
this.logger.debug(`${player.name}'s hand (${player.hand.length}): ${player.hand.map(tile => tile.toString())}`);
}
async start(): Promise<void> { async start(): Promise<void> {
try { try {
// Initalize game // Initalize game
this.gameInProgress = false; this.gameInProgress = false;
this.resetPlayersScore(); this.resetPlayersScore();
this.tileSelectionPhase = true; this.tileSelectionPhase = true;
// await this.notificationManager.notifyGameState(this);
// await this.notificationManager.notifyPlayersState(this.players);
this.deal(); this.deal();
const extractStates = (p: PlayerInterface) => { const extractStates = (p: PlayerInterface) => ({
return p.getState() player: p.getState(true),
}; gameState: this.getGameState()
});
await this.notificationService.sendEventToPlayers('server:hand-dealt', this.players, extractStates); await this.notificationService.sendEventToPlayers('server:hand-dealt', this.players, extractStates);
this.tileSelectionPhase = false; this.tileSelectionPhase = false;
@ -237,10 +275,9 @@ export class DominoesGame extends EventEmitter {
printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`); printLine(`${this.players[this.currentPlayerIndex].name} is the starting player:`);
this.logger.debug("Before play turn") this.logger.debug("Before play turn")
this.playTurn(); this.playTurn();
// await this.notificationManager.notifyGameState(this);
// await this.notificationManager.notifyPlayersState(this.players);
} catch (error) { } catch (error) {
this.logger.error(error, 'Error starting game'); 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) { 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.notificationService.notifyGameState(this);
// await this.notificationService.notifyPlayersState(this.players);
if (this.board.boneyard.length === 0) { if (this.board.boneyard.length === 0) {
break; break;
} }
@ -292,16 +327,22 @@ export class DominoesGame extends EventEmitter {
gameBlocked: this.gameBlocked, gameBlocked: this.gameBlocked,
gameTied: this.gameTied, gameTied: this.gameTied,
gameId: this.id, 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()), players: this.players.map(player => player.getState()),
currentPlayer: currentPlayer.getState(), currentPlayer: currentPlayer.getState(),
board: this.board.tiles.map(tile => ({ board: this.board.tiles.map(tile => tile.getState(true)),
id: tile.id,
playerId: tile.playerId,
pips: tile.pips
})),
boardFreeEnds: this.board.getFreeEnds(), 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);
}
}
} }

View File

@ -22,6 +22,7 @@ export class MatchSession {
private notificationService = new PlayerNotificationService(); private notificationService = new PlayerNotificationService();
private winnerIndex: number | null = null; private winnerIndex: number | null = null;
private clientsReady: string[] = []; private clientsReady: string[] = [];
private gameSummaries: GameSummary[] = [];
id: string; id: string;
matchInProgress: boolean = false; matchInProgress: boolean = false;
@ -35,7 +36,7 @@ export class MatchSession {
scoreboard: Map<string, number> = new Map(); scoreboard: Map<string, number> = new Map();
seed!: string seed!: string
sessionInProgress: boolean = false; sessionInProgress: boolean = false;
state: string = 'created' status: string = 'created'
constructor(public creator: PlayerInterface, public name?: string, seed?: string) { constructor(public creator: PlayerInterface, public name?: string, seed?: string) {
this.seed = seed || getRandomSeed(); this.seed = seed || getRandomSeed();
@ -71,6 +72,13 @@ export class MatchSession {
this.logger.trace(`Client ${userId} is ready`) this.logger.trace(`Client ${userId} is ready`)
this.clientsReady.push(userId); this.clientsReady.push(userId);
} }
this.logger.trace(`${this.clientsReady.length}`);
}
setCurrentGameClientReady(userId: string) {
if (this.currentGame) {
this.currentGame.setClientReady(userId);
}
} }
async checkAllClientsReadyBeforeStart() { async checkAllClientsReadyBeforeStart() {
@ -80,13 +88,15 @@ export class MatchSession {
this.logger.trace(`Clients ready: ${this.clientsReady.length}/${this.numHumanPlayers}`); this.logger.trace(`Clients ready: ${this.clientsReady.length}/${this.numHumanPlayers}`);
return 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.logger.info(`Game #${this.gameNumber} started`);
this.currentGame.start(); this.currentGame.start();
this.gameInProgress = true; this.gameInProgress = true;
this.clientsReady = [];
} }
} catch (error) { } catch (error) {
this.logger.error(error, 'Error starting game'); this.logger.error(error, 'Error starting game');
throw new Error('Error starting game (checkAllClientsReadyBeforeStart)');
} }
} }
@ -95,6 +105,8 @@ export class MatchSession {
} }
playerMove(move: any) { playerMove(move: any) {
this.logger.trace('Handling player move (playerMove)');
this.logger.trace(`${this.clientsReady.length}`);
if (this.currentGame) { if (this.currentGame) {
if ((move === null) || (move === undefined) || move.type === 'pass') { if ((move === null) || (move === undefined) || move.type === 'pass') {
this.currentGame.finishTurn(null); this.currentGame.finishTurn(null);
@ -131,6 +143,7 @@ export class MatchSession {
} }
private continueMatch(gameSummary: GameSummary) { private continueMatch(gameSummary: GameSummary) {
this.gameSummaries.push(gameSummary);
this.winnerIndex = this.players.findIndex(player => player.id === gameSummary?.winner?.id); this.winnerIndex = this.players.findIndex(player => player.id === gameSummary?.winner?.id);
if (this.winnerIndex !== null) { if (this.winnerIndex !== null) {
this.currentGame?.setForcedInitialPlayerIndex(this.winnerIndex); this.currentGame?.setForcedInitialPlayerIndex(this.winnerIndex);
@ -139,19 +152,20 @@ export class MatchSession {
this.checkMatchWinner(); this.checkMatchWinner();
this.resetPlayers(); this.resetPlayers();
if (!this.matchInProgress) { if (!this.matchInProgress) {
this.state = 'end' this.status = 'end'
this.notificationService.sendEventToPlayers('server:match-finished', this.players, { this.notificationService.sendEventToPlayers('server:match-finished', this.players, {
lastGame: gameSummary, lastGame: gameSummary,
sessionState: this.getState(), sessionState: this.getState(),
}); });
} else { } else {
this.state = 'waiting' this.status = 'waiting'
// await this.playerNotificationManager.notifyMatchState(this); // await this.playerNotificationManager.notifyMatchState(this);
this.notificationService.sendEventToPlayers('server:game-finished', this.players, { this.notificationService.sendEventToPlayers('server:game-finished', this.players, {
lastGame: gameSummary, lastGame: gameSummary,
sessionState: this.getState() sessionState: this.getState()
}); });
this.waitingForPlayers = true; this.waitingForPlayers = true;
this.startGame();
} }
} }
@ -169,7 +183,7 @@ export class MatchSession {
private async startMatch(seed: string) { private async startMatch(seed: string) {
try { try {
this.state = 'in-game' this.status = 'in-game'
this.rng = seedrandom(seed); this.rng = seedrandom(seed);
this.resetScoreboard() this.resetScoreboard()
this.gameNumber = 0; this.gameNumber = 0;
@ -180,7 +194,7 @@ export class MatchSession {
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
this.matchInProgress = false; this.matchInProgress = false;
this.state = 'error' this.status = 'error'
} }
} }
@ -296,11 +310,7 @@ export class MatchSession {
id: this.id, id: this.id,
name: this.name!, name: this.name!,
creator: this.creator.id, creator: this.creator.id,
players: this.players.map(player =>( { players: this.players.map(player => player.getState()),
id: player.id,
name: player.name,
ready: player.ready,
})),
playersReady: this.numPlayersReady, playersReady: this.numPlayersReady,
sessionInProgress: this.sessionInProgress, sessionInProgress: this.sessionInProgress,
maxPlayers: this.maxPlayers, maxPlayers: this.maxPlayers,
@ -313,7 +323,8 @@ export class MatchSession {
status: this.sessionInProgress ? 'in progress' : 'waiting', status: this.sessionInProgress ? 'in progress' : 'waiting',
scoreboard: [...this.scoreboard.entries()], scoreboard: [...this.scoreboard.entries()],
matchWinner: this.matchWinner?.getState() || null, matchWinner: this.matchWinner?.getState() || null,
matchInProgress: this.matchInProgress matchInProgress: this.matchInProgress,
gameSummaries: this.gameSummaries,
}; };
} }
} }

View File

@ -34,13 +34,14 @@ export class PlayerInteractionAI implements PlayerInteractionInterface {
} else { } else {
move = this.chooseTileGreed(board); 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(() => { setTimeout(() => {
this.interactionService.playerMove({ this.interactionService.playerMoveAI({
sessionId: (<PlayerAI>this.player).sessionId, sessionId: (<PlayerAI>this.player).sessionId,
move move
}); });
this.logger.trace('Move sent to server (AI'); this.logger.trace('Move sent to server (AI)');
}, rndWait); }, rndWait);
} }

View File

@ -8,17 +8,20 @@ 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'; import { InteractionService } from '../server/services/InteractionService';
import { LoggingService } from '../common/LoggingService';
export class PlayerInteractionNetwork implements PlayerInteractionInterface { export class PlayerInteractionNetwork implements PlayerInteractionInterface {
player: PlayerInterface; player: PlayerInterface;
interactionService: InteractionService = new InteractionService(); interactionService: InteractionService = new InteractionService();
clientNotifier = new NetworkClientNotifier(); clientNotifier = new NetworkClientNotifier();
logger: LoggingService = new LoggingService();
constructor(player: PlayerInterface) { constructor(player: PlayerInterface) {
this.player = player; this.player = player;
} }
askForMove(board: Board): void { askForMove(board: Board): void {
this.logger.trace('Asking for move (Player)');
this.clientNotifier.sendEvent(this.player as NetworkPlayer, 'server:player-turn', { this.clientNotifier.sendEvent(this.player as NetworkPlayer, 'server:player-turn', {
freeHands: board.getFreeEnds(), freeHands: board.getFreeEnds(),
isFirstMove: board.tiles.length === 0 isFirstMove: board.tiles.length === 0

View File

@ -5,5 +5,6 @@ export interface GameSummary {
isBlocked: boolean; isBlocked: boolean;
isTied: boolean; isTied: boolean;
winner: PlayerDto; winner: PlayerDto;
score: { name: string; score: number; }[] score: { id: string, name: string; score: number; }[],
players?: PlayerDto[];
} }

View File

@ -1,3 +1,4 @@
import { GameSummary } from "./GameSummary";
import { PlayerDto } from "./PlayerDto"; import { PlayerDto } from "./PlayerDto";
export interface MatchSessionState { export interface MatchSessionState {
@ -17,5 +18,6 @@ export interface MatchSessionState {
scoreboard: [string, number][]; scoreboard: [string, number][];
matchWinner: PlayerDto | null; matchWinner: PlayerDto | null;
matchInProgress: boolean; matchInProgress: boolean;
playersReady: number playersReady: number,
gameSummaries: GameSummary[];
} }

View File

@ -1,8 +1,16 @@
export interface TileDto {
id: string;
pips?: number[];
flipped: boolean;
revealed: boolean;
playerId: string | undefined;
}
export interface PlayerDto { export interface PlayerDto {
id: string; id: string;
name: string; name: string;
score?: number; score?: number;
hand?: any[]; hand?: TileDto[];
teamedWith?: PlayerDto | null; teamedWith?: PlayerDto | null;
ready: boolean; ready: boolean;
} }

View File

@ -28,6 +28,16 @@ export class Tile {
this.flipped = !this.flipped; 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 { toString(): string {
if (!this.revealed) { if (!this.revealed) {
return '[ | ]'; return '[ | ]';

View File

@ -6,7 +6,7 @@ import { LoggingService } from "../../../common/LoggingService";
import { EventEmitter } from "stream"; import { EventEmitter } from "stream";
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface"; import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
import { uuid } from "../../../common/utilities"; import { uuid } from "../../../common/utilities";
import { PlayerDto } from "../../dto/PlayerDto"; import { PlayerDto, TileDto } from "../../dto/PlayerDto";
export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface { export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface {
hand: Tile[] = []; hand: Tile[] = [];
@ -56,19 +56,8 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
id: this.id, id: this.id,
name: this.name, name: this.name,
score: this.score, score: this.score,
hand: this.hand.map(tile => { hand: this.hand.map(tile => tile.getState(showPips)),
const d = { teamedWith: this.teamedWith?.getState(showPips) ?? null,
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,
ready: this.ready, ready: this.ready,
}; };
} }

View File

@ -1,9 +1,7 @@
import { PlayerInteractionInterface } from "../../PlayerInteractionInterface"; import { PlayerInteractionInterface } from "../../PlayerInteractionInterface";
import { Board } from "../Board"; import { Board } from "../Board";
import { GameState } from "../../dto/GameState";
import { PlayerMove } from "../PlayerMove"; import { PlayerMove } from "../PlayerMove";
import { Tile } from "../Tile"; import { Tile } from "../Tile";
import { MatchSessionState } from "../../dto/MatchSessionState";
import { PlayerDto } from "../../dto/PlayerDto"; import { PlayerDto } from "../../dto/PlayerDto";
export interface PlayerInterface { export interface PlayerInterface {
@ -24,5 +22,5 @@ export interface PlayerInterface {
sendEvent(event: string, data: any): Promise<void>; sendEvent(event: string, data: any): Promise<void>;
sendEventWithAck(event: string, data: any): Promise<any>; sendEventWithAck(event: string, data: any): Promise<any>;
getState(): PlayerDto; getState(showPips?: boolean): PlayerDto;
} }

View File

@ -22,7 +22,7 @@ export class ApiKeyController extends BaseController{
async createApiKey(req: Request, res: Response) { async createApiKey(req: Request, res: Response) {
try { try {
const token: Token = this._createTokenObject(req); const token: Token = this._createTokenObject(req);
await this.apiTokenManager.addToken(token); await this.apiTokenManager.create(token);
res.status(201).end(); res.status(201).end();
} catch (error) { } catch (error) {
this.handleError(res, error); this.handleError(res, error);
@ -62,7 +62,7 @@ export class ApiKeyController extends BaseController{
async createNamespaceApiKey(req: Request, res: Response) { async createNamespaceApiKey(req: Request, res: Response) {
try { try {
const token = this._createTokenObject(req); const token = this._createTokenObject(req);
await this.apiTokenManager.addToken(token); await this.apiTokenManager.create(token);
res.status(201).end(); res.status(201).end();
} catch (error) { } catch (error) {
this.handleError(res, error); this.handleError(res, error);
@ -81,9 +81,9 @@ export class ApiKeyController extends BaseController{
type type
}; };
if (type === 'namespace') { if (type === 'namespace') {
newToken.namespaceId = toObjectId(namespaceId); newToken.namespaceId = namespaceId;
} else if (type === 'user') { } else if (type === 'user') {
newToken.userId = toObjectId(userId); newToken.userId = userId;
} }
return newToken; return newToken;
} }

View File

@ -142,7 +142,7 @@ export class UserController extends BaseController {
} }
this.temporalTokenManager.deleteAllByUserAndType(userId.toString(), TemporalTokenMongoManager.Types.PASSWORD_RECOVERY); 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); await this.mailService.sendRecoveryPasswordEmail(firstname, lastname, email, pin);
res.status(200).end(); res.status(200).end();
} catch (error: any) { } catch (error: any) {

View File

@ -14,7 +14,7 @@ export function matchSessionAdapter(session: MatchSession) : DbMatchSession {
numPlayers: session.numPlayers, numPlayers: session.numPlayers,
scoreboard: Array.from(session.scoreboard.entries()).map(([player, score]) => ({ player, score })), scoreboard: Array.from(session.scoreboard.entries()).map(([player, score]) => ({ player, score })),
matchWinner: session.matchWinner ? session.matchWinner.id : null, matchWinner: session.matchWinner ? session.matchWinner.id : null,
state: session.state status: session.status
} }
} }

View File

@ -1,14 +1,12 @@
import { ObjectId } from "mongodb";
export interface Entity { export interface Entity {
createdAt?: number | null; createdAt?: number;
modifiedAt?: number | null; modifiedAt?: number;
createdBy?: ObjectId | null; createdBy?: string;
modifiedBy?: ObjectId | null; modifiedBy?: string;
} }
export interface EntityMongo extends Entity { export interface EntityMongo extends Entity {
_id?: ObjectId; _id?: string;
} }
export interface Score { export interface Score {
@ -21,14 +19,14 @@ export interface Namespace extends EntityMongo {
description?: string; description?: string;
default: boolean; default: boolean;
type: string | null; type: string | null;
ownerId?: ObjectId; ownerId?: string;
users?: any[]; users?: any[];
} }
export interface User extends EntityMongo { export interface User extends EntityMongo {
id: string, id: string,
username: string; username: string;
namespaceId: ObjectId; namespaceId: string;
hash?: string; hash?: string;
roles: string[]; roles: string[];
firstname?: string; firstname?: string;
@ -55,13 +53,13 @@ export interface DbMatchSession extends EntityMongo {
numPlayers: number; numPlayers: number;
scoreboard: Score[]; scoreboard: Score[];
matchWinner: string | null; matchWinner: string | null;
state: string; status: string;
} }
export interface DbUser extends EntityMongo { export interface DbUser extends EntityMongo {
id: string, id: string,
username: string; username: string;
namespaceId: ObjectId; namespaceId: string;
hash?: string; hash?: string;
roles: string[]; roles: string[];
firstname?: string; firstname?: string;
@ -77,16 +75,16 @@ export interface DbNamespace extends EntityMongo {
description?: string; description?: string;
default: boolean; default: boolean;
type: string | null; type: string | null;
ownerId?: ObjectId; ownerId?: string;
} }
export interface Token extends EntityMongo { export interface Token extends EntityMongo {
token: string; token: string;
userId?: ObjectId; userId?: string;
roles?: string[]; roles?: string[];
expiresAt?: number | null; expiresAt?: number | null;
type: string; type: string;
namespaceId?: ObjectId namespaceId?: string
description?: string; description?: string;
} }
@ -100,3 +98,14 @@ export interface Role {
permissions: string[]; permissions: string[];
} }
export interface DbListResponse{
pagination?: {
page: number;
next?: number;
size: number;
total: number;
totalPages: number;
}
sort?: any;
data: EntityMongo[];
}

View File

@ -5,13 +5,6 @@ import { Token } from '../interfaces';
export class ApiTokenMongoManager extends BaseMongoManager{ export class ApiTokenMongoManager extends BaseMongoManager{
collection = 'tokens'; 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<Token[]> { async getTokens(userId: string): Promise<Token[]> {
return await mongoExecute(async ({ collection }) => { return await mongoExecute(async ({ collection }) => {
return await collection?.find({ userId: this.toObjectId(userId) }).toArray(); return await collection?.find({ userId: this.toObjectId(userId) }).toArray();

View File

@ -11,19 +11,6 @@ export class NamespacesMongoManager extends BaseMongoManager{
super(); super();
} }
async createNamespace(namespace: Namespace): Promise<string> {
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<number> { async updateNamespace(id: string, namespace: Namespace): Promise<number> {
return await mongoExecute(async ({collection}) => { return await mongoExecute(async ({collection}) => {
const now = new Date().getTime(); const now = new Date().getTime();

View File

@ -1,6 +1,5 @@
import { ObjectId } from "mongodb";
import { mongoExecute } from "./mongoDBPool"; import { mongoExecute } from "./mongoDBPool";
import { Entity, EntityMongo } from "../../interfaces"; import { DbListResponse, Entity, EntityMongo } from "../../interfaces";
import { LoggingService } from "../../../../common/LoggingService"; import { LoggingService } from "../../../../common/LoggingService";
import toObjectId from "./mongoUtils"; import toObjectId from "./mongoUtils";
@ -9,12 +8,12 @@ export abstract class BaseMongoManager {
protected abstract collection?: string; protected abstract collection?: string;
logger = new LoggingService().logger; logger = new LoggingService().logger;
async create(data: Entity): Promise<ObjectId | undefined> { async create(data: EntityMongo): Promise<string | undefined> {
this.stampEntity(data); 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);
return result?.insertedId; return result?.insertedId.toString() || undefined;
}, },
{ colName: this.collection } { colName: this.collection }
); );
@ -58,7 +57,7 @@ export abstract class BaseMongoManager {
); );
} }
async list(sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise<EntityMongo[]> { async list(sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise<DbListResponse> {
return mongoExecute( return mongoExecute(
async ({ collection }) => { async ({ collection }) => {
const cursor = collection?.find(); const cursor = collection?.find();
@ -68,13 +67,39 @@ export abstract class BaseMongoManager {
if (pagination) { if (pagination) {
cursor?.skip(pagination.pageSize * (pagination.page - 1)).limit(pagination.pageSize); 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 } { colName: this.collection }
); );
} }
async listByFilter(filter: any, sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise<EntityMongo[]> { async listByFilter(filter: any, sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise<DbListResponse> {
return mongoExecute( return mongoExecute(
async ({ collection }) => { async ({ collection }) => {
const cursor = collection?.find(filter); const cursor = collection?.find(filter);
@ -84,7 +109,35 @@ export abstract class BaseMongoManager {
if (pagination) { if (pagination) {
cursor?.skip(pagination.pageSize * (pagination.page - 1)).limit(pagination.pageSize); 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 } { colName: this.collection }
); );

View File

@ -1,5 +1,8 @@
import { ObjectId } from "mongodb"; import { ObjectId } from "mongodb";
export default function toObjectId(id: string) { export default function toObjectId(oid: string | ObjectId): ObjectId {
return ObjectId.createFromHexString(id); if (oid instanceof ObjectId) {
return oid;
}
return ObjectId.createFromHexString(oid);
} }

View File

@ -27,14 +27,18 @@ export class InteractionService extends ServiceBase{
this.logger.trace(`Handling event: ${event}`); this.logger.trace(`Handling event: ${event}`);
switch(event) { switch(event) {
case 'client:player-move': case 'client:player-move':
this.onClientMoveResponse(eventData); this.playerMoveHuman(eventData);
break; break;
case 'client:set-client-ready-for-next-game':
case 'client:set-client-ready': case 'client:set-client-ready':
this.onClientReady(eventData); this.onClientReady(eventData);
break; break;
case EventActions.PLAYER_READY: case EventActions.PLAYER_READY:
this.onPlayerReady(eventData); this.onPlayerReady(eventData);
break; break;
case 'client:animation-ended':
this.onClientsAnimationEnded(eventData);
break;
default: default:
PubSub.publish(event, eventData); PubSub.publish(event, eventData);
break; break;
@ -60,7 +64,9 @@ export class InteractionService extends ServiceBase{
for (let i = 0; i < missingHumans; i++) { for (let i = 0; i < missingHumans; i++) {
session.addPlayerToSession(session.createPlayerAI(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(); session.start();
return { return {
status: 'ok' status: 'ok'
@ -68,15 +74,23 @@ export class InteractionService extends ServiceBase{
} }
} }
public playerMove(data: any) { public playerMoveAI(data: any) {
this.onClientMoveResponse(data); 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 { sessionId, move }: { sessionId: string, move: PlayerMove } = data;
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId); const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
if (session !== undefined) { if (session !== undefined) {
session.playerMove(move); if (notifyClientReady) {
session.setCurrentGameClientReady(move.playerId);
}
session.playerMove(move);
return { return {
status: 'ok' status: 'ok'
}; };
@ -100,8 +114,19 @@ export class InteractionService extends ServiceBase{
return { return {
status: 'ok' 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 { public updateSocketId(sessionId: string, userId: string, socketId: string): any {
return this.sessionManager.updateSocketId(sessionId, userId, socketId); return this.sessionManager.updateSocketId(sessionId, userId, socketId);
} }

View File

@ -1,4 +1,5 @@
import { Namespace, User } from "../db/interfaces"; import { Namespace, User } from "../db/interfaces";
import toObjectId from "../db/mongo/common/mongoUtils";
import { NamespacesMongoManager } from "../db/mongo/NamespacesMongoManager"; import { NamespacesMongoManager } from "../db/mongo/NamespacesMongoManager";
import { UsersService } from "./UsersService"; import { UsersService } from "./UsersService";
@ -7,8 +8,12 @@ export class NamespacesService {
usersService = new UsersService(); usersService = new UsersService();
async createNamespace(namespace: Namespace, user: User) { async createNamespace(namespace: Namespace, user: User) {
const insertedId = await this.namespacesManager.createNamespace({ ownerId: user._id, ...namespace, createdBy: user._id }); const userId = user._id
await this._updateNamespaceUsers(namespace.users ?? [], insertedId); 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; return insertedId;
} }

View File

@ -5,7 +5,7 @@ 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, DbMatchSessionUpdate } from "../db/interfaces"; import { DbListResponse, 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";
@ -62,11 +62,11 @@ export class SessionService extends ServiceBase{
return sessionId return sessionId
} }
public async listJoinableSessions(): Promise<DbMatchSession[]> { public async listJoinableSessions(): Promise<DbListResponse> {
return await this.dbManager.listByFilter( return await this.dbManager.listByFilter(
{ state: 'created' }, { status: 'created' },
{ createdAt: -1 }, { createdAt: -1 },
{ page: 1, pageSize: 5 }) as DbMatchSession[]; { page: 1, pageSize: 12 }) as DbListResponse;
} }
public async getSession(sessionId: string): Promise<DbMatchSession | undefined> { public async getSession(sessionId: string): Promise<DbMatchSession | undefined> {
@ -76,8 +76,8 @@ export class SessionService extends ServiceBase{
public async deleteSession(sessionId: string): Promise<any> { public async deleteSession(sessionId: string): Promise<any> {
this.sessionManager.deleteSession(sessionId); this.sessionManager.deleteSession(sessionId);
const session = { const session = {
_id: toObjectId(sessionId), _id: sessionId,
state: 'deleted' status: 'deleted'
} as DbMatchSessionUpdate; } as DbMatchSessionUpdate;
return this.dbManager.update(session); return this.dbManager.update(session);
} }

View File

@ -106,7 +106,7 @@ export class SocketIoService extends ServiceBase{
if (event.startsWith('client:') && args.length > 0) { if (event.startsWith('client:') && args.length > 0) {
logStr = `${logStr} (${args[0].event})`; logStr = `${logStr} (${args[0].event})`;
} }
this.logger.debug(logStr); this.logger.trace(logStr);
}); });
this.pingClients() this.pingClients()

View File

@ -69,7 +69,7 @@ export class UsersService extends ServiceBase {
this.logger.info(`${password === undefined}`); this.logger.info(`${password === undefined}`);
if (_id !== undefined) { if (_id !== undefined) {
user._id = toObjectId(_id); user._id = _id;
} }
if (password !== undefined && typeof password === 'string' && password.length > 0) { if (password !== undefined && typeof password === 'string' && password.length > 0) {

View File

@ -1,7 +1,7 @@
import { PlayerAI } from "./game/entities/player/PlayerAI"; import { PlayerAI } from "./game/entities/player/PlayerAI";
import { PlayerHuman } from "./game/entities/player/PlayerHuman"; import { PlayerHuman } from "./game/entities/player/PlayerHuman";
import {LoggingService} from "./common/LoggingService"; import {LoggingService} from "./common/LoggingService";
import { GameSession } from "./game/GameSession"; import { MatchSession } from "./game/MatchSession";
console.log('process.arg :>> ', process.argv); console.log('process.arg :>> ', process.argv);
@ -18,35 +18,35 @@ console.log('process.arg :>> ', process.argv);
// } // }
async function playSolo(seed?: string) { 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}`); console.log(`Session (${session.id}) created by: ${session.creator.name}`);
setTimeout(() => session.addPlayer(new PlayerAI("AI 2")), 1000); setTimeout(() => session.addPlayerToSession(new PlayerAI("AI 2")), 1000);
setTimeout(() => session.addPlayer(new PlayerAI("AI 3")), 2000); setTimeout(() => session.addPlayerToSession(new PlayerAI("AI 3")), 2000);
setTimeout(() => session.addPlayer(new PlayerAI("AI 4")), 3000); setTimeout(() => session.addPlayerToSession(new PlayerAI("AI 4")), 3000);
session.start(seed); session.start(seed);
} }
async function playHumans(seed?: string) { async function playHumans(seed?: string) {
const session = new GameSession(new PlayerHuman("Jose"), "Test Game"); const session = new MatchSession(new PlayerHuman("Jose"), "Test Game");
session.addPlayer(new PlayerHuman("Pepe")); session.addPlayerToSession(new PlayerHuman("Pepe"));
session.addPlayer(new PlayerHuman("Juan")); session.addPlayerToSession(new PlayerHuman("Juan"));
session.addPlayer(new PlayerHuman("Luis")); session.addPlayerToSession(new PlayerHuman("Luis"));
session.start(seed); session.start(seed);
} }
async function playAIs(seed?: string) { async function playAIs(seed?: string) {
const session = new GameSession(new PlayerAI("AI 1"), "Test Game"); const session = new MatchSession(new PlayerAI("AI 1"), "Test Game");
session.addPlayer(new PlayerAI("AI 2")); session.addPlayerToSession(new PlayerAI("AI 2"));
session.addPlayer(new PlayerAI("AI 3")); session.addPlayerToSession(new PlayerAI("AI 3"));
session.addPlayer(new PlayerAI("AI 4")); session.addPlayerToSession(new PlayerAI("AI 4"));
session.start(seed); session.start(seed);
} }
async function playTeams(seed?: string) { async function playTeams(seed?: string) {
const session = new GameSession(new PlayerHuman("Jose"), "Test Game"); const session = new MatchSession(new PlayerHuman("Jose"), "Test Game");
session.addPlayer(new PlayerAI("AI 1")); session.addPlayerToSession(new PlayerAI("AI 1"));
session.addPlayer(new PlayerHuman("Juan")); session.addPlayerToSession(new PlayerHuman("Juan"));
session.addPlayer(new PlayerAI("AI 2")); session.addPlayerToSession(new PlayerAI("AI 2"));
session.start(seed); session.start(seed);
} }

206
text.txt Normal file
View File

@ -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.
Heres 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
<!DOCTYPE html>
<html>
<head>
<title>Dominoes Game</title>
</head>
<body>
<h1>Dominoes Game</h1>
<div id="game"></div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
socket.on('connect', () => {
const name = prompt("Enter your name:");
socket.emit('joinGame', name);
});
socket.on('updateGame', (game) => {
console.log('Game updated:', game);
// Update UI based on game state
});
socket.on('invalidMove', (tile) => {
alert(`Invalid move: ${tile}`);
});
// Example of playing a tile
function playTile(tile) {
socket.emit('playTile', tile);
}
</script>
</body>
</html>
```
### 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.