From 03be784a518d9ca24f81ce4b1264845a94e99dfa Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Mon, 5 Jan 2026 15:40:31 +0100 Subject: [PATCH] feat(pong/database): store match outcomes in database --- src/@shared/src/database/index.ts | 4 +- src/@shared/src/database/init.dbml | 27 ------ src/@shared/src/database/init.sql | 21 +++-- src/@shared/src/database/mixin/pong.ts | 110 +++++++++++++++++++++++++ src/pong/src/state.ts | 13 ++- 5 files changed, 139 insertions(+), 36 deletions(-) delete mode 100644 src/@shared/src/database/init.dbml create mode 100644 src/@shared/src/database/mixin/pong.ts diff --git a/src/@shared/src/database/index.ts b/src/@shared/src/database/index.ts index d4ddfc7..61bf688 100644 --- a/src/@shared/src/database/index.ts +++ b/src/@shared/src/database/index.ts @@ -5,12 +5,14 @@ import { Database as DbImpl } from './mixin/_base'; import { IUserDb, UserImpl } from './mixin/user'; import { IBlockedDb, BlockedImpl } from './mixin/blocked'; import { ITicTacToeDb, TicTacToeImpl } from './mixin/tictactoe'; +import { IPongDb, PongImpl } from './mixin/pong'; Object.assign(DbImpl.prototype, UserImpl); Object.assign(DbImpl.prototype, BlockedImpl); Object.assign(DbImpl.prototype, TicTacToeImpl); +Object.assign(DbImpl.prototype, PongImpl); -export interface Database extends DbImpl, IUserDb, IBlockedDb, ITicTacToeDb { } +export interface Database extends DbImpl, IUserDb, IBlockedDb, ITicTacToeDb, IPongDb { } // When using .decorate you have to specify added properties for Typescript declare module 'fastify' { diff --git a/src/@shared/src/database/init.dbml b/src/@shared/src/database/init.dbml deleted file mode 100644 index 133e482..0000000 --- a/src/@shared/src/database/init.dbml +++ /dev/null @@ -1,27 +0,0 @@ -Project Transcendance { - Note: ''' - # DBML for Transcendance - DBML (database markup language) is a simple, readable DSL language designed to define database structures. - - ## Benefits - - * It is simple, flexible and highly human-readable - * It is database agnostic, focusing on the essential database structure definition without worrying about the detailed syntaxes of each database - * Comes with a free, simple database visualiser at [dbdiagram.io](http://dbdiagram.io) - - # how to use it - - ask Maieul :) - ''' -} - -Table user { - id text [PK, not null] - login text [unique] - name text [not null, unique] - password text [null, Note: "If password is NULL, this means that the user is created through OAUTH2 or guest login"] - otp text [null, Note: "If otp is NULL, then the user didn't configure 2FA"] - guest integer [not null, default: 0] - oauth2 text [null, default: `NULL` , Note: "format: :; null if not logged via provider"] - Note: "Represent a user" -} diff --git a/src/@shared/src/database/init.sql b/src/@shared/src/database/init.sql index 02c20f3..b5d93d1 100644 --- a/src/@shared/src/database/init.sql +++ b/src/@shared/src/database/init.sql @@ -10,18 +10,14 @@ CREATE TABLE IF NOT EXISTS user ( allow_guest_message INTEGER NOT NULL DEFAULT 1 ); - CREATE TABLE IF NOT EXISTS blocked ( id INTEGER PRIMARY KEY NOT NULL, user TEXT NOT NULL, blocked TEXT NOT NULL, - - FOREIGN KEY(user) REFERENCES user(id) - FOREIGN KEY(blocked) REFERENCES user(id) + FOREIGN KEY (user) REFERENCES user (id) FOREIGN KEY (blocked) REFERENCES user (id) ); -CREATE UNIQUE INDEX IF NOT EXISTS idx_blocked_user_pair - ON blocked(user, blocked); +CREATE UNIQUE INDEX IF NOT EXISTS idx_blocked_user_pair ON blocked (user, blocked); CREATE TABLE IF NOT EXISTS tictactoe ( id TEXT PRIMARY KEY NOT NULL, @@ -31,3 +27,16 @@ CREATE TABLE IF NOT EXISTS tictactoe ( FOREIGN KEY(player1) REFERENCES user(id), FOREIGN KEY(player2) REFERENCES user(id) ); + +CREATE TABLE IF NOT EXISTS pong ( + id TEXT PRIMARY KEY NOT NULL, + time TEXT NOT NULL default (datetime('now')), + playerL TEXT NOT NULL, + playerR TEXT NOT NULL, + scoreL INTEGER NOT NULL, + scoreR INTEGER NOT NULL, + outcome TEXT NOT NULL, + local INTEGER NOT NULL, + FOREIGN KEY (playerL) REFERENCES user (id), + FOREIGN KEY (playerR) REFERENCES user (id) +); diff --git a/src/@shared/src/database/mixin/pong.ts b/src/@shared/src/database/mixin/pong.ts new file mode 100644 index 0000000..592a2ed --- /dev/null +++ b/src/@shared/src/database/mixin/pong.ts @@ -0,0 +1,110 @@ +import UUID from '@shared/utils/uuid'; +import type { Database } from './_base'; +import { UserId } from './user'; +import { isNullish } from '@shared/utils'; + +export type PongGameOutcome = 'winR' | 'winL' | 'other'; + +// describe every function in the object +export interface IPongDb extends Database { + setPongGameOutcome( + this: IPongDb, + id: PongGameId, + left: { id: UserId, score: number }, + right: { id: UserId, score: number }, + outcome: PongGameOutcome, + local: boolean, + ): void; + getPongGameFromId( + this: IPongDb, + id: PongGameId, + ): PongGame | undefined, +} + +export const PongImpl: Omit = { + /** + * @brief Write the outcome of the specified game to the database. + * + * @param gameId The game we want to write the outcome of. + * + */ + setPongGameOutcome( + this: IPongDb, + id: PongGameId, + left: { id: UserId, score: number }, + right: { id: UserId, score: number }, + outcome: PongGameOutcome, + local: boolean, + ): void { + // Find a way to retrieve the outcome of the game. + this.prepare( + 'INSERT INTO pong (id, playerL, playerR, scoreL, scoreR, outcome, local) VALUES (@id, @playerL, @playerR, @scoreL, @scoreR, @outcome, @local)', + ).run({ id, playerL: left.id, scoreL: left.score, playerR: right.id, scoreR: right.score, outcome, local: local ? 1 : 0 }); + }, + + getPongGameFromId( + this: IPongDb, + id: PongGameId, + ): PongGame | undefined { + const q = this.prepare('SELECT * FROM pong WHERE id = @id').get({ id }) as Partial | undefined; + return pongGameFromRow(q); + }, +}; + +export type PongGameId = UUID & { readonly __uuid: unique symbol }; + +export type PongGame = { + readonly id: PongGameId; + readonly left: { id: UserId, score: number }; + readonly right: { id: UserId, score: number }; + readonly outcome: PongGameOutcome; + readonly time: Date; +}; + +// this is an internal type, never to be seen outside +type PongGameTable = { + id: PongGameId, + playerL: UserId, + playerR: UserId, + scoreL: number, + scoreR: number, + outcome: PongGameOutcome, + time: string, +}; + +function pongGameFromRow(r: Partial | undefined): PongGame | undefined { + if (isNullish(r)) return undefined; + if (isNullish(r.id)) return undefined; + if (isNullish(r.playerL)) return undefined; + if (isNullish(r.playerR)) return undefined; + if (isNullish(r.scoreL)) return undefined; + if (isNullish(r.scoreR)) return undefined; + if (isNullish(r.outcome)) return undefined; + if (isNullish(r.time)) return undefined; + + if (r.outcome !== 'winR' && r.outcome !== 'winL' && r.outcome !== 'other') return undefined; + const date = Date.parse(r.time); + if (Number.isNaN(date)) return undefined; + + + return { + id: r.id, + left: { id: r.playerL, score: r.scoreL }, + right: { id: r.playerR, score: r.scoreR }, + outcome: r.outcome, + time: new Date(date), + }; +} + +// this function will be able to be called from everywhere +// export async function freeFloatingExportedFunction(): Promise { +// return false; +// } + +// this function will never be able to be called outside of this module +// async function privateFunction(): Promise { +// return undefined; +// } + +// silence warnings +// void privateFunction; diff --git a/src/pong/src/state.ts b/src/pong/src/state.ts index 97863bc..cf431b3 100644 --- a/src/pong/src/state.ts +++ b/src/pong/src/state.ts @@ -4,6 +4,7 @@ import { FastifyInstance } from 'fastify'; import { Pong } from './game'; import { GameMove, GameUpdate, SSocket } from './socket'; import { isNullish } from '@shared/utils'; +import { PongGameId, PongGameOutcome } from '@shared/database/mixin/pong'; type PUser = { id: UserId; @@ -13,7 +14,7 @@ type PUser = { updateInterval: NodeJS.Timeout, }; -type GameId = string & { readonly __brand: unique symbol }; +type GameId = PongGameId; class StateI { public static readonly UPDATE_INTERVAL_FRAMES: number = 60; @@ -72,7 +73,9 @@ class StateI { g.tick(); this.gameUpdate(gameId, u1.socket); this.gameUpdate(gameId, u2.socket); - if (g.checkWinner() !== null) { this.cleanupGame(gameId, g); } + if (g.checkWinner() !== null) { + this.cleanupGame(gameId, g); + } }, 1000 / StateI.UPDATE_INTERVAL_FRAMES); } } @@ -175,6 +178,12 @@ class StateI { player.currentGame = null; player.socket.emit('gameEnd'); } + const rOutcome = game.checkWinner(); + let outcome: PongGameOutcome = 'other'; + if (rOutcome === 'left') { outcome = 'winL'; } + if (rOutcome === 'right') { outcome = 'winR'; } + this.fastify.db.setPongGameOutcome(gameId, { id: game.userLeft, score: game.score[0] }, { id: game.userRight, score: game.score[1] }, outcome, game.local); + this.fastify.log.info('SetGameOutcome !'); // do something here with the game result before deleting the game at the end }