const ID = window.crypto.randomUUID(); // Timeout to consider a player disconnected const TIMEOUT = 30_000; let players = {}; let us = null; let currentPlayer = () => Object.values(players).filter((p) => p.isPlaying)[0]; const WAITING = 0; const PRE_GAME = 1; const PLAYING = 2; const POST_GAME = 3; let gameState = WAITING; let socket; let random; let barrier; // Not totally reliable but better than nothing. window.addEventListener("beforeunload", () => { socket.emit("message", Packet.createDisconnect()); }); document.addEventListener("DOMContentLoaded", () => { socket = io(); random = new Random(); barrier = new Barrier(); socket.on("connect", () => { console.log("Connected!"); socket.emit("message", Packet.createAnnounce()); // Create self players[ID] = new Player(ID, name); us = players[ID]; }); socket.on("message", async (data) => { switch (data.type) { case "ANNOUNCE": if (data.author === ID) { return; } playerConnected(data); break; case "DISCONNECT": playerDisconnected(data); break; case "KEEPALIVE": if (data.author === ID) { return; } keepAlive(data); break; case "READY": if (data.author === ID) { return; } await setReady(data); break; case "RANDOM": if (data.author === ID) { return; } await random.processCooperativeRandom(data); break; case "BARRIER": if (data.author === ID) { return; } barrier.resolve(data); break; case "ACT": if (data.author !== currentPlayer().id) { return; } if (!allRegionsClaimed()) { // Claim a region in the pregame. if (currentPlayer().claim(data)) { // Increment to next player. currentPlayer().endTurn(); } } else if (!allReinforcementsPlaced()) { if (currentPlayer().reinforce(data)) { currentPlayer().endTurn(); } } updateDom(); break; } }); // Emit keepalive messages to inform other players we are still here window.setInterval(() => { socket.emit("message", Packet.createKeepAlive()); }, TIMEOUT / 5); }); /** * Process player connect packets: these inform that a new player has joined. * * @param data Packet received */ function playerConnected(data) { // Block players from joining mid-game if (gameState !== WAITING) { return; } // When a new player is seen, all announce to ensure they know all players. if (players[data.author] === undefined) { players[data.author] = new Player(data.author, data.name); socket.emit("message", Packet.createAnnounce()); players[data.author].resetTimeout(); } else { } updateDom(); } function playerDisconnected(data) { console.log("deleting player"); delete players[data.author]; updateDom(); } /** * Process keep-alive packets: these are packets that check players are still online. * * @param data Packet received */ function keepAlive(data) { players[data.author].resetTimeout(); } /** * Process sync packets: update player details like status and name. * * @param data Packet received */ async function setReady(data) { players[data.author].name = data.name; players[data.author].ready = data.ready; updateDom(); if (allPlayersReady()) { await startPregame(); } } function allPlayersReady() { for (let player of Object.values(players)) { if (!player.ready) { return false; } } return true; } async function startPregame() { console.log("all players ready."); gameState = PRE_GAME; let firstPlayerIndex = await random.get(Object.keys(players).length, "first-player"); let firstPlayer = Object.values(players).sort((a, b) => (a.id < b.id ? -1 : 1))[ firstPlayerIndex ]; firstPlayer.isPlaying = true; await barrier.wait(); updateDom(); }