code dump from kkc19

master
Serge Bazanski 2019-04-13 17:09:11 +02:00
commit 9cd9873e20
9 changed files with 1221 additions and 0 deletions

23
client.py Normal file
View File

@ -0,0 +1,23 @@
import asyncio
import socketio
sio = socketio.AsyncClient()
@sio.on('state')
def sio_state(state):
print(state)
@sio.on('identified')
def sio_identified(msg):
if msg.get('identified') != True:
print('COULD NOT IDENTIFY')
return
print('Identified.')
async def main():
cl = await sio.connect('http://127.0.0.1:5000')
await sio.emit('admin', {'password': 'changeme', 'set_role': {'who': '716a784c', 'what': 'HOST'}})
await asyncio.sleep(2)
asyncio.run(main())

20
default.nix Normal file
View File

@ -0,0 +1,20 @@
with import <nixpkgs> {};
stdenv.mkDerivation rec {
name = "venv";
env = buildEnv { name = name; paths = buildInputs; };
buildInputs = [
python37
] ++ (with python37Packages; [
virtualenv
pip
flask
redis
flask-socketio
eventlet
ecdsa
python-jose
aiohttp
]);
}

55
example-round.js Normal file
View File

@ -0,0 +1,55 @@
{
"type": "simple",
"categories": [
{
"name": "foo",
"questions": [
{"clue": "foo1", "answer": "foo1!"},
{"clue": "foo2", "answer": "foo2!"},
{"clue": "foo3", "answer": "foo3!"},
{"clue": "foo4", "answer": "foo4!"},
{"clue": "foo5", "answer": "foo5!"}
]
},
{
"name": "bar",
"questions": [
{"clue": "bar1", "answer": "bar1!"},
{"clue": "bar2", "answer": "bar2!"},
{"clue": "bar3", "answer": "bar3!"},
{"clue": "bar4", "answer": "bar4!"},
{"clue": "bar5", "answer": "bar5!"}
]
},
{
"name": "baz",
"questions": [
{"clue": "baz1", "answer": "baz1!"},
{"clue": "baz2", "answer": "baz2!"},
{"clue": "baz3", "answer": "baz3!"},
{"clue": "baz4", "answer": "baz4!"},
{"clue": "baz5", "answer": "baz5!"}
]
},
{
"name": "barfoo",
"questions": [
{"clue": "barfoo1", "answer": "barfoo1!"},
{"clue": "barfoo2", "answer": "barfoo2!"},
{"clue": "barfoo3", "answer": "barfoo3!"},
{"clue": "barfoo4", "answer": "barfoo4!"},
{"clue": "barfoo5", "answer": "barfoo5!"}
]
},
{
"name": "barbaz",
"questions": [
{"clue": "barbaz1", "answer": "barbaz1!"},
{"clue": "barbaz2", "answer": "barbaz2!"},
{"clue": "barbaz3", "answer": "barbaz3!"},
{"clue": "barbaz4", "answer": "barbaz4!"},
{"clue": "barbaz5", "answer": "barbaz5!"}
]
}
]
}

468
jeopardy.py Normal file
View File

@ -0,0 +1,468 @@
import secrets
import os
import logging
import json
import ecdsa
import binascii
from jose.jwk import ECKey
from flask import Flask, render_template, g, session
from flask_socketio import SocketIO, emit
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
app.config['ADMIN_PASSWORD'] = 'changeme'
socketio = SocketIO(app)
log = logging.getLogger()
log.setLevel(logging.DEBUG)
log.addHandler(logging.StreamHandler())
log.info("starting up...")
class Board:
def __init__(self, type):
self.type = type
self.categories = []
def add_category(self, c):
self.categories.append(c)
def serialize(self):
res = {'categories': [c.serialize() for c in self.categories]}
return res
def copy(self):
r = Board(self.type)
for c in self.categories:
r.add_category(c.copy())
return r
@classmethod
def unserialize(cls, j):
r = cls(j)
print('UNSERIALIZING ROUND', j)
if 'categories' not in j:
log.error("No categories")
return
for cid, c in enumerate(j['categories']):
if 'name' not in c:
log.error("No category name")
if 'questions' not in c or len(c['questions']) != 5:
log.error("Invalid question count")
cat = Category(c['name'])
r.add_category(cat)
questions = c['questions']
for qid, q in enumerate(questions):
if 'clue' not in q:
log.error("No clue in question")
if 'answer' not in q:
log.error("No answer in question")
value = (qid+1)*100
ques = Question(value, q['clue'], q['answer'], c['name'], qid, cid)
cat.add_question(ques)
return r
class Category:
def __init__(self, name):
self.name = name
self.questions = []
def add_question(self, q):
self.questions.append(q)
def serialize(self):
return {'name': self.name, 'questions': [q.serialize() for q in self.questions]}
def copy(self):
c = Category(self.name)
for q in self.questions:
c.add_question(q.copy())
return c
class Question:
def __init__(self, value, clue, answer, category, qid, cid, answered=False):
self.value = value
self.clue = clue
self.answer = answer
self.category = category
self.answered = answered
self.qid = qid
self.cid = cid
def serialize(self):
return {
'value': self.value,
'clue': self.clue,
'answer': self.answer,
'category': self.category,
'answered': self.answered,
}
def copy(self):
return Question(self.value, self.clue, self.answer, self.category, self.qid, self.cid, self.answered)
class Compaction:
def __init__(self):
self.ct = 0
self.board = None
self.roles = {}
self.identity = {}
self.contestants = {}
self.question = None
@property
def buzzing(self):
for _, c in self.contestants.items():
if c.buzzing:
return c.identity
return None
def feed(self, entry):
log.info("Compacting {}".format(entry))
if entry['event'] == 'new round':
self.board = entry['round'].copy()
log.info("LOG ENTRY, new round")
elif entry['event'] == 'role set':
who, what = entry['identity'], entry['what']
self.roles[who] = what
if what == 'CONTESTANT' and who not in self.contestants:
self.contestants[who] = Contestant(who, who[:8])
elif who in self.contestants:
del self.contestants[who]
log.info("LOG ENTRY, role set, {} is {}".format(who, what))
elif entry['event'] == 'pretty set':
self.identity[entry['pretty']] = entry['identity']
log.info("LOG ENTRY, pretty set, {} is {}".format(entry['pretty'], entry['identity']))
elif entry['event'] == 'question':
cid = entry['cid']
qid = entry['qid']
log.info("LOG ENTRY, question activated, {}/{}".format(cid, qid))
self.question = self.board.categories[cid].questions[qid].copy()
self.question.losers = set()
for _, c in self.contestants.items():
c.buzzing = False
elif entry['event'] == 'buzz':
if not self.question:
self.ct += 1
return
log.info("LOG ENTRY, buzz")
who = entry['identity']
if not self.buzzing and who not in self.question.losers:
self.contestants[who].buzzing = True
elif entry['event'] == 'answered':
if not self.question:
self.ct += 1
return
log.info("LOG ENTRY, answer")
ok = entry['ok']
value = self.question.value
who = self.buzzing
if not who:
self.ct += 1
return
self.contestants[who].buzzing = False
if who in self.question.losers:
self.ct += 1
return
if ok :
self.contestants[who].points += value
self.board.categories[self.question.cid].questions[self.question.qid].answered = True
self.question = None
self.ct += 1
return
self.contestants[who].points -= value
self.question.losers.add(who)
elif entry['event'] == 'give up':
if not self.question:
self.ct += 1
return
self.board.categories[self.question.cid].questions[self.question.qid].answered = True
self.question = None
elif entry['event'] == 'set name':
who = entry['identity']
name = entry['name']
self.contestants[who].name = name
else:
log.error("Invalid log entry type {}".format(entry['event']))
self.ct += 1
def serialize(self):
return {
'board': self.board.serialize() if self.board else None,
'roles': dict(self.roles),
'identity': dict(self.identity),
'contestants': [c.serialize() for (_, c) in self.contestants.items()],
'question': self.question.serialize() if self.question else None,
'losers': list(self.question.losers) if self.question else [],
}
class Contestant:
def __init__(self, identity, name):
self.identity = identity
self.name = name
self.points = 0
self.buzzing = False
def serialize(self):
return {
'name': self.name,
'points': self.points,
'buzzing': self.buzzing,
'identity': self.identity,
}
class Game:
def __init__(self):
self.state = 'IDLE'
self.log = []
self._compaction = None
def start_round(self, path):
with open(path) as f:
j = json.load(f)
r = Board.unserialize(j)
self.log.append({'event': 'new round', 'round': r})
self.save()
def activate_question(self, qid, cid):
if self.compacted.board.categories[cid].questions[qid].answered:
return
self.log.append({'event': 'question', 'qid': qid, 'cid': cid})
self.save()
def set_role(self, pretty, what):
self.log.append({'event': 'role set', 'identity': self.identity[pretty], 'what': what})
self.save()
def set_pretty(self, pretty, identity):
self.log.append({'event': 'pretty set', 'pretty': pretty, 'identity': identity})
self.save()
def answer(self, ok):
self.log.append({'event': 'answered', 'ok': ok})
self.save()
def give_up(self):
self.log.append({'event': 'give up'})
self.save()
def set_name(self, who, name):
self.log.append({'event': 'set name', 'identity': who, 'name': name})
@property
def roles(self):
return self.compacted.roles
@property
def identity(self):
return self.compacted.identity
@property
def compacted(self):
if self._compaction is None:
self._compaction = Compaction()
if self._compaction.ct == len(self.log):
return self._compaction
missing = self.log[self._compaction.ct:]
for m in missing:
self._compaction.feed(m)
return self._compaction
def save(self):
with open('jeopardy.dat.new', 'wb') as f:
for l in self.log:
if l['event'] == 'new round':
l = dict(l)
l['round'] = l['round'].serialize()
f.write(binascii.hexlify(json.dumps(l).encode()))
f.write(b'\n')
os.rename('jeopardy.dat.new', 'jeopardy.dat')
def load(self):
if not os.path.exists('jeopardy.dat'):
log.info("New game...")
return
log.info("Loading game...")
with open('jeopardy.dat', 'r') as f:
for line in f:
line = line.strip()
entry = json.loads(binascii.unhexlify(line))
print('RESTORING', entry)
if entry['event'] == 'new round':
entry['round'] = Board.unserialize(entry['round'])
self.log.append(entry)
self._compaction = None
def client_state(self, identity, admin):
role = self.roles.get(identity, 'SPECTATOR')
return {
'role': role,
'admin': admin,
'state': self.state,
'compacted': self.compacted_filter(self.compacted.serialize(), role, admin),
'ct': self.compacted.ct,
}
def compacted_filter(self, d, role, admin):
if admin:
return d
if role in ('SPECTATOR', 'CONTESTANT'):
del d['roles']
del d['identity']
if d['question']:
del d['question']['answer']
# filter out answers
if d['board'] is not None:
for i, c in enumerate(d['board']['categories']):
for j, q in enumerate(c['questions']):
del d['board']['categories'][i]['questions'][j]['answer']
return d
def buzz(self, identity):
if self.compacted.question is None:
return
for _, c in self.compacted.contestants.items():
if c.buzzing:
return
self.log.append({'event': 'buzz', 'identity': identity})
app.game = Game()
app.game.load()
#app.game.start_round('questions/final.js')
#app.game.save()
def emit_state():
identity = session['identity']
admin = session['admin']
emit('state', app.game.client_state(identity, admin))
@app.route('/')
def view_index():
return render_template("index.html")
@socketio.on('connect')
def sio_connect():
log.info("Client connected")
session['identity'] = None
session['admin'] = False
session['nonce'] = secrets.token_hex(64)
emit_state()
emit('nonce', session['nonce'])
@socketio.on('ping')
def sio_ping():
emit_state()
@socketio.on('proof')
def sio_proof(data):
log.info("Client attempting identity proof")
if data.get('nonce', '') != session['nonce']:
emit('identified', {'identified': False})
return
# ,___,
#jwk = data.get('jwk', {})
#k = ECKey(jwk, 'ES384')
#proof = binascii.unhexlify(data.get('proof'))
#if not k.verify(session['nonce'], proof):
# emit('identified', {'identified': False})
# return
identity = data.get('identity')
session['identity'] = identity
app.game.set_pretty(identity[:8], identity)
emit('identified', {'identified': True})
emit_state()
@socketio.on('admin')
def sio_admin(data):
log.info("Admin connecting...")
if data.get('password') != app.config['ADMIN_PASSWORD']:
emit('identified', {'identifier': True})
return
emit('identified', {'identified': True})
session['admin'] = True
if data.get('set_role') != None:
who = data['set_role'].get('who')
what = data['set_role'].get('what')
app.game.set_role(who, what)
emit_state()
@socketio.on('host-question')
def sio_host_question(data):
if app.game.roles[session['identity']] != 'HOST':
return
cid = data.get('category')
qid = data.get('question')
if cid not in (0, 1, 2, 3, 4):
return
if qid not in (0, 1, 2, 3, 4):
return
app.game.activate_question(qid, cid)
@socketio.on('host-set-role')
def sio_host_contestant(data):
if app.game.roles[session['identity']] != 'HOST':
return
if not data.get('who'):
log.info("Who?")
return
if data.get('what') not in ('SPECTATOR', 'CONTESTANT'):
log.info("What?")
return
app.game.set_role(data['who'], data['what'])
@socketio.on('buzz')
def sio_buz():
role = app.game.roles.get(session['identity'])
if role != 'CONTESTANT':
return
app.game.buzz(session['identity'])
@socketio.on('answer')
def sio_answer(data):
if app.game.roles[session['identity']] != 'HOST':
return
ok = data.get('ok', False)
app.game.answer(ok)
@socketio.on('give up')
def sio_give_up():
if app.game.roles[session['identity']] != 'HOST':
return
app.game.give_up()
@socketio.on('set name')
def sio_set_name(msg):
if app.game.roles[session['identity']] != 'HOST':
return
app.game.set_name(msg['identity'], msg['name'])
if __name__ == '__main__':
socketio.run(app, debug=True, host='0.0.0.0')

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

189
static/main.css Normal file
View File

@ -0,0 +1,189 @@
body {
background-color: black;
color: white;
font-size: 100%;
font-family: sans-serif;
overflow: hidden;
}
div#error {
position: absolute;
top: 50%;
left: 50%;
width: 40rem;
margin-left: -20rem;
margin-top: -1.5rem;
background-color: red;
font-size: 1.5rem;
text-align: center;
z-index: 100;
padding: 0.8rem;
border-radius: 1rem;
display: none;
}
div#status {
height: 1.2rem;
font-size: 0.7rem;
position: absolute;
top: 100%;
left: 0;
margin-top: -1.2rem;
width: 100%;
z-index: 50;
text-align: right;
}
div#status p {
padding: 0;
padding-right: 0.8rem;
margin: 0;
display: inline;
}
div#board {
display: flex;
flex-direction: row;
justify-content: center;
}
div#board div.category {
margin: 0.25rem;
}
div#board div.panel, div#board div.header {
background-color: #0d0b70;
width: 15rem;
height: 4rem;
margin-bottom: 0.5rem;
text-align: center;
border-radius: 0.25rem;
display: flex;
flex-direction: column;
justify-content: center;
}
div#board div.header {
font-size: 1.5rem;
margin-bottom: 1.5rem;
height: 8rem;
}
div#board div.panel {
font-size: 2rem;
}
div#board div.solved {
background-color: #000010;
color: #202020;
}
div#board div.narrow {
width: 10rem;
height: 5rem;
}
div#host {
width: 30rem;
background-color: red;
padding: 0.5rem;
margin-left: 1rem;
}
div#contestants {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
}
div.contestant {
width: 18rem;
height: 8rem;
background-color: #1d1b90;
border-radius: 0.5rem;
margin-left: 1.5rem;
margin-right: 1.5rem;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
}
div.contestant h1 {
margin: 0;
padding: 0;
}
div.contestant h2 {
margin: 0;
padding: 0;
}
div.contestant h3 {
font-size: 1rem;
margin: 0;
padding: 0;
}
div.buzz {
border: 4px solid red;
}
div#question {
position: absolute;
top: 50%;
left: 50%;
width: 80rem;
height: 40rem;
margin-left: -40rem;
margin-top: -20rem;
background-color: #1d1b90;
border: 4px solid black;
font-size: 1.5rem;
text-align: center;
z-index: 90;
border-radius: 1rem;
display: none;
flex-direction: column;
justify-content: space-between;
}
div#question div.header {
font-size: 3rem;
margin-top: 1rem;
}
div#question div.inner {
}
div#question div.inner img {
width: auto;
height: 30rem;
}
div.buzz {
background-color: #901d1b;
height: 5rem;
}
div.choice {
display: flex;
flex-direction: row;
justify-content: center;
}
div.ack {
background-color: green;
width: 10rem;
height: 2em;
}
div.nack {
background-color: red;
width: 10rem;
height: 2em;
}

442
static/main.js Normal file
View File

@ -0,0 +1,442 @@
"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);
});
};

3
static/socket.io.min.js vendored Normal file

File diff suppressed because one or more lines are too long

21
templates/index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Hacker Jeopardy!</title>
<link href="/static/main.css" rel="stylesheet" />
<link href="/static/favicon.png" rel="icon" />
<script src="/static/socket.io.min.js"></script>
<script src="/static/main.js"></script>
</head>
<body>
<div class="nojs">
<p>You'll have to enable JavaScript, sorry.</p>
</div>
<div id="error"></div>
<div id="status"><p>Client state: No JS?</p></div>
<div id="board"></div>
<div id="contestants"></div>
<div id="question"></div>
</body>
</html>