// jshint asi:true // prettify adds classes to various types, returning an HTML string. function prettify(key, val) { console.log(key, val) switch (key) { case "Body": return '[HTML]' } return val } // devel_addin drops a bunch of development extensions into element e. // It will only modify stuff inside e. function devel_addin(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 hobj = JSON.stringify(window.puzzle, prettify, 2) let d = e.appendChild(document.createElement("pre")) d.classList.add("object") d.innerHTML = hobj } // Hash routine used in v3.4 and earlier 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 } // The routine used to hash answers in compiled puzzle packages async function sha256Hash(message) { const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string return hashHex; } // Is the provided answer possibly correct? async function possiblyCorrect(answer) { let pattern = window.puzzle.Pre.AnswerPattern || [] for (let correctHash of window.puzzle.Pre.AnswerHashes) { if (djb2hash(answer) == correctHash) { return answer } for (let end = 0; end <= answer.length; end += 1) { if (pattern.includes("end") && (end != answer.length)) { continue } for (let beg = 0; beg < answer.length; beg += 1) { if (pattern.includes("begin") && (beg != 0)) { continue } let sub = answer.substring(beg, end) let digest = await sha256Hash(sub) if (digest == correctHash) { return sub } } } } return false } // 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() let data = new FormData(e.target) // Kludge for patterned answers let xAnswer = data.get("xAnswer") if (xAnswer) { data.set("answer", xAnswer) } window.data = data fetch("answer", { method: "POST", body: data, }) .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) }) } async function loadPuzzle(categoryName, points, puzzleId) { let puzzle = document.getElementById("puzzle") let base = "content/" + categoryName + "/" + puzzleId + "/" let resp = await fetch(base + "puzzle.json") if (! resp.ok) { console.log(resp) let err = await resp.text() Array.from(puzzle.childNodes).map(e => e.remove()) p = puzzle.appendChild(document.createElement("p")) p.classList.add("Error") p.textContent = err return } // Make the whole puzzle available window.puzzle = await resp.json() // Populate authors document.getElementById("authors").textContent = window.puzzle.Pre.Authors.join(", ") // If answers are provided, this is the devel server if (window.puzzle.Answers) { devel_addin(document.getElementById("devel")) } // Load scripts for (let script of (window.puzzle.Pre.Scripts || [])) { let st = document.createElement("script") document.head.appendChild(st) st.src = base + script } // List associated files for (let fn of (window.puzzle.Pre.Attachments || [])) { 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(window.puzzle.Pre.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 (window.puzzle.Pre.AnswerPattern) { document.querySelector("#answer").pattern = window.puzzle.Pre.AnswerPattern } // 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)) 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 } possiblyCorrect(answer) .then (correct => { document.querySelector("[name=xAnswer").value = correct || answer if (correct) { 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() }