CodeEscape als serverautoritativen Spielmodus umsetzen
mindflux-ci / build (push) Successful in 21s

This commit is contained in:
AliceDarkest
2026-07-12 22:24:47 +02:00
parent 986f6226ef
commit edb5b36408
31 changed files with 192 additions and 5 deletions
@@ -0,0 +1,37 @@
{"scenarios":[
{"slug":"verlassener-serverraum","title":"Der verlassene Serverraum","difficulty":"leicht","examScope":"AP1","description":"Zahlensysteme, Ports und Netzwerkgrundlagen öffnen den Weg nach draußen.","timeLimitSeconds":900,"rooms":[
{"title":"Rack 01","intro":"Notlicht flackert über einem verriegelten Rack. Das Terminal verlangt drei Fragmente.","doorPrompt":"Alle Fragmente in Fundreihenfolge eingeben.","doorCode":"4122SYN","successText":"Das Rack entriegelt. Hinter der Rückwand führt ein Wartungsgang weiter.","puzzles":[
{"type":"code","prompt":"Binärterminal","points":100,"fragment":"41","payload":{"narrative":"Auf dem Display pulsiert 0b101001.","instruction":"Wandle die Binärzahl in Dezimal um.","placeholder":"Dezimalwert","solutions":["41"],"hints":["Gewichte von rechts: 1, 2, 4, 8, 16, 32.","32 + 8 + 1"],"explanation":"101001₂ entspricht 32+8+1 = 41."}},
{"type":"choice","prompt":"Offener Wartungsport","points":100,"fragment":"22","payload":{"instruction":"Welcher Standardport gehört zu SSH?","options":["21","22","23","25"],"correct":"22","hints":["SSH ersetzt unsichere Fernwartung per Telnet.","Der Port liegt direkt nach FTP."],"explanation":"SSH nutzt standardmäßig TCP-Port 22."}},
{"type":"order","prompt":"TCP-Verbindung","points":120,"fragment":"SYN","payload":{"instruction":"Bringe den TCP-Drei-Wege-Handshake in Reihenfolge.","items":["SYN","SYN-ACK","ACK"],"solution":["SYN","SYN-ACK","ACK"],"hints":["Der Client beginnt mit einer Synchronisationsanfrage.","Die Mitte bestätigt und synchronisiert zugleich."],"explanation":"SYN → SYN-ACK → ACK."}}]},
{"title":"Patchfeld","intro":"Lose Leitungen hängen über dem Boden. Ein altes Patchpanel blockiert die Schleuse.","doorPrompt":"Setze alle Fragmente in Fundreihenfolge zusammen.","doorCode":"6253UP","successText":"Die Link-LEDs springen an und die Schleuse gleitet auf.","puzzles":[
{"type":"code","prompt":"Subnetz-Kapazität","points":120,"fragment":"62","payload":{"instruction":"Wie viele nutzbare IPv4-Hosts bietet ein /26-Netz klassisch?","solutions":["62"],"hints":["Ein /26 lässt 6 Hostbits.","2⁶ Adressen minus Netz und Broadcast."],"explanation":"64 Gesamtadressen minus 2 = 62 Hosts."}},
{"type":"match","prompt":"Dienste verbinden","points":120,"fragment":"53","payload":{"instruction":"Ordne Dienst und Standardport zu.","pairs":[["DNS","53"],["HTTP","80"],["HTTPS","443"]],"hints":["Web unverschlüsselt liegt auf 80.","DNS nutzt 53, TLS-Web 443."],"explanation":"DNS 53, HTTP 80, HTTPS 443."}},
{"type":"choice","prompt":"Kabeldiagnose","points":100,"fragment":"UP","payload":{"instruction":"Welche Anzeige bestätigt eine physische Ethernet-Verbindung?","options":["Link LED","DNS Cache","Default Route","TTL"],"correct":"Link LED","hints":["Gesucht ist Schicht 1.","Sie sitzt meist direkt an der Buchse."],"explanation":"Die Link-LED zeigt den physischen Link."}}]},
{"title":"Kontrollraum","intro":"Die Hauptkonsole wartet auf die letzte Startsequenz.","doorPrompt":"Nutze die Fragmente der beiden Kontrollrätsel.","doorCode":"DHCP67","successText":"Die Außentür öffnet sich. Frische Nachtluft strömt herein.","puzzles":[
{"type":"order","prompt":"DHCP-Ablauf","points":140,"fragment":"DHCP","payload":{"instruction":"Sortiere den DORA-Ablauf.","items":["Discover","Offer","Request","Acknowledge"],"solution":["Discover","Offer","Request","Acknowledge"],"hints":["Der Client sucht zuerst.","Discover, Offer, Request, Acknowledge."],"explanation":"DORA beschreibt die DHCP-Adressvergabe."}},
{"type":"code","prompt":"DHCP-Serverport","points":100,"fragment":"67","payload":{"instruction":"Welchen UDP-Port nutzt der DHCP-Server?","solutions":["67"],"hints":["Client und Server verwenden zwei benachbarte Ports.","Der Server nutzt den kleineren: 67."],"explanation":"DHCP-Server: UDP 67, Client: UDP 68."}}]}]},
{"slug":"lockdown-rechenzentrum","title":"Lockdown im Rechenzentrum","difficulty":"mittel","examScope":"AP2_FISI","description":"RAID, USV, Backup und Virtualisierung unter Zeitdruck.","timeLimitSeconds":1100,"rooms":[
{"title":"Stromschleuse","intro":"Die USV hält nur noch wenige Minuten. Das Energiemanagement ist verriegelt.","doorPrompt":"Gib alle drei Fragmente in Fundreihenfolge ein.","doorCode":"3TBUSVR5","successText":"Die Last wechselt auf den sicheren Pfad.","puzzles":[
{"type":"code","prompt":"RAID-5-Kapazität","points":150,"fragment":"3TB","payload":{"instruction":"Vier 1-TB-Platten im RAID 5: nutzbare Kapazität?","solutions":["3 TB","3TB","3"],"hints":["Eine Plattenkapazität geht für Parität verloren.","(4−1) × 1 TB"],"explanation":"RAID 5 nutzt n−1 Plattenkapazitäten."}},
{"type":"choice","prompt":"USV-Typ","points":140,"fragment":"USV","payload":{"instruction":"Welche USV entkoppelt Verbraucher dauerhaft über den Wechselrichter?","options":["Offline","Line-interactive","Online-Doppelwandler","Steckdosenleiste"],"correct":"Online-Doppelwandler","hints":["Die Last wird permanent neu erzeugt.","VFI/Online-Doppelwandler."],"explanation":"Online-USV: Netz → Gleichrichter → Zwischenkreis → Wechselrichter."}},
{"type":"match","prompt":"RAID-Eigenschaften","points":140,"fragment":"R5","payload":{"instruction":"Ordne Level und Kerneigenschaft.","pairs":[["RAID 0","Striping"],["RAID 1","Spiegelung"],["RAID 5","verteilte Parität"]],"hints":["RAID 1 dupliziert Daten.","RAID 5 verteilt Paritätsinformationen."],"explanation":"Die Zuordnung beschreibt die Grundverfahren."}}]},
{"title":"Backup-Tresor","intro":"Ein Ransomware-Alarm sperrt das Backup-System.","doorPrompt":"Verbinde Backupfolge und Schutzprinzip.","doorCode":"VOLL321","successText":"Das unveränderliche Backup wird freigegeben.","puzzles":[
{"type":"order","prompt":"GFS-Wiederherstellung","points":160,"fragment":"VOLL","payload":{"instruction":"Sortiere eine Wiederherstellung mit inkrementellen Backups.","items":["Vollsicherung","Inkrement 1","Inkrement 2","Inkrement 3"],"solution":["Vollsicherung","Inkrement 1","Inkrement 2","Inkrement 3"],"hints":["Basis ist immer das letzte Vollbackup.","Danach jedes Inkrement chronologisch."],"explanation":"Inkrementelle Ketten benötigen Vollbackup plus alle Folgeinkremente."}},
{"type":"code","prompt":"3-2-1-Regel","points":140,"fragment":"321","payload":{"instruction":"Welche drei Ziffern beschreiben Kopien, Medientypen und externen Standort?","solutions":["321","3-2-1"],"hints":["Drei Kopien auf zwei Medien.","Eine Kopie außer Haus."],"explanation":"3 Kopien, 2 Medientypen, 1 extern."}}]},
{"title":"Hypervisor-Kern","intro":"Die Managementkonsole meldet Ressourcenüberbuchung.","doorPrompt":"Kombiniere alle Fragmente in Fundreihenfolge.","doorCode":"TYPE1NOSNAPISO","successText":"Der Cluster übernimmt und der Lockdown endet.","puzzles":[
{"type":"choice","prompt":"Bare-Metal-Hypervisor","points":150,"fragment":"TYPE1","payload":{"instruction":"Welcher Hypervisor läuft direkt auf der Hardware?","options":["Typ 1","Typ 2","Container Runtime","JVM"],"correct":"Typ 1","hints":["Kein Hostbetriebssystem dazwischen.","Auch Bare-Metal genannt."],"explanation":"Typ-1-Hypervisor laufen direkt auf der Hardware."}},
{"type":"code","prompt":"Snapshot-Falle","points":150,"fragment":"NOSNAP","payload":{"instruction":"Ist ein VM-Snapshot ein vollwertiges Backup? Antworte ja oder nein.","solutions":["nein","no"],"hints":["Snapshot und Basisdatenträger bleiben gekoppelt.","Ein Defekt des Datastores betrifft beides."],"explanation":"Snapshots ersetzen kein unabhängiges Backup."}},
{"type":"match","prompt":"Virtualisierungsgrenzen","points":150,"fragment":"ISO","payload":{"instruction":"Ordne Technik und Isolation.","pairs":[["VM","eigener Kernel"],["Container","geteilter Host-Kernel"],["VLAN","logische Netzsegmentierung"]],"hints":["Container teilen den Kernel.","VLAN segmentiert das Netz, nicht Prozesse."],"explanation":"VM, Container und VLAN isolieren auf verschiedenen Ebenen."}}]}]},
{"slug":"gehacktes-buero","title":"Das gehackte Büro","difficulty":"schwer","examScope":"CROSS","description":"Zertifikate, Kryptografie, SQL und Incident Response auf AP2-Niveau.","timeLimitSeconds":1200,"rooms":[
{"title":"Forensik-Labor","intro":"Ein kompromittierter Client hält die Beweise verschlüsselt.","doorPrompt":"Setze alle Fragmente in Fundreihenfolge zusammen.","doorCode":"SHA256443CIA","successText":"Das Beweisarchiv wird lesbar.","puzzles":[
{"type":"choice","prompt":"Hashverfahren","points":180,"fragment":"SHA256","payload":{"instruction":"Welches Verfahren ist für Integritätsprüfungen heute geeignet?","options":["MD5","SHA-1","SHA-256","ROT13"],"correct":"SHA-256","hints":["MD5 und SHA-1 gelten als kollisionsgeschwächt.","Die gesuchte SHA-2-Variante hat 256 Bit."],"explanation":"SHA-256 ist eine verbreitete kryptografische Hashfunktion."}},
{"type":"code","prompt":"TLS-Port","points":140,"fragment":"443","payload":{"instruction":"Standardport für HTTPS?","solutions":["443"],"hints":["HTTP nutzt 80.","HTTPS nutzt 443."],"explanation":"HTTPS verwendet TCP 443."}},
{"type":"match","prompt":"Schutzziele","points":170,"fragment":"CIA","payload":{"instruction":"Ordne CIA-Schutzziel und deutsche Bedeutung.","pairs":[["Confidentiality","Vertraulichkeit"],["Integrity","Integrität"],["Availability","Verfügbarkeit"]],"hints":["CIA ist nicht der Nachrichtendienst in diesem Kontext.","C=Confidentiality, I=Integrity, A=Availability."],"explanation":"Die CIA-Triade bildet zentrale Informationssicherheitsziele."}}]},
{"title":"Datenbank-Bunker","intro":"Die Angreifer haben Spuren in einer unsicheren Suchmaske hinterlassen.","doorPrompt":"Verbinde SQL-Schutz und Transaktionsfragment.","doorCode":"PREPAREDACID","successText":"Der Datenbankzugang ist wieder unter Kontrolle.","puzzles":[
{"type":"choice","prompt":"SQL-Injection stoppen","points":190,"fragment":"PREPARED","payload":{"instruction":"Welche Maßnahme trennt SQL-Code zuverlässig von Eingabedaten?","options":["Prepared Statements","HTML-Escaping","Base64","Mehr RAM"],"correct":"Prepared Statements","hints":["Parameter werden gebunden statt konkateniert.","Prepared Statements."],"explanation":"Parametrisierte Statements verhindern das Einschleusen von SQL-Syntax."}},
{"type":"order","prompt":"Sichere Transaktion","points":180,"fragment":"ACID","payload":{"instruction":"Ordne den vereinfachten Ablauf.","items":["BEGIN","Änderungen ausführen","Validieren","COMMIT"],"solution":["BEGIN","Änderungen ausführen","Validieren","COMMIT"],"hints":["Transaktion öffnen, arbeiten, prüfen, bestätigen.","COMMIT steht am Ende."],"explanation":"Erst nach erfolgreicher Validierung wird committed."}}]},
{"title":"Incident-Leitstand","intro":"Die letzte Tür verlangt eine saubere Reaktion statt hektischer Einzelaktionen.","doorPrompt":"Setze alle Fragmente in Fundreihenfolge zusammen.","doorCode":"ISOLATECALEAST","successText":"Die Systeme sind eingedämmt. Der Ausgang ist frei.","puzzles":[
{"type":"order","prompt":"Incident Response","points":200,"fragment":"ISOLATE","payload":{"instruction":"Sortiere die ersten Reaktionsschritte.","items":["Erkennen","Eindämmen","Beweise sichern","Bereinigen","Wiederherstellen"],"solution":["Erkennen","Eindämmen","Beweise sichern","Bereinigen","Wiederherstellen"],"hints":["Vor Bereinigung muss der Vorfall eingedämmt und dokumentiert sein.","Erkennen → Eindämmen → Beweise → Bereinigen → Wiederherstellen."],"explanation":"Die Reihenfolge erhält Beweise und begrenzt Schaden."}},
{"type":"choice","prompt":"Zertifikatsvertrauen","points":180,"fragment":"CA","payload":{"instruction":"Wer bestätigt die Bindung zwischen Identität und öffentlichem Schlüssel?","options":["Certificate Authority","DHCP-Server","Hypervisor","Switch"],"correct":"Certificate Authority","hints":["Sie signiert Zertifikate.","Certificate Authority (CA)."],"explanation":"Eine CA validiert und signiert Zertifikate."}},
{"type":"code","prompt":"Prinzip der Rechte","points":160,"fragment":"LEAST","payload":{"instruction":"Ergänze: Principle of ___ Privilege.","solutions":["least"],"caseSensitive":false,"hints":["Nur minimal notwendige Rechte.","Das englische Wort bedeutet ‚am wenigsten‘."],"explanation":"Least Privilege reduziert Angriffsfläche."}}]}]}]}
@@ -0,0 +1,27 @@
CREATE TABLE escape_scenarios (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
slug VARCHAR(150) NOT NULL UNIQUE,
title VARCHAR(200) NOT NULL,
difficulty VARCHAR(30) NOT NULL DEFAULT 'leicht',
exam_scope VARCHAR(20) NOT NULL DEFAULT 'AP1',
description VARCHAR(1200),
time_limit_seconds INT NOT NULL DEFAULT 900,
sort_order INT NOT NULL DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT TRUE,
CONSTRAINT chk_escape_exam_scope CHECK (exam_scope IN ('AP1','AP2_FIAE','AP2_FISI','CROSS'))
);
CREATE TABLE escape_rooms (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
scenario_id BIGINT NOT NULL REFERENCES escape_scenarios(id) ON DELETE CASCADE,
title VARCHAR(200) NOT NULL, intro VARCHAR(2000), door_prompt VARCHAR(500),
door_code VARCHAR(50) NOT NULL, success_text VARCHAR(1000), sort_order INT NOT NULL DEFAULT 0
);
CREATE TABLE escape_puzzles (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
room_id BIGINT NOT NULL REFERENCES escape_rooms(id) ON DELETE CASCADE,
puzzle_type VARCHAR(30) NOT NULL, prompt VARCHAR(500) NOT NULL,
points INT NOT NULL DEFAULT 100, code_fragment VARCHAR(20) NOT NULL,
sort_order INT NOT NULL DEFAULT 0, payload VARCHAR(8000) NOT NULL
);
CREATE INDEX idx_escape_rooms_scenario ON escape_rooms(scenario_id);
CREATE INDEX idx_escape_puzzles_room ON escape_puzzles(room_id);
File diff suppressed because one or more lines are too long
+2
View File
@@ -123,6 +123,8 @@ a { color:var(--secondary); }
.mf-play { margin-top:.9rem; align-self:flex-start; display:inline-flex; align-items:center; gap:.45rem; font-size:.74rem; font-weight:800; letter-spacing:.08em; color:var(--accent); border:1px solid var(--border-accent); border-radius:8px; padding:.5rem .85rem; text-decoration:none; transition:all .18s; }
.mf-card:hover .mf-play { background:var(--accent); color:var(--accent-ink); box-shadow:0 0 16px var(--glow-s); }
.mf-play svg { width:14px; height:14px; }
.mf-card-codeescape { --accent:var(--esc-accent); --glow-s:var(--esc-glow); --glow-w:color-mix(in srgb,var(--esc-accent) 16%,transparent); background:repeating-linear-gradient(0deg,rgba(255,255,255,.015) 0 1px,transparent 1px 4px),var(--surface); }
.mf-card-codeescape::after { content:"ACCESS"; position:absolute; right:12px; top:9px; color:var(--esc-accent); font:700 .52rem/1 "Courier New",monospace; letter-spacing:.12em; opacity:.6; }
/* ===== Panels / Widgets ===== */
.mf-panel { background:var(--surface); border:1px solid var(--border); border-radius:14px; padding:1.1rem; box-shadow:var(--g-soft); }
@@ -0,0 +1,17 @@
(function(){'use strict';const view=document.getElementById('escView'),timerEl=document.getElementById('escTimer');let catalog=[],session=null,roomIndex=0,start=0,tickId=null,answers=new Map(),fragments=new Map(),selected=null;
const api=(url,opt)=>fetch('/api/codeescape'+url,{headers:{'Content-Type':'application/json'},...opt}).then(async r=>{const j=await r.json();if(!r.ok)throw new Error(j.message||'Serverfehler');return j});
function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}function post(url,data){return api(url,{method:'POST',body:JSON.stringify(data)});}function seconds(){return Math.max(1,Math.round((Date.now()-start)/1000));}
function startTimer(){clearInterval(tickId);start=Date.now();tickId=setInterval(()=>{let left=session.timeLimitSeconds-seconds();timerEl.textContent=(left<0?'+':'')+fmt(Math.abs(left));timerEl.classList.toggle('alarm',left<=0);},1000);}function fmt(s){return String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0');}
function loadCatalog(){api('/scenarios').then(d=>{catalog=d.scenarios;renderLobby();}).catch(error);}
function renderLobby(){timerEl.textContent='--:--';view.innerHTML='<div class="esc-hero"><span class="esc-eyebrow">FACILITY ARCHIVE // 03 SZENARIEN</span><h1>Wähle deinen Fluchtweg</h1><p>Jeder Raum enthält IT-Rätsel. Sichere die Code-Fragmente, öffne die Schleusen und entkomme, bevor der Zeitbonus verfällt.</p></div><div class="esc-grid">'+catalog.map(s=>`<article class="esc-card"><span class="esc-eyebrow">${esc(s.examScope)} · ${esc(s.difficulty)}</span><h2>${esc(s.title)}</h2><p>${esc(s.description)}</p><div class="esc-meta"><span class="esc-chip">${s.roomCount} Räume</span><span class="esc-chip">${Math.round(s.timeLimitSeconds/60)} Minuten</span></div><button class="esc-btn" data-start="${esc(s.slug)}">FACILITY BETRETEN</button></article>`).join('')+'</div>';view.querySelectorAll('[data-start]').forEach(b=>b.onclick=()=>begin(b.dataset.start));}
function begin(slug){api('/session/'+encodeURIComponent(slug)).then(d=>{session=d;roomIndex=0;answers.clear();fragments.clear();startTimer();renderRoom();}).catch(error);}
function renderRoom(){const r=session.rooms[roomIndex];view.innerHTML=`<div class="esc-room-head"><div><span class="esc-eyebrow">RAUM ${roomIndex+1} / ${session.rooms.length} · ${esc(session.examScope)}</span><h1>${esc(r.title)}</h1><p>${esc(r.intro)}</p></div><div class="esc-progress">${'● '.repeat(roomIndex+1)}${'○ '.repeat(session.rooms.length-roomIndex-1)}</div></div><div class="esc-puzzles">${r.puzzles.map((p,i)=>`<button class="esc-puzzle ${answers.has(p.puzzleId)?'done':''}" data-puzzle="${p.puzzleId}"><span class="esc-eyebrow">NODE ${String(i+1).padStart(2,'0')} · ${esc(p.type)}</span><h3>${esc(p.prompt)}</h3><div class="status">${answers.has(p.puzzleId)?'✓ FRAGMENT GESICHERT':'ZUGRIFF AUSSTEHEND'}</div></button>`).join('')}</div><div class="esc-inventory"><span class="esc-eyebrow">FRAGMENT-INVENTAR</span><div class="esc-fragments">${r.puzzles.map(p=>`<span class="esc-fragment">${fragments.has(p.puzzleId)?esc(fragments.get(p.puzzleId)):'•••'}</span>`).join('')}</div></div><div class="esc-door"><label>${esc(r.doorPrompt)}<input class="esc-input" id="doorCode" autocomplete="off" ${r.puzzles.every(p=>answers.has(p.puzzleId))?'':'disabled'}></label><button class="esc-btn primary" id="doorBtn" ${r.puzzles.every(p=>answers.has(p.puzzleId))?'':'disabled'}>SCHLEUSE ÖFFNEN</button><button class="esc-btn danger" id="quitBtn">AUFGEBEN</button></div>`;view.querySelectorAll('[data-puzzle]').forEach(b=>b.onclick=()=>openPuzzle(r.puzzles.find(p=>p.puzzleId==b.dataset.puzzle)));document.getElementById('doorBtn').onclick=()=>openDoor(r);document.getElementById('quitBtn').onclick=()=>finish(false);}
function control(p){if(p.type==='code')return `<input class="esc-input" id="answer" placeholder="${esc(p.placeholder||'Antwort')}">`;if(p.type==='choice')return `<div class="esc-options">${p.options.map(x=>`<button class="esc-option" data-value="${esc(x)}">${esc(x)}</button>`).join('')}</div>`;if(p.type==='order')return `<div class="esc-options">${p.items.map((x,i)=>`<label>${i+1}.<select class="esc-select order">${p.items.map(y=>`<option>${esc(y)}</option>`).join('')}</select></label>`).join('')}</div>`;if(p.type==='match')return `<div class="esc-pairs">${p.left.map(x=>`<label>${esc(x)}<select class="esc-select pair" data-left="${esc(x)}">${p.right.map(y=>`<option>${esc(y)}</option>`).join('')}</select></label>`).join('')}</div>`;return'';}
function openPuzzle(p){selected=null;document.body.insertAdjacentHTML('beforeend',`<div class="esc-modal"><div class="esc-dialog"><span class="esc-eyebrow">SECURE TERMINAL · ${esc(p.type)}</span><h2>${esc(p.prompt)}</h2><p>${esc(p.narrative||p.instruction||'Löse das Zugriffsrätsel.')}</p>${p.narrative&&p.instruction?`<p>${esc(p.instruction)}</p>`:''}${control(p)}<div class="esc-feedback" id="feedback"></div><div class="esc-actions"><button class="esc-btn primary" id="checkBtn">PRÜFEN</button><button class="esc-btn" id="hintBtn">HINWEIS</button><button class="esc-btn danger" id="revealBtn">AUFLÖSEN</button><button class="esc-btn" id="closeBtn">SCHLIESSEN</button></div></div></div>`);const modal=document.querySelector('.esc-modal');modal.querySelectorAll('.esc-option').forEach(b=>b.onclick=()=>{modal.querySelectorAll('.esc-option').forEach(x=>x.classList.remove('selected'));b.classList.add('selected');selected=b.dataset.value;});document.getElementById('closeBtn').onclick=()=>modal.remove();let hintLevel=0;document.getElementById('hintBtn').onclick=()=>post('/hint',{puzzleId:p.puzzleId,level:Math.min(2,++hintLevel)}).then(x=>feedback(x.text,'ok'));document.getElementById('checkBtn').onclick=()=>checkPuzzle(p,false,modal);document.getElementById('revealBtn').onclick=()=>checkPuzzle(p,true,modal);}
function submission(p,modal){if(p.type==='code')return{answer:modal.querySelector('#answer').value};if(p.type==='choice')return{answer:selected||''};if(p.type==='order')return{items:[...modal.querySelectorAll('.order')].map(x=>x.value)};if(p.type==='match')return{pairs:[...modal.querySelectorAll('.pair')].map(x=>[x.dataset.left,x.value])};return{};}
function checkPuzzle(p,reveal,modal){const sub=submission(p,modal);post('/check',{puzzleId:p.puzzleId,reveal,submission:sub}).then(x=>{if(x.resolved){answers.set(p.puzzleId,{puzzleId:p.puzzleId,submission:sub});fragments.set(p.puzzleId,x.fragment);feedback(reveal?'Auflösung freigegeben. Fragment gesichert.':'Zugriff bestätigt. Fragment gesichert.','ok');setTimeout(()=>{modal.remove();renderRoom();},700);}else feedback('Zugriff verweigert. Prüfe deine Eingabe.','bad');}).catch(e=>feedback(e.message,'bad'));}
function feedback(t,c){const f=document.getElementById('feedback');if(f){f.textContent=t;f.className='esc-feedback '+c;}}
function openDoor(r){post('/door',{roomId:r.roomId,code:document.getElementById('doorCode').value}).then(x=>{if(!x.open){document.getElementById('doorCode').classList.add('bad');return;}if(roomIndex<session.rooms.length-1){roomIndex++;renderRoom();}else finish(true);}).catch(error);}
function finish(escaped){clearInterval(tickId);post('/finish',{slug:session.slug,durationSeconds:seconds(),escaped,items:[...answers.values()],doors:[]}).then(renderResult).catch(error);}
function renderResult(r){timerEl.textContent='ESCAPED';view.innerHTML=`<section class="esc-result"><span class="esc-eyebrow">FACILITY RUN ABGESCHLOSSEN</span><h1>${r.escaped?'AUSGANG ERREICHT':'RUN BEENDET'}</h1><div class="score">${r.score}</div><p>${'★'.repeat(r.stars)}${'☆'.repeat(3-r.stars)}</p><div class="esc-stats"><div><b>${r.baseScore}</b>Basis</div><div><b>+${r.timeBonus}</b>Zeitbonus</div><div><b>${r.solvedPuzzles}</b>Rätsel</div><div><b>${r.roomsOpened}</b>Räume</div><div><b>${r.highscore}</b>Highscore</div><div><b>${r.plays}</b>Runs</div></div><button class="esc-btn primary" id="again">NEUER RUN</button></section>`;document.getElementById('again').onclick=loadCatalog;}
function error(e){view.innerHTML=`<div class="esc-result"><h1>SYSTEMFEHLER</h1><p>${esc(e.message||e)}</p><button class="esc-btn" onclick="location.reload()">NEU LADEN</button></div>`;}loadCatalog();})();
@@ -0,0 +1 @@
<!doctype html><html lang="de" data-theme="dark" xmlns:th="http://www.thymeleaf.org"><head><meta charset="utf-8"><title>CodeEscape Raum</title><link rel="stylesheet" href="/css/theme.css"><script src="/js/theme.js"></script></head><body><main class="mf-container"><a class="mf-back" th:href="@{/admin/codeescape/{id}(id=${room.scenario.id})}">← Szenario</a><h1 th:text="${room.title}" style="margin:1rem 0;color:var(--heading)"></h1><form class="mf-panel" method="post" th:action="@{/admin/codeescape/rooms/{id}/puzzles(id=${room.id})}"><h2>Rätsel hinzufügen</h2><div class="mf-field"><label class="mf-label">Typ</label><select class="mf-select" name="type"><option>code</option><option>choice</option><option>order</option><option>match</option></select></div><div class="mf-field"><label class="mf-label">Titel</label><input class="mf-input" name="prompt" required></div><div class="mf-field"><label class="mf-label">Punkte</label><input class="mf-input" type="number" name="points" value="100"></div><div class="mf-field"><label class="mf-label">Fragment</label><input class="mf-input" name="codeFragment" required></div><div class="mf-field"><label class="mf-label">Payload JSON</label><textarea class="mf-input" rows="10" name="payload" required>{"instruction":"...","solutions":["..."]}</textarea></div><button class="mf-btn">Rätsel anlegen</button></form><table class="mf-table" style="margin-top:1rem"><tr><th>Typ</th><th>Titel</th><th>Fragment</th><th></th></tr><tr th:each="p:${room.puzzles}"><td th:text="${p.type}"></td><td th:text="${p.prompt}"></td><td th:text="${p.codeFragment}"></td><td><form method="post" th:action="@{/admin/codeescape/puzzles/{id}/delete(id=${p.id})}"><input type="hidden" name="roomId" th:value="${room.id}"><button class="mf-btn danger">Löschen</button></form></td></tr></table></main></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="de" data-theme="dark" xmlns:th="http://www.thymeleaf.org"><head><meta charset="utf-8"><title>CodeEscape Szenario</title><link rel="stylesheet" href="/css/theme.css"><script src="/js/theme.js"></script></head><body><main class="mf-container"><a class="mf-back" href="/admin/codeescape">← Szenarien</a><h1 th:text="${scenario.title}" style="margin:1rem 0;color:var(--heading)"></h1><form class="mf-panel" method="post" th:action="@{/admin/codeescape/{id}/rooms(id=${scenario.id})}"><h2>Raum hinzufügen</h2><div class="mf-field"><label class="mf-label">Titel</label><input class="mf-input" name="title" required></div><div class="mf-field"><label class="mf-label">Intro</label><textarea class="mf-input" name="intro"></textarea></div><div class="mf-field"><label class="mf-label">Tür-Hinweis</label><input class="mf-input" name="doorPrompt"></div><div class="mf-field"><label class="mf-label">Türcode</label><input class="mf-input" name="doorCode" required></div><div class="mf-field"><label class="mf-label">Erfolgstext</label><input class="mf-input" name="successText"></div><button class="mf-btn">Raum anlegen</button></form><div class="mf-grid" style="margin-top:1rem"><article class="mf-panel" th:each="r:${scenario.rooms}"><h2><a th:href="@{/admin/codeescape/rooms/{id}(id=${r.id})}" th:text="${r.title}"></a></h2><p th:text="${r.intro}"></p><form method="post" th:action="@{/admin/codeescape/rooms/{id}/delete(id=${r.id})}"><button class="mf-btn danger">Raum löschen</button></form></article></div></main></body></html>
@@ -0,0 +1 @@
<!doctype html><html lang="de" data-theme="dark" xmlns:th="http://www.thymeleaf.org"><head><meta charset="utf-8"><title>Admin · CodeEscape</title><link rel="stylesheet" href="/css/theme.css"><script src="/js/theme.js"></script></head><body><main class="mf-container"><a class="mf-back" href="/admin">← Admin</a><h1 style="margin:1rem 0;color:var(--heading)">CodeEscape · Szenarien</h1><form class="mf-panel" method="post" action="/admin/codeescape/add"><div class="mf-field"><label class="mf-label">Titel</label><input class="mf-input" name="title" required></div><div class="mf-field"><label class="mf-label">Schwierigkeit</label><select class="mf-select" name="difficulty"><option>leicht</option><option>mittel</option><option>schwer</option></select></div><div class="mf-field"><label class="mf-label">Prüfungsbereich</label><select class="mf-select" name="examScope"><option>AP1</option><option>AP2_FIAE</option><option>AP2_FISI</option><option>CROSS</option></select></div><div class="mf-field"><label class="mf-label">Beschreibung</label><textarea class="mf-input" name="description"></textarea></div><div class="mf-field"><label class="mf-label">Zeitlimit Sekunden</label><input class="mf-input" type="number" name="timeLimitSeconds" value="900"></div><button class="mf-btn">Szenario anlegen</button></form><table class="mf-table" style="margin-top:1rem"><tr><th>Titel</th><th>Scope</th><th>Status</th><th>Aktion</th></tr><tr th:each="s:${scenarios}"><td><a th:href="@{/admin/codeescape/{id}(id=${s.id})}" th:text="${s.title}"></a></td><td th:text="${s.examScope}"></td><td th:text="${s.active?'aktiv':'inaktiv'}"></td><td><form style="display:inline" method="post" th:action="@{/admin/codeescape/{id}/toggle(id=${s.id})}"><button class="mf-btn outline">Toggle</button></form> <form style="display:inline" method="post" th:action="@{/admin/codeescape/{id}/delete(id=${s.id})}"><button class="mf-btn danger">Löschen</button></form></td></tr></table></main></body></html>
@@ -48,6 +48,7 @@
<a th:href="@{/admin/techtacular}" class="admin-card">🎯 TechTacular-Fragen verwalten</a>
<a th:href="@{/admin/syntaxsaga}" class="admin-card">📜 Syntax-Saga-Missionen verwalten</a>
<a th:href="@{/admin/devlab}" class="admin-card">⚗️ Dev-Lab-Aufgaben verwalten</a>
<a th:href="@{/admin/codeescape}" class="admin-card">🚪 CodeEscape-Szenarien verwalten</a>
</div>
<p style="margin-top:1.1rem; color:var(--muted);">
Hier entsteht noch Großes... ✨ Bleib dran!
@@ -0,0 +1 @@
<!doctype html><html lang="de" data-theme="dark" xmlns:th="http://www.thymeleaf.org"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CodeEscape · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/codeescape.css?v=1"><script src="/js/theme.js"></script></head><body><main class="esc-app"><header class="esc-top"><a href="/dashboard" class="esc-back">← Dashboard</a><div class="esc-brand"><b>CODEESCAPE</b><span>FACILITY ACCESS PROTOCOL</span></div><div class="esc-timer" id="escTimer">--:--</div></header><section id="escView" class="esc-view"><div class="esc-loading">FACILITY WIRD INITIALISIERT …</div></section></main><script src="/js/codeescape.js?v=1"></script></body></html>
+2 -2
View File
@@ -135,14 +135,14 @@
['TechTacular','code','Das IT-Quizduell im Jeopardy-Stil – teste dein Wissen in verschiedenen Kategorien.','/techtacular'],
['DevLab','lab','Das Entwicklerlabor bündelt Java, SQL, CSS, PHP, HTML, Git, REST und Regex.','/devlab'],
['SyntaxSaga','terminal','Interaktive IT-Geschichten mit Lückentexten – wie ein Textadventure zum Mitdenken.','/syntaxsaga'],
['CodeEscape','lock','Entkomme mit IT-Rätseln aus digitalen Räumen – sei schnell und clever.','/comingsoon'],
['CodeEscape','lock','Entkomme mit IT-Rätseln aus digitalen Räumen – sei schnell und clever.','/codeescape'],
['ServiceClash','sword','Der chaotische Admin-Alltag als Spiel – meistere Anfragen & halte Systeme am Laufen!','/comingsoon'],
['BugHunter','bug','Finde Fehler im Code, denke wie ein Debugger und löse knifflige Bugs.','/comingsoon'],
['NetWorker','network','Verbinde Netzwerke, konfiguriere IPs und löse Routing-Herausforderungen.','/comingsoon'],
['FinalFlux','trophy','Dein Power-Modul für die Prüfung – gezielt lernen, strukturiert trainieren, sicher bestehen.','/finalflux']
];
document.getElementById('grid').innerHTML = GAMES.map(([t,ic,d,href])=>`
<a class="mf-card" href="${href}">
<a class="mf-card ${t === 'CodeEscape' ? 'mf-card-codeescape' : ''}" href="${href}">
<span class="ico"><svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">${GI[ic]}</svg></span>
<h3>${t}</h3><p>${d}</p>
<span class="mf-play">SPIEL STARTEN <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>