Skip to content
Snippets Groups Projects
Select Git revision
  • master default
  • database/sp
  • database/sp1
3 results

quiz.js

Blame
  • quiz.js 6.78 KiB
    // This horrible file is distributed under the CC0 license.  Consult
    // ./LICENSE for more details.
    
    "use strict";
    const version = "$Id: quiz.js,v 1.17 2024/01/20 11:19:11 oj14ozun Exp oj14ozun $";
    
    const js_version  = document.getElementById("js-version");
    js_version.innerText = version;
    
    const gitlab_base = "https://gitlab.cs.fau.de/oj14ozun/sp-quiz";
    
    const answers     = document.getElementById("answers");
    const text        = document.getElementById("text");
    const media       = document.getElementById("media");
    const metadata    = document.getElementById("metadata-data");
    const info        = document.getElementById("info");
    const info_toggle = document.getElementById("info-toggle");
    const action      = document.getElementById("action");
    const stats       = document.getElementById("stats");
    const report      = document.getElementById("report");
    const correct     = document.getElementById("correct");
    const wrong       = document.getElementById("wrong");
    
    const class_list  = document.documentElement.classList;
    
    // toggle information panel
    info_toggle.onclick = _ => class_list.toggle("open");
    info_toggle.onkeydown = ev => ev.keyCode != 13;
    
    // state variables
    var questions;                  // quiz data
    var current;                    // current question
    var done  = {};                 // question object -> correct answer
    var opts  = [];                 // DOM objects of each answer
    
    var progress = JSON.parse(window.localStorage.getItem("progress")) || {};
    
    function shuf(arr) {            // shuffle arr using Fisher–Yates
        for (let i = 0; i < arr.length - 1; i++) {
            const j = Math.floor(Math.random() * arr.length);
            const tmp = arr[j];
            arr[j] = arr[i];
            arr[i] = tmp;
        }
    }
    
    function get_data(q) {
        return progress[q.id] || { "right": 0, "wrong": 0 };
    }
    
    function evaluate(q) {
        const data = get_data(q);
        return 2 * data["right"] - data["wrong"] + 4 * Math.random() - 2;
    }
    
    function update_stats(q) {
        const data = get_data(q);
    
        // update stats
        correct.innerText = data.right;
        wrong.innerText = data.wrong;
    }
    
    function remember(q, ok) {
        const data = get_data(q);
    
        if (ok) {
            data["right"]++;
        } else {
            data["wrong"]++;
        }
    
        progress[q.id] = data;
        window.sessionStorage.setItem("progress", progress);
    }
    
    function pick() {
        let choice = questions[0];
        let worst  = evaluate(choice);
    
        // FIXME: Replace this with a proper priority queue
        for (let i = 0; i < questions.length; i++) {
            const q = questions[i];
            let val = evaluate(q);
    
            if (val <= worst) {
                choice = q;
                worst = val;
            }
        }
    
        return choice;
    }
    
    // function to load random questions
    function next() {
        const q = pick();
    
        // load question
        text.innerHTML = q.question;
        // load metadata
        metadata.innerText = q.source;
        // load image, if specified
        if (q.media) {
            media.style.display = (media.src = q.media) ? "block" : "none";
        } else {
            media.style.display = "none";
        }
    
        // remove all previous questions
        while (answers.firstChild)
            answers.removeChild(answers.firstChild);
        opts = [];
    
        shuf(q.options);
    
        // insert questions
        for (let i = 0; i < q.options.length; i++) {
            const o = q.options[i];
            const li = document.createElement("li");
            const input = document.createElement("input");
            const answ = document.createElement("p");
            const comm = document.createElement("p");
    
            // prepare button
            input.type = q.multiple ? "checkbox" : "radio";
            input.name = "answer";
            input.onclick = _ => class_list.add("tried");
            action.disabled = false;
            opts[i] = input;
    
            // prepare answer
            answ.classList.add("option");
            answ.innerHTML = o.option;
            answ.onclick = _ => input.click();
    
            // prepare comment
            let fallback = "Falsch";
            comm.classList.add("comment");
            switch (o.correct) {
            case true:
                comm.classList.add("correct");
                fallback = "Wahr";
                break;
            case null:
                comm.classList.add("maybe");
                fallback = "Unsicher";
                break;
            }
    
            comm.innerHTML = o.comment || fallback;
    
            li.appendChild(input);
            li.appendChild(answ);
            li.appendChild(comm);
    
            answers.appendChild(li);
        }
    
        // set current question
        current = q;
    
        // unset answer mode
        class_list.remove("answer");
        class_list.remove("right");
    
        // display question metadata
        update_stats(q);
    
        // update report button
        let title = `Problem mit der Frage: "${text.innerText}" (${q.source})`;
        let desc = "(Ersetze diesen Text mit einer Beschreibung des Problems)"
        report.href = `${gitlab_base}/issues/new?issue[title]=${encodeURIComponent(title)}&issue[description]=${encodeURIComponent(desc)}`;
    }
    
    function submit() {
        let ok = true;
    
        // check all answers
        for (let i = 0; i < current.options.length; i++) {
            const checked = opts[i].checked;
            const correct = current.options[i].correct;
    
            if (correct != null && checked != correct) { // wrong answer
                ok = false;
            }
    
            opts[i].disabled = true;
        }
    
        // respond to result
        if (ok) {
            class_list.add("right");
        }
        remember(current, ok);
    
        // set answer mode
        class_list.add("answer");
        class_list.remove("tried");
        action.disabled = false;
    }
    
    // listen to keys
    document.onkeyup = evt => {
        switch (evt.code) {
            // shortcuts for first 10 options
        case "Digit1": if (0 in opts) opts[0].click(); break;
        case "Digit2": if (1 in opts) opts[1].click(); break;
        case "Digit3": if (2 in opts) opts[2].click(); break;
        case "Digit4": if (3 in opts) opts[3].click(); break;
        case "Digit5": if (4 in opts) opts[4].click(); break;
        case "Digit6": if (5 in opts) opts[5].click(); break;
        case "Digit7": if (6 in opts) opts[6].click(); break;
        case "Digit8": if (7 in opts) opts[7].click(); break;
        case "Digit9": if (8 in opts) opts[8].click(); break;
        case "Digit0": if (9 in opts) opts[9].click(); break;
    
        case "Space":
        case "Enter":               // shortcut for action button
            action.click();
            break;
    
        case "Escape":              // close info section
            document.documentElement.classList.remove("open");
            break;
        }
    };
    
    // setup action button
    action.onclick = _ => {
        if (class_list.contains("answer"))
            next();
        else if (class_list.contains("tried"))
            submit();
    }
    
    // query quiz data (unpleasant hack)
    (async function() {
        // query json specification of quiz
        questions  = await (await fetch("./quiz.json")).json();
        shuf(questions);
    
        // when done, load first question
        next();
    })();
    
    // Local Variables:
    // indent-tabs-mode: nil
    // End: