feat(pong/database): store match outcomes in database

This commit is contained in:
Maieul BOYER 2026-01-05 15:40:31 +01:00 committed by Nigel
parent d59e8f93c8
commit 03be784a51
5 changed files with 139 additions and 36 deletions

View file

@ -5,12 +5,14 @@ import { Database as DbImpl } from './mixin/_base';
import { IUserDb, UserImpl } from './mixin/user'; import { IUserDb, UserImpl } from './mixin/user';
import { IBlockedDb, BlockedImpl } from './mixin/blocked'; import { IBlockedDb, BlockedImpl } from './mixin/blocked';
import { ITicTacToeDb, TicTacToeImpl } from './mixin/tictactoe'; import { ITicTacToeDb, TicTacToeImpl } from './mixin/tictactoe';
import { IPongDb, PongImpl } from './mixin/pong';
Object.assign(DbImpl.prototype, UserImpl); Object.assign(DbImpl.prototype, UserImpl);
Object.assign(DbImpl.prototype, BlockedImpl); Object.assign(DbImpl.prototype, BlockedImpl);
Object.assign(DbImpl.prototype, TicTacToeImpl); 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 // When using .decorate you have to specify added properties for Typescript
declare module 'fastify' { declare module 'fastify' {

View file

@ -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: <provider>:<unique_id>; null if not logged via provider"]
Note: "Represent a user"
}

View file

@ -10,18 +10,14 @@ CREATE TABLE IF NOT EXISTS user (
allow_guest_message INTEGER NOT NULL DEFAULT 1 allow_guest_message INTEGER NOT NULL DEFAULT 1
); );
CREATE TABLE IF NOT EXISTS blocked ( CREATE TABLE IF NOT EXISTS blocked (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
user TEXT NOT NULL, user TEXT NOT NULL,
blocked 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 CREATE UNIQUE INDEX IF NOT EXISTS idx_blocked_user_pair ON blocked (user, blocked);
ON blocked(user, blocked);
CREATE TABLE IF NOT EXISTS tictactoe ( CREATE TABLE IF NOT EXISTS tictactoe (
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
@ -31,3 +27,16 @@ CREATE TABLE IF NOT EXISTS tictactoe (
FOREIGN KEY(player1) REFERENCES user(id), FOREIGN KEY(player1) REFERENCES user(id),
FOREIGN KEY(player2) 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)
);

View file

@ -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<IPongDb, keyof Database> = {
/**
* @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<PongGameTable> | 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<PongGameTable> | 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<boolean> {
// return false;
// }
// this function will never be able to be called outside of this module
// async function privateFunction(): Promise<string | undefined> {
// return undefined;
// }
// silence warnings
// void privateFunction;

View file

@ -4,6 +4,7 @@ import { FastifyInstance } from 'fastify';
import { Pong } from './game'; import { Pong } from './game';
import { GameMove, GameUpdate, SSocket } from './socket'; import { GameMove, GameUpdate, SSocket } from './socket';
import { isNullish } from '@shared/utils'; import { isNullish } from '@shared/utils';
import { PongGameId, PongGameOutcome } from '@shared/database/mixin/pong';
type PUser = { type PUser = {
id: UserId; id: UserId;
@ -13,7 +14,7 @@ type PUser = {
updateInterval: NodeJS.Timeout, updateInterval: NodeJS.Timeout,
}; };
type GameId = string & { readonly __brand: unique symbol }; type GameId = PongGameId;
class StateI { class StateI {
public static readonly UPDATE_INTERVAL_FRAMES: number = 60; public static readonly UPDATE_INTERVAL_FRAMES: number = 60;
@ -72,7 +73,9 @@ class StateI {
g.tick(); g.tick();
this.gameUpdate(gameId, u1.socket); this.gameUpdate(gameId, u1.socket);
this.gameUpdate(gameId, u2.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); }, 1000 / StateI.UPDATE_INTERVAL_FRAMES);
} }
} }
@ -175,6 +178,12 @@ class StateI {
player.currentGame = null; player.currentGame = null;
player.socket.emit('gameEnd'); 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 // do something here with the game result before deleting the game at the end
} }