Add barrier type. Transition into playing phase properly.

This commit is contained in:
jude 2023-02-02 11:27:52 +00:00
parent 8afe512062
commit 8d1a7e14e3
10 changed files with 155 additions and 46 deletions

View File

@ -23,3 +23,17 @@
font-size: 2em; font-size: 2em;
color: green; color: green;
} }
.status-span {
display: inline-block;
font-weight: bold;
width: 20px;
}
.ready {
color: green;
}
.not-ready {
color: red;
}

30
static/js/barrier.js Normal file
View File

@ -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();
}
}
}

View File

@ -2,17 +2,36 @@ function updatePlayerDom() {
let list = document.querySelector("#playerList"); let list = document.querySelector("#playerList");
list.replaceChildren(); list.replaceChildren();
let newDom = document.createElement("li"); for (let playerId of Object.keys(players).sort()) {
newDom.textContent = ID + " (you)"; let player = players[playerId];
newDom.style.color = "grey";
list.appendChild(newDom);
for (let playerId of Object.keys(players)) { let statusSpan = document.createElement("div");
if (playerId !== ID) { statusSpan.classList.add("status-span");
let newDom = document.createElement("li"); if (game_state === WAITING) {
newDom.textContent = playerId; if (player.ready) {
list.appendChild(newDom); 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.classList.toggle("active");
ev.target.textContent = nowReady ? "Ready" : "Not ready"; ev.target.textContent = nowReady ? "Ready" : "Not ready";
socket.emit("message", { socket.emit("message", Packet.createSetReady(nowReady));
type: "SYNC",
author: ID, updatePlayerDom();
ready: nowReady,
name: "",
});
if (allPlayersReady()) { if (allPlayersReady()) {
await startPregame(); await startPregame();

View File

@ -14,29 +14,21 @@ let game_state = WAITING;
let socket; let socket;
let random; let random;
let barrier;
// Not totally reliable but better than nothing. // Not totally reliable but better than nothing.
window.addEventListener("beforeunload", () => { window.addEventListener("beforeunload", () => {
socket.emit("message", { socket.emit("message", Packet.createDisconnect());
type: "DISCONNECT",
id: window.crypto.randomUUID(),
author: ID,
name: "",
});
}); });
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
socket = io(); socket = io();
random = new Random(); random = new Random();
barrier = new Barrier();
socket.on("connect", () => { socket.on("connect", () => {
console.log("Connected!"); console.log("Connected!");
socket.emit("message", { socket.emit("message", Packet.createAnnounce());
type: "ANNOUNCE",
id: window.crypto.randomUUID(),
author: ID,
name: "",
});
// Create self // Create self
players[ID] = new Player(ID, name); players[ID] = new Player(ID, name);
us = players[ID]; us = players[ID];
@ -61,23 +53,23 @@ document.addEventListener("DOMContentLoaded", () => {
keepAlive(data); keepAlive(data);
break; break;
case "SYNC": case "READY":
await sync(data); await setReady(data);
break; break;
case "RANDOM": case "RANDOM":
await random.processCooperativeRandom(data); await random.processCooperativeRandom(data);
break; break;
case "BARRIER":
barrier.resolve(data);
break;
} }
}); });
// Emit keepalive messages to inform other players we are still here // Emit keepalive messages to inform other players we are still here
window.setInterval(() => { window.setInterval(() => {
socket.emit("message", { socket.emit("message", Packet.createKeepAlive());
type: "KEEPALIVE",
id: window.crypto.randomUUID(),
author: ID,
});
}, TIMEOUT / 5); }, TIMEOUT / 5);
}); });
@ -95,12 +87,7 @@ function playerConnected(data) {
// When a new player is seen, all announce to ensure they know all players. // When a new player is seen, all announce to ensure they know all players.
if (players[data.author] === undefined) { if (players[data.author] === undefined) {
players[data.author] = new Player(data.author, data.name); players[data.author] = new Player(data.author, data.name);
socket.emit("message", { socket.emit("message", Packet.createAnnounce());
type: "ANNOUNCE",
id: window.crypto.randomUUID(),
author: ID,
name: "",
});
players[data.author].resetTimeout(); players[data.author].resetTimeout();
} else { } else {
} }
@ -128,10 +115,12 @@ function keepAlive(data) {
* *
* @param data Packet received * @param data Packet received
*/ */
async function sync(data) { async function setReady(data) {
players[data.author].name = data.name; players[data.author].name = data.name;
players[data.author].ready = data.ready; players[data.author].ready = data.ready;
updatePlayerDom();
if (allPlayersReady()) { if (allPlayersReady()) {
await startPregame(); await startPregame();
} }
@ -148,11 +137,20 @@ function allPlayersReady() {
} }
async function startPregame() { async function startPregame() {
console.log("All players ready."); console.log("all players ready.");
game_state = PRE_GAME; 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();
} }

35
static/js/packet.js Normal file
View File

@ -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");
}
}

View File

@ -4,6 +4,7 @@ class Player {
this.timeout = null; this.timeout = null;
this.id = id; this.id = id;
this.ready = false; this.ready = false;
this.isPlaying = false;
} }
resetTimeout() { resetTimeout() {

View File

@ -27,7 +27,6 @@ class Random {
let promise; let promise;
await navigator.locks.request(`random-${sessionId}`, () => { await navigator.locks.request(`random-${sessionId}`, () => {
console.log("in lock now");
if (this.sessions[sessionId].finalValue === null) { if (this.sessions[sessionId].finalValue === null) {
let session = this.sessions[sessionId]; let session = this.sessions[sessionId];
let resolver; let resolver;

View File

@ -12,6 +12,8 @@
<script src="{{ url_for('static', filename='js/player.js') }}"></script> <script src="{{ url_for('static', filename='js/player.js') }}"></script>
<script src="{{ url_for('static', filename='js/dom.js') }}"></script> <script src="{{ url_for('static', filename='js/dom.js') }}"></script>
<script src="{{ url_for('static', filename='js/random.js') }}"></script> <script src="{{ url_for('static', filename='js/random.js') }}"></script>
<script src="{{ url_for('static', filename='js/barrier.js') }}"></script>
<script src="{{ url_for('static', filename='js/packet.js') }}"></script>
</head> </head>
<body> <body>
@ -21,6 +23,10 @@
</ul> </ul>
</div> </div>
<div id="turn-title">
<span class="current-player"></span>'s Turn!
</div>
<div id="ready"> <div id="ready">
<button id="ready-button">Not ready</button> <button id="ready-button">Not ready</button>
</div> </div>

Binary file not shown.

View File

@ -236,7 +236,17 @@ RSA blinding can incur a security risk, as by using the same keys to sign and en
\section{Implementation} \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} \bibliography{Dissertation}