code dump from kkc19
commit
9cd9873e20
|
@ -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())
|
|
@ -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
|
||||||
|
]);
|
||||||
|
}
|
|
@ -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!"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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')
|
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
File diff suppressed because one or more lines are too long
|
@ -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>
|
Loading…
Reference in New Issue