jeopardy/static/main.js

443 lines
13 KiB
JavaScript

"use strict";
let Identity = function() {
this.key = null;
this.pretty = null;
};
Identity.prototype.start = async function() {
const lskey = "hacker-jeopardy-key-6";
const algo = {
name: "ECDSA",
namedCurve: "P-384",
};
const uses = ["sign", "verify"];
let existing = localStorage.getItem(lskey);
if (existing == null) {
console.log("Generating identity...");
const pair = await crypto.subtle.generateKey(algo, true, uses);
this.pubkey = pair.publicKey;
this.privkey = pair.privateKey;
this.identity = (await this.hexsign("hacker-jeopardy-whoami"));
const ls = {
"pubkey": await crypto.subtle.exportKey("jwk", this.pubkey),
"privkey": await crypto.subtle.exportKey("jwk", this.privkey),
"identity": this.identity,
};
this.jwk_pubkey = ls.pubkey;
localStorage.setItem(lskey, JSON.stringify(ls));
} else {
console.log("Loading identity...");
const ls = JSON.parse(existing);
this.privkey = await crypto.subtle.importKey("jwk", ls.privkey, algo, false, ["sign"]);
this.pubkey = await crypto.subtle.importKey("jwk", ls.pubkey, algo, false, ["verify"]);
this.jwk_pubkey = ls.pubkey;
this.identity = ls.identity;
}
this.pretty = this.identity.substr(0, 8);
console.log("Pretty identity: %s", this.pretty);
};
Identity.prototype.hexsign = async function(nonce) {
const ugh = (new TextEncoder()).encode(nonce);
const ugnh = await crypto.subtle.sign({
name: "ECDSA",
hash: {name: "SHA-384"},
}, this.privkey, ugh);
const ungh = Array.from(new Uint8Array(ugnh));
const argh = ungh.map((b) => { return b.toString(16).padStart(2, "0"); }).join("");
console.log("signed encoded", argh);
return argh;
};
const ClientState = Object.freeze({
CONNECTING: Symbol("CONNECTING"),
SYNCING: Symbol("SYNCING"),
SYNCED: Symbol("SYNCED"),
BACKOFF: Symbol("BACKOFF"),
});
let Game = function() {
this.clientState = ClientState.CONNECTING;
this.role = 'SPECTATOR';
this.error = null;
this.interval = null;
this.serverState = null;
this.identified = false;
this.ct = null;
this.rct = null;
};
Game.prototype.run = async function() {
let self = this;
this.identity = new Identity();
await this.identity.start();
console.log("Opening socket...");
this.socket = io.connect('https://jeopardy.rhw.q3k.org');
this.socket.on('connect', function() { self.socketConnected(); });
this.socket.on('disconnect', function() { self.socketDisconnected(); });
this.socket.on('state', function(msg) { self.socketState(msg); });
this.socket.on('nonce', function(nonce) { self.socketNonce(nonce); });
this.socket.on('identified', function(msg) { self.socketIdentified(msg); });
if (this.timeout == null) {
this.interval = setInterval(function() { self.tick(); }, 1000);
}
this.lastStateUpdate = null;
this.serverState = null;
};
Game.prototype.mutate = function(newState) {
if (this.clientState != newState) {
console.log("Client %s -> %s", this.clientState, newState);
}
this.clientState = newState;
this.render();
};
Game.prototype.fatal = function(msg) {
console.error("FATAL in %s: %s", this.clientState, msg);
this.error = msg;
this.mutate(ClientState.BACKOFF);
//setTimeout(() => this.backoffDone(), 3000);
};
Game.prototype.backoffDone = function() {
this.socket.close();
}
Game.prototype.socketConnected = function() {
console.log("Socket connected");
if (this.clientState != ClientState.CONNECTING) {
this.fatal("Unexpected 'connect'");
}
this.error = null;
this.mutate(ClientState.SYNCING);
};
Game.prototype.socketDisconnected = function() {
console.log("Socket disconnected");
this.identified = false;
this.mutate(ClientState.CONNECTING);
this.socket.open();
this.error = "Disconnected";
};
Game.prototype.socketState = function(msg) {
this.serverState = msg.compacted;
this.ct = msg.ct;
this.role = msg.role;
this.lastStateUpdate = Date.now();
this.mutate(ClientState.SYNCED);
if (this.role == 'HOST') {
for (let [pretty, identity] of Object.entries(msg.compacted.identity)) {
let role = msg.compacted.roles[identity];
if (role == undefined || role == null) {
continue;
}
}
}
};
Game.prototype.socketNonce = async function(nonce) {
console.log("Received nonce, sending identity proof.");
const proof = await this.identity.hexsign(nonce);
this.socket.emit('proof', {'identity': this.identity.identity, 'nonce': nonce, 'jwk': this.identity.jwk_pubkey, 'proof': proof});
};
Game.prototype.socketIdentified = async function(msg) {
console.log("Identification result: ", msg);
if (msg.identified != true) {
this.fatal("Identification failed!")
return;
}
this.identified = true;
this.render();
};
Game.prototype.tick = function() {
let self = this;
this.socket.emit('ping')
//this.render();
if (this.clientState == ClientState.CONNECTING) {
if (this.socket.disconnected) {
console.log("Reconnecting...");
this.socket.open();
}
}
if (this.clientState != ClientState.SYNCED) {
return;
}
};
Game.prototype.render = function() {
if (this.rct == this.ct) {
return
}
this.rct = this.ct;
if (this.error != null) {
let err = document.querySelector("#error");
err.style.display = "block";
err.textContent = "Error: " + this.error;
} else {
let err = document.querySelector("#error");
err.style.display = "none";
}
let status = document.querySelector("#status p");
status.textContent = "State: " + this.clientState.description;
status.textContent += ", identity: " + this.identity.pretty;
status.textContent += ", role: " + this.role;
status.textContent += ".";
this.renderGame();
};
Game.prototype.renderGame = function() {
if (this.serverState == null) {
return;
}
this.renderBoard();
this.renderContestants();
this.renderQuestion();
};
Game.prototype.renderBoard = function() {
let host = (this.role == 'HOST');
let b = document.createElement("div");
b.id = "board";
if (this.serverState.board == null) {
document.querySelector("#board").replaceWith(b);
return;
}
let desc = this.serverState.board;
desc.categories.forEach((category, cid) => {
let c = document.createElement("div");
c.className = "category";
b.appendChild(c);
let h = document.createElement("div");
h.className = "header";
if (host) {
h.className += " narrow";
};
h.textContent = category.name;
c.appendChild(h);
category.questions.forEach((question, qid) => {
let p = document.createElement("div");
p.className = "panel";
if (question.answered) {
p.className += " solved";
}
if (host) {
p.className += " narrow";
};
if (this.role == "HOST") {
p.onclick = (() => {
this.socket.emit("host-question", {"category": cid, "question": qid})
});
}
let t = document.createElement("div");
t.className = "text";
t.textContent = "" + question.value + " SOG";
p.appendChild(t);
c.appendChild(p);
});
});
if (host) {
let h = document.createElement("div");
h.id = "host";
h.textContent = "Host menu";
b.appendChild(h);
let u = document.createElement("ul");
h.appendChild(u);
for (let [pretty, identifier] of Object.entries(this.serverState.identity)) {
let role = this.serverState.roles[identifier];
if (role == undefined) {
role = "SPECTATOR";
}
let li = document.createElement("li");
li.textContent = pretty + " " + role + " ";
u.appendChild(li);
let setRole = (who, what) => {
this.socket.emit('host-set-role', {'who': pretty, 'what': what});
};
if (role == "SPECTATOR") {
let a = document.createElement("a");
a.textContent = "contestant";
a.href = "#";
a.onclick = () => { setRole(pretty, 'CONTESTANT'); };
li.appendChild(a);
}
if (role == "CONTESTANT") {
let a = document.createElement("a");
a.textContent = "spectator";
a.href = "#";
a.onclick = () => { setRole(pretty, 'SPECTATOR'); };
li.appendChild(a);
let b = document.createElement("a");
b.textContent = "set name";
b.href = "#";
b.onclick = () => { this.socket.emit('set name', {'identity': identifier, 'name': prompt("name?")}); };
li.appendChild(b);
}
}
}
document.querySelector("#board").replaceWith(b);
}
Game.prototype.renderContestants = function() {
let contestants = document.createElement("div");
contestants.id = "contestants";
this.serverState.contestants.forEach((c) => {
let name = c.name;
let cont = document.createElement("div");
cont.className = "contestant";
if (c.buzz) {
cont.className += " buzz";
}
let h1 = document.createElement("h1");
h1.textContent = name;
let h2 = document.createElement("h2");
h2.textContent = c.points + " SOG";
cont.appendChild(h1);
if (c.identity == this.identity.identity) {
let h3 = document.createElement("h3");
h3.textContent = "(that's you!)";
cont.appendChild(h3);
}
cont.appendChild(h2);
contestants.appendChild(cont);
});
document.querySelector("#contestants").replaceWith(contestants);
};
Game.prototype.renderQuestion = function() {
let question = document.createElement("div");
question.id = "question";
let q = this.serverState.question;
if (q == undefined || q == null) {
document.querySelector("#question").replaceWith(question);
return;
}
let contestant = (this.role == "CONTESTANT");
let host = (this.role == "HOST");
let buzzed = false;
let buzzing = null;
this.serverState.contestants.forEach((c) => {
if (c.buzzing) {
buzzed = true;
buzzing = c;
}
});
let loser = false;
this.serverState.losers.forEach((c) => {
if (c == this.identity.identity) {
loser = true;
};
});
question.style = "display: flex;";
let header = document.createElement("div");
header.className = "header";
header.textContent = q.category + " - " + q.value;
question.appendChild(header);
let inner = document.createElement("div");
inner.className = "inner";
inner.innerHTML = q.clue;
question.appendChild(inner);
if (!buzzed && !loser && contestant) {
let b = document.createElement("div");
b.className = "buzz";
b.textContent = "BUZZ!";
b.onclick = () => {
this.socket.emit("buzz");
};
question.append(b);
} else if (buzzed && host) {
let choice = document.createElement("div");
choice.className = "choice";
choice.textContent = "buzzing: " + buzzing.name;
question.append(choice);
let ack = document.createElement("div");
ack.className = "ack";
ack.textContent = "OK";
ack.onclick = () => {
this.socket.emit("answer", {'ok': true});
};
let nack = document.createElement("div");
nack.className = "nack";
nack.textContent = "WRONG";
nack.onclick = () => {
this.socket.emit("answer", {'ok': false});
};
choice.appendChild(ack);
choice.appendChild(nack);
} else if (host) {
let choice = document.createElement("div");
choice.className = "choice";
choice.textContent = "losers: " + this.serverState.losers.map((e) => { return e.substr(0, 8); }).join(", ") + ", answer: " + this.serverState.question.answer;
question.append(choice);
let giveup = document.createElement("div");
giveup.className = "nack";
giveup.textContent = "CANCEL QUESTION";
giveup.onclick = () => {
this.socket.emit("give up");
};
choice.appendChild(giveup);
} else {
question.appendChild(document.createElement("div"));
};
document.querySelector("#question").replaceWith(question);
};
window.onload = function() {
let nojs = document.querySelector("div.nojs");
nojs.parentNode.removeChild(nojs);
let run = async () => {
let g = new Game();
await g.run();
g.render();
}
run().catch((e) => {
console.log(e);
console.log(e.stack);
console.log(e.message);
console.log(e.name);
});
};