From cf0c9135e1ac4e39720280fd98fed7cd6be78aa6 Mon Sep 17 00:00:00 2001 From: jude Date: Fri, 3 Mar 2023 17:34:15 +0000 Subject: [PATCH] Mid refactor to stop polluting the namespace so much --- static/css/style.css | 2 +- static/js/index.js | 199 ------------------ static/js/modules/crypto/main.js | 2 + static/js/modules/crypto/math.js | 14 ++ static/js/{ => modules/crypto}/paillier.js | 17 +- .../js/{ => modules/crypto}/random_primes.js | 23 +- static/js/{ => modules/interface}/barrier.js | 5 +- static/js/{ => modules/interface}/dom.js | 23 +- static/js/modules/interface/game.js | 82 ++++++++ static/js/modules/interface/main.js | 135 ++++++++++++ static/js/{ => modules/interface}/map.js | 89 ++++---- static/js/{ => modules/interface}/packet.js | 7 +- static/js/{ => modules/interface}/player.js | 26 ++- static/js/{ => modules/interface}/random.js | 4 +- templates/index.html | 13 +- 15 files changed, 349 insertions(+), 292 deletions(-) delete mode 100644 static/js/index.js create mode 100644 static/js/modules/crypto/main.js create mode 100644 static/js/modules/crypto/math.js rename static/js/{ => modules/crypto}/paillier.js (73%) rename static/js/{ => modules/crypto}/random_primes.js (97%) rename static/js/{ => modules/interface}/barrier.js (85%) rename static/js/{ => modules/interface}/dom.js (95%) create mode 100644 static/js/modules/interface/game.js create mode 100644 static/js/modules/interface/main.js rename static/js/{ => modules/interface}/map.js (56%) rename static/js/{ => modules/interface}/packet.js (91%) rename static/js/{ => modules/interface}/player.js (93%) rename static/js/{ => modules/interface}/random.js (98%) diff --git a/static/css/style.css b/static/css/style.css index f9663dc..76f95fd 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,4 +1,4 @@ -#players { +#players-div { position: absolute; right: 0; top: 0; diff --git a/static/js/index.js b/static/js/index.js deleted file mode 100644 index 3a554d0..0000000 --- a/static/js/index.js +++ /dev/null @@ -1,199 +0,0 @@ -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) { - if (data.action === "DEFENSE") { - await players[data.author].setDefense(data.amount); - } - - return; - } - - if (gameState === PRE_GAME) { - 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(); - } - } - - if (allReinforcementsPlaced()) { - gameState = PLAYING; - updateDom(); - } - } else { - if (await currentPlayer().act(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(); -} diff --git a/static/js/modules/crypto/main.js b/static/js/modules/crypto/main.js new file mode 100644 index 0000000..a468613 --- /dev/null +++ b/static/js/modules/crypto/main.js @@ -0,0 +1,2 @@ +import { generate_keypair } from "./paillier.js"; +export { generate_keypair }; diff --git a/static/js/modules/crypto/math.js b/static/js/modules/crypto/math.js new file mode 100644 index 0000000..55eeb48 --- /dev/null +++ b/static/js/modules/crypto/math.js @@ -0,0 +1,14 @@ +export function mod_exp(a, b, n) { + let res = 1n; + + while (b > 0n) { + if (b % 2n === 1n) { + res = (res * a) % n; + } + + b >>= 1n; + a = (a * a) % n; + } + + return res; +} diff --git a/static/js/paillier.js b/static/js/modules/crypto/paillier.js similarity index 73% rename from static/js/paillier.js rename to static/js/modules/crypto/paillier.js index 5d65c74..4d0e12e 100644 --- a/static/js/paillier.js +++ b/static/js/modules/crypto/paillier.js @@ -1,3 +1,6 @@ +import { random2048, generate_prime } from "./random_primes.js"; +import { mod_exp } from "./math.js"; + let p, q, pubKey, privKey; class PubKey { @@ -18,7 +21,7 @@ class PubKey { // Compute g^m by binomial theorem. let gm = (1n + this.n * m) % this.n ** 2n; // Compute g^m r^n from crt - return (gm * fastModularExponentiation(r, this.n, this.n ** 2n)) % this.n ** 2n; + return (gm * mod_exp(r, this.n, this.n ** 2n)) % this.n ** 2n; } } @@ -26,19 +29,17 @@ class PrivKey { constructor(p, q) { this.n = p * q; this.lambda = (p - 1n) * (q - 1n); - this.mu = fastModularExponentiation(this.lambda, this.lambda - 1n, this.n); + this.mu = mod_exp(this.lambda, this.lambda - 1n, this.n); } decrypt(c) { return ( - (((fastModularExponentiation(c, this.lambda, this.n ** 2n) - 1n) / this.n) * - this.mu) % - this.n + (((mod_exp(c, this.lambda, this.n ** 2n) - 1n) / this.n) * this.mu) % this.n ); } } -document.addEventListener("DOMContentLoaded", () => { +export function generate_keypair() { if (window.sessionStorage.getItem("p") === null) { p = generate_prime(); window.sessionStorage.setItem("p", p); @@ -55,4 +56,6 @@ document.addEventListener("DOMContentLoaded", () => { pubKey = new PubKey(p, q); privKey = new PrivKey(p, q); -}); + + return { pubKey, privKey }; +} diff --git a/static/js/random_primes.js b/static/js/modules/crypto/random_primes.js similarity index 97% rename from static/js/random_primes.js rename to static/js/modules/crypto/random_primes.js index 0e8a2ff..d2bbe02 100644 --- a/static/js/random_primes.js +++ b/static/js/modules/crypto/random_primes.js @@ -1,4 +1,6 @@ -function random2048() { +import { mod_exp } from "./math.js"; + +export function random2048() { const byteArray = new BigUint64Array(32); window.crypto.getRandomValues(byteArray); let intRepr = 0n; @@ -55,7 +57,7 @@ function miller_rabin(n, k) { for (; k > 0; k--) { let a = random2048(); - let x = fastModularExponentiation(a, d, n); + let x = mod_exp(a, d, n); if (x === 1n || x === n - 1n) { continue; @@ -77,7 +79,7 @@ function miller_rabin(n, k) { return true; } -function generate_prime() { +export function generate_prime() { while (true) { let n = generate_bigint(); if (small_prime_test(n) && miller_rabin(n, 40)) { @@ -86,21 +88,6 @@ function generate_prime() { } } -function fastModularExponentiation(a, b, n) { - let res = 1n; - - while (b > 0n) { - if (b % 2n === 1n) { - res = (res * a) % n; - } - - b >>= 1n; - a = (a * a) % n; - } - - return res; -} - const SMALL_PRIMES = [ 2n, 3n, diff --git a/static/js/barrier.js b/static/js/modules/interface/barrier.js similarity index 85% rename from static/js/barrier.js rename to static/js/modules/interface/barrier.js index 9194cea..e0b8fee 100644 --- a/static/js/barrier.js +++ b/static/js/modules/interface/barrier.js @@ -1,9 +1,12 @@ +import { socket, players } from "./main.js"; +import { Packet } from "./packet.js"; + /** * Typical barrier type. * * Block all clients until everyone has hit the barrier. */ -class Barrier { +export class Barrier { constructor() { let resolver; this.promise = new Promise((resolve) => { diff --git a/static/js/dom.js b/static/js/modules/interface/dom.js similarity index 95% rename from static/js/dom.js rename to static/js/modules/interface/dom.js index f3f062b..a4ae5e7 100644 --- a/static/js/dom.js +++ b/static/js/modules/interface/dom.js @@ -1,4 +1,19 @@ -function unlockMapDom() { +import { + gameState, + WAITING, + PRE_GAME, + PLAYING, + us, + socket, + players, + ID, + allPlayersReady, + startPregame, +} from "./main.js"; +import { Region } from "./map.js"; +import { Packet } from "./packet.js"; + +export function unlockMapDom() { Object.values(REGIONS).forEach((region) => { if (!allRegionsClaimed() && region.owner === null) { document @@ -12,11 +27,11 @@ function unlockMapDom() { }); } -function lockMapDom() { +export function lockMapDom() { document.querySelectorAll(".actions").forEach((e) => e.classList.add("hidden")); } -function updateDom() { +export function updateDom() { if (gameState !== WAITING) { document.querySelector("#ready-button").style.display = "none"; } @@ -49,7 +64,7 @@ function updateMapDom() { } } - for (let region of Object.values(REGIONS)) { + for (let region of Region.getAllRegions()) { const element = document.querySelector(`.node[data-name=${region.name}]`); element.querySelector(".strength").textContent = region.strength || ""; element.style.backgroundColor = diff --git a/static/js/modules/interface/game.js b/static/js/modules/interface/game.js new file mode 100644 index 0000000..3b8af95 --- /dev/null +++ b/static/js/modules/interface/game.js @@ -0,0 +1,82 @@ +import { Player } from "./player"; + +const WAITING = 0; +const PRE_GAME = 1; +const PLAYING = 2; + +export class Game { + constructor() { + this.us = null; + this.players = {}; + this.state = WAITING; + } + + isWaiting() { + return this.state === WAITING; + } + + isPregame() { + return this.state === PRE_GAME; + } + + isPlaying() { + return this.state === PLAYING; + } + + incrementState() { + this.state += 1; + } + + currentPlayer() { + return Object.values(this.players).filter((p) => p.isPlaying)[0]; + } + + addPlayer(id, name, is_us) { + let is_new = this.players[id] === undefined; + + if (this.isWaiting()) { + this.players[id] = new Player(id, name, is_us); + if (is_us === true) { + this.us = this.players[id]; + } + } + + if (is_new) { + const event = new CustomEvent("addPlayer"); + document.dispatchEvent(event); + } + + return is_new; + } + + removePlayer(id) { + if (this.players[id] !== undefined) { + const event = new CustomEvent("removePlayer"); + document.dispatchEvent(event); + delete this.players[id]; + } + } + + keepAlive(id) { + if (id !== this.us.id) { + this.players[id].resetTimeout(this); + } + } + + setReady(id, ready) { + this.players[id].readyState = ready; + + if (this._allPlayersReady()) { + this.incrementState(); + } + } + + _allPlayersReady() { + for (let player of Object.values(this.players)) { + if (!player.readyState) { + return false; + } + } + return true; + } +} diff --git a/static/js/modules/interface/main.js b/static/js/modules/interface/main.js new file mode 100644 index 0000000..156fca8 --- /dev/null +++ b/static/js/modules/interface/main.js @@ -0,0 +1,135 @@ +import { generate_keypair } from "../crypto/main.js"; +import { Random } from "./random.js"; +import { Barrier } from "./barrier.js"; +import { Packet } from "./packet.js"; +import { updateDom } from "./dom.js"; + +export const ID = window.crypto.randomUUID(); +export let us = null; + +export const game = new Game(); + +export let socket; +let random; +let barrier; +let keys; + +// 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(); + keys = generate_keypair(); + + socket.on("connect", () => { + window.console.log("Connected!"); + socket.emit("message", Packet.createAnnounce()); + game.addPlayer(ID, name, true); + }); + + socket.on("message", async (data) => { + // todo validate signature + + switch (data.type) { + case "RANDOM": + if (data.author === ID) { + return; + } + await random.processCooperativeRandom(data); + break; + + case "BARRIER": + if (data.author === ID) { + return; + } + barrier.resolve(data); + break; + + default: + const event = new CustomEvent(data.type, { detail: data }); + document.dispatchEvent(event); + break; + } + }); +}); + +/** + * Process player connect packets: these inform that a new player has joined. + * + * @param data Packet received + */ +document.addEventListener("ANNOUNCE", (data) => { + if (data.author === ID) return; + + let is_new = game.addPlayer(data.author, data.name, false); + + // When a new player is seen, all announce to ensure they know all players. + if (is_new) { + socket.emit("message", Packet.createAnnounce()); + } +}); + +document.addEventListener("DISCONNECT", (data) => { + game.removePlayer(data.author); +}); + +/** + * Process keep-alive packets: these are packets that check players are still online. + * + * @param data Packet received + */ +document.addEventListener("KEEPALIVE", (data) => { + game.keepAlive(data.author); +}); + +document.addEventListener("ACT", async (data) => { + if (data.author !== game.currentPlayer().id) { + if (data.action === "DEFENSE") { + await game.players[data.author].setDefense(data.amount); + } + + return; + } + + if (game.isWaiting()) { + game.setReady(data.author, data.ready); + } else if (game.isPregame()) { + if (!Region.allRegionsClaimed()) { + // Claim a region in the pregame. + if (game.currentPlayer().claim(data)) { + // Increment to next player. + game.currentPlayer().endTurn(); + } + } else if (!Region.allReinforcementsPlaced()) { + if (game.currentPlayer().reinforce(data)) { + game.currentPlayer().endTurn(); + } + } + + if (Region.allReinforcementsPlaced()) { + game.incrementState(); + } + } else { + if (await game.currentPlayer().act(data)) { + game.currentPlayer().endTurn(); + } + } +}); + +export async function startPregame() { + 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(); +} diff --git a/static/js/map.js b/static/js/modules/interface/map.js similarity index 56% rename from static/js/map.js rename to static/js/modules/interface/map.js index 2f7bf2f..0c67e15 100644 --- a/static/js/map.js +++ b/static/js/modules/interface/map.js @@ -1,3 +1,10 @@ +let allPlaced = false; + +// In standard Risk, this is 5 +const _REINFORCEMENT_MULTIPLIER = 1; + +export const REGIONS = {}; + class Continent { constructor(name) { this.name = name; @@ -5,9 +12,7 @@ class Continent { } } -const REGIONS = {}; - -class Region { +export class Region { constructor(name, continent) { this.name = name; this.owner = null; @@ -23,10 +28,50 @@ class Region { region2.neighbours.add(region1); } + static allRegionsClaimed() { + return ( + Object.values(REGIONS).find((region) => region.owner === null) === undefined + ); + } + + static reinforcementsRemaining() { + if (allPlaced) { + return 0; + } else { + let totalStrength = Object.values(REGIONS) + .filter((region) => region.owner === us) + .reduce((counter, region) => counter + region.strength, 0); + let numPlayers = Object.values(players).length; + + return _REINFORCEMENT_MULTIPLIER * (10 - numPlayers) - totalStrength; + } + } + + static allReinforcementsPlaced() { + if (allPlaced) { + return true; + } else { + let totalStrength = Object.values(REGIONS).reduce( + (counter, region) => counter + region.strength, + 0 + ); + let numPlayers = Object.values(players).length; + + allPlaced = + totalStrength >= + numPlayers * _REINFORCEMENT_MULTIPLIER * (10 - numPlayers); + return allPlaced; + } + } + static getRegion(name) { return REGIONS[name]; } + static getAllRegions() { + return Object.values(REGIONS); + } + claim(player) { this.owner = player; this.strength = 1; @@ -66,41 +111,3 @@ Region.setNeighbours(F, G); Region.setNeighbours(G, H); Region.setNeighbours(G, I); Region.setNeighbours(H, I); - -function allRegionsClaimed() { - return Object.values(REGIONS).find((region) => region.owner === null) === undefined; -} - -let allPlaced = false; - -// In standard Risk, this is 5 -const _REINFORCEMENT_MULTIPLIER = 1; - -function reinforcementsRemaining() { - if (allPlaced) { - return 0; - } else { - let totalStrength = Object.values(REGIONS) - .filter((region) => region.owner === us) - .reduce((counter, region) => counter + region.strength, 0); - let numPlayers = Object.values(players).length; - - return _REINFORCEMENT_MULTIPLIER * (10 - numPlayers) - totalStrength; - } -} - -function allReinforcementsPlaced() { - if (allPlaced) { - return true; - } else { - let totalStrength = Object.values(REGIONS).reduce( - (counter, region) => counter + region.strength, - 0 - ); - let numPlayers = Object.values(players).length; - - allPlaced = - totalStrength >= numPlayers * _REINFORCEMENT_MULTIPLIER * (10 - numPlayers); - return allPlaced; - } -} diff --git a/static/js/packet.js b/static/js/modules/interface/packet.js similarity index 91% rename from static/js/packet.js rename to static/js/modules/interface/packet.js index cdc172e..73791a8 100644 --- a/static/js/packet.js +++ b/static/js/modules/interface/packet.js @@ -1,4 +1,6 @@ -class Packet { +import { ID } from "./main.js"; + +export class Packet { static _createBase(name) { return { type: name, @@ -24,7 +26,8 @@ class Packet { static createSetReady(nowReady) { return { - ...this._createBase("READY"), + ...this._createBase("ACT"), + action: "READY", ready: nowReady, }; } diff --git a/static/js/player.js b/static/js/modules/interface/player.js similarity index 93% rename from static/js/player.js rename to static/js/modules/interface/player.js index 7b45e41..666343e 100644 --- a/static/js/player.js +++ b/static/js/modules/interface/player.js @@ -1,12 +1,18 @@ +import { Packet } from "./packet.js"; +import { socket } from "./main.js"; +import { updateDom } from "./dom.js"; + +// Timeout to consider a player disconnected +const TIMEOUT = 30_000; + const PHASE_REINFORCE = 1; const PHASE_ATTACK = 2; const PHASE_FORTIFY = 3; let totalDice = 0; -class Player { - constructor(id, name) { - this.name = name; +export class Player { + constructor(id, local) { this.timeout = null; this.id = id; this.ready = false; @@ -23,18 +29,22 @@ class Player { this.defenderAmount = null; this.resetColor(); + + if (local) { + // Emit keepalive messages to inform other players we are still here + window.setInterval(() => { + socket.emit("message", Packet.createKeepAlive()); + }, TIMEOUT / 5); + } } - resetTimeout() { + resetTimeout(game) { if (this.timeout !== null) { window.clearTimeout(this.timeout); } this.timeout = window.setTimeout(() => { - if (players[this.id] !== undefined) { - delete players[this.id]; - } - updateDom(); + game.removePlayer(this); }, TIMEOUT); } diff --git a/static/js/random.js b/static/js/modules/interface/random.js similarity index 98% rename from static/js/random.js rename to static/js/modules/interface/random.js index 38ea73f..4d2013b 100644 --- a/static/js/random.js +++ b/static/js/modules/interface/random.js @@ -1,3 +1,5 @@ +import { socket, ID, players } from "./main.js"; + class RandomSession { constructor(range) { this.range = range; @@ -15,7 +17,7 @@ class RandomSession { } } -class Random { +export class Random { constructor() { this.sessions = {}; } diff --git a/templates/index.html b/templates/index.html index 14c2435..80a6d9b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,15 +7,8 @@ - - - - - - - - - + + -
+
Players