mirror of https://github.com/dirtbags/moth.git
227 lines
5.9 KiB
JavaScript
227 lines
5.9 KiB
JavaScript
// jshint asi:true
|
|
|
|
// devel_addin drops a bunch of development extensions into element e.
|
|
// It will only modify stuff inside e.
|
|
function devel_addin(obj, e) {
|
|
let h = document.createElement("h2")
|
|
e.appendChild(h)
|
|
h.textContent = "Development Options"
|
|
|
|
let g = document.createElement("p")
|
|
e.appendChild(g)
|
|
g.innerText = "This section will not appear for participants."
|
|
|
|
let keys = Object.keys(obj)
|
|
keys.sort()
|
|
for (let key of keys) {
|
|
switch (key) {
|
|
case "body":
|
|
continue
|
|
}
|
|
let val = obj[key]
|
|
|
|
if ((! val) || (val.length === 0)) {
|
|
// Empty, skip it
|
|
continue
|
|
}
|
|
|
|
let d = document.createElement("div")
|
|
e.appendChild(d)
|
|
d.classList.add("kvpair")
|
|
|
|
let ktxt = document.createElement("span")
|
|
d.appendChild(ktxt)
|
|
ktxt.textContent = key
|
|
|
|
if (Array.isArray(val)) {
|
|
let vi = document.createElement("select")
|
|
d.appendChild(vi)
|
|
vi.multiple = true
|
|
for (let a of val) {
|
|
let opt = document.createElement("option")
|
|
vi.appendChild(opt)
|
|
opt.innerText = a
|
|
}
|
|
} else {
|
|
let vi = document.createElement("input")
|
|
d.appendChild(vi)
|
|
vi.value = val
|
|
vi.disabled = true
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// The routine used to hash answers in compiled puzzle packages
|
|
function djb2hash(buf) {
|
|
let h = 5381
|
|
for (let c of (new TextEncoder).encode(buf)) { // Encode as UTF-8 and read in each byte
|
|
// JavaScript converts everything to a signed 32-bit integer when you do bitwise operations.
|
|
// So we have to do "unsigned right shift" by zero to get it back to unsigned.
|
|
h = (((h * 33) + c) & 0xffffffff) >>> 0
|
|
}
|
|
return h
|
|
}
|
|
|
|
|
|
// Pop up a message
|
|
function toast(message, timeout=5000) {
|
|
let p = document.createElement("p")
|
|
|
|
p.innerText = message
|
|
document.getElementById("messages").appendChild(p)
|
|
setTimeout(
|
|
e => { p.remove() },
|
|
timeout
|
|
)
|
|
}
|
|
|
|
// When the user submits an answer
|
|
function submit(e) {
|
|
e.preventDefault()
|
|
fetch("answer", {
|
|
method: "POST",
|
|
body: new FormData(e.target),
|
|
})
|
|
.then(resp => {
|
|
if (resp.ok) {
|
|
resp.json()
|
|
.then(obj => {
|
|
toast(obj.data.description)
|
|
})
|
|
} else {
|
|
toast("Error submitting your answer. Try again in a few seconds.")
|
|
console.log(resp)
|
|
}
|
|
})
|
|
.catch(err => {
|
|
toast("Error submitting your answer. Try again in a few seconds.")
|
|
console.log(err)
|
|
})
|
|
}
|
|
|
|
function loadPuzzle(categoryName, points, puzzleId) {
|
|
let puzzle = document.getElementById("puzzle")
|
|
let base = "content/" + categoryName + "/" + puzzleId + "/"
|
|
|
|
fetch(base + "puzzle.json")
|
|
.then(resp => {
|
|
return resp.json()
|
|
})
|
|
.then(obj => {
|
|
// Populate authors
|
|
document.getElementById("authors").textContent = obj.authors.join(", ")
|
|
|
|
// Make the whole puzzle available
|
|
window.puzzle = obj
|
|
|
|
// If answers are provided, this is the devel server
|
|
if (obj.answers) {
|
|
devel_addin(obj, document.getElementById("devel"))
|
|
}
|
|
|
|
// Load scripts
|
|
for (let script of obj.scripts) {
|
|
let st = document.createElement("script")
|
|
document.head.appendChild(st)
|
|
st.src = base + script
|
|
}
|
|
|
|
// List associated files
|
|
for (let fn of obj.files) {
|
|
let li = document.createElement("li")
|
|
let a = document.createElement("a")
|
|
a.href = base + fn
|
|
a.innerText = fn
|
|
li.appendChild(a)
|
|
document.getElementById("files").appendChild(li)
|
|
}
|
|
|
|
// Prefix `base` to relative URLs in the puzzle body
|
|
let doc = new DOMParser().parseFromString(obj.body, "text/html")
|
|
for (let se of doc.querySelectorAll("[src],[href]")) {
|
|
se.outerHTML = se.outerHTML.replace(/(src|href)="([^/]+)"/i, "$1=\"" + base + "$2\"")
|
|
}
|
|
|
|
// If a validation pattern was provided, set that
|
|
if (obj.pattern) {
|
|
document.querySelector("#answer").pattern = obj.pattern
|
|
}
|
|
|
|
// Replace puzzle children with what's in `doc`
|
|
Array.from(puzzle.childNodes).map(e => e.remove())
|
|
Array.from(doc.body.childNodes).map(e => puzzle.appendChild(e))
|
|
})
|
|
.catch(err => {
|
|
// Show error to the user
|
|
Array.from(puzzle.childNodes).map(e => e.remove())
|
|
let p = document.createElement("p")
|
|
puzzle.appendChild(p)
|
|
p.classList.add("Error")
|
|
p.textContent = err
|
|
})
|
|
|
|
document.title = categoryName + " " + points
|
|
document.querySelector("body > h1").innerText = document.title
|
|
document.querySelector("input[name=cat]").value = categoryName
|
|
document.querySelector("input[name=points]").value = points
|
|
}
|
|
|
|
// Check to see if the answer might be correct
|
|
// This might be better done with the "constraint validation API"
|
|
// https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation#Validating_forms_using_JavaScript
|
|
function answerCheck(e) {
|
|
let answer = e.target.value
|
|
let ok = document.querySelector("#answer_ok")
|
|
|
|
// You have to provide someplace to put the check
|
|
if (! ok) {
|
|
return
|
|
}
|
|
|
|
let possiblyCorrect = false
|
|
let answerHash = djb2hash(answer)
|
|
for (let correctHash of window.puzzle.hashes) {
|
|
if (correctHash == answerHash) {
|
|
possiblyCorrect = true
|
|
}
|
|
}
|
|
|
|
if (possiblyCorrect) {
|
|
ok.textContent = ""
|
|
ok.title = "Possibly correct"
|
|
} else {
|
|
ok.textContent = "🙅"
|
|
ok.title = "Definitely not correct"
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
let params = new URLSearchParams(window.location.search)
|
|
let categoryName = params.get("cat")
|
|
let points = params.get("points")
|
|
let puzzleId = params.get("pid")
|
|
|
|
if (categoryName && points && puzzleId) {
|
|
loadPuzzle(categoryName, points, puzzleId)
|
|
}
|
|
|
|
let teamId = sessionStorage.getItem("id")
|
|
if (teamId) {
|
|
document.querySelector("input[name=id]").value = teamId
|
|
}
|
|
|
|
if (document.querySelector("#answer")) {
|
|
document.querySelector("#answer").addEventListener("input", answerCheck)
|
|
}
|
|
document.querySelector("form").addEventListener("submit", submit)
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init)
|
|
} else {
|
|
init()
|
|
}
|
|
|