(tic-tac-toe): game outcomes are now written to tictatoe database. :)

This commit is contained in:
Alessandro Petitcollin 2026-01-05 12:04:21 +01:00 committed by Maix0
parent 7724e24e4c
commit f9801dafe7
2 changed files with 158 additions and 151 deletions

View file

@ -24,7 +24,7 @@ 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 INTEGER PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
player1 TEXT NOT NULL, player1 TEXT NOT NULL,
player2 TEXT NOT NULL, player2 TEXT NOT NULL,
outcome TEXT NOT NULL outcome TEXT NOT NULL

View file

@ -1,198 +1,205 @@
import { UserId } from '@shared/database/mixin/user'; import {UserId} from '@shared/database/mixin/user';
import { FastifyInstance } from 'fastify'; import {FastifyInstance} from 'fastify';
import { GameMove, SSocket } from './socket'; import {GameMove, SSocket} from './socket';
import { isNullish } from '@shared/utils'; import {isNullish} from '@shared/utils';
import { newUUID } from '@shared/utils/uuid'; import {newUUID} from '@shared/utils/uuid';
import { GameId } from '@shared/database/mixin/tictactoe'; import {GameId} from '@shared/database/mixin/tictactoe';
import { TTC } from './game'; import {TTC} from './game';
type TTTUser = { type TTTUser = {
socket: SSocket, socket: SSocket,
userId: UserId, userId: UserId,
windowId: string, windowId: string,
updateInterval: NodeJS.Timeout, updateInterval: NodeJS.Timeout,
currentGame: GameId | null, currentGame: GameId | null,
} }
export class StateI { export class StateI {
private users: Map<UserId, TTTUser> = new Map(); private users: Map<UserId, TTTUser> = new Map();
private queue: Set<UserId> = new Set(); private queue: Set<UserId> = new Set();
private queueInterval: NodeJS.Timeout; private queueInterval: NodeJS.Timeout;
private games: Map<GameId, TTC> = new Map(); private games: Map<GameId, TTC> = new Map();
constructor(private fastify: FastifyInstance) { constructor(private fastify: FastifyInstance) {
this.queueInterval = setInterval(() => this.queuerFunction()); this.queueInterval = setInterval(() => this.queuerFunction());
void this.queueInterval; void this.queueInterval;
} }
private queuerFunction(): void { public registerUser(socket: SSocket): void {
const values = Array.from(this.queue.values()); this.fastify.log.info('Registering new user');
while (values.length >= 2) { if (this.users.has(socket.authUser.id)) {
const id1 = values.pop(); socket.emit('forceDisconnect', 'Already Connected');
const id2 = values.pop(); socket.disconnect();
return;
}
this.users.set(socket.authUser.id, {
socket,
userId: socket.authUser.id,
windowId: socket.id,
updateInterval: setInterval(() => this.updateClient(socket), 3000),
currentGame: null,
});
this.fastify.log.info('Registered new user');
if (isNullish(id1) || isNullish(id2)) { socket.on('disconnect', () => this.cleanupUser(socket));
continue; socket.on('enqueue', () => this.enqueueUser(socket));
} socket.on('dequeue', () => this.dequeueUser(socket));
socket.on('debugInfo', () => this.debugSocket(socket));
const u1 = this.users.get(id1); socket.on('gameMove', (e) => this.gameMove(socket, e));
const u2 = this.users.get(id2); if (socket) {
console.log('Socket:', socket.id);
}
if (isNullish(u1) || isNullish(u2)) { }
continue;
}
this.queue.delete(id1);
this.queue.delete(id2);
const gameId = newUUID() as unknown as GameId; private queuerFunction(): void {
const g = new TTC(u1.userId, u2.userId); const values = Array.from(this.queue.values());
const iState = { while (values.length >= 2) {
boardState: g.board, const id1 = values.pop();
currentPlayer: g.getCurrentState(), const id2 = values.pop();
playerX: g.playerX,
playerO: g.playerO,
gameState: g.checkState(),
gameId: gameId,
};
u1.socket.emit('newGame', iState); if (isNullish(id1) || isNullish(id2)) {
u2.socket.emit('newGame', iState); continue;
this.games.set(gameId, g); }
u1.currentGame = gameId; const u1 = this.users.get(id1);
u2.currentGame = gameId; const u2 = this.users.get(id2);
g.gameUpdate = setInterval(() => { if (isNullish(u1) || isNullish(u2)) {
this.gameUpdate(gameId, u1.socket); continue;
this.gameUpdate(gameId, u2.socket); }
if (g.checkState() !== 'ongoing') { this.cleanupGame(gameId, g); } this.queue.delete(id1);
}, 100); this.queue.delete(id2);
}
}
public registerUser(socket: SSocket): void { const gameId = newUUID() as unknown as GameId;
this.fastify.log.info('Registering new user'); const g = new TTC(u1.userId, u2.userId);
if (this.users.has(socket.authUser.id)) { const iState = {
socket.emit('forceDisconnect', 'Already Connected'); boardState: g.board,
socket.disconnect(); currentPlayer: g.getCurrentState(),
return; playerX: g.playerX,
} playerO: g.playerO,
this.users.set(socket.authUser.id, { gameState: g.checkState(),
socket, gameId: gameId,
userId: socket.authUser.id, };
windowId: socket.id,
updateInterval: setInterval(() => this.updateClient(socket), 3000),
currentGame: null,
});
this.fastify.log.info('Registered new user');
socket.on('disconnect', () => this.cleanupUser(socket)); u1.socket.emit('newGame', iState);
socket.on('enqueue', () => this.enqueueUser(socket)); u2.socket.emit('newGame', iState);
socket.on('dequeue', () => this.dequeueUser(socket)); this.games.set(gameId, g);
socket.on('debugInfo', () => this.debugSocket(socket));
socket.on('gameMove', (e) => this.gameMove(socket, e)); u1.currentGame = gameId;
} u2.currentGame = gameId;
private updateClient(socket: SSocket): void { g.gameUpdate = setInterval(() => {
socket.emit('updateInformation', { this.gameUpdate(gameId, u1.socket);
inQueue: this.queue.size, this.gameUpdate(gameId, u2.socket);
totalUser: this.users.size, if (g.checkState() !== 'ongoing') {
}); this.cleanupGame(gameId, g);
} this.fastify.db.setGameOutcome(gameId, u1.userId, u2.userId, g.checkState());
}
}, 100);
}
}
private cleanupUser(socket: SSocket): void { private updateClient(socket: SSocket): void {
if (!this.users.has(socket.authUser.id)) return; socket.emit('updateInformation', {
inQueue: this.queue.size,
totalUser: this.users.size,
});
}
clearInterval(this.users.get(socket.authUser.id)?.updateInterval); private cleanupUser(socket: SSocket): void {
this.users.delete(socket.authUser.id); if (!this.users.has(socket.authUser.id)) return;
this.queue.delete(socket.authUser.id);
}
private cleanupGame(gameId: GameId, game: TTC): void { clearInterval(this.users.get(socket.authUser.id)?.updateInterval);
clearInterval(game.gameUpdate ?? undefined); this.users.delete(socket.authUser.id);
this.games.delete(gameId); this.queue.delete(socket.authUser.id);
let player: TTTUser | undefined = undefined; }
if ((player = this.users.get(game.playerO)) !== undefined) {
player.currentGame = null;
player.socket.emit('gameEnd');
}
if ((player = this.users.get(game.playerX)) !== undefined) {
player.currentGame = null;
player.socket.emit('gameEnd');
}
// do something here with the game result before deleting the game at the end
}
private enqueueUser(socket: SSocket): void { private cleanupGame(gameId: GameId, game: TTC): void {
if (!this.users.has(socket.authUser.id)) return; clearInterval(game.gameUpdate ?? undefined);
this.games.delete(gameId);
let player: TTTUser | undefined;
if ((player = this.users.get(game.playerO)) !== undefined) {
player.currentGame = null;
player.socket.emit('gameEnd');
}
if ((player = this.users.get(game.playerX)) !== undefined) {
player.currentGame = null;
player.socket.emit('gameEnd');
}
// do something here with the game result before deleting the game at the end
}
if (this.queue.has(socket.authUser.id)) return; private enqueueUser(socket: SSocket): void {
if (!this.users.has(socket.authUser.id)) return;
if (this.users.get(socket.authUser.id)?.currentGame !== null) return; if (this.queue.has(socket.authUser.id)) return;
this.queue.add(socket.authUser.id); if (this.users.get(socket.authUser.id)?.currentGame !== null) return;
socket.emit('queueEvent', 'registered');
}
private dequeueUser(socket: SSocket): void { this.queue.add(socket.authUser.id);
if (!this.users.has(socket.authUser.id)) return; socket.emit('queueEvent', 'registered');
}
if (!this.queue.has(socket.authUser.id)) return; private dequeueUser(socket: SSocket): void {
if (!this.users.has(socket.authUser.id)) return;
this.queue.delete(socket.authUser.id); if (!this.queue.has(socket.authUser.id)) return;
socket.emit('queueEvent', 'unregistered');
}
private debugSocket(socket: SSocket): void { this.queue.delete(socket.authUser.id);
console.log(({ socket.emit('queueEvent', 'unregistered');
message: `socket debug for ${socket.id}`, }
userid: socket.authUser.id,
queue: this.queue,
users: this.users,
}));
}
private gameUpdate(gameId: GameId, socket: SSocket): void { private debugSocket(socket: SSocket): void {
if (!this.users.has(socket.authUser.id)) return; console.log(({
const user = this.users.get(socket.authUser.id)!; message: `socket debug for ${socket.id}`,
userid: socket.authUser.id,
queue: this.queue,
users: this.users,
}));
}
if (user.currentGame !== gameId || !this.games.has(user.currentGame)) return; private gameUpdate(gameId: GameId, socket: SSocket): void {
if (!this.users.has(socket.authUser.id)) return;
const user = this.users.get(socket.authUser.id)!;
const games = this.games.get(gameId)!; if (user.currentGame !== gameId || !this.games.has(user.currentGame)) return;
socket.emit('gameBoard', { const games = this.games.get(gameId)!;
boardState: games.board,
currentPlayer: games.getCurrentState(),
playerX: games.playerX,
playerO: games.playerO,
gameState: games.checkState(),
gameId: gameId,
});
}
private gameMove(socket: SSocket, update: GameMove) { socket.emit('gameBoard', {
if (!this.users.has(socket.authUser.id)) return 'unknownError'; boardState: games.board,
const user = this.users.get(socket.authUser.id)!; currentPlayer: games.getCurrentState(),
playerX: games.playerX,
playerO: games.playerO,
gameState: games.checkState(),
gameId: gameId,
});
}
if (user.currentGame !== null && !this.games.has(user.currentGame)) return 'unknownError'; private gameMove(socket: SSocket, update: GameMove) {
const game = this.games.get(user.currentGame!)!; if (!this.users.has(socket.authUser.id)) return 'unknownError';
const user = this.users.get(socket.authUser.id)!;
game.makeMove(socket.authUser.id, update.index); if (user.currentGame !== null && !this.games.has(user.currentGame)) return 'unknownError';
} const game = this.games.get(user.currentGame!)!;
game.makeMove(socket.authUser.id, update.index);
}
} }
// this value will be overriten // this value will be overriten
export let State: StateI = undefined as unknown as StateI; export let State: StateI = undefined as unknown as StateI;
export function createState(fastify: FastifyInstance) { export function createState(fastify: FastifyInstance) {
if (State !== undefined) { if (State !== undefined) {
throw new Error('State was already created !!!!'); throw new Error('State was already created !!!!');
} }
State = new StateI(fastify); State = new StateI(fastify);
} }