This commit is contained in:
Jose Conde 2024-07-07 23:23:49 +02:00
parent a974f576b3
commit f67c262b0e
24 changed files with 369 additions and 211 deletions

View File

@ -1,7 +1,6 @@
export class ErrorBase extends Error { export class ErrorBase extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);
console.log('this.constructor.name :>> ', this.constructor.name);
this.name = this.constructor.name; this.name = this.constructor.name;
this.stack = (new Error()).stack; this.stack = (new Error()).stack;
} }

View File

@ -0,0 +1,7 @@
import { ErrorBase } from "./ErrorBase";
export class SessionCreationError extends ErrorBase {
constructor() {
super('Session creation error');
}
}

View File

@ -0,0 +1,7 @@
import { ErrorBase } from "./ErrorBase";
export class SessionNotFoundError extends ErrorBase {
constructor() {
super('Session not found');
}
}

View File

@ -14,6 +14,21 @@ export async function wait(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
export const whileNotUndefined = async (fn: Function, maxQueries: number = 20, millis: number = 500): Promise<any> => {
return new Promise(async (resolve, reject) => {
let result;
while (result === undefined) {
await wait(millis);
result = fn()
if (maxQueries-- < 0) {
reject()
return;
}
}
resolve(result);
});
}
export function askQuestion(question: string): Promise<string> { export function askQuestion(question: string): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve) => {
// console.log(chalk.yellow(question)); // console.log(chalk.yellow(question));

View File

@ -6,7 +6,7 @@ import { Tile } from "./entities/Tile";
import { LoggingService } from "../common/LoggingService"; import { LoggingService } from "../common/LoggingService";
import { printBoard, printLine, uuid, wait } from '../common/utilities'; import { printBoard, printLine, uuid, wait } from '../common/utilities';
import { GameSummary } from './dto/GameSummary'; import { GameSummary } from './dto/GameSummary';
import { PlayerNotificationManager } from './PlayerNotificationManager'; import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
import { GameState } from './dto/GameState'; import { GameState } from './dto/GameState';
export class DominoesGame { export class DominoesGame {
@ -25,13 +25,12 @@ export class DominoesGame {
winner: PlayerInterface | null = null; winner: PlayerInterface | null = null;
rng: PRNG; rng: PRNG;
handSize: number = 7; handSize: number = 7;
notificationManager: PlayerNotificationManager = new PlayerNotificationManager(); notificationManager: PlayerNotificationService = new PlayerNotificationService();
lastMove: PlayerMove | null = null; lastMove: PlayerMove | null = null;
constructor(public players: PlayerInterface[], seed: PRNG) { constructor(public players: PlayerInterface[], seed: PRNG) {
this.id = uuid(); this.id = uuid();
this.logger.info(`Game ID: ${this.id}`); this.logger.info(`Game ID: ${this.id}`);
this.logger.info(`Seed: ${this.seed}`);
this.rng = seed this.rng = seed
this.board = new Board(seed); this.board = new Board(seed);
this.initializeGame(); this.initializeGame();

View File

@ -4,7 +4,7 @@ import { PlayerInterface } from "./entities/player/PlayerInterface";
import { LoggingService } from "../common/LoggingService"; import { LoggingService } from "../common/LoggingService";
import { getRandomSeed, uuid, wait } from "../common/utilities"; import { getRandomSeed, uuid, wait } from "../common/utilities";
import { MatchSessionState } from "./dto/MatchSessionState"; import { MatchSessionState } from "./dto/MatchSessionState";
import { PlayerNotificationManager } from './PlayerNotificationManager'; import { PlayerNotificationService } from '../server/services/PlayerNotificationService';
import seedrandom, { PRNG } from "seedrandom"; import seedrandom, { PRNG } from "seedrandom";
import { NetworkPlayer } from "./entities/player/NetworkPlayer"; import { NetworkPlayer } from "./entities/player/NetworkPlayer";
import { PlayerHuman } from "./entities/player/PlayerHuman"; import { PlayerHuman } from "./entities/player/PlayerHuman";
@ -16,11 +16,11 @@ export class MatchSession {
private waitingForPlayers: boolean = true; private waitingForPlayers: boolean = true;
private waitingSeconds: number = 0; private waitingSeconds: number = 0;
private logger: LoggingService = new LoggingService(); private logger: LoggingService = new LoggingService();
private playerNotificationManager = new PlayerNotificationManager(); private playerNotificationManager = new PlayerNotificationService();
id: string; id: string;
matchInProgress: boolean = false; matchInProgress: boolean = false;
matchWinner: PlayerInterface | null = null; matchWinner?: PlayerInterface = undefined;
maxPlayers: number = 4; maxPlayers: number = 4;
mode: string = 'classic'; mode: string = 'classic';
players: PlayerInterface[] = []; players: PlayerInterface[] = [];
@ -31,20 +31,19 @@ export class MatchSession {
sessionInProgress: boolean = false; sessionInProgress: boolean = false;
state: string = 'created' state: string = 'created'
constructor(public creator: PlayerInterface, public name?: string) { constructor(public creator: PlayerInterface, public name?: string, seed?: string) {
this.seed = seed || getRandomSeed();
this.id = uuid(); this.id = uuid();
this.name = name || `Game ${this.id}`; this.name = name || `Game ${this.id}`;
this.addPlayer(creator); this.addPlayer(creator);
this.creator = creator; this.creator = creator;
this.logger.info(`Match session created by: ${creator.name}`); this.logger.info(`Match session created by: ${creator.name}`);
this.logger.info(`Match session ID: ${this.id}`); this.logger.info(`Match session ID: ${this.id}`);
this.logger.info(`Match session name: ${this.name}`); this.logger.info(`Match session name: ${this.name}`);
this.logger.info(`Points to win: ${this.pointsToWin}`); this.logger.info(`Points to win: ${this.pointsToWin}`);
this.sessionInProgress = true; this.sessionInProgress = true;
this.matchInProgress = false; this.matchInProgress = false;
this.playerNotificationManager.notifyMatchState(this);
this.playerNotificationManager.notifyPlayersState(this.players);
} }
get numPlayers() { get numPlayers() {
@ -77,15 +76,18 @@ export class MatchSession {
this.state = 'started' this.state = 'started'
this.logger.info(`Game #${gameNumber} started`); this.logger.info(`Game #${gameNumber} started`);
// this.game.reset() // this.game.reset()
await this.currentGame.start(); const gameSummary = await this.currentGame.start();
this.logger.debug('gameSummary :>> ', gameSummary);
this.setScores(); this.setScores();
this.checkMatchWinner(); this.checkMatchWinner();
this.resetReadiness(); this.resetPlayers();
this.state = 'waiting' this.state = 'waiting'
await this.playerNotificationManager.notifyMatchState(this); await this.playerNotificationManager.notifyMatchState(this);
this.playerNotificationManager.sendEventToPlayers('game-finished', this.players); this.playerNotificationManager.sendEventToPlayers('game-finished', this.players);
if (this.matchInProgress) {
await this.checkHumanPlayersReady(); await this.checkHumanPlayersReady();
} }
}
this.state = 'end' this.state = 'end'
// await this.game.start(); // await this.game.start();
return this.endGame(); return this.endGame();
@ -104,9 +106,9 @@ export class MatchSession {
}); });
} }
resetReadiness() { resetPlayers() {
this.players.forEach(player => { this.players.forEach(player => {
player.ready = false player.reset()
}); });
} }
@ -114,7 +116,10 @@ export class MatchSession {
const scores = Array.from(this.scoreboard.values()); const scores = Array.from(this.scoreboard.values());
const maxScore = Math.max(...scores); const maxScore = Math.max(...scores);
if (maxScore >= this.pointsToWin) { if (maxScore >= this.pointsToWin) {
this.matchWinner = this.players.find(player => this.scoreboard.get(player.id) === maxScore)!; this.matchWinner = this.players.find(player => this.scoreboard.get(player.name) === maxScore);
if (!this.matchWinner) {
throw new Error('Match winner not found');
}
this.logger.info(`Match winner: ${this.matchWinner.name} with ${maxScore} points`); this.logger.info(`Match winner: ${this.matchWinner.name} with ${maxScore} points`);
this.matchInProgress = false; this.matchInProgress = false;
} }
@ -122,17 +127,19 @@ export class MatchSession {
resetScoreboard() { resetScoreboard() {
this.scoreboard = new Map(); this.scoreboard = new Map();
this.players.forEach(player => { this.players.forEach(player => {
this.scoreboard.set(player.id, 0); this.scoreboard.set(player.name, 0);
}); });
} }
setScores() { setScores() {
const totalPips = this.currentGame?.players.reduce((acc, player) => acc + player.pipsCount(), 0); const totalPips = this.currentGame?.players.reduce((acc, player) => acc + player.pipsCount(), 0) || 0;
if (this.currentGame && this.currentGame.winner !== null) { if (this.currentGame && this.currentGame.winner !== null) {
const winner = this.currentGame.winner; const winner = this.currentGame.winner;
this.scoreboard.set(winner.id, this.scoreboard.get(winner.id)! + totalPips!); const currentPips = this.scoreboard.get(winner.name) || 0;
this.logger.debug (`${winner.name} has ${currentPips} points`);
this.scoreboard.set(winner.name, currentPips + totalPips);
if (winner.teamedWith !== null) { if (winner.teamedWith !== null) {
this.scoreboard.set(winner.teamedWith.id, this.scoreboard.get(winner.teamedWith.id)! + totalPips!); this.scoreboard.set(winner.teamedWith.name, currentPips + totalPips);
} }
} }
} }
@ -181,9 +188,7 @@ export class MatchSession {
return player; return player;
} }
async start(seed?: string) { async start() {
this.seed = seed || getRandomSeed();
console.log('seed :>> ', this.seed);
if (this.matchInProgress) { if (this.matchInProgress) {
throw new Error("Game already in progress"); throw new Error("Game already in progress");
} }
@ -207,8 +212,9 @@ export class MatchSession {
this.logger.info(`${player.name} joined the game!`); this.logger.info(`${player.name} joined the game!`);
} }
setPlayerReady(user: string) { setPlayerReady(userId: string) {
const player = this.players.find(player => player.name === user); this.logger.debug(userId)
const player = this.players.find(player => player.id === userId);
if (!player) { if (!player) {
throw new Error("Player not found"); throw new Error("Player not found");
} }
@ -242,7 +248,7 @@ export class MatchSession {
mode: this.mode, mode: this.mode,
pointsToWin: this.pointsToWin, pointsToWin: this.pointsToWin,
status: this.sessionInProgress ? 'in progress' : 'waiting', status: this.sessionInProgress ? 'in progress' : 'waiting',
scoreboard: this.scoreboard, scoreboard: [...this.scoreboard.entries()],
matchWinner: this.matchWinner?.getState() || null, matchWinner: this.matchWinner?.getState() || null,
matchInProgress: this.matchInProgress matchInProgress: this.matchInProgress
}; };

View File

@ -30,7 +30,15 @@ export class NetworkClientNotifier {
} }
async sendEvent(player: NetworkPlayer, event: string, data?: any) { async sendEvent(player: NetworkPlayer, event: string, data?: any) {
this.io.to(player.socketId).emit(event, data); const eventData = { event, data };
this.io.to(player.socketId).emit('game-event', eventData);
}
async sendEventWithAck(player: NetworkPlayer, event: string, data: any, timeoutSecs: number = 900) {
const eventData = { event, data };
const response = await this.io.to(player.socketId)
.timeout(timeoutSecs * 1000).emitWithAck('game-event-ack', eventData);
return response[0];
} }
async broadcast(event: string, data: any) { async broadcast(event: string, data: any) {

View File

@ -6,7 +6,7 @@ import { Tile } from './entities/Tile';
import { NetworkClientNotifier } from './NetworkClientNotifier'; import { NetworkClientNotifier } from './NetworkClientNotifier';
import { NetworkPlayer } from './entities/player/NetworkPlayer'; import { NetworkPlayer } from './entities/player/NetworkPlayer';
import { PlayerMoveSide, PlayerMoveSideType } from './constants'; import { PlayerMoveSide, PlayerMoveSideType } from './constants';
import { SocketDisconnectedError } from '../common/exceptions/SocketDisconnectedError'; import { SocketDisconnectedError } from '../common/errors/SocketDisconnectedError';
export class PlayerInteractionNetwork implements PlayerInteractionInterface { export class PlayerInteractionNetwork implements PlayerInteractionInterface {
player: PlayerInterface; player: PlayerInterface;
@ -19,7 +19,7 @@ export class PlayerInteractionNetwork implements PlayerInteractionInterface {
async makeMove(board: Board): Promise<PlayerMove | null> { async makeMove(board: Board): Promise<PlayerMove | null> {
let response = undefined; let response = undefined;
try { try {
response = await this.clientNotifier.notifyPlayer(this.player as NetworkPlayer, 'makeMove', { response = await this.clientNotifier.sendEventWithAck(this.player as NetworkPlayer, 'ask-client-for-move', {
freeHands: board.getFreeEnds(), freeHands: board.getFreeEnds(),
}); });
} catch (error) { } catch (error) {

View File

@ -14,7 +14,7 @@ export interface MatchSessionState {
maxPlayers: number; maxPlayers: number;
numPlayers: number; numPlayers: number;
waitingSeconds: number; waitingSeconds: number;
scoreboard: Map<string, number>; scoreboard: [string, number][];
matchWinner: PlayerDto | null; matchWinner: PlayerDto | null;
matchInProgress: boolean; matchInProgress: boolean;
playersReady: number playersReady: number

View File

@ -6,8 +6,6 @@ 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 { GameState } from "../../dto/GameState";
import { MatchSessionState } from "../../dto/MatchSessionState";
import { PlayerDto } from "../../dto/PlayerDto"; import { PlayerDto } from "../../dto/PlayerDto";
export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface { export abstract class AbstractPlayer extends EventEmitter implements PlayerInterface {
@ -27,20 +25,16 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter
abstract chooseTile(board: Board): Promise<Tile>; abstract chooseTile(board: Board): Promise<Tile>;
async notifyGameState(state: GameState): Promise<void> { async sendEventWithAck(event: string, data: any): Promise<void> {
} }
async notifyPlayerState(state: PlayerDto): Promise<void> { async sendEvent(event: string, data: any = {}): Promise<void> {
} }
async notifyMatchState(state: MatchSessionState): Promise<void> { reset(): void {
} this.hand = [];
this.score = 0;
async waitForAction(actionId: string): Promise<boolean> { this.ready = false;
return true;
}
async sendEvent(event: string): Promise<void> {
} }
pipsCount(): number { pipsCount(): number {

View File

@ -4,55 +4,22 @@ import { PlayerHuman } from "./PlayerHuman";
import { NetworkClientNotifier } from "../../NetworkClientNotifier"; import { NetworkClientNotifier } from "../../NetworkClientNotifier";
import { Tile } from "../Tile"; import { Tile } from "../Tile";
import { Board } from "../Board"; import { Board } from "../Board";
import { GameState } from "../../dto/GameState";
import { PlayerDto } from "../../dto/PlayerDto";
import { MatchSessionState } from "../../dto/MatchSessionState";
import { SocketDisconnectedError } from "../../../common/exceptions/SocketDisconnectedError";
export class NetworkPlayer extends PlayerHuman { export class NetworkPlayer extends PlayerHuman {
socketId: string; socketId!: string;
playerInteraction: PlayerInteractionInterface = new PlayerInteractionNetwork(this); playerInteraction: PlayerInteractionInterface = new PlayerInteractionNetwork(this);
clientNotifier: NetworkClientNotifier = new NetworkClientNotifier(); clientNotifier: NetworkClientNotifier = new NetworkClientNotifier();
constructor(name: string, socketId: string) { constructor(id: string, name: string, socketId: string ) {
super(name); super(id, name);
this.socketId = socketId; this.socketId = socketId;
} }
async notifyGameState(state: GameState): Promise<void> { async sendEvent(event: string, data:any = {}): Promise<void> {
const response = await this.clientNotifier.notifyPlayer(this, 'gameState', state); this.clientNotifier.sendEvent(this, event, data);
console.log('game state notified :>> ', response);
if (response === undefined || response.status !== 'ok' ) {
throw new SocketDisconnectedError();
} }
} async sendEventWithAck(event: string, data: any): Promise<any> {
return await this.clientNotifier.sendEventWithAck(this, event, data);
async notifyPlayerState(state: PlayerDto): Promise<void> {
const response = await this.clientNotifier.notifyPlayer(this, 'playerState', state);
console.log('player state notified :>> ', response);
if (response === undefined || response.status !== 'ok' ) {
throw new SocketDisconnectedError();
}
}
async notifyMatchState(state: MatchSessionState): Promise<void> {
const response = await this.clientNotifier.notifyPlayer(this, 'matchState', state);
console.log('session state notified :>> ', response);
if (response === undefined || response.status !== 'ok' ) {
throw new SocketDisconnectedError();
}
}
async waitForAction(actionId: string): Promise<boolean> {
const response = await this.clientNotifier.notifyPlayer(this, actionId);
if (response === undefined || response.status !== 'ok' ) {
throw new SocketDisconnectedError();
}
const { actionResult } = response;
return actionResult;
}
async sendEvent(event: string): Promise<void> {
this.clientNotifier.sendEvent(this, event);
} }
async chooseTile(board: Board): Promise<Tile> { async chooseTile(board: Board): Promise<Tile> {

View File

@ -8,8 +8,9 @@ import { PlayerInteractionInterface } from '../../PlayerInteractionInterface';
export class PlayerHuman extends AbstractPlayer { export class PlayerHuman extends AbstractPlayer {
playerInteraction: PlayerInteractionInterface = new PlayerInteractionConsole(this); playerInteraction: PlayerInteractionInterface = new PlayerInteractionConsole(this);
constructor(name: string) { constructor(id: string, name: string) {
super(name); super(name);
this.id = id;
} }
async makeMove(board: Board): Promise<PlayerMove | null> { async makeMove(board: Board): Promise<PlayerMove | null> {

View File

@ -18,10 +18,10 @@ export interface PlayerInterface {
makeMove(gameState: Board): Promise<PlayerMove | null>; makeMove(gameState: Board): Promise<PlayerMove | null>;
chooseTile(board: Board): Promise<Tile>; chooseTile(board: Board): Promise<Tile>;
pipsCount(): number; pipsCount(): number;
notifyGameState(state: GameState): Promise<void>; reset(): void;
notifyPlayerState(state: PlayerDto): Promise<void>;
notifyMatchState(state: MatchSessionState): Promise<void>; sendEvent(event: string, data: any): Promise<void>;
waitForAction(actionId: string, data: any): Promise<boolean>; sendEventWithAck(event: string, data: any): Promise<any>;
sendEvent(event: string): Promise<void>;
getState(): PlayerDto; getState(): PlayerDto;
} }

View File

@ -0,0 +1,39 @@
import { Request, Response } from "express";
import { BaseController } from "./BaseController";
import { SessionService } from "../services/SessionService";
export class GameController extends BaseController {
private sessionService: SessionService = new SessionService();
public async createMatch(req: Request, res: Response) {
try {
const { user, body } = req;
const { sessionName, seed } = body;
const sessionId = await this.sessionService.createSession(user, sessionName, seed);
res.status(201).json({ sessionId });
} catch (error) {
this.handleError(res, error);
}
}
public joinMatch(req: Request, res: Response) {
try {
const { user, body } = req;
const { sessionId } = body;
this.sessionService.joinSession(user, sessionId);
res.status(200).json({ status: 'ok' });
} catch (error) {
this.handleError(res, error);
}
}
public listMatches(req: Request, res: Response) {
try {
this.sessionService.listSessions().then((sessions) => {
res.status(200).json(sessions);
});
} catch (error) {
this.handleError(res, error);
}
}
}

View File

@ -9,11 +9,11 @@ export abstract class BaseMongoManager {
protected abstract collection?: string; protected abstract collection?: string;
logger = new LoggingService().logger; logger = new LoggingService().logger;
create(data: Entity) { create(data: Entity): Promise<ObjectId | undefined>{
return mongoExecute( return mongoExecute(
async ({ collection }) => { async ({ collection }) => {
await collection?.insertOne(data as any); const result = await collection?.insertOne(data as any);
return data; return result?.insertedId;
}, },
{ colName: this.collection } { colName: this.collection }
); );

View File

@ -1,86 +1,38 @@
import { MatchSession } from "../../game/MatchSession"; import { MatchSession } from "../../game/MatchSession";
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
import { SessionService } from "../services/SessionService";
import { ManagerBase } from "./ManagerBase"; import { ManagerBase } from "./ManagerBase";
export class SessionManager extends ManagerBase { export class SessionManager extends ManagerBase {
private static sessions: any = {}; private static sessions: Map<string, MatchSession> = new Map();
private sessionService: SessionService = new SessionService();
constructor() { constructor() {
super(); super();
this.logger.info('SessionController created'); this.logger.info('SessionController created');
} }
createSession(data: any, socketId: string): any { getSessionPlayer(sessionId: string, userId: string): any {
const { user, sessionName } = data; const session: MatchSession | undefined = SessionManager.sessions.get(sessionId);
const player = new NetworkPlayer(user, socketId); if (session !== undefined) {
const session = new MatchSession(player, sessionName); return session.players.find(player => player.id === userId);
SessionManager.sessions[session.id] = session;
this.sessionService.createSession(session);
return {
status: 'ok',
sessionId: session.id,
playerId: player.id
};
}
joinSession(data: any, socketId: string): any {
this.logger.debug('joinSession data :>> ')
this.logger.object(data);
const { user, sessionId } = data;
const session: MatchSession = SessionManager.sessions[sessionId];
const player = new NetworkPlayer(user, socketId);
session.addPlayer(player);
this.sessionService.updateSession(session);
return {
status: 'ok',
sessionId: session.id,
playerId: player.id
};
}
setPlayerReady(data: any): any {
const { user, sessionId } = data;
const session: MatchSession = SessionManager.sessions[sessionId];
session.setPlayerReady(user)
}
startSession(data: any): any {
const sessionId: string = data.sessionId;
const seed: string | undefined = data.seed;
const session = SessionManager.sessions[sessionId];
if (!session) {
return ({
status: 'error',
message: 'Session not found'
});
} else if (session.gameInProgress) {
return {
status: 'error',
message: 'Game already in progress'
};
} else {
const missingHumans = session.maxPlayers - session.numPlayers;
for (let i = 0; i < missingHumans; i++) {
session.addPlayer(session.createPlayerAI(i));
}
session.start(seed);
return {
status: 'ok'
};
} }
} }
updateSocketId(sessionId: string, userId: string, socketId: string): any {
const player = this.getSessionPlayer(sessionId, userId);
if (player !== undefined) {
player.socketId = socketId;
}
}
setSession(session: MatchSession) {
SessionManager.sessions.set(session.id, session);
}
deleteSession(session: MatchSession) {
SessionManager.sessions.delete(session.id);
}
getSession(id: string) { getSession(id: string) {
return SessionManager.sessions[id]; return SessionManager.sessions.get(id);
}
deleteSession(id: string) {
delete SessionManager.sessions[id];
} }
} }

View File

@ -3,6 +3,7 @@ import { AuthController } from '../controllers/AuthController';
import adminRouter from './adminRouter'; import adminRouter from './adminRouter';
import userRouter from './userRouter'; import userRouter from './userRouter';
import gameRouter from './gameRouter';
export default function(): Router { export default function(): Router {
const router = Router(); const router = Router();
@ -15,8 +16,10 @@ export default function(): Router {
router.post('/auth/code', (req: Request, res: Response) => authController.twoFactorCodeAuthentication(req, res)); router.post('/auth/code', (req: Request, res: Response) => authController.twoFactorCodeAuthentication(req, res));
router.post('/login', (req: Request, res: Response) => authController.login(req, res)); router.post('/login', (req: Request, res: Response) => authController.login(req, res));
router .use('/admin', adminRouter()); router.use('/admin', adminRouter());
router .use('/user', userRouter()); router.use('/user', userRouter());
router.use('/game', gameRouter());
return router; return router;
} }

View File

@ -0,0 +1,17 @@
import { Request, Response, Router } from 'express';
import { AuthController } from '../controllers/AuthController';
import { GameController } from '../controllers/GameController';
export default function(): Router {
const router = Router();
const gameController = new GameController();
const { authenticate } = AuthController
router.post('/match', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.createMatch(req, res));
router.patch('/match/join', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.joinMatch(req, res));
router.get('/match/list', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.listMatches(req, res));
return router;
}

View File

@ -1,36 +1,35 @@
import { DominoesGame } from "./DominoesGame"; import { DominoesGame } from "../../game/DominoesGame";
import { MatchSession } from "./MatchSession"; import { MatchSession } from "../../game/MatchSession";
import { GameState } from "./dto/GameState"; import { GameState } from "../../game/dto/GameState";
import { PlayerInterface } from "./entities/player/PlayerInterface"; import { PlayerInterface } from "../../game/entities/player/PlayerInterface";
export class PlayerNotificationManager { export class PlayerNotificationService {
async notifyGameState(game: DominoesGame) { async notifyGameState(game: DominoesGame) {
const gameState: GameState = game.getGameState(); const gameState: GameState = game.getGameState();
const { players } = game; const { players } = game;
let promises: Promise<void>[] = players.map(player => player.notifyGameState(gameState)); let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-game-state', gameState));
return await Promise.all(promises); return await Promise.all(promises);
} }
async notifyPlayersState(players: PlayerInterface[]) { async notifyPlayersState(players: PlayerInterface[]) {
let promises: Promise<void>[] = players.map(player => player.notifyPlayerState(player.getState())); let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-player-state', player.getState()));
return await Promise.all(promises); return await Promise.all(promises);
} }
async notifyMatchState(session: MatchSession) { async notifyMatchState(session: MatchSession) {
const { players } = session; const { players } = session;
let promises: Promise<void>[] = players.map(player => player.notifyMatchState(session.getState())); let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-match-session-state', session.getState()));
return await Promise.all(promises); return await Promise.all(promises);
} }
async waitForPlayersAction(actionId: string, data: any = {}, players: PlayerInterface[]) { async sendEventToPlayers(event: string, players: PlayerInterface[], data: any = {}) {
let promises: Promise<boolean>[] = players.map(player => player.waitForAction(actionId, data)); let promises: Promise<void>[] = players.map(player => player.sendEvent(event, data));
return await Promise.all(promises); return await Promise.all(promises);
} }
async sendEventToPlayers(event: string, players: PlayerInterface[]) { async sendEvent(event: string, player: PlayerInterface, data: any = {}) {
let promises: Promise<void>[] = players.map(player => player.sendEvent(event)); player.sendEvent(event, data)
return await Promise.all(promises);
} }
} }

View File

@ -1,19 +1,107 @@
import { SessionCreationError } from "../../common/errors/SessionCreationError";
import { SessionNotFoundError } from "../../common/errors/SessionNotFoundError";
import { wait, whileNotUndefined } from "../../common/utilities";
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
import { MatchSession } from "../../game/MatchSession"; import { MatchSession } from "../../game/MatchSession";
import { PlayerNotificationService } from "./PlayerNotificationService";
import { matchSessionAdapter } from "../db/DbAdapter"; import { matchSessionAdapter } from "../db/DbAdapter";
import { DbMatchSession } from "../db/interfaces";
import { MatchSessionMongoManager } from "../db/mongo/MatchSessionMongoManager"; import { MatchSessionMongoManager } from "../db/mongo/MatchSessionMongoManager";
import { SessionManager } from "../managers/SessionManager";
import { ServiceBase } from "./ServiceBase"; import { ServiceBase } from "./ServiceBase";
import { SocketIoService } from "./SocketIoService";
export class SessionService extends ServiceBase{ export class SessionService extends ServiceBase{
private dbManager: MatchSessionMongoManager = new MatchSessionMongoManager(); private dbManager: MatchSessionMongoManager = new MatchSessionMongoManager();
private sessionManager: SessionManager = new SessionManager();
private notifyService = new PlayerNotificationService();
constructor() { constructor() {
super() super()
} }
public createSession(session: MatchSession): any { public async createSession(user: any, sessionName: string, seed: string ): Promise<string> {
this.dbManager.create(matchSessionAdapter(session)); let socketClient;
try {
socketClient = await whileNotUndefined(() => SocketIoService.getClient(user._id));
} catch (error) {
throw new SessionCreationError();
}
const player = new NetworkPlayer(user._id, user.username, socketClient.socketId);
const session = new MatchSession(player, sessionName, seed);
const dbSessionId = await this.dbManager.create(matchSessionAdapter(session));
if (dbSessionId === undefined) {
throw new SessionCreationError();
}
session.id = dbSessionId.toString();
socketClient.sessionId = session.id;
this.sessionManager.setSession(session);
this.notifyService.notifyMatchState(session);
this.notifyService.notifyPlayersState(session.players);
return session.id;
} }
public updateSession(session: MatchSession): any { public async joinSession(user: any, sessionId: string): Promise<void> {
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
if (session === undefined) {
throw new SessionNotFoundError();
} let socketClient;
try {
socketClient = await whileNotUndefined(() => SocketIoService.getClient(user._id));
} catch (error) {
throw new SessionCreationError();
}
const player = new NetworkPlayer(user._id, user.name, socketClient.socketId);
session.addPlayer(player);
socketClient.sessionId = session.id;
this.dbManager.replaceOne({id: session.id}, matchSessionAdapter(session)); this.dbManager.replaceOne({id: session.id}, matchSessionAdapter(session));
} }
public listSessions(): Promise<DbMatchSession[]> {
return this.dbManager.listByFilter({});
}
public updateSocketId(sessionId: string, userId: string, socketId: string): any {
this.sessionManager.updateSocketId(sessionId, userId, socketId);
}
setPlayerReady(data: any): any {
const { userId, sessionId } = data;
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
if (session !== undefined) {
session.setPlayerReady(userId)
this.notifyService.notifyMatchState(session);
this.notifyService.notifyPlayersState(session.players);
}
}
startSession(data: any): any {
const sessionId: string = data.sessionId;
const seed: string | undefined = data.seed;
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
if (session === undefined) {
return ({
status: 'error',
message: 'Session not found'
});
} else if (session.matchInProgress) {
return {
status: 'error',
message: 'Game already in progress'
};
} else {
const missingHumans = session.maxPlayers - session.numPlayers;
for (let i = 0; i < missingHumans; i++) {
session.addPlayer(session.createPlayerAI(i));
}
session.start();
return {
status: 'ok'
};
}
}
// public updateSession(session: MatchSession): any {
// this.dbManager.replaceOne({id: session.id}, matchSessionAdapter(session));
// }
} }

View File

@ -2,14 +2,39 @@ import { Server as HttpServer } from "http";
import { ServiceBase } from "./ServiceBase"; import { ServiceBase } from "./ServiceBase";
import { Server } from "socket.io"; import { Server } from "socket.io";
import { SessionManager } from "../managers/SessionManager"; import { SessionManager } from "../managers/SessionManager";
import { SecurityManager } from "../managers/SecurityManager";
import { User } from "../db/interfaces";
import { Socket } from "socket.io";
import { SessionService } from "./SessionService";
export class SocketIoService extends ServiceBase{ export class SocketIoService extends ServiceBase{
io: Server io: Server
clients: Map<string, any> = new Map(); private static clients: Map<string, any> = new Map();
private sessionService: SessionService = new SessionService();
static getClient(id: string) {
return this.clients.get(id);
}
security = new SecurityManager();
constructor(private httpServer: HttpServer) { constructor(private httpServer: HttpServer) {
super() super()
this.io = this.socketIo(httpServer); this.io = this.socketIo(httpServer);
this.io.use(async (socket: Socket, next: any) => {
if (socket.handshake.auth && socket.handshake.auth.token) {
const token = socket.handshake.auth.token;
try {
const user: User = await this.security.verifyJwt(token);
socket.user = user;
next();
} catch (err) {
this.logger.error(err);
next(new Error('Authentication error'));
}
} else {
next(new Error('Authentication error'));
}
});
this.initListeners(); this.initListeners();
} }
@ -17,8 +42,12 @@ export class SocketIoService extends ServiceBase{
return this.io; return this.io;
} }
getUserId(socket: Socket) {
const { user } = socket;
return user?._id?.toString();
}
private initListeners() { private initListeners() {
const sessionController = new SessionManager();
this.io.on('connection', (socket) => { this.io.on('connection', (socket) => {
this.logger.debug(`connect ${socket.id}`); this.logger.debug(`connect ${socket.id}`);
if (socket.recovered) { if (socket.recovered) {
@ -28,59 +57,79 @@ export class SocketIoService extends ServiceBase{
this.logger.debug("socket.data:", socket.data); this.logger.debug("socket.data:", socket.data);
} else { } else {
this.logger.debug("new connection"); this.logger.debug("new connection");
this.clients.set(socket.id, { alive: true }); const { id: socketId, user } = socket;
if (user !== undefined && user._id !== undefined) {
const userId = user._id.toString();
if (!SocketIoService.clients.has(userId)) {
SocketIoService.clients.set(userId, { socketId, alive: true, user: socket.user });
socket.join('room-general') socket.join('room-general')
socket.data.foo = "bar"; } else {
const client = SocketIoService.clients.get(userId);
this.sessionService.updateSocketId(client.sessionId, userId, socketId);
client.socketId = socketId;
this.logger.debug(`User '${user.username}' already connected. Updating socketId to ${socketId}`);
client.alive = true;
} }
} else {
this.logger.error('User not found');
socket.disconnect();
}
}
socket.on('disconnect', () => {
const id = this.getUserId(socket);
if (id) {
this.logger.info('user disconnected');
SocketIoService.clients.delete(id);
}
});
// socket.on('createSession', (data, callback) => {
// const response = sessionController.createSession(data, socket.id);
// callback(response);
// });
socket.on('startSession', (data, callback) => {
const response = this.sessionService.startSession(data);
callback(response);
});
// socket.on('joinSession', (data, callback) => {
// const response = sessionController.joinSession(data, socket.id);
// callback(response);
// });
socket.on('playerReady', (data, callback) => {
const response = this.sessionService.setPlayerReady(data);
callback(response);
});
socket.on('pong', () => { socket.on('pong', () => {
if (this.clients.has(socket.id)) { const id = this.getUserId(socket);
this.clients.set(socket.id, { alive: true }); if (id && SocketIoService.clients.has(id)) {
const client = SocketIoService.clients.get(id);
SocketIoService.clients.set(id, {...client, alive: true });
} }
}) })
socket.on('disconnect', () => {
this.logger.debug('user disconnected');
this.clients.delete(socket.id);
});
socket.on('createSession', (data, callback) => {
const response = sessionController.createSession(data, socket.id);
callback(response);
});
socket.on('startSession', (data, callback) => {
const response = sessionController.startSession(data);
callback(response);
});
socket.on('joinSession', (data, callback) => {
const response = sessionController.joinSession(data, socket.id);
callback(response);
});
socket.on('playerReady', (data, callback) => {
const response = sessionController.setPlayerReady(data);
callback(response);
});
this.pingClients() this.pingClients()
}); });
} }
private pingClients() { private pingClients() {
setInterval(() => { setInterval(() => {
for (let [id, client] of this.clients.entries()) { for (let [id, client] of SocketIoService.clients.entries()) {
if (!client.alive) { if (!client.alive) {
this.logger.debug(`Client ${id} did not respond. Disconnecting.`); this.logger.debug(`Client ${id} did not respond. Disconnecting.`);
this.io.to(id).disconnectSockets(true); // Disconnect client this.io.to(id).disconnectSockets(true); // Disconnect client
this.clients.delete(id); SocketIoService.clients.delete(id);
} else { } else {
client.alive = false; // Reset alive status for the next ping client.alive = false; // Reset alive status for the next ping
this.io.to(id).emit('ping'); // Send ping message this.io.to(client.socketId).emit('ping'); // Send ping message
} }
} }
}, 30000); }, 10000);
} }
private socketIo(httpServer: HttpServer): Server { private socketIo(httpServer: HttpServer): Server {

8
src/server/types/socket/index.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
import { Socket } from 'socket.io';
import { User } from '../../db/interfaces';
declare module 'socket.io' {
interface Socket {
user?: User;
}
}