Mid refactor to stop polluting the namespace so much

This commit is contained in:
jude 2023-03-03 17:34:15 +00:00
parent 7d9531f0d9
commit cf0c9135e1
15 changed files with 349 additions and 292 deletions

View File

@ -1,4 +1,4 @@
#players { #players-div {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;

View File

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

View File

@ -0,0 +1,2 @@
import { generate_keypair } from "./paillier.js";
export { generate_keypair };

View File

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

View File

@ -1,3 +1,6 @@
import { random2048, generate_prime } from "./random_primes.js";
import { mod_exp } from "./math.js";
let p, q, pubKey, privKey; let p, q, pubKey, privKey;
class PubKey { class PubKey {
@ -18,7 +21,7 @@ class PubKey {
// Compute g^m by binomial theorem. // Compute g^m by binomial theorem.
let gm = (1n + this.n * m) % this.n ** 2n; let gm = (1n + this.n * m) % this.n ** 2n;
// Compute g^m r^n from crt // 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) { constructor(p, q) {
this.n = p * q; this.n = p * q;
this.lambda = (p - 1n) * (q - 1n); 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) { decrypt(c) {
return ( return (
(((fastModularExponentiation(c, this.lambda, this.n ** 2n) - 1n) / this.n) * (((mod_exp(c, this.lambda, this.n ** 2n) - 1n) / this.n) * this.mu) % this.n
this.mu) %
this.n
); );
} }
} }
document.addEventListener("DOMContentLoaded", () => { export function generate_keypair() {
if (window.sessionStorage.getItem("p") === null) { if (window.sessionStorage.getItem("p") === null) {
p = generate_prime(); p = generate_prime();
window.sessionStorage.setItem("p", p); window.sessionStorage.setItem("p", p);
@ -55,4 +56,6 @@ document.addEventListener("DOMContentLoaded", () => {
pubKey = new PubKey(p, q); pubKey = new PubKey(p, q);
privKey = new PrivKey(p, q); privKey = new PrivKey(p, q);
});
return { pubKey, privKey };
}

View File

@ -1,4 +1,6 @@
function random2048() { import { mod_exp } from "./math.js";
export function random2048() {
const byteArray = new BigUint64Array(32); const byteArray = new BigUint64Array(32);
window.crypto.getRandomValues(byteArray); window.crypto.getRandomValues(byteArray);
let intRepr = 0n; let intRepr = 0n;
@ -55,7 +57,7 @@ function miller_rabin(n, k) {
for (; k > 0; k--) { for (; k > 0; k--) {
let a = random2048(); let a = random2048();
let x = fastModularExponentiation(a, d, n); let x = mod_exp(a, d, n);
if (x === 1n || x === n - 1n) { if (x === 1n || x === n - 1n) {
continue; continue;
@ -77,7 +79,7 @@ function miller_rabin(n, k) {
return true; return true;
} }
function generate_prime() { export function generate_prime() {
while (true) { while (true) {
let n = generate_bigint(); let n = generate_bigint();
if (small_prime_test(n) && miller_rabin(n, 40)) { 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 = [ const SMALL_PRIMES = [
2n, 2n,
3n, 3n,

View File

@ -1,9 +1,12 @@
import { socket, players } from "./main.js";
import { Packet } from "./packet.js";
/** /**
* Typical barrier type. * Typical barrier type.
* *
* Block all clients until everyone has hit the barrier. * Block all clients until everyone has hit the barrier.
*/ */
class Barrier { export class Barrier {
constructor() { constructor() {
let resolver; let resolver;
this.promise = new Promise((resolve) => { this.promise = new Promise((resolve) => {

View File

@ -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) => { Object.values(REGIONS).forEach((region) => {
if (!allRegionsClaimed() && region.owner === null) { if (!allRegionsClaimed() && region.owner === null) {
document document
@ -12,11 +27,11 @@ function unlockMapDom() {
}); });
} }
function lockMapDom() { export function lockMapDom() {
document.querySelectorAll(".actions").forEach((e) => e.classList.add("hidden")); document.querySelectorAll(".actions").forEach((e) => e.classList.add("hidden"));
} }
function updateDom() { export function updateDom() {
if (gameState !== WAITING) { if (gameState !== WAITING) {
document.querySelector("#ready-button").style.display = "none"; 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}]`); const element = document.querySelector(`.node[data-name=${region.name}]`);
element.querySelector(".strength").textContent = region.strength || ""; element.querySelector(".strength").textContent = region.strength || "";
element.style.backgroundColor = element.style.backgroundColor =

View File

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

View File

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

View File

@ -1,3 +1,10 @@
let allPlaced = false;
// In standard Risk, this is 5
const _REINFORCEMENT_MULTIPLIER = 1;
export const REGIONS = {};
class Continent { class Continent {
constructor(name) { constructor(name) {
this.name = name; this.name = name;
@ -5,9 +12,7 @@ class Continent {
} }
} }
const REGIONS = {}; export class Region {
class Region {
constructor(name, continent) { constructor(name, continent) {
this.name = name; this.name = name;
this.owner = null; this.owner = null;
@ -23,10 +28,50 @@ class Region {
region2.neighbours.add(region1); 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) { static getRegion(name) {
return REGIONS[name]; return REGIONS[name];
} }
static getAllRegions() {
return Object.values(REGIONS);
}
claim(player) { claim(player) {
this.owner = player; this.owner = player;
this.strength = 1; this.strength = 1;
@ -66,41 +111,3 @@ Region.setNeighbours(F, G);
Region.setNeighbours(G, H); Region.setNeighbours(G, H);
Region.setNeighbours(G, I); Region.setNeighbours(G, I);
Region.setNeighbours(H, 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;
}
}

View File

@ -1,4 +1,6 @@
class Packet { import { ID } from "./main.js";
export class Packet {
static _createBase(name) { static _createBase(name) {
return { return {
type: name, type: name,
@ -24,7 +26,8 @@ class Packet {
static createSetReady(nowReady) { static createSetReady(nowReady) {
return { return {
...this._createBase("READY"), ...this._createBase("ACT"),
action: "READY",
ready: nowReady, ready: nowReady,
}; };
} }

View File

@ -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_REINFORCE = 1;
const PHASE_ATTACK = 2; const PHASE_ATTACK = 2;
const PHASE_FORTIFY = 3; const PHASE_FORTIFY = 3;
let totalDice = 0; let totalDice = 0;
class Player { export class Player {
constructor(id, name) { constructor(id, local) {
this.name = name;
this.timeout = null; this.timeout = null;
this.id = id; this.id = id;
this.ready = false; this.ready = false;
@ -23,18 +29,22 @@ class Player {
this.defenderAmount = null; this.defenderAmount = null;
this.resetColor(); 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) { if (this.timeout !== null) {
window.clearTimeout(this.timeout); window.clearTimeout(this.timeout);
} }
this.timeout = window.setTimeout(() => { this.timeout = window.setTimeout(() => {
if (players[this.id] !== undefined) { game.removePlayer(this);
delete players[this.id];
}
updateDom();
}, TIMEOUT); }, TIMEOUT);
} }

View File

@ -1,3 +1,5 @@
import { socket, ID, players } from "./main.js";
class RandomSession { class RandomSession {
constructor(range) { constructor(range) {
this.range = range; this.range = range;
@ -15,7 +17,7 @@ class RandomSession {
} }
} }
class Random { export class Random {
constructor() { constructor() {
this.sessions = {}; this.sessions = {};
} }

View File

@ -7,15 +7,8 @@
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script> <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script src="{{ url_for('static', filename='js/index.js') }}"></script> <script src="{{ url_for('static', filename='js/modules/interface/main.js') }}" type="module"></script>
<script src="{{ url_for('static', filename='js/player.js') }}"></script> <script src="{{ url_for('static', filename='js/modules/crypto/main.js') }}" type="module"></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>
<script src="{{ url_for('static', filename='js/map.js') }}"></script>
<script src="{{ url_for('static', filename='js/random_primes.js') }}"></script>
<script src="{{ url_for('static', filename='js/paillier.js') }}"></script>
</head> </head>
<body> <body>
<div id="modal" class="hidden modal"> <div id="modal" class="hidden modal">
@ -52,7 +45,7 @@
</div> </div>
</div> </div>
<div id="players"> <div id="players-div">
<strong>Players</strong> <strong>Players</strong>
<ul id="playerList"> <ul id="playerList">
</ul> </ul>