/* Generic game workflow, from client's point of view: - send: {type: 'load'} -- when page loaded or reloaded - receive: {type: 'status', progress: ...} -- if the game just started - receive: {type: 'status', progress: ..., puzzle: data} -- in case of reload midgame - send: {type: 'next'} -- request puzzle - receive: {type: 'puzzle', puzzle: data, progress: ...} - display the puzzle and progress - wait for user to answer - send: {type: 'answer', 'answer': ...} - receive: {type: 'feedback', is_correct: ..., progress: ..., retries_left: ...} - display feedback and progress - if the anwser is wrong and retries allowed: - wait for new answer from user - lock submitting for `retry_delay` - wait for `puzzle_delay` seconds - request next puzzle - receive {type: 'status', progress: ..., iterations_left: 0} -- when max_iterations exhausted */ let isFrozen = false; // retry delay active, submitting is blocked let image = document.getElementById("captcha-img"); let input = document.getElementById("answer-inp"); let warn = document.getElementById("warning-txt"); let submit_btn = document.getElementById("send-btn"); let fields = { iter: document.getElementById("iter-txt"), solved: document.getElementById("solved-txt"), failed: document.getElementById("failed-txt"), }; document.addEventListener("DOMContentLoaded", (event) => { liveSend({ type: "load" }); // expect 'status' response }); function liveRecv(message) { switch (message.type) { case "status": if (message.puzzle) { // restoring existing state newPuzzle(message.puzzle); } else if (message.progress.iteration === 0) { // start of the game liveSend({ type: "next" }); } else if (message.iterations_left === 0) { // exhausted max iterations document.getElementById("form").submit(); } break; case "puzzle": newPuzzle(message.puzzle); break; case "feedback": showFeedback(message); if (message.is_correct === false && message.retries_left > 0) { // allow retry tempFreeze(js_vars.params.retry_delay); enableInput(); } else { moveForward(js_vars.params.puzzle_delay); } break; case "solution": cheat(message.solution); break; } if ("progress" in message) { // can be added to message of any type showProgress(message.progress); } } input.oninput = function (ev) { resetFeedback(); }; input.onkeydown = function (ev) { if (ev.key === "Enter") { submitAnswer(); } }; submit_btn.onclick = function (ev) { submitAnswer(); }; function newPuzzle(data) { showPuzzle(data); resetFeedback(); resetInput(); enableInput(); } function resetPuzzle() { // a trick to avoid image collapsing/blinking image.width = image.width; image.height = image.height; image.src = ""; } function showPuzzle(data) { image.src = data.image; } function resetInput() { input.value = ""; } function lockInput() { input.disabled = true; input.blur(); } function enableInput() { input.disabled = false; input.focus(); } function submitAnswer() { if (isFrozen || input.value === "") return; lockInput(); resetFeedback(); liveSend({ type: "answer", answer: input.value }); } function resetFeedback() { input.classList.remove("is-valid", "is-invalid"); } function showFeedback(data) { input.classList.add(data.is_correct ? "is-valid" : "is-invalid"); } function moveForward(wait) { window.setTimeout(gotoNext, wait * 1000); } function gotoNext() { resetPuzzle(); resetInput(); resetFeedback(); liveSend({ type: "next" }); } function showProgress(data) { fields.iter.textContent = data.iteration; fields.solved.textContent = data.num_correct; fields.failed.textContent = data.num_incorrect; } function waitMsg(seconds) { warn.textContent = `Wait ${Math.round(seconds)} seconds before trying again`; } function setFrozen(val) { // freeze/unfreeze submitting submit_btn.disabled = val; isFrozen = val; } function tempFreeze(countdown) { setFrozen(true); waitMsg(countdown); let timer = window.setInterval(function () { countdown -= 1; if (countdown > 0) { waitMsg(countdown); } else { setFrozen(false); warn.textContent = ""; clearInterval(timer); } }, 1000); }