feat(pong/database): store match outcomes in database
This commit is contained in:
parent
d59e8f93c8
commit
03be784a51
5 changed files with 139 additions and 36 deletions
|
|
@ -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' {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
);
|
||||||
|
|
|
||||||
110
src/@shared/src/database/mixin/pong.ts
Normal file
110
src/@shared/src/database/mixin/pong.ts
Normal 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;
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue