diff --git a/.hmrc b/.hmrc index 31678dd..53bb6bb 100644 --- a/.hmrc +++ b/.hmrc @@ -1,34 +1,134 @@ { "path": "G:\\Other\\Development\\Projects\\[ideas]\\domino", - "name": "domino", - "initialVersion": "1.0.0", - "version": "1.0.0", + "name": "domino-server", + "initialVersion": "0.1.1", + "version": "0.1.2", "docker": { - "repository": "arhuako/domino" + "useRegistry": true, + "registry": "192.168.1.115:5000", + "repository": "arhuako/domino-server" }, "repository": { - "type": "github", - "user": "jmconde", - "name": "domino", - "manage": true, - "createOnInit": true + "type": "other", + "url": "https://gitea.xintanalabs.net/arhuako/domino-server.git", + "manage": true }, "changelog": { - "create": true, "managed": true, "createHTML": true, "htmlPath": "public" }, "_backupInitial": { - "name": "domino", - "version": "1.0.0", + "name": "domino-server", + "version": "0.1.1", "description": "", "main": "index.js", + "engines": { + "node": ">=20.6.0" + }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "build": "tsc", + "dev": "node --env-file=.env --watch -r ts-node/register src/server/index.ts", + "create": "node --env-file=.env -r ts-node/register ./create.ts", + "test": "node --env-file=.env -r ts-node/register src/test.ts", + "test:watch": "node --env-file=.env --watch -r ts-node/register src/test.ts", + "docker-build": "docker build -t arhuako/domino:latest .", + "docker-tag": "docker tag arhuako/domino:latest arhuako/domino:1.0.0", + "docker-push": "docker push arhuako/domino:latest && docker push arhuako/domino:1.0.0", + "publish": "npm run docker-build && npm run docker-tag && npm run docker-push" }, "keywords": [], - "author": "", - "license": "ISC" + "author": "arhuako", + "license": "ISC", + "type": "commonjs", + "reposityory": "github:jmconde/domino", + "dependencies": { + "bcryptjs": "^2.4.3", + "chalk": "^4.1.2", + "cors": "^2.8.5", + "express": "^4.19.2", + "express-validator": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "mongodb": "^6.8.0", + "nodemailer": "^6.9.14", + "nodemailer-express-handlebars": "^6.1.2", + "pino": "^9.2.0", + "pino-http": "^10.2.0", + "pino-pretty": "^11.2.1", + "pino-rotating-file-stream": "^0.0.2", + "pubsub-js": "^1.9.4", + "seedrandom": "^3.0.5", + "socket.io": "^4.7.5" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20.14.8", + "@types/nodemailer": "^6.4.15", + "@types/nodemailer-express-handlebars": "^4.0.5", + "@types/pubsub-js": "^1.8.6", + "@types/seedrandom": "^3.0.8", + "ts-node": "^10.9.2", + "typescript": "^5.5.2" + } + }, + "_backup": { + "name": "domino-server", + "version": "0.1.1", + "description": "", + "main": "index.js", + "engines": { + "node": ">=20.6.0" + }, + "scripts": { + "build": "tsc", + "dev": "node --env-file=.env --watch -r ts-node/register src/server/index.ts", + "create": "node --env-file=.env -r ts-node/register ./create.ts", + "test": "node --env-file=.env -r ts-node/register src/test.ts", + "test:watch": "node --env-file=.env --watch -r ts-node/register src/test.ts", + "docker-build": "docker build -t 192.168.1.115:5000/arhuako/domino-server:latest .", + "docker-tag": "docker tag 192.168.1.115:5000/arhuako/domino-server:latest 192.168.1.115:5000/arhuako/domino-server:0.1.1", + "docker-push": "docker push 192.168.1.115:5000/arhuako/domino-server:latest && docker push 192.168.1.115:5000/arhuako/domino-server:0.1.1", + "publish": "npm run docker-build && npm run docker-tag && npm run docker-push" + }, + "keywords": [], + "author": "arhuako", + "license": "ISC", + "type": "commonjs", + "reposityory": { + "type": "git", + "url": "https://gitea.xintanalabs.net/arhuako/domino-server.git" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "chalk": "^4.1.2", + "cors": "^2.8.5", + "express": "^4.19.2", + "express-validator": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "mongodb": "^6.8.0", + "nodemailer": "^6.9.14", + "nodemailer-express-handlebars": "^6.1.2", + "pino": "^9.2.0", + "pino-http": "^10.2.0", + "pino-pretty": "^11.2.1", + "pino-rotating-file-stream": "^0.0.2", + "pubsub-js": "^1.9.4", + "seedrandom": "^3.0.5", + "socket.io": "^4.7.5" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20.14.8", + "@types/nodemailer": "^6.4.15", + "@types/nodemailer-express-handlebars": "^4.0.5", + "@types/pubsub-js": "^1.8.6", + "@types/seedrandom": "^3.0.8", + "ts-node": "^10.9.2", + "typescript": "^5.5.2" + } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index e8cfef1..b8a7074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,4 +2,7 @@ All notable changes to this project will be documented in this file. ## Unreleased -Initial commit + +## 0.1.2 - 2024-07-17 +### Added +- This changelog diff --git a/package.json b/package.json index 5468d5d..e0764eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "domino", - "version": "1.0.0", + "name": "domino-server", + "version": "0.1.2", "description": "", "main": "index.js", "engines": { @@ -12,16 +12,19 @@ "create": "node --env-file=.env -r ts-node/register ./create.ts", "test": "node --env-file=.env -r ts-node/register src/test.ts", "test:watch": "node --env-file=.env --watch -r ts-node/register src/test.ts", - "docker-build": "docker build -t arhuako/domino:latest .", - "docker-tag": "docker tag arhuako/domino:latest arhuako/domino:1.0.0", - "docker-push": "docker push arhuako/domino:latest && docker push arhuako/domino:1.0.0", + "docker-build": "docker build -t 192.168.1.115:5000/arhuako/domino-server:latest .", + "docker-tag": "docker tag 192.168.1.115:5000/arhuako/domino-server:latest 192.168.1.115:5000/arhuako/domino-server:0.1.2", + "docker-push": "docker push 192.168.1.115:5000/arhuako/domino-server:latest && docker push 192.168.1.115:5000/arhuako/domino-server:0.1.2", "publish": "npm run docker-build && npm run docker-tag && npm run docker-push" }, "keywords": [], "author": "arhuako", "license": "ISC", "type": "commonjs", - "reposityory": "github:jmconde/domino", + "reposityory": { + "type": "git", + "url": "https://gitea.xintanalabs.net/arhuako/domino-server.git" + }, "dependencies": { "bcryptjs": "^2.4.3", "chalk": "^4.1.2", diff --git a/public/CHANGELOG.html b/public/CHANGELOG.html new file mode 100644 index 0000000..2fa5aaf --- /dev/null +++ b/public/CHANGELOG.html @@ -0,0 +1,7 @@ +

Changelog

+

All notable changes to this project will be documented in this file.

+

0.1.2 - 2024-07-17

+

Added

+ diff --git a/src/game/DominoesGame.ts b/src/game/DominoesGame.ts index badefff..76592a8 100644 --- a/src/game/DominoesGame.ts +++ b/src/game/DominoesGame.ts @@ -288,12 +288,19 @@ export class DominoesGame extends EventEmitter { if (this.winner !== null) { const winner = this.winner; winner.score = totalPips; - if (winner.teamedWith !== null) { - winner.teamedWith.score = totalPips; + if (winner.teamedWith !== undefined) { + const p = this.getPlayer(winner.teamedWith) + if (p !== undefined) { + p.score = totalPips; + } } } } + private getPlayer(userId: string) { + return this.players.find(player => player.id === userId) || undefined; + } + private autoDealTiles() { for (let i = 0; i < this.handSize; i++) { for (let player of this.players) { diff --git a/src/game/MatchSession.ts b/src/game/MatchSession.ts index a243ea8..2f98894 100644 --- a/src/game/MatchSession.ts +++ b/src/game/MatchSession.ts @@ -11,6 +11,7 @@ import { GameSummary } from "./dto/GameSummary"; import { PlayerMove } from "./entities/PlayerMove"; import { SessionService } from "../server/services/SessionService"; import { Score } from "../server/db/interfaces"; +import { MatchSessionOptions } from "./dto/MatchSessionOptions"; export class MatchSession { @@ -33,26 +34,30 @@ export class MatchSession { maxPlayers: number = 4; mode: string = 'classic'; players: PlayerInterface[] = []; - pointsToWin: number = 50; + // pointsToWin: number = 50; rng!: PRNG scoreboard: Map = new Map(); seed!: string sessionInProgress: boolean = false; status: string = 'created' + name: string - constructor(public creator: PlayerInterface, public name?: string, seed?: string) { + constructor(public creator: PlayerInterface, private options: MatchSessionOptions) { + const { sessionName, seed, winType, winTarget } = options; this.seed = seed || getRandomSeed(); this.id = uuid(); - this.name = name || `Game ${this.id}`; + this.name = sessionName || `Match ${this.id}`; this.addPlayerToSession(creator); this.creator = creator; this.logger.info(`Match session created by: ${creator.name}`); this.logger.info(`Match session ID: ${this.id}`); this.logger.info(`Match session name: ${this.name}`); - this.logger.info(`Points to win: ${this.pointsToWin}`); - this.sessionInProgress = true; + this.logger.info(`Win type: ${options.winType}`); + this.logger.info(`Win target: ${options.winTarget}`); + this.sessionInProgress = false; this.waitingForPlayers = true; + this.status = 'created'; this.logger.info('Waiting for players to be ready'); } @@ -103,7 +108,7 @@ export class MatchSession { } getPlayer(userId: string) { - return this.players.find(player => player.id === userId); + return this.players.find(player => player.id === userId) || null; } playerMove(move: any) { @@ -122,19 +127,42 @@ export class MatchSession { if (!tile) { throw new Error("Tile not found"); } - const newMove = new PlayerMove(tile, move.type, move. playerId) + const newMove = new PlayerMove(tile, move.type, move.playerId, move.direction) this.currentGame.finishTurn(newMove); } } // This is the entry point for the game, method called by session host - async start() { + async start(data: any) { if (this.matchInProgress) { throw new Error("Game already in progress"); } this.waitingForPlayers = false; + this.sessionInProgress = true; + this.status = 'started' + this.sessionService.updateSession(this); await this.startMatch(this.seed); - } + } + + setTeams(data: any) { + if (data.teamedWith !== undefined) { + const creatorTeam = this.getPlayer(data.teamedWith) + if (!creatorTeam) { + throw new Error("Teamed player not found"); + } + this.creator.teamedWith = data.teamedWith; + this.creator.team = 1 + creatorTeam.teamedWith = this.creator.id; + creatorTeam.team = 1; + + const others = this.players.filter(player => player.team === 0); + others[0].teamedWith = others[1].id; + others[0].team = 2; + others[1].teamedWith = others[0].id; + others[1].team = 2; + this.players = [this.creator, others[0], creatorTeam, others[1]]; + } + } addPlayerToSession(player: PlayerInterface) { if (this.numPlayers >= this.maxPlayers) { @@ -225,7 +253,16 @@ export class MatchSession { checkMatchWinner() { const scores = Array.from(this.scoreboard.values()); const maxScore = Math.max(...scores); - if (maxScore >= this.pointsToWin) { + + if (this.options.winType === 'rounds') { + this.checkRoundsWinner(maxScore); + } else if (this.options.winType === 'points') { + this.checkPointsWinner(maxScore); + } + } + + checkRoundsWinner(maxScore: number) { + if (maxScore >= this.options.winTarget) { this.matchWinner = this.players.find(player => this.scoreboard.get(player.name) === maxScore); if (!this.matchWinner) { throw new Error('Match winner not found'); @@ -234,6 +271,20 @@ export class MatchSession { this.matchInProgress = false; } } + + checkPointsWinner(maxScore: number) { + if (maxScore >= this.options.winTarget) { + 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.matchInProgress = false; + } + } + + + resetScoreboard() { this.scoreboard = new Map(); this.players.forEach(player => { @@ -245,6 +296,22 @@ export class MatchSession { if (!gameSummary) { return; } + if (this.options.winType === 'rounds') { + this.setScoresRounds(gameSummary); + } else if (this.options.winType === 'points') { + this.setScoresPoints(gameSummary); + } + } + + setScoresRounds(gameSummary: GameSummary) { + const { winner } = gameSummary; + if (winner !== undefined) { + const currentScore = this.scoreboard.get(winner.name) ?? 0; + this.scoreboard.set(winner.name, 1 + currentScore); + } + } + + setScoresPoints(gameSummary: GameSummary) { const { score } = gameSummary; score.forEach(playerScore => { const currentScore = this.scoreboard.get(playerScore.name) ?? 0; @@ -324,12 +391,12 @@ export class MatchSession { waitingSeconds: this.waitingSeconds, seed: this.seed, mode: this.mode, - pointsToWin: this.pointsToWin, - status: this.sessionInProgress ? 'in progress' : 'waiting', + status: this.status, scoreboard: this.getScoreBoardState(), matchWinner: this.matchWinner?.getState(true) || null, matchInProgress: this.matchInProgress, gameSummaries: this.gameSummaries, + options: this.options, }; } diff --git a/src/game/dto/MatchSessionOptions.ts b/src/game/dto/MatchSessionOptions.ts new file mode 100644 index 0000000..84e62ee --- /dev/null +++ b/src/game/dto/MatchSessionOptions.ts @@ -0,0 +1,13 @@ +export interface MatchSessionOptions { + boardScale?: number + handScale?: number + width?: number + height?: number + background: string + teamed: boolean + winTarget: number + winType: 'points' | 'rounds' + seed: string + sessionName: string + numPlayers: 1 | 2 | 3 | 4 +} \ No newline at end of file diff --git a/src/game/dto/MatchSessionState.ts b/src/game/dto/MatchSessionState.ts index 8d5540b..437ab8c 100644 --- a/src/game/dto/MatchSessionState.ts +++ b/src/game/dto/MatchSessionState.ts @@ -1,5 +1,6 @@ import { Score } from "../../server/db/interfaces"; import { GameSummary } from "./GameSummary"; +import { MatchSessionOptions } from "./MatchSessionOptions"; import { PlayerDto } from "./PlayerDto"; export interface MatchSessionState { @@ -10,7 +11,6 @@ export interface MatchSessionState { seed: string; waitingForPlayers: boolean; mode: string; - pointsToWin: number; sessionInProgress: boolean; status: string; maxPlayers: number; @@ -21,4 +21,5 @@ export interface MatchSessionState { matchInProgress: boolean; playersReady: number, gameSummaries: GameSummary[]; + options: MatchSessionOptions } \ No newline at end of file diff --git a/src/game/dto/PlayerDto.ts b/src/game/dto/PlayerDto.ts index 29a1bce..4de00e7 100644 --- a/src/game/dto/PlayerDto.ts +++ b/src/game/dto/PlayerDto.ts @@ -11,7 +11,7 @@ export interface PlayerDto { name: string; score?: number; hand?: TileDto[]; - teamedWith?: PlayerDto | null; + teamedWith?: string; ready: boolean; isHuman: boolean; } \ No newline at end of file diff --git a/src/game/entities/PlayerMove.ts b/src/game/entities/PlayerMove.ts index 6604350..3f60d02 100644 --- a/src/game/entities/PlayerMove.ts +++ b/src/game/entities/PlayerMove.ts @@ -4,7 +4,7 @@ import { Tile } from "./Tile"; export class PlayerMove { id: string = uuid(); - constructor(public tile: Tile, public type: PlayerMoveSideType | null, public playerId: string, direction?: string) {} + constructor(public tile: Tile, public type: PlayerMoveSideType | null, public playerId: string, public direction?: string) {} toString() { return `PlayerMove:([${this.tile.pips[0]}|${this.tile.pips[1]}] ${this.type})`; diff --git a/src/game/entities/player/AbstractPlayer.ts b/src/game/entities/player/AbstractPlayer.ts index 6eee596..920545b 100644 --- a/src/game/entities/player/AbstractPlayer.ts +++ b/src/game/entities/player/AbstractPlayer.ts @@ -13,10 +13,11 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter hand: Tile[] = []; score: number = 0; logger: LoggingService = new LoggingService(); - teamedWith: PlayerInterface | null = null; + teamedWith?: string; playerInteraction: PlayerInteractionInterface = undefined as any; id: string = uuid(); ready: boolean = false; + team: number = 0; constructor(public name: string) { super(); @@ -58,7 +59,7 @@ export abstract class AbstractPlayer extends EventEmitter implements PlayerInter name: this.name, score: this.score, hand: this.hand.map(tile => tile.getState(showPips)), - teamedWith: this.teamedWith?.getState(showPips) ?? null, + teamedWith: this.teamedWith, ready: this.ready, isHuman: this instanceof PlayerHuman }; diff --git a/src/game/entities/player/PlayerInterface.ts b/src/game/entities/player/PlayerInterface.ts index 524e7f0..e49581b 100644 --- a/src/game/entities/player/PlayerInterface.ts +++ b/src/game/entities/player/PlayerInterface.ts @@ -9,7 +9,8 @@ export interface PlayerInterface { name: string; score: number; hand: Tile[]; - teamedWith: PlayerInterface | null; + teamedWith?: string; + team: number; playerInteraction: PlayerInteractionInterface; ready: boolean; diff --git a/src/server/controllers/GameController.ts b/src/server/controllers/GameController.ts index 1398bc1..a02964b 100644 --- a/src/server/controllers/GameController.ts +++ b/src/server/controllers/GameController.ts @@ -8,8 +8,8 @@ export class GameController extends BaseController { public async createMatch(req: Request, res: Response) { try { const { user, body } = req; - const { sessionName, seed, options } = body; - const sessionId = await this.sessionService.createSession(user, sessionName, seed, options); + const { options } = body; + const sessionId = await this.sessionService.createSession(user, options); res.status(201).json({ sessionId }); } catch (error) { this.handleError(res, error); diff --git a/src/server/db/DbAdapter.ts b/src/server/db/DbAdapter.ts index 3c4f6d9..0adcd24 100644 --- a/src/server/db/DbAdapter.ts +++ b/src/server/db/DbAdapter.ts @@ -10,7 +10,7 @@ export function matchSessionAdapter(session: MatchSession, showPips: boolean = f players: state.players, seed: state.seed, mode: state.mode, - pointsToWin: state.pointsToWin, + options: state.options, maxPlayers: state.maxPlayers, numPlayers: state.numPlayers, scoreboard: state.scoreboard, diff --git a/src/server/db/interfaces.ts b/src/server/db/interfaces.ts index dda4837..a4ad7c6 100644 --- a/src/server/db/interfaces.ts +++ b/src/server/db/interfaces.ts @@ -1,4 +1,5 @@ import { GameSummary } from "../../game/dto/GameSummary"; +import { MatchSessionOptions } from "../../game/dto/MatchSessionOptions"; import { PlayerDto } from "../../game/dto/PlayerDto"; export interface Entity { @@ -52,7 +53,7 @@ export interface DbMatchSession extends EntityMongo { players: PlayerDto[]; seed: string; mode: string; - pointsToWin: number; + options: MatchSessionOptions; maxPlayers: number; numPlayers: number; scoreboard: Score[]; diff --git a/src/server/services/InteractionService.ts b/src/server/services/InteractionService.ts index 8010e44..9be5bb0 100644 --- a/src/server/services/InteractionService.ts +++ b/src/server/services/InteractionService.ts @@ -64,10 +64,11 @@ export class InteractionService extends ServiceBase{ for (let i = 0; i < missingHumans; i++) { session.addPlayerToSession(session.createPlayerAI(i)); } + session.setTeams(data); this.notifyService.sendEventToPlayers('server:match-starting', session.players, { sessionState: session.getState() }); - session.start(); + session.start(data); return { status: 'ok' }; diff --git a/src/server/services/SessionService.ts b/src/server/services/SessionService.ts index db43197..895bd91 100644 --- a/src/server/services/SessionService.ts +++ b/src/server/services/SessionService.ts @@ -10,6 +10,7 @@ import { MatchSessionMongoManager } from "../db/mongo/MatchSessionMongoManager"; import { SessionManager } from "../managers/SessionManager"; import { ServiceBase } from "./ServiceBase"; import { SocketIoService } from "./SocketIoService"; +import { MatchSessionOptions } from "../../game/dto/MatchSessionOptions"; export class SessionService extends ServiceBase{ private dbManager: MatchSessionMongoManager = new MatchSessionMongoManager(); @@ -19,7 +20,7 @@ export class SessionService extends ServiceBase{ super() } - public async createSession(user: any, sessionName: string, seed: string, options: any ): Promise { + public async createSession(user: any, options: MatchSessionOptions ): Promise { let socketClient; try { socketClient = await whileNotUndefined(() => SocketIoService.getClient(user._id)); @@ -27,8 +28,7 @@ export class SessionService extends ServiceBase{ throw new SessionCreationError(); } const player = new NetworkPlayer(user._id, user.username, socketClient.socketId); - const session = new MatchSession(player, sessionName, seed); - session.pointsToWin = options.pointsToWin; + const session = new MatchSession(player, options); const dbSessionId = await this.dbManager.create(matchSessionAdapter(session)); if (dbSessionId === undefined) { throw new SessionCreationError();