diff --git a/frontend/src/pages/ttt/socket.ts b/frontend/src/pages/ttt/socket.ts index 2450d9c..59e8eb1 100644 --- a/frontend/src/pages/ttt/socket.ts +++ b/frontend/src/pages/ttt/socket.ts @@ -20,21 +20,22 @@ export type GameUpdate = { export type GameMove = { index: number; } -export type GameMoveResponse = 'success' | 'invalidMove' | 'unknownError'; export interface ClientToServer { enqueue: () => void; dequeue: () => void; debugInfo: () => void; - gameMove: (up: GameMove) => GameMoveResponse; + gameMove: (up: GameMove) => void; + connectedToGame: (gameId: string) => void; }; export interface ServerToClient { forceDisconnect: (reason: string) => void; queueEvent: (msg: 'registered' | 'unregistered') => void; updateInformation: (info: UpdateInfo) => void, - newGame: (gameId: string) => void, + newGame: (initState: GameUpdate) => void, gameBoard: (state: GameUpdate) => void, + gameEnd: () => void; }; export type SSocket = Socket; diff --git a/frontend/src/pages/ttt/ttt.ts b/frontend/src/pages/ttt/ttt.ts index bd31fc1..ccdf213 100644 --- a/frontend/src/pages/ttt/ttt.ts +++ b/frontend/src/pages/ttt/ttt.ts @@ -1,8 +1,9 @@ import { addRoute, type RouteHandlerReturn } from "@app/routing"; import tttPage from "./ttt.html?raw"; -import { showError, showInfo, showSuccess } from "@app/toast"; +import { showError, showInfo, showSuccess, showWarn } from "@app/toast"; import { io } from "socket.io-client"; -import type { CSocket as Socket } from "./socket"; +import type { GameUpdate, CSocket as Socket } from "./socket"; +import { updateUser } from "@app/auth"; declare module 'ft_state' { @@ -22,6 +23,8 @@ export function getSocket(): Socket { return window.__state.tttSock; } +type CurrentGameInfo = GameUpdate & { lastState: GameUpdate['gameState'] | null }; + // Route handler for the Tic-Tac-Toe page. // Instantiates the game logic and binds UI events. async function handleTTT(): Promise { @@ -33,10 +36,17 @@ async function handleTTT(): Promise { if (!app) { return; } + let user = await updateUser(); + if (user === null) + return; + let curGame: CurrentGameInfo | null = null; socket.on('updateInformation', (e) => showInfo(`UpdateInformation: t=${e.totalUser};q=${e.inQueue}`)); socket.on('queueEvent', (e) => showInfo(`QueueEvent: ${e}`)); - socket.on('newGame', (e) => showInfo(`newGame: ${e}`)); + socket.on('newGame', (gameState) => { + showInfo(`newGame: ${gameState.gameId}`) + curGame = { ...gameState, lastState: null }; + }); socket.emit('enqueue'); const cells = app.querySelectorAll(".ttt-grid-cell"); @@ -48,27 +58,59 @@ async function handleTTT(): Promise { }); }; + const makeEnd = (type: 'win' | 'conceded' | 'draw', player: 'X' | 'O') => { + if (type === 'draw') { + showWarn('It\'s a draw !') + } + // if the other player conceded, switch the side so you won + if (type === 'conceded') { + player = player === 'X' ? 'O' : 'X'; + const youWin = (curGame?.playerX === user.id); + if (youWin) + showSuccess('The other player Conceded !'); + else + showError('You Conceded :('); + } + + if (type === 'win') { + const youWin = (curGame?.playerX === user.id); + if (youWin) + showSuccess('You won the game !'); + else + showError('You lost the game :('); + } + }; + + socket.on('gameEnd', () => { + curGame = null; + socket.emit('enqueue'); + showInfo('Game is finished, enqueuing directly') + }) + socket.on('gameBoard', (u) => { + if (curGame === null) { + return showError('Got game State, but no in a game ?'); + } updateUI(u.boardState); if (u.gameState && u.gameState !== "ongoing") { grid?.classList.add("pointer-events-none"); - if (u.gameState === "winX") { - showSuccess("X won !"); - } - if (u.gameState === "winO") { - showSuccess("O won !"); - } - if (u.gameState === "draw") { - showInfo("Draw !"); - } - if (u.gameState === 'concededX' ) - { - showInfo("concededX"); - } - if (u.gameState === 'concededO' ) - { - showInfo("concededO"); + + if (u.gameState !== curGame.lastState) { + curGame.lastState = u.gameState; + switch (u.gameState) { + case 'winX': + case 'winO': + makeEnd('win', u.gameState[3] as 'X' | 'O'); + break; + case 'concededX': + case 'concededO': + makeEnd('conceded', u.gameState[8] as 'X' | 'O'); + break; + case 'draw': + makeEnd('draw', 'X'); + break; + } } } }); diff --git a/src/tic-tac-toe/src/socket.ts b/src/tic-tac-toe/src/socket.ts index 5e21511..b3a91c5 100644 --- a/src/tic-tac-toe/src/socket.ts +++ b/src/tic-tac-toe/src/socket.ts @@ -20,21 +20,22 @@ export type GameUpdate = { export type GameMove = { index: number; } -export type GameMoveResponse = 'success' | 'invalidMove' | 'unknownError'; export interface ClientToServer { enqueue: () => void; dequeue: () => void; debugInfo: () => void; - gameMove: (up: GameMove) => GameMoveResponse; + gameMove: (up: GameMove) => void; + connectedToGame: (gameId: string) => void; }; export interface ServerToClient { forceDisconnect: (reason: string) => void; queueEvent: (msg: 'registered' | 'unregistered') => void; updateInformation: (info: UpdateInfo) => void, - newGame: (gameId: string) => void, + newGame: (initState: GameUpdate) => void, gameBoard: (state: GameUpdate) => void, + gameEnd: () => void; }; export type SSocket = Socket; diff --git a/src/tic-tac-toe/src/state.ts b/src/tic-tac-toe/src/state.ts index 6141ead..24b0d2e 100644 --- a/src/tic-tac-toe/src/state.ts +++ b/src/tic-tac-toe/src/state.ts @@ -1,6 +1,6 @@ import { UserId } from '@shared/database/mixin/user'; import { FastifyInstance } from 'fastify'; -import { GameMove, GameMoveResponse, SSocket } from './socket'; +import { GameMove, SSocket } from './socket'; import { isNullish } from '@shared/utils'; import { newUUID } from '@shared/utils/uuid'; import { GameId } from '@shared/database/mixin/tictactoe'; @@ -48,17 +48,27 @@ export class StateI { this.queue.delete(id2); const gameId = newUUID() as unknown as GameId; - u1.socket.emit('newGame', gameId); - u2.socket.emit('newGame', gameId); + const g = new TTC(u1.userId, u2.userId); + const iState = { + boardState: g.board, + currentPlayer: g.getCurrentState(), + playerX: g.playerX, + playerO: g.playerO, + gameState: g.checkState(), + gameId: gameId, + }; - this.games.set(gameId, new TTC(u1.userId, u2.userId)); + u1.socket.emit('newGame', iState); + u2.socket.emit('newGame', iState); + this.games.set(gameId, g); u1.currentGame = gameId; u2.currentGame = gameId; - this.games.get(gameId)!.gameUpdate = setInterval(() => { + g.gameUpdate = setInterval(() => { this.gameUpdate(gameId, u1.socket); this.gameUpdate(gameId, u2.socket); + if (g.checkState() !== 'ongoing') { this.cleanupGame(gameId, g); } }, 100); } } @@ -102,6 +112,21 @@ export class StateI { this.queue.delete(socket.authUser.id); } + private cleanupGame(gameId: GameId, game: TTC): void { + clearInterval(game.gameUpdate ?? undefined); + this.games.delete(gameId); + 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 { if (!this.users.has(socket.authUser.id)) return; @@ -149,14 +174,14 @@ export class StateI { }); } - private gameMove(socket: SSocket, update: GameMove): GameMoveResponse { + private gameMove(socket: SSocket, update: GameMove) { if (!this.users.has(socket.authUser.id)) return 'unknownError'; const user = this.users.get(socket.authUser.id)!; if (user.currentGame !== null && !this.games.has(user.currentGame)) return 'unknownError'; const game = this.games.get(user.currentGame!)!; - return game.makeMove(socket.authUser.id, update.index); + game.makeMove(socket.authUser.id, update.index); } }