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 20c3910..a98ef64 100644 Binary files a/whitepaper/Dissertation.pdf and b/whitepaper/Dissertation.pdf differ diff --git a/whitepaper/Dissertation.tex b/whitepaper/Dissertation.tex index 1cf2923..9fc45d0 100644 --- a/whitepaper/Dissertation.tex +++ b/whitepaper/Dissertation.tex @@ -236,7 +236,17 @@ RSA blinding can incur a security risk, as by using the same keys to sign and en \section{Implementation} -The implementation provided uses WebSockets as the communication primitive. Whilst this is therefore a centralised implementation, no verification occurs in the server code, which instead simply "echoes" messages received to all connected clients. +The implementation provided uses WebSockets as the communication primitive. This is therefore a centralised implementation. However, no verification occurs in the server code, which instead simply "echoes" messages received to all connected clients. + +Despite this approach being centralised, it does emulate a fully peer-to-peer environment, and has notable benefits: \begin{itemize} + \item It is faster to develop, use, and test than using a physical system such as mail; + \item There is no need for hole-punching or port-forwarding; + \item WebSockets are highly flexible in how data is structured and interpreted. +\end{itemize} + +In particular, the final point allows for the use of purely JSON messages, which are readily parsed and processed by the client-side JavaScript. + + \bibliography{Dissertation}