Implement BugHunter game
mindflux-ci / build (push) Successful in 23s

This commit is contained in:
AliceDarkest
2026-07-13 20:27:38 +02:00
parent ea1ba38cf0
commit dbd1960424
27 changed files with 612 additions and 9 deletions
+72
View File
@@ -0,0 +1,72 @@
(function () {
'use strict';
const view = document.getElementById('bhView');
let catalog = [], session, index = 0, started = 0, selectedFile = '', selectedLine = 0, selected = '', options = [], answers = [];
const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const api = (url, opt) => fetch('/api/bughunter' + url, {headers:{'Content-Type':'application/json'}, ...opt}).then(async response => { const body = await response.json(); if (!response.ok) throw new Error(body.message || 'Serverfehler'); return body; });
const post = (url, body) => api(url, {method:'POST', body:JSON.stringify(body)});
const seconds = () => Math.max(1, Math.round((Date.now() - started) / 1000));
const current = () => session.bugs[index];
const state = () => answers[index];
function loadCatalog() { api('/hunts').then(data => { catalog = data.hunts || []; renderCatalog(); }).catch(error); }
function renderCatalog() {
view.innerHTML = `<section class="bh-hero"><span class="bh-stamp">VERTRAULIC</span><span class="bh-kicker">DETektivbüro · DEBUGGING-FALLAKTEN</span><h1>Welche Spur verfolgst du?</h1><p>Arbeite dich durch echte Fehlerbilder: Ort, Kategorie und Fix müssen zusammenpassen. Präzision baut deine Ermittlungsserie auf.</p></section><section class="bh-grid">${catalog.map(h => `<article class="bh-case"><span class="bh-case-no">${esc(h.examScope)} · ${esc(h.difficulty).toUpperCase()}</span><h2>${esc(h.title)}</h2><p>${esc(h.description)}</p><div class="bh-tags"><span>${esc(h.language)}</span><span>${h.fileCount} Dateien</span><span>${h.bugCount} Bug${h.bugCount === 1 ? '' : 's'}</span></div><button class="bh-btn primary" data-start="${esc(h.slug)}">FALL ÖFFNEN</button></article>`).join('')}</section>`;
view.querySelectorAll('[data-start]').forEach(button => button.addEventListener('click', () => begin(button.dataset.start)));
}
function begin(slug) { api('/session/' + encodeURIComponent(slug)).then(data => { if (!data.found) throw new Error('Fallakte nicht verfügbar.'); session = data; index = 0; answers = session.bugs.map(() => ({locate:null, classify:null, fix:null})); started = Date.now(); renderBriefing(); }).catch(error); }
function renderBriefing() {
view.innerHTML = `<section class="bh-briefing"><span class="bh-stamp">NEUE AKTE</span><span class="bh-kicker">${esc(session.examScope)} · ${esc(session.difficulty).toUpperCase()} · ${esc(session.language)}</span><h1>${esc(session.title)}</h1><p>${esc(session.briefing)}</p><pre>${esc(session.consoleOutput)}</pre><div class="bh-rules"><div><b>1</b> Ort markieren</div><div><b>2</b> Fehler einordnen</div><div><b>3</b> sauberen Fix wählen</div></div><button class="bh-btn primary" id="bhStart">ERMITTLUNG STARTEN</button></section>`;
document.getElementById('bhStart').addEventListener('click', () => { selectedFile = session.files[0]?.filename || ''; selectedLine = 0; selected = ''; options = []; renderHunt(); });
}
function activePhase() { const s = state(); return !s.locate?.done ? 'locate' : !s.classify?.done ? 'classify' : 'fix'; }
function renderHunt() {
const bug = current(), s = state(), phase = activePhase();
const evidence = bug.evidence || 'Untersuche die Fallakte und folge den Spuren.';
view.innerHTML = `<div class="bh-hunt-head"><div><span class="bh-kicker">AKTE ${index + 1} / ${session.bugs.length} · ${esc(session.difficulty).toUpperCase()}</span><h1>${esc(session.title)}</h1></div><div class="bh-combo">${comboText()}<small>saubere Serie</small></div></div><div class="bh-progress">${session.bugs.map((_, i) => `<i class="${i < index ? 'done' : i === index ? 'current' : ''}">${i + 1}</i>`).join('')}</div><div class="bh-layout"><section class="bh-code-area"><div class="bh-tabs">${session.files.map(file => `<button class="${file.filename === selectedFile ? 'active' : ''}" data-file="${esc(file.filename)}">${esc(file.filename)}</button>`).join('')}</div>${renderCode(phase)} </section><aside class="bh-side"><div class="bh-evidence"><span class="bh-stamp">BEWEISSTÜCK</span><p>${esc(evidence)}</p><pre>${esc(session.consoleOutput)}</pre></div>${renderAction(phase, bug, s)}</aside></div>`;
view.querySelectorAll('[data-file]').forEach(tab => tab.addEventListener('click', () => { selectedFile = tab.dataset.file; selectedLine = 0; renderHunt(); }));
bindAction(phase, bug);
}
function renderCode(phase) {
const file = session.files.find(f => f.filename === selectedFile) || session.files[0];
if (!file) return '<p>Keine Datei in der Akte.</p>';
return `<div class="bh-code" aria-label="Code-Datei ${esc(file.filename)}">${file.lines.map((line, i) => { const lineNo = i + 1, marked = phase === 'locate' && selectedLine === lineNo; return `<button class="bh-line ${marked ? 'selected' : ''}" data-line="${lineNo}" ${phase === 'locate' ? '' : 'disabled'}><span>${lineNo}</span><code>${esc(line) || ' '}</code></button>`; }).join('')}</div>`;
}
function renderAction(phase, bug, s) {
const labels = {locate:'1 · ORTEN', classify:'2 · KLASSIFIZIEREN', fix:'3 · FIXEN'};
let content = '';
if (phase === 'locate') content = `<p>Klicke die fehlerhafte Zeile im Code an.</p><p class="bh-selected">${selectedLine ? `${esc(selectedFile)}:${selectedLine}` : 'Noch keine Zeile markiert'}</p>`;
if (phase === 'classify') content = `<p>Welche Fehlerklasse erklärt den Fund am besten?</p><div class="bh-options">${[['syntax','Syntax'],['logik','Logik'],['laufzeit','Laufzeit'],['sicherheit','Sicherheit']].map(([value,label]) => `<button data-choice="${value}" class="${selected === value ? 'selected' : ''}">${label}</button>`).join('')}</div>`;
if (phase === 'fix') content = `<p>Wähle die Zeile, die den Fehler sauber behebt.</p><div class="bh-options">${options.map(value => `<button data-choice="${esc(value)}" class="${selected === value ? 'selected' : ''}"><code>${esc(value)}</code></button>`).join('')}</div>`;
const attempts = s[phase]?.attempts || 0;
return `<section class="bh-action"><span class="bh-kicker">${labels[phase]}</span><h2>${phase === 'locate' ? 'Wo liegt der Bug?' : phase === 'classify' ? 'Wie heißt das Problem?' : 'Wie wird er behoben?'}</h2>${content}<p class="bh-attempts">Versuche: ${attempts}/3</p><div id="bhFeedback" class="bh-feedback" aria-live="polite"></div><div class="bh-actions"><button class="bh-btn primary" id="bhCheck">PRÜFEN</button><button class="bh-btn" id="bhReveal">AUFLÖSEN</button></div></section>`;
}
function bindAction(phase, bug) {
if (phase === 'locate') view.querySelectorAll('[data-line]').forEach(line => line.addEventListener('click', () => { selectedLine = Number(line.dataset.line); renderHunt(); }));
view.querySelectorAll('[data-choice]').forEach(button => button.addEventListener('click', () => { selected = button.dataset.choice; view.querySelectorAll('[data-choice]').forEach(item => item.classList.remove('selected')); button.classList.add('selected'); }));
document.getElementById('bhCheck').addEventListener('click', () => submit(phase, bug, false));
document.getElementById('bhReveal').addEventListener('click', () => submit(phase, bug, true));
}
function submission(phase) { return phase === 'locate' ? {file:selectedFile, line:selectedLine} : phase === 'classify' ? {category:selected} : {choice:selected}; }
function submit(phase, bug, reveal) {
const sub = submission(phase); if (!reveal && ((phase === 'locate' && !sub.line) || (phase !== 'locate' && !selected))) { feedback('Triff zuerst eine Auswahl.', 'bad'); return; }
const phaseState = state()[phase] || {attempts:0}; phaseState.attempts = Math.min(3, phaseState.attempts + 1); phaseState.submission = sub;
post('/check', {bugId:bug.bugId, phase, reveal, submission:sub}).then(result => {
if (!result.resolved) { state()[phase] = phaseState; feedback('Noch nicht stimmig. Lies die Beweise erneut.', 'bad'); return; }
phaseState.revealed = reveal; phaseState.done = true; state()[phase] = phaseState;
if (phase === 'classify') options = result.fixOptions || []; if (phase === 'fix') { showDebrief(result); return; }
selected = ''; if (phase === 'locate') selectedLine = result.location?.line || selectedLine;
feedback(reveal ? 'Auflösung notiert – weiter zur nächsten Phase.' : 'Spur bestätigt – weiter zur nächsten Phase.', 'ok'); setTimeout(renderHunt, 650);
}).catch(err => feedback(err.message, 'bad'));
}
function showDebrief(result) {
const bug = current(); view.innerHTML = `<section class="bh-debrief"><span class="bh-stamp">GELÖST</span><span class="bh-kicker">AKTE ${index + 1} ABGESCHLOSSEN</span><h1>Die Spur ist gesichert.</h1><p>${esc(result.explanation || 'Die Fallakte wurde aktualisiert.')}</p><div class="bh-diff"><span>− fehlerhafte Zeile</span><code>${esc(selectedFile)}:${state().locate?.submission?.line || '?'}</code><span>+ Korrektur</span><code>${esc(result.fixedLine || '')}</code></div><button class="bh-btn primary" id="bhNext">${index + 1 < session.bugs.length ? 'NÄCHSTE AKTE' : 'ERGEBNIS AUSWERTEN'}</button></section>`;
document.getElementById('bhNext').addEventListener('click', () => { index++; selected = ''; selectedLine = 0; options = []; if (index >= session.bugs.length) finish(); else { selectedFile = session.files[0]?.filename || ''; renderHunt(); } });
}
function finish() { post('/finish', {slug:session.slug, durationSeconds:seconds(), items:answers.map((answer, i) => ({bugId:session.bugs[i].bugId, locate:strip(answer.locate), classify:strip(answer.classify), fix:strip(answer.fix)}))}).then(renderResult).catch(error); }
function strip(value) { return value ? {submission:value.submission, attempts:value.attempts, revealed:!!value.revealed} : null; }
function comboText() { let combo = 0; for (let i = 0; i < index; i++) { const a = answers[i]; const clean = ['locate','classify','fix'].every(key => a[key] && a[key].attempts === 1 && !a[key].revealed); combo = clean ? combo + 1 : 0; } return `×${combo}`; }
function renderResult(result) { view.innerHTML = `<section class="bh-result"><span class="bh-stamp">ERMITTLUNG ABGESCHLOSSEN</span><h1>Fallakten geschlossen.</h1><div class="bh-score">${result.score}</div><p class="bh-stars">${'★'.repeat(result.stars)}${'☆'.repeat(3-result.stars)}</p><div class="bh-stats"><div><b>${result.baseScore}</b>Basis</div><div><b>+${result.comboBonus}</b>Combo</div><div><b>${result.bugsSolved}/${result.totalBugs}</b>gelöst</div><div><b>×${result.maxCombo}</b>Serie</div><div><b>${result.highscore}</b>Highscore</div><div><b>${result.plays}</b>Akten</div></div><button class="bh-btn primary" id="bhAgain">WEITERE FÄLLE</button></section>`; document.getElementById('bhAgain').addEventListener('click', loadCatalog); }
function feedback(message, type) { const target = document.getElementById('bhFeedback'); if (target) { target.textContent = message; target.className = 'bh-feedback ' + type; } }
function error(err) { view.innerHTML = `<section class="bh-result"><h1>AKTE NICHT LESBAR</h1><p>${esc(err.message || err)}</p><button class="bh-btn" id="bhReload">ERNEUT VERSUCHEN</button></section>`; document.getElementById('bhReload').addEventListener('click', loadCatalog); }
loadCatalog();
})();