Initial commit

This commit is contained in:
Alessandro Petitcollin 2026-01-10 18:12:04 +01:00 committed by Maix0
parent 19cae8ae63
commit 830d733f1b

View file

@ -1,39 +1,39 @@
import "./ttt.css" import "./ttt.css"
import { addRoute, type RouteHandlerReturn } from "@app/routing"; import {addRoute, type RouteHandlerReturn} from "@app/routing";
import tttPage from "./ttt.html?raw"; import tttPage from "./ttt.html?raw";
import { showError, showInfo, showSuccess, showWarn } from "@app/toast"; import {showError, showInfo, showSuccess, showWarn} from "@app/toast";
import { io } from "socket.io-client"; import {io} from "socket.io-client";
import type { GameUpdate, CSocket as Socket } from "./socket"; import type {CSocket as Socket, GameUpdate} from "./socket";
import { updateUser } from "@app/auth"; import {updateUser} from "@app/auth";
import client from "@app/api"; import client from "@app/api";
declare module 'ft_state' { declare module 'ft_state' {
interface State { interface State {
tttSock?: Socket; tttSock?: Socket;
tttkeepAliveInterval?: ReturnType<typeof setInterval>; tttkeepAliveInterval?: ReturnType<typeof setInterval>;
} }
} }
enum QueueState { enum QueueState {
InQueue = "In Queue", InQueue = "In Queue",
InGame = "In Game", InGame = "In Game",
Idle = "Join Queue", Idle = "Join Queue",
}; }
document.addEventListener("ft:pageChange", () => { document.addEventListener("ft:pageChange", () => {
if (window.__state.tttSock !== undefined) window.__state.tttSock.close(); if (window.__state.tttSock !== undefined) window.__state.tttSock.close();
if (window.__state.tttkeepAliveInterval !== undefined) clearInterval(window.__state.tttkeepAliveInterval); if (window.__state.tttkeepAliveInterval !== undefined) clearInterval(window.__state.tttkeepAliveInterval);
window.__state.tttSock = undefined; window.__state.tttSock = undefined;
window.__state.tttkeepAliveInterval = undefined; window.__state.tttkeepAliveInterval = undefined;
}); });
export function getSocket(): Socket { export function getSocket(): Socket {
if (window.__state.tttSock === undefined) if (window.__state.tttSock === undefined)
window.__state.tttSock = io(window.location.host, { path: "/api/ttt/socket.io/" }) as any as Socket; window.__state.tttSock = io(window.location.host, {path: "/api/ttt/socket.io/"}) as any as Socket;
if (window.__state.tttkeepAliveInterval === undefined) if (window.__state.tttkeepAliveInterval === undefined)
window.__state.tttkeepAliveInterval = setInterval(() => window.__state.tttSock?.emit('keepalive'), 100); window.__state.tttkeepAliveInterval = setInterval(() => window.__state.tttSock?.emit('keepalive'), 100);
return window.__state.tttSock; return window.__state.tttSock;
} }
type CurrentGameInfo = GameUpdate & { lastState: GameUpdate['gameState'] | null }; type CurrentGameInfo = GameUpdate & { lastState: GameUpdate['gameState'] | null };
@ -41,164 +41,164 @@ type CurrentGameInfo = GameUpdate & { lastState: GameUpdate['gameState'] | null
// Route handler for the Tic-Tac-Toe page. // Route handler for the Tic-Tac-Toe page.
// Instantiates the game logic and binds UI events. // Instantiates the game logic and binds UI events.
async function handleTTT(): Promise<RouteHandlerReturn> { async function handleTTT(): Promise<RouteHandlerReturn> {
const socket: Socket = getSocket(); const socket: Socket = getSocket();
void socket; void socket;
return { return {
html: tttPage, postInsert: async (app) => { html: tttPage, postInsert: async (app) => {
if (!app) { if (!app) {
return; return;
} }
let user = await updateUser(); let user = await updateUser();
if (user === null) if (user === null)
return; return;
const userXString = document.getElementById("playerX-name"); const userXString = document.getElementById("playerX-name");
const userOString = document.getElementById("playerO-name"); const userOString = document.getElementById("playerO-name");
const currentPlayerIndicator = document.getElementById("currentPlayer"); const currentPlayerIndicator = document.getElementById("currentPlayer");
const joinQueueBtn = document.getElementById("JoinQueueBtn"); const joinQueueBtn = document.getElementById("JoinQueueBtn");
const currentPlayerTimer = document.getElementById("currentPlayerTimer") const currentPlayerTimer = document.getElementById("currentPlayerTimer")
if (!userXString || !userOString || !currentPlayerIndicator || !joinQueueBtn || !currentPlayerTimer) { if (!userXString || !userOString || !currentPlayerIndicator || !joinQueueBtn || !currentPlayerTimer) {
return showError('fatal error'); return showError('fatal error');
} }
joinQueueBtn.addEventListener("click", () => {
console.log('===== JOIN QUEUE BUTTON PRESSED =====');
if (joinQueueBtn.innerText !== QueueState.Idle) {
console.log("== Entering in first if ==");
if (joinQueueBtn.innerText === QueueState.InQueue) {
console.log("== Entering in second if ==");
socket.emit("dequeue");
joinQueueBtn.innerText = QueueState.Idle;
}
return;
}
joinQueueBtn.innerText = QueueState.InQueue;
socket.emit("enqueue");
});
let curGame: CurrentGameInfo | null = null; joinQueueBtn.addEventListener("click", () => {
let curGameX: {id: string, name: string} | null = null; console.log('===== JOIN QUEUE BUTTON PRESSED =====');
let curGameO: {id: string, name: string} | null = null; if (joinQueueBtn.innerText !== QueueState.Idle) {
console.log("== Entering in first if ==");
socket.on('updateInformation', (e) => showInfo(`UpdateInformation: t=${e.totalUser};q=${e.inQueue}`)); if (joinQueueBtn.innerText === QueueState.InQueue) {
socket.on('queueEvent', (e) => showInfo(`QueueEvent: ${e}`)); console.log("== Entering in second if ==");
socket.on('newGame', async (gameState) => { socket.emit("dequeue");
showInfo(`newGame: ${gameState.gameId}`) joinQueueBtn.innerText = QueueState.Idle;
}
return;
}
joinQueueBtn.innerText = QueueState.InQueue;
socket.emit("enqueue");
});
currentPlayerTimer.innerText = ""; let curGame: CurrentGameInfo | null = null;
joinQueueBtn.innerText = QueueState.InGame; let curGameX: { id: string, name: string } | null = null;
curGame = { ...gameState, lastState: null }; let curGameO: { id: string, name: string } | null = null;
let resX = await client.getUser({user: curGame.playerX});
curGameX = {id: curGame.playerX, name: 'Player X'};
if (resX.kind === 'success')
curGameX.name = resX.payload.name;
else
showError(`Unable to get player information: ${resX.msg}`);
let resO = await client.getUser({user: curGame.playerO});
curGameO = {id: curGame.playerO, name: 'Player O'};
if (resO.kind === 'success')
curGameO.name = resO.payload.name;
else
showError(`Unable to get player information: ${resO.msg}`);
if (user.id === curGameO.id) { socket.on('updateInformation', (e) => showInfo(`UpdateInformation: t=${e.totalUser};q=${e.inQueue}`));
userOString.classList.add('text-red-800'); socket.on('queueEvent', (e) => showInfo(`QueueEvent: ${e}`));
userOString.classList.remove('text-gray-800'); socket.on('newGame', async (gameState) => {
userXString.classList.remove('text-red-800'); showInfo(`newGame: ${gameState.gameId}`)
userXString.classList.add('text-gray-800');
}
else if (user.id === curGameX.id) {
userXString.classList.add('text-red-800');
userXString.classList.remove('text-gray-800');
userOString.classList.remove('text-red-800');
userOString.classList.add('text-gray-800');
}
userXString.innerText = curGameX.name;
userOString.innerText = curGameO.name;
});
const cells = app.querySelectorAll<HTMLDivElement>(".ttt-cell"); currentPlayerTimer.innerText = "";
joinQueueBtn.innerText = QueueState.InGame;
curGame = {...gameState, lastState: null};
const updateUI = (boardState: (string | null)[]) => { let resX = await client.getUser({user: curGame.playerX});
boardState.forEach((state, idx) => {
cells[idx].innerText = state || " ";
});
};
const makeEnd = (type: 'win' | 'conceded' | 'draw', player: 'X' | 'O') => { curGameX = {id: curGame.playerX, name: 'Player X'};
if (type === 'draw') { if (resX.kind === 'success')
showWarn('It\'s a draw !') curGameX.name = resX.payload.name;
} else
showError(`Unable to get player information: ${resX.msg}`);
let resO = await client.getUser({user: curGame.playerO});
if (type === 'win') { curGameO = {id: curGame.playerO, name: 'Player O'};
let youWin: boolean; if (resO.kind === 'success')
switch (player) { curGameO.name = resO.payload.name;
case 'X': else
youWin = (curGame?.playerX === user.id); showError(`Unable to get player information: ${resO.msg}`);
break;
case 'O':
youWin = (curGame?.playerO === user.id);
break;
default:
return;
}
if (youWin)
showSuccess('You won the game !');
else
showError('You lost the game :(');
}
};
socket.on('gameEnd', () => { if (user.id === curGameO.id) {
curGame = null; userOString.classList.add('text-red-800');
currentPlayerTimer.innerText = "Waiting for match..."; userOString.classList.remove('text-gray-800');
joinQueueBtn.innerText = QueueState.Idle; userXString.classList.remove('text-red-800');
}) userXString.classList.add('text-gray-800');
} else if (user.id === curGameX.id) {
userXString.classList.add('text-red-800');
userXString.classList.remove('text-gray-800');
userOString.classList.remove('text-red-800');
userOString.classList.add('text-gray-800');
}
userXString.innerText = curGameX.name;
userOString.innerText = curGameO.name;
});
socket.on('gameBoard', (u) => { const cells = app.querySelectorAll<HTMLDivElement>(".ttt-cell");
if (curGame === null) {
return showError('Got game State, but no in a game ?');
}
curGame = {...u, lastState: curGame.lastState}; const updateUI = (boardState: (string | null)[]) => {
boardState.forEach((state, idx) => {
cells[idx].innerText = state || " ";
});
};
if (curGame.currentPlayer === 'X') const makeEnd = (type: 'win' | 'conceded' | 'draw', player: 'X' | 'O') => {
{ // TODO: Enhance the draw notification
currentPlayerIndicator.innerText = "<"; if (type === 'draw') {
} else if (curGame.currentPlayer === 'O') { showWarn('It\'s a draw !')
currentPlayerIndicator.innerText = ">"; }
}
updateUI(u.boardState); if (type === 'win') {
let youWin: boolean;
switch (player) {
case 'X':
youWin = (curGame?.playerX === user.id);
break;
case 'O':
youWin = (curGame?.playerO === user.id);
break;
default:
return;
}
// TODO: Enhance the win/loss notification
if (youWin)
showSuccess('You won the game !');
else
showError('You lost the game :(');
}
};
if (u.gameState && u.gameState !== "ongoing") { socket.on('gameEnd', () => {
if (u.gameState !== curGame.lastState) { curGame = null;
curGame.lastState = u.gameState; currentPlayerTimer.innerText = "Waiting for match...";
switch (u.gameState) { joinQueueBtn.innerText = QueueState.Idle;
case 'winX': })
case 'winO':
makeEnd('win', u.gameState[3] as 'X' | 'O');
break;
default:
makeEnd('draw', 'X');
break;
}
}
}
});
cells?.forEach(function(c, idx) { socket.on('gameBoard', (u) => {
c.addEventListener("click", () => { if (curGame === null) {
if (socket) { return showError('Got game State, but no in a game ?');
socket.emit("gameMove", { index: idx }); }
}
}); curGame = {...u, lastState: curGame.lastState};
});
}, if (curGame.currentPlayer === 'X') {
}; currentPlayerIndicator.innerText = "<";
} else if (curGame.currentPlayer === 'O') {
currentPlayerIndicator.innerText = ">";
}
updateUI(u.boardState);
if (u.gameState && u.gameState !== "ongoing") {
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;
default:
makeEnd('draw', 'X');
break;
}
}
}
});
cells?.forEach(function (c, idx) {
c.addEventListener("click", () => {
if (socket) {
socket.emit("gameMove", {index: idx});
}
});
});
},
};
} }
addRoute("/ttt", handleTTT); addRoute("/ttt", handleTTT);