From 8d1a7e14e3e4926376f8f40e47aec72536fb6990 Mon Sep 17 00:00:00 2001 From: jude Date: Thu, 2 Feb 2023 11:27:52 +0000 Subject: [PATCH] Add barrier type. Transition into playing phase properly. --- static/css/style.css | 14 +++++++++ static/js/barrier.js | 30 +++++++++++++++++++ static/js/dom.js | 46 +++++++++++++++++++---------- static/js/index.js | 56 +++++++++++++++++------------------- static/js/packet.js | 35 ++++++++++++++++++++++ static/js/player.js | 1 + static/js/random.js | 1 - templates/index.html | 6 ++++ whitepaper/Dissertation.pdf | Bin 258738 -> 259212 bytes whitepaper/Dissertation.tex | 12 +++++++- 10 files changed, 155 insertions(+), 46 deletions(-) create mode 100644 static/js/barrier.js create mode 100644 static/js/packet.js diff --git a/static/css/style.css b/static/css/style.css index 64abaf9..facd455 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -23,3 +23,17 @@ font-size: 2em; color: green; } + +.status-span { + display: inline-block; + font-weight: bold; + width: 20px; +} + +.ready { + color: green; +} + +.not-ready { + color: red; +} diff --git a/static/js/barrier.js b/static/js/barrier.js new file mode 100644 index 0000000..9194cea --- /dev/null +++ b/static/js/barrier.js @@ -0,0 +1,30 @@ +/** + * Typical barrier type. + * + * Block all clients until everyone has hit the barrier. + */ +class Barrier { + constructor() { + let resolver; + this.promise = new Promise((resolve) => { + resolver = resolve; + }); + this.resolver = resolver; + this.hits = new Set(); + } + + wait() { + socket.emit("message", Packet.createBarrierSignal()); + + return this.promise; + } + + resolve(data) { + this.hits.add(data.author); + + if (this.hits.size === Object.keys(players).length - 1) { + this.hits = new Set(); + this.resolver(); + } + } +} diff --git a/static/js/dom.js b/static/js/dom.js index c9edde8..52a8f0e 100644 --- a/static/js/dom.js +++ b/static/js/dom.js @@ -2,17 +2,36 @@ function updatePlayerDom() { let list = document.querySelector("#playerList"); list.replaceChildren(); - let newDom = document.createElement("li"); - newDom.textContent = ID + " (you)"; - newDom.style.color = "grey"; - list.appendChild(newDom); + for (let playerId of Object.keys(players).sort()) { + let player = players[playerId]; - for (let playerId of Object.keys(players)) { - if (playerId !== ID) { - let newDom = document.createElement("li"); - newDom.textContent = playerId; - list.appendChild(newDom); + let statusSpan = document.createElement("div"); + statusSpan.classList.add("status-span"); + if (game_state === WAITING) { + if (player.ready) { + statusSpan.textContent = "R"; + statusSpan.classList.add("ready"); + } else { + statusSpan.textContent = "N"; + statusSpan.classList.add("not-ready"); + } + } else { + if (player.isPlaying) { + statusSpan.textContent = "P"; + } } + + let idSpan = document.createElement("span"); + if (playerId === ID) { + idSpan.textContent = `${playerId} (you)`; + } else { + idSpan.textContent = playerId; + } + + let newDom = document.createElement("li"); + newDom.appendChild(statusSpan); + newDom.appendChild(idSpan); + list.appendChild(newDom); } } @@ -24,12 +43,9 @@ document.addEventListener("DOMContentLoaded", () => { ev.target.classList.toggle("active"); ev.target.textContent = nowReady ? "Ready" : "Not ready"; - socket.emit("message", { - type: "SYNC", - author: ID, - ready: nowReady, - name: "", - }); + socket.emit("message", Packet.createSetReady(nowReady)); + + updatePlayerDom(); if (allPlayersReady()) { await startPregame(); diff --git a/static/js/index.js b/static/js/index.js index 8a4e2bb..be8abac 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -14,29 +14,21 @@ let game_state = WAITING; let socket; let random; +let barrier; // Not totally reliable but better than nothing. window.addEventListener("beforeunload", () => { - socket.emit("message", { - type: "DISCONNECT", - id: window.crypto.randomUUID(), - author: ID, - name: "", - }); + 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", { - type: "ANNOUNCE", - id: window.crypto.randomUUID(), - author: ID, - name: "", - }); + socket.emit("message", Packet.createAnnounce()); // Create self players[ID] = new Player(ID, name); us = players[ID]; @@ -61,23 +53,23 @@ document.addEventListener("DOMContentLoaded", () => { keepAlive(data); break; - case "SYNC": - await sync(data); + case "READY": + await setReady(data); break; case "RANDOM": await random.processCooperativeRandom(data); break; + + case "BARRIER": + barrier.resolve(data); + break; } }); // Emit keepalive messages to inform other players we are still here window.setInterval(() => { - socket.emit("message", { - type: "KEEPALIVE", - id: window.crypto.randomUUID(), - author: ID, - }); + socket.emit("message", Packet.createKeepAlive()); }, TIMEOUT / 5); }); @@ -95,12 +87,7 @@ function playerConnected(data) { // 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", { - type: "ANNOUNCE", - id: window.crypto.randomUUID(), - author: ID, - name: "", - }); + socket.emit("message", Packet.createAnnounce()); players[data.author].resetTimeout(); } else { } @@ -128,10 +115,12 @@ function keepAlive(data) { * * @param data Packet received */ -async function sync(data) { +async function setReady(data) { players[data.author].name = data.name; players[data.author].ready = data.ready; + updatePlayerDom(); + if (allPlayersReady()) { await startPregame(); } @@ -148,11 +137,20 @@ function allPlayersReady() { } async function startPregame() { - console.log("All players ready."); + console.log("all players ready."); game_state = PRE_GAME; - let player1 = await random.get(Object.keys(players).length, "first-player"); + let firstPlayerIndex = await random.get(Object.keys(players).length, "first-player"); - console.log(player1); + let firstPlayer = Object.values(players).sort((a, b) => (a.id < b.id ? -1 : 1))[ + firstPlayerIndex + ]; + + firstPlayer.isPlaying = true; + game_state = PLAYING; + + await barrier.wait(); + + updatePlayerDom(); } diff --git a/static/js/packet.js b/static/js/packet.js new file mode 100644 index 0000000..47642e7 --- /dev/null +++ b/static/js/packet.js @@ -0,0 +1,35 @@ +class Packet { + static _createBase(name) { + return { + type: name, + id: window.crypto.randomUUID(), + author: ID, + }; + } + + static createAnnounce() { + return { + ...this._createBase("ANNOUNCE"), + name: "", + }; + } + + static createDisconnect() { + return this._createBase("DISCONNECT"); + } + + static createKeepAlive() { + return this._createBase("KEEPALIVE"); + } + + static createSetReady(nowReady) { + return { + ...this._createBase("READY"), + ready: nowReady, + }; + } + + static createBarrierSignal() { + return this._createBase("BARRIER"); + } +} diff --git a/static/js/player.js b/static/js/player.js index 75790b9..425fc1d 100644 --- a/static/js/player.js +++ b/static/js/player.js @@ -4,6 +4,7 @@ class Player { this.timeout = null; this.id = id; this.ready = false; + this.isPlaying = false; } resetTimeout() { diff --git a/static/js/random.js b/static/js/random.js index 5312342..f753bd3 100644 --- a/static/js/random.js +++ b/static/js/random.js @@ -27,7 +27,6 @@ class Random { let promise; await navigator.locks.request(`random-${sessionId}`, () => { - console.log("in lock now"); if (this.sessions[sessionId].finalValue === null) { let session = this.sessions[sessionId]; let resolver; diff --git a/templates/index.html b/templates/index.html index 13dbb53..580a066 100644 --- a/templates/index.html +++ b/templates/index.html @@ -12,6 +12,8 @@ + + @@ -21,6 +23,10 @@ +
+ 's Turn! +
+
diff --git a/whitepaper/Dissertation.pdf b/whitepaper/Dissertation.pdf index 20c39106413d276a139b702165706145ac144b11..a98ef64aa5d04483849ccc7258c0bb1ccf4e5eb1 100644 GIT binary patch delta 7086 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