Implement NetWorker network puzzle game
mindflux-ci / build (push) Successful in 23s

This commit is contained in:
AliceDarkest
2026-07-13 22:09:31 +02:00
parent 3eef7cdb40
commit 73e74a2859
25 changed files with 292 additions and 5 deletions
@@ -2,7 +2,7 @@
function load(){try{return Object.assign({},defaults,JSON.parse(localStorage.getItem(KEY)||'{}'));}catch(e){return Object.assign({},defaults);}}var state=load();
function apply(){var h=document.documentElement;h.setAttribute('data-mf-font',String(state.font));h.setAttribute('data-mf-readable',state.readable);h.setAttribute('data-mf-contrast',state.contrast);h.setAttribute('data-mf-color',state.color?'safe':'standard');h.setAttribute('data-mf-mono',state.mono);h.setAttribute('data-mf-links',state.links);h.setAttribute('data-mf-motion',state.motion);h.setAttribute('data-mf-cursor',state.cursor);try{localStorage.setItem(KEY,JSON.stringify(state));}catch(e){}}
apply();
function applySkin(){var root=document.querySelector('[data-a11y-skin]'),skin=root&&root.getAttribute('data-a11y-skin');if(!skin&&document.querySelector('.esc-app'))skin='codeescape';if(!skin&&document.querySelector('.bh-app'))skin='bughunter';if(skin)document.body.classList.add('mf-a11y-skin-'+skin);}
function applySkin(){var root=document.querySelector('[data-a11y-skin]'),skin=root&&root.getAttribute('data-a11y-skin');if(!skin&&document.querySelector('.esc-app'))skin='codeescape';if(!skin&&document.querySelector('.bh-app'))skin='bughunter';if(!skin&&document.querySelector('.nw-app'))skin='networker';if(skin)document.body.classList.add('mf-a11y-skin-'+skin);}
function button(id,icon,label,detail){return '<button type="button" class="mf-a11y-option" data-a11y="'+id+'"><strong aria-hidden="true">'+icon+'</strong><span>'+label+'</span>'+(detail?'<small>'+detail+'</small>':'')+'</button>';}
function init(){if(document.querySelector('.mf-a11y-trigger'))return;var trigger=document.createElement('button');trigger.type='button';trigger.className='mf-a11y-trigger';trigger.setAttribute('aria-label','Barrierefreiheit öffnen');trigger.setAttribute('aria-expanded','false');trigger.setAttribute('aria-controls','mf-a11y-panel');trigger.innerHTML='<svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><circle cx="18" cy="5" r="3" fill="currentColor"/><path d="M13 11l5-2 5 4m-7-2-2 7-6 1m8-4 6 2 3 7M13 18a7 7 0 1 0 6 7"/></svg>';
var panel=document.createElement('section');panel.id='mf-a11y-panel';panel.className='mf-a11y-panel';panel.hidden=true;panel.setAttribute('role','dialog');panel.setAttribute('aria-modal','false');panel.setAttribute('aria-labelledby','mf-a11y-title');panel.innerHTML='<div class="mf-a11y-head"><button type="button" class="mf-a11y-close" aria-label="Barrierefreiheit schließen">×</button><h2 id="mf-a11y-title">Barrierefreiheit</h2></div><div class="mf-a11y-grid">'+button('font','T⁺','Text vergrößern','100 %')+button('contrast','◐','Hochkontrast')+button('color','◉','Farbenblind-Modus')+button('mono','◒','Monochrom')+button('readable','Aa','Lesemodus (Dyslexie)','Verdana + Abstände')+button('links','🔗','Links unterstreichen')+button('motion','Ⅱ','Animationen stoppen')+button('cursor','➤','Großer Cursor')+button('reset','↺','Alles zurücksetzen')+'</div>';
+12
View File
@@ -0,0 +1,12 @@
(function(){'use strict';const view=document.getElementById('nwView');let catalog=[],session=null,index=0,started=0,configs=[];const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));const api=(url,opt)=>fetch('/api/networker'+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;});const post=(url,body)=>api(url,{method:'POST',body:JSON.stringify(body)});const seconds=()=>Math.max(1,Math.round((Date.now()-started)/1000));
function load(){api('/catalog').then(data=>{catalog=data.worlds||[];renderCatalog();}).catch(error)}
function renderCatalog(){view.innerHTML=`<section class="nw-hero"><span class="nw-kicker">BLUEPRINT ARCHIVE</span><h1>Welche Verbindung stellst du wieder her?</h1><p>Konfiguriere Geräte, prüfe Paketwege und lies die Diagnose wie ein Netzwerktechniker.</p></section><section class="nw-worlds">${catalog.map(w=>`<article class="nw-world"><span>${esc(w.examScope)} · ${esc(w.difficulty).toUpperCase()}</span><h2>${esc(w.title)}</h2><p>${esc(w.description)}</p><small>${w.levelCount} Level</small><button class="nw-btn primary" data-world="${esc(w.slug)}">NETZPLAN ÖFFNEN</button></article>`).join('')}</section>`;view.querySelectorAll('[data-world]').forEach(b=>b.addEventListener('click',()=>begin(b.dataset.world)))}
function begin(slug){api('/session/'+encodeURIComponent(slug)).then(data=>{session=data;index=0;started=Date.now();configs=data.levels.map(()=>null);renderIntro()}).catch(error)}
function renderIntro(){view.innerHTML=`<section class="nw-brief"><span class="nw-kicker">${esc(session.examScope)} · ${esc(session.difficulty).toUpperCase()}</span><h1>${esc(session.title)}</h1><p>${esc(session.description)}</p><div class="nw-rules"><div><b>1</b> Werte prüfen</div><div><b>2</b> Pings simulieren</div><div><b>3</b> Diagnose lesen</div></div><button class="nw-btn primary" id="nwStart">KALIBRIERUNG STARTEN</button></section>`;document.getElementById('nwStart').addEventListener('click',renderLevel)}
function renderLevel(){const level=session.levels[index],net=level.network;view.innerHTML=`<header class="nw-level-head"><div><span class="nw-kicker">LEVEL ${index+1} / ${session.levels.length} · ${esc(level.type).toUpperCase()}</span><h1>${esc(level.title)}</h1></div><div class="nw-score">${level.points}<small>PTS</small></div></header><div class="nw-progress">${session.levels.map((_,i)=>`<i class="${i<index?'done':i===index?'active':''}">${i+1}</i>`).join('')}</div><div class="nw-layout"><section class="nw-board">${drawNet(net)}</section><aside class="nw-panel"><h2>Konfiguration</h2>${drawInputs(net)}<section class="nw-goals"><h2>Ping-Ziele</h2>${net.goals.map(g=>`<div><b>${esc(g.from)} → ${esc(g.to)}</b><span id="goal-${esc(g.id)}">offen</span></div>`).join('')}</section><div id="nwFeedback" class="nw-feedback" aria-live="polite"></div><div class="nw-actions"><button class="nw-btn primary" id="nwCheck">PINGS TESTEN</button><button class="nw-btn" id="nwReveal">LÖSUNG ANZEIGEN</button></div></aside></div>`;document.getElementById('nwCheck').addEventListener('click',()=>check(false));document.getElementById('nwReveal').addEventListener('click',()=>check(true))}
function drawNet(net){const nodeById=Object.fromEntries(net.nodes.map(n=>[n.id,n]));return `<svg viewBox="0 0 1000 560" role="img" aria-label="Netzwerkplan">${(net.links||[]).map(([a,b])=>{const x=nodeById[a],y=nodeById[b];return x&&y?`<path class="nw-link" d="M${x.x+65} ${x.y+35} L${y.x+65} ${y.y+35}"/>`:''}).join('')}${net.nodes.map(n=>`<g class="nw-node nw-${esc(n.type)}" transform="translate(${n.x||0} ${n.y||0})"><rect width="130" height="70" rx="8"/><text x="65" y="30" text-anchor="middle">${esc(n.label||n.id)}</text><text x="65" y="52" text-anchor="middle">${esc(n.type||'node').toUpperCase()}</text></g>`).join('')}</svg>`}
function drawInputs(net){const rows=net.nodes.filter(n=>n.editable?.length).map(n=>`<fieldset data-node="${esc(n.id)}"><legend>${esc(n.label||n.id)}</legend>${n.editable.map(f=>f==='routes'?`<label>Statische Route <textarea data-field="routes" rows="3">${esc(JSON.stringify(n.routes||[]))}</textarea></label>`:`<label>${f==='prefix'?'Präfix /':' '+f.toUpperCase()}<input data-field="${f}" value="${esc(n[f]??'')}"></label>`).join('')}</fieldset>`).join('');return rows||'<p>In diesem Plan sind keine Werte editierbar.</p>'}
function collect(){const nodes=[];view.querySelectorAll('[data-node]').forEach(box=>{const node={id:box.dataset.node};box.querySelectorAll('[data-field]').forEach(input=>{if(input.dataset.field==='routes'){try{node.routes=JSON.parse(input.value)}catch(e){node.routes=[]}}else node[input.dataset.field]=input.value});nodes.push(node)});return{nodes}}
function check(reveal){const level=session.levels[index];post('/check',{levelId:level.levelId,reveal,config:collect()}).then(result=>{result.goals.forEach(g=>{const target=document.getElementById('goal-'+g.id);if(target){target.textContent=g.passed?'ERREICHT':'BLOCKIERT · '+g.message;target.className=g.passed?'ok':'bad'}});const feedback=document.getElementById('nwFeedback');if(result.resolved){if(result.revealed&&result.solution){level.network=result.solution;configs[index]=null;renderLevel();document.getElementById('nwFeedback').textContent='Auflösung geladen. Prüfe den korrigierten Plan und gehe weiter.'}else{feedback.textContent='Paketweg bestätigt.';feedback.className='nw-feedback ok'}const next=document.createElement('button');next.className='nw-btn primary';next.textContent=index+1<session.levels.length?'NÄCHSTES LEVEL':'WELT AUSWERTEN';next.addEventListener('click',()=>{index++;if(index>=session.levels.length)finish();else renderLevel()});document.querySelector('.nw-actions').replaceChildren(next)}else{feedback.textContent=`Noch nicht stimmig · Versuch ${result.attempts}/3`;feedback.className='nw-feedback bad'}}).catch(error)}
function finish(){post('/finish',{slug:session.slug,durationSeconds:seconds(),levelIds:session.levels.map(l=>l.levelId)}).then(r=>{view.innerHTML=`<section class="nw-result"><span class="nw-kicker">NETZWERKPROTOKOLL</span><h1>${r.perfect?'Alle Paketwege fehlerfrei.':'Welt abgeschlossen.'}</h1><div class="nw-final-score">${r.score}</div><p>${'★'.repeat(r.stars)}${'☆'.repeat(3-r.stars)}</p><div class="nw-stats"><div><b>${r.solved}/${r.total}</b>Level</div><div><b>${r.highscore}</b>Highscore</div><div><b>${r.plays}</b>Welten</div></div><button class="nw-btn primary" id="nwAgain">WEITERE WELTEN</button></section>`;document.getElementById('nwAgain').addEventListener('click',load)}).catch(error)}
function error(err){view.innerHTML=`<section class="nw-result"><h1>NETZPLAN NICHT LESBAR</h1><p>${esc(err.message||err)}</p><button class="nw-btn" id="nwReload">ERNEUT VERSUCHEN</button></section>`;document.getElementById('nwReload').addEventListener('click',load)}load()})();
+2 -2
View File
@@ -32,13 +32,13 @@
document.head.appendChild(accessibilityCss);
var accessibilityPlacementCss = document.createElement('link');
accessibilityPlacementCss.rel = 'stylesheet';
accessibilityPlacementCss.href = '/css/accessibility-placement.css?v=5';
accessibilityPlacementCss.href = '/css/accessibility-placement.css?v=6';
accessibilityPlacementCss.setAttribute('data-accessibility', '');
document.head.appendChild(accessibilityPlacementCss);
}
if (!document.querySelector('script[data-accessibility]')) {
var accessibilityScript = document.createElement('script');
accessibilityScript.src = '/js/accessibility.js?v=5';
accessibilityScript.src = '/js/accessibility.js?v=6';
accessibilityScript.defer = true;
accessibilityScript.setAttribute('data-accessibility', '');
document.head.appendChild(accessibilityScript);