- (
points pts)
- (
tokens tks)
+ (
points pts) (
tokens tks)
diff --git a/public/buzzer.js b/public/buzzer.js
index 4cb7d0f..04e8111 100644
--- a/public/buzzer.js
+++ b/public/buzzer.js
@@ -10,89 +10,27 @@ const storage = window.sessionStorage
// - measure/report latency
// - use server reported time to find winner
-import config from "./config.js"
+import {
+ clear,
+ isEmpty,
+ isString,
+ ms_ns,
+ node,
+ off,
+ on,
+ q,
+ s_ns,
+ servertime_now_ns,
+ session_id,
+ session_id_from_url,
+ session_url,
+} from "./shared.js"
const buzzer_key = {
code: 0x20,
name: "space bar",
}
-function isString(x) {
- return typeof x === "string"
-}
-
-function isEmpty(x) {
- return (Array.isArray(x) ? x : Object.keys(x)).length === 0
-}
-
-const q = (selector, root = document) => root.querySelector(selector)
-
-const _evlisteners = {}
-/**
- * @param {String} event [description]
- * @param {Function} cb [description]
- * @param {?{once: bool, target: Element = document}} options
- */
-function on(event, cb, options = {}) {
- const target = options.target || document
- if ((_evlisteners[target] ?? {})[event]) {
- console.warn(`Replacing previous event listener for '${event}' on target:`, target)
- off(event, target)
- }
- target.addEventListener(event, cb, { once: !!options.once })
- if (!_evlisteners[target]) {
- _evlisteners[target] = {}
- }
- _evlisteners[target][event] = cb
-}
-
-function off(event, target = document) {
- if (!_evlisteners[target] || !_evlisteners[target][event]) {
- return
- }
- target.removeEventListener(event, _evlisteners[target][event])
- delete _evlisteners[target][event]
- if (isEmpty(_evlisteners[target])) {
- delete _evlisteners[target]
- }
-}
-
-function node(type, { appendTo, cls, text, data, style, ...attrs } = {}) {
- let elem = isString(type) ? document.createElement(type) : type
- if (cls) {
- if (isString(cls)) {
- elem.className = cls
- } else {
- for (const [name, on] of Object.entries(cls)) {
- if (on) {
- elem.classList.add(name)
- } else {
- elem.classList.remove(name)
- }
- }
- }
- }
- if (text) {
- elem.textContent = text
- }
- Object.assign(elem.dataset, data ?? {})
- Object.assign(elem.style, style ?? {})
- for (const name in attrs) {
- elem.setAttribute(name, attrs[name])
- }
- if (appendTo) {
- elem = appendTo.appendChild(elem)
- }
- return elem
-}
-
-/**
- * Some duration conversion constants.
- */
-const ms_ns = 1_000_000 // nanoseconds in a millisecond
-const s_ms = 1_000 // milliseconds in a second
-const s_ns = 1_000_000_000 // nanoseconds in a second
-
let socket,
servertime,
toffset_ms,
@@ -114,11 +52,6 @@ function find_client(client_id) {
return clients.find((c) => c.id === client_id)
}
-function session_id() {
- const match = /^#?(.+)/.exec(document.location.hash)
- return match ? match[1] : null
-}
-
function new_session_id() {
if (!crypto) {
return Math.random().toString(36).substr(2)
@@ -130,7 +63,7 @@ function new_session_id() {
function setup_url() {
const sid = session_id() || new_session_id()
- document.location.hash = sid
+ location.hash = sid
}
function send(type, value) {
@@ -138,13 +71,6 @@ function send(type, value) {
socket.send(JSON.stringify({ type, value }))
}
-function clear(container) {
- while (container.children.length > 0) {
- const child = container.children[0]
- child.remove()
- }
-}
-
const player_list = q("#info ul")
function redraw_clients(me, clients) {
if (!me) {
@@ -196,15 +122,6 @@ function highlight(client_id, until_ns) {
}
}
-/**
- * Guess the exact current server time.
- */
-function servertime_now_ns() {
- const now_ms = performance.now()
- const delta_ns = ms_ns * (now_ms - toffset_ms)
- return servertime + delta_ns
-}
-
function setup_ui() {
on("focus", (event) => {
hide("active")
@@ -306,20 +223,10 @@ function set_name(name) {
send("name", name)
}
-function session_url(sid) {
- return `${config.wsurl}/${sid}`
-}
-
-function session_id_from_url(url) {
- const wsurl = new URL(config.wsurl)
- const match = RegExp(`${wsurl.pathname}/([^/]+)$`).exec(url)
- return !match ? null : match[1]
-}
-
function setup_ws() {
const sid = session_id()
const credentials = { id: storage["my_uid"], key: storage["my_key"] }
- socket = new WebSocket(`${session_url(sid)}`)
+ socket = new WebSocket(session_url(sid))
socket.addEventListener("open", function (event) {
if (sid === storage["my_sid"]) {
send("login", credentials)
@@ -362,6 +269,8 @@ function setup_ws() {
clients.push(client)
}
redraw_clients(me, clients)
+ } else if (type === "control") {
+ // ignore
} else if (type === "error") {
console.error(`Error: ${value.reason}`)
const errorbox = q("#error")
diff --git a/public/monitor.css b/public/monitor.css
new file mode 100644
index 0000000..77209ae
--- /dev/null
+++ b/public/monitor.css
@@ -0,0 +1,60 @@
+body {
+ font-family: "Arial Rounded MT Bold", sans-serif;
+ background-color: black;
+ margin: 0px auto;
+ overflow: hidden;
+}
+* {
+ margin: 0;
+ padding: 0;
+}
+/*
+# colors
+citrus: c2d72f
+bleue: 0093ff
+*/
+#info {
+ background-color: #c2d72f;
+ padding: 1em 0 0 1em;
+ width: 1280px;
+ height: 720px;
+ outline: 10px solid gold;
+}
+.player {
+ width: 336px;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
+.player + .player {
+ margin-top: 1.5em;
+}
+.box {
+ background-color: #0f0;
+ height: 189px;
+ outline: 0.3em solid grey;
+ -moz-outline-radius: 1em;
+}
+.points {
+ position: absolute;
+ top: 1em;
+ right: 1em;
+ background-color: #c2d72f;
+ width: 3em;
+ padding: 0.2em;
+ text-align: right;
+ border-radius: 0.5em;
+ box-shadow: 0 0 0.3em #c2d72f, 0 0 0.8em #c2d72f, 0 0 1em #c2d72f, 0 0 1.2em #c2d72f;
+}
+.player.buzzing .box {
+ outline: 0.7em solid #0093ff;
+}
+.name {
+ margin-top: 0.5em;
+ text-align: center;
+ letter-spacing: 0.3em;
+ font-variant: small-caps;
+ /*font-weight: bold;*/
+}
+/*1280 x 720*/
+/*1920 x 1080*/
diff --git a/public/monitor.html b/public/monitor.html
new file mode 100644
index 0000000..25eb39b
--- /dev/null
+++ b/public/monitor.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/public/monitor.js b/public/monitor.js
new file mode 100644
index 0000000..1c9e311
--- /dev/null
+++ b/public/monitor.js
@@ -0,0 +1,149 @@
+"use strict"
+
+/* global document, window */
+const location = document.location
+const performance = window.performance
+
+import {
+ clear,
+ ms_ns,
+ node,
+ q,
+ s_ns,
+ servertime_now_ns,
+ session_id,
+ session_url,
+} from "./shared.js"
+
+let socket,
+ servertime,
+ toffset_ms,
+ clients = {},
+ me
+
+function setup_url() {
+ const sid = session_id() || ""
+ location.hash = sid
+}
+
+function send(type, value) {
+ // console.debug('sending', value)
+ socket.send(JSON.stringify({ type, value }))
+}
+
+function prettynum(n) {
+ let i = Math.abs(n) | 0
+ let s = []
+ while (true) {
+ const m = i % 1000
+ i = (i / 1000) | 0
+ if (i === 0) {
+ s.unshift(String(m))
+ break
+ } else {
+ s.unshift(String(m).padStart(3, "0"))
+ }
+ }
+ return (n < 0 ? "-" : "") + s.join("'")
+}
+
+function assert(expected, got) {
+ if (expected !== got) {
+ throw Error("Assertion failed.")
+ }
+}
+
+function test_prettynum() {
+ assert("0", prettynum(0))
+ assert("1", prettynum(1))
+ assert("1'000", prettynum(1000))
+ assert("36'000", prettynum(36000))
+ assert("0", prettynum(-0))
+ assert("-1", prettynum(-1))
+ assert("-1'000", prettynum(-1000))
+ assert("-36'000", prettynum(-36000))
+ assert("-36'600", prettynum(-36600))
+}
+test_prettynum()
+
+const player_list = q("#info")
+function redraw_clients(me, clients) {
+ if (!me) {
+ return
+ }
+ clear(player_list)
+ for (const c of Object.values(clients)) {
+ if (c.id === me.id) {
+ continue
+ }
+ const player = node("div", { data: { cid: c.id }, cls: "player" })
+ node("div", { cls: "points", text: prettynum(c.points), appendTo: player })
+ node("div", { cls: "box", appendTo: player })
+ node("div", { cls: "name", text: c.name, appendTo: player })
+ player_list.appendChild(player)
+ }
+}
+
+let highlighted = false
+function highlight(client_id, until_ns) {
+ if (highlighted) {
+ return
+ }
+ const timeout_ms = (until_ns - servertime_now_ns()) / ms_ns
+ if (timeout_ms <= 10) {
+ console.warn("That highlight timeout was ridiculously low:", client_id, timeout_ms)
+ return
+ }
+ for (const li of player_list.children) {
+ if (li.dataset.cid === client_id) {
+ li.classList.add("buzzing")
+ highlighted = true
+ setTimeout(() => {
+ highlighted = false
+ li.classList.remove("buzzing")
+ }, timeout_ms)
+ return
+ }
+ }
+}
+
+function setup_ws() {
+ const sid = session_id()
+ socket = new WebSocket(session_url(sid))
+ socket.addEventListener("open", function (event) {
+ send("name", "Monitor")
+ })
+ socket.addEventListener("message", function (event) {
+ const msg = JSON.parse(event.data)
+ const { type, value } = msg
+ if (msg.type === "time") {
+ servertime = value.time
+ toffset_ms = performance.now()
+ } else if (msg.type === "id") {
+ me = value
+ redraw_clients(me, clients)
+ } else if (msg.type === "buzz") {
+ const buzztime_ns = value.time
+ const client_id = value.client
+ const duration_ns = 12 * s_ns
+ const until_ns = buzztime_ns + duration_ns
+ highlight(client_id, until_ns)
+ } else if (msg.type === "clients") {
+ clients = Object.fromEntries(value.clients.map((c) => [c.id, c]))
+ redraw_clients(me, clients)
+ } else if (msg.type === "client") {
+ const client = value.client
+ clients[client.id] = client
+ redraw_clients(me, clients)
+ } else if (type === "session_key") {
+ // ignore
+ } else if (type === "error") {
+ console.error(`Error: ${value.reason}`)
+ } else {
+ console.error(`Unknown message: ${event.data}`)
+ }
+ })
+}
+
+setup_url()
+setup_ws()
diff --git a/public/shared.js b/public/shared.js
new file mode 100644
index 0000000..18257d3
--- /dev/null
+++ b/public/shared.js
@@ -0,0 +1,115 @@
+"use strict"
+
+/* global document, window */
+const location = document.location
+const performance = window.performance
+
+import config from "./config.js"
+
+export function isString(x) {
+ return typeof x === "string"
+}
+
+export function isEmpty(x) {
+ return (Array.isArray(x) ? x : Object.keys(x)).length === 0
+}
+
+export const q = (selector, root = document) => root.querySelector(selector)
+
+const _evlisteners = {}
+
+/**
+ * @param {String} event [description]
+ * @param {Function} cb [description]
+ * @param {?{once: bool, target: Element = document}} options
+ */
+export function on(event, cb, options = {}) {
+ const target = options.target || document
+ if ((_evlisteners[target] ?? {})[event]) {
+ console.warn(`Replacing previous event listener for '${event}' on target:`, target)
+ off(event, target)
+ }
+ target.addEventListener(event, cb, { once: !!options.once })
+ if (!_evlisteners[target]) {
+ _evlisteners[target] = {}
+ }
+ _evlisteners[target][event] = cb
+}
+
+export function off(event, target = document) {
+ if (!_evlisteners[target] || !_evlisteners[target][event]) {
+ return
+ }
+ target.removeEventListener(event, _evlisteners[target][event])
+ delete _evlisteners[target][event]
+ if (isEmpty(_evlisteners[target])) {
+ delete _evlisteners[target]
+ }
+}
+
+export function node(type, { appendTo, cls, text, data, style, ...attrs } = {}) {
+ let elem = isString(type) ? document.createElement(type) : type
+ if (cls) {
+ if (isString(cls)) {
+ elem.className = cls
+ } else {
+ for (const [name, on] of Object.entries(cls)) {
+ if (on) {
+ elem.classList.add(name)
+ } else {
+ elem.classList.remove(name)
+ }
+ }
+ }
+ }
+ if (text) {
+ elem.textContent = text
+ }
+ Object.assign(elem.dataset, data ?? {})
+ Object.assign(elem.style, style ?? {})
+ for (const name in attrs) {
+ elem.setAttribute(name, attrs[name])
+ }
+ if (appendTo) {
+ elem = appendTo.appendChild(elem)
+ }
+ return elem
+}
+
+/**
+ * Some duration conversion constants.
+ */
+export const ms_ns = 1_000_000 // nanoseconds in a millisecond
+export const s_ms = 1_000 // milliseconds in a second
+export const s_ns = 1_000_000_000 // nanoseconds in a second
+
+export function session_url(sid) {
+ return `${config.wsurl}/${sid}`
+}
+
+export function session_id_from_url(url) {
+ const wsurl = new URL(config.wsurl)
+ const match = RegExp(`${wsurl.pathname}/([^/]+)$`).exec(url)
+ return !match ? null : match[1]
+}
+
+export function clear(container) {
+ while (container.children.length > 0) {
+ const child = container.children[0]
+ child.remove()
+ }
+}
+
+export function session_id() {
+ const match = /^#?(.+)/.exec(location.hash)
+ return match ? match[1] : null
+}
+
+/**
+ * Guess the exact current server time.
+ */
+export function servertime_now_ns() {
+ const now_ms = performance.now()
+ const delta_ns = ms_ns * (now_ms - toffset_ms)
+ return servertime + delta_ns
+}
diff --git a/stubs/websockets.pyi b/stubs/websockets.pyi
index 3451014..311877e 100644
--- a/stubs/websockets.pyi
+++ b/stubs/websockets.pyi
@@ -104,5 +104,3 @@ class Serve:
__iter__ = __await__
serve = Serve
-
-WebSocketServerProtocol