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;
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");
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();

View File

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

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.id = id;
this.ready = false;
this.isPlaying = false;
}
resetTimeout() {

View File

@ -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;

View File

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