/** * codegen.js * Python Live-Code-Generator für den Split-Screen * Wird von editor.html geladen */ // ── Progressive unlock tracking ───────────────────────────── const unlocked = new Set(); let lastKey = null; // which section was LAST interacted with → gets yellow highlight function setCodeFocus(key){ unlocked.add(key); lastKey = key; scheduleCodeUpdate(50); } // Focus listener for text inputs document.addEventListener('focusin', e=>{ const id = e.target.id || ''; if(id==='s1devname') setCodeFocus('dev'); if(id==='s1name') setCodeFocus('gamename'); if(id==='s1desc') setCodeFocus('gamedesc'); if(id && id.startsWith('qq')) setCodeFocus('quiz'); }); // ───────────────────────────────────────────── function buildPythonCode(){ const s = ST; const fig = FIGURES.find(f=>f.id===s.figure); const bg = BACKGROUNDS.find(b=>b.id===s.background); // Empty until first interaction if(unlocked.size===0 && !s.devName && !s.name && !s.figure) return []; const lines = []; // hi() returns true only for the last-touched key const hi = key => lastKey === key; // Header + imports — always shown once anything unlocked lines.push({t:'cm', v:'# ═══════════════════════════════════════════'}); lines.push({t:'cm', v:'# boardgame.py — Spiel-Generator PH Weingarten'}); lines.push({t:'cm', v:'# ═══════════════════════════════════════════'}); lines.push({t:'bl'}); lines.push({t:'cm', v:'# Module importieren'}); lines.push({t:'kw', v:'import', rest:' boardgame_engine as engine'}); lines.push({t:'kw', v:'import', rest:' minigames'}); lines.push({t:'kw', v:'from', rest:' config import GameConfig, Field, StoryText'}); lines.push({t:'bl'}); // DEVELOPER if(unlocked.has('dev') || s.devName){ lines.push({t:'cm', v:'# Wer hat dieses Spiel programmiert?'}); if(s.devName) lines.push({t:'ass', var:'developer_name', val:`"${s.devName}"`, hi:hi('dev')}); else lines.push({t:'ph', var:'developer_name', hint:'← tippe deinen Namen'}); lines.push({t:'bl'}); } // GAME NAME if(unlocked.has('gamename') || s.name){ lines.push({t:'cm', v:'# Wie heißt das Spiel?'}); if(s.name) lines.push({t:'ass', var:'game_name', val:`"${s.name}"`, hi:hi('gamename')}); else lines.push({t:'ph', var:'game_name', hint:'← tippe den Spielnamen'}); lines.push({t:'bl'}); } // DESCRIPTION if(unlocked.has('gamedesc') || s.desc){ lines.push({t:'cm', v:'# Kurze Beschreibung des Spiels'}); if(s.desc) lines.push({t:'ass', var:'game_description', val:`"${s.desc}"`, hi:hi('gamedesc')}); else lines.push({t:'ph', var:'game_description', hint:'← tippe eine Beschreibung'}); lines.push({t:'bl'}); } // FIGURE if(unlocked.has('figure') || s.figure){ lines.push({t:'cm', v:'# Spielfigur auswählen'}); if(s.figure) lines.push({t:'ass', var:'player_figure', val:`"${s.figure}" # ${fig?fig.e+' '+fig.n:''}`, hi:hi('figure')}); else lines.push({t:'ph', var:'player_figure', hint:'← klicke eine Figur an'}); lines.push({t:'bl'}); } // BACKGROUND if(unlocked.has('background') || s.background){ lines.push({t:'cm', v:'# Spielwelt / Hintergrund'}); if(s.background) lines.push({t:'ass', var:'world_setting', val:`"${s.background}" # ${bg?bg.e+' '+bg.n:''}`, hi:hi('background')}); else lines.push({t:'ph', var:'world_setting', hint:'← klicke eine Welt an'}); lines.push({t:'bl'}); } // MOVEMENT if(unlocked.has('movement')){ lines.push({t:'cm', v:'# Wie viele Felder pro Runde?'}); lines.push({t:'ass', var:'movement_type', val: s.rules.movement==='step' ? '"schritt" # immer genau 1 Feld' : '"wuerfeln" # zufällig 1-6 Felder', hi:hi('movement')}); lines.push({t:'bl'}); } // FAIL MODE if(unlocked.has('failmode')){ lines.push({t:'cm', v:'# Was passiert wenn man verliert?'}); if(s.rules.fail==='lives'){ lines.push({t:'ass', var:'verlust_system', val:'"leben"', hi:hi('failmode')}); lines.push({t:'ass', var:'anzahl_leben', val:String(s.rules.lives||3), hi:hi('lives')}); } else { lines.push({t:'ass', var:'verlust_system', val:'"punkte"', hi:hi('failmode')}); lines.push({t:'ass', var:'punkte_pro_sieg',val:String(s.rules.pts||10), hi:hi('pts')}); } lines.push({t:'bl'}); } // LIVES / PTS sub-option if(unlocked.has('lives') && s.rules.fail==='lives'){ // already rendered above, just make sure hi updates } // FIELD COUNT if(unlocked.has('fieldcount')){ lines.push({t:'cm', v:'# Wie groß ist das Spielfeld?'}); lines.push({t:'ass', var:'anzahl_felder', val:String(s.fieldCount), hi:hi('fieldcount')}); lines.push({t:'bl'}); } // BOARD SETUP — only once a game was dropped const MG_PY ={snake:'schlangen_spiel',flappy:'flug_spiel',memory:'memory',quiz:'quiz',reaction:'reaktionstest',basketball:'basketball',catch:'fangen',maze:'labyrinth',simon:'simon_says',puzzle:'raetsel',spotdiff:'unterschiede',typing:'tipp_rennen'}; const MG_CM ={snake:'Schlange steuern',flappy:'Durch Hindernisse fliegen',memory:'Paare finden',quiz:'Frage beantworten',reaction:'Schnell reagieren',basketball:'Ball werfen',catch:'Objekte fangen',maze:'Ausweg finden',simon:'Sequenz merken',puzzle:'Rätsel lösen',spotdiff:'Unterschiede finden',typing:'Text eintippen'}; const hasAnyField = (s.fields||[]).some(Boolean); if(unlocked.has('fields') || hasAnyField){ lines.push({t:'cm', v:'# Spielfeld aufbauen — jedes Feld wird definiert'}); lines.push({t:'fn-def', name:'setup_spielfeld', args:''}); lines.push({t:'ind', v:'felder = []'}); lines.push({t:'bl'}); lines.push({t:'ind-cm', v:'# Startfeld (Index 0)'}); lines.push({t:'ind', v:'felder.append(Field(index=0, typ="start"))'}); for(let i=1;i0){ const MG_CM2={snake:'Schlange steuern ohne Wand zu treffen',flappy:'Durch alle Hindernisse fliegen',memory:'Alle Kartenpaare finden',quiz:'Die richtige Antwort wählen',reaction:'Schnell auf Signal drücken',basketball:'Ball zum richtigen Moment werfen',catch:'Fallende Objekte auffangen',maze:'Den Ausgang aus dem Labyrinth finden',simon:'Farbreihenfolge merken und wiederholen',puzzle:'Zahlen in die richtige Reihenfolge bringen',spotdiff:'Alle Unterschiede im Bild finden',typing:'Text so schnell wie möglich eintippen'}; lines.push({t:'cm', v:'# Für jedes Mini-Game gibt es eine eigene Funktion'}); usedGames.forEach(id=>{ const pyName=MG_PY[id]||id; lines.push({t:'bl'}); lines.push({t:'fn-def', name:`spiele_${pyName}`, args:'spieler'}); lines.push({t:'ind-cm', v:`# Aufgabe: ${MG_CM2[id]||id}`}); lines.push({t:'ind', v:`ergebnis = minigames.${pyName}.starten(spieler)`}); lines.push({t:'ind', v:`return ergebnis.gewonnen`}); }); lines.push({t:'bl'}); } // QUIZ const quizItems=(s.quizData||[]).filter(q=>q&&q.question); if(quizItems.length>0 || unlocked.has('quiz')){ lines.push({t:'cm', v:'# Quiz-Fragen für das Spiel'}); lines.push({t:'ass', var:'fragen', val:'[', hi:false}); quizItems.forEach(q=>{ const clean=q.question.replace(/"/g,"'").slice(0,50); const ans=q.answers.map(a=>`"${(a||'').replace(/"/g,"'")}"`).join(', '); const cor=q.correct!=null?`"${(q.answers[q.correct]||'').replace(/"/g,"'")}"`:'None'; lines.push({t:'ind', v:`{"frage": "${clean}", "antworten": [${ans}], "richtig": ${cor}},`}); }); if(quizItems.length===0) lines.push({t:'ind-cm', v:'# noch keine Fragen eingetragen...'}); lines.push({t:'ass-end', v:']'}); lines.push({t:'bl'}); } // MAIN BLOCK lines.push({t:'cm', v:'# ── Spiel starten ───────────────────────────'}); lines.push({t:'kw', v:'if', rest:" __name__ == '__main__':"}); lines.push({t:'ind-cm', v:'# Konfiguration zusammenstellen und Spiel starten'}); lines.push({t:'ind', v:'config = GameConfig('}); lines.push({t:'ind2', v:`name=${s.name?`"${s.name}"`:'""'},`}); lines.push({t:'ind2', v:`developer=${s.devName?`"${s.devName}"`:'""'},`}); if(s.figure) lines.push({t:'ind2', v:`figure="${s.figure}",`}); if(s.background) lines.push({t:'ind2', v:`setting="${s.background}",`}); if(hasAnyField) lines.push({t:'ind2', v:'board=setup_spielfeld(),'}); if(quizItems.length) lines.push({t:'ind2', v:'questions=fragen,'}); lines.push({t:'ind', v:')'}); lines.push({t:'bl'}); lines.push({t:'ind', v:'engine.run(config)'}); return lines; } function renderToken(line){ const esc = s => String(s).replace(/&/g,'&').replace(//g,'>'); let r=''; switch(line.t){ case 'cm': r=`${esc(line.v)}`; break; case 'bl': return ''; case 'ph': r=`${esc(line.var)} = "???" ${esc(line.hint||'')}`; break; case 'kw': r=`${esc(line.v)}${esc(line.rest||'')}`; break; case 'ass': r=`${esc(line.var)} = ${esc(String(line.val))}`; break; case 'ass-end':r=`${esc(line.v)}`; break; case 'fn-def': r=`def ${esc(line.name)}(${esc(line.args||'')}):`; break; case 'ind': r=` ${esc(line.v)}`; break; case 'ind2': r=` ${esc(line.v)}`; break; case 'ind-cm': r=` ${esc(line.v)}`; break; default: return esc(line.v||''); } return line.hi ? `${r}` : r; } let codeTypingTimer = null; let lastLineCount = 0; let lastKey_scroll = null; // prevent scroll thrash function updateCodePane(){ const lines = buildPythonCode(); const codeEl = document.getElementById('codeContent'); const numsEl = document.getElementById('codeLineNums'); const linesEl= document.getElementById('codeLines'); if(!codeEl) return; // Filename const dev = ST.devName ? ST.devName.toLowerCase().replace(/[^a-z0-9]/g,'_') : 'dev'; const gn = ST.name ? ST.name.toLowerCase().replace(/[^a-z0-9]/g,'_').slice(0,20) : 'boardgame'; const fnEl = document.getElementById('codeFilename'); if(fnEl) fnEl.textContent = (ST.name||ST.devName) ? `${gn}_von_${dev}.py` : 'boardgame.py'; if(lines.length===0){ codeEl.innerHTML='# Klicke ein Feld an — dein Code erscheint hier ✨'; numsEl.innerHTML='
1
'; if(linesEl) linesEl.textContent='1 Zeile'; lastLineCount=0; return; } let html='', lineNum=1; const lineNums=[]; lines.forEach(line=>{ if(line.t==='bl'){ html+='\n'; } else { html+=renderToken(line)+'\n'; } lineNums.push(lineNum++); }); const newCount = lines.length; const grew = newCount > lastLineCount; const prevCount= lastLineCount; lastLineCount = newCount; codeEl.innerHTML = html; numsEl.innerHTML = lineNums.map(n=>`
${n}
`).join(''); if(linesEl) linesEl.textContent = lineNum+' Zeilen'; const status = document.getElementById('codeStatus'); if(status){ status.textContent='● Live'; setTimeout(()=>{ status.textContent='● Bereit'; },800); } // Scroll: only when new lines appeared AND key changed (avoid thrash while typing) const scroll = document.getElementById('codeScroll'); if(scroll && grew && lastKey !== lastKey_scroll){ lastKey_scroll = lastKey; // Find the first highlighted line's position setTimeout(()=>{ const hiEl = codeEl.querySelector('.py-hi'); if(hiEl){ const pre = codeEl; const preTop = pre.getBoundingClientRect().top; const hiTop = hiEl.getBoundingClientRect().top; const offset = hiTop - preTop; const scrollTarget = scroll.scrollTop + offset - 80; scroll.scrollTo({top: Math.max(0, scrollTarget), behavior:'smooth'}); } }, 80); } } function scheduleCodeUpdate(delay=80){ clearTimeout(codeTypingTimer); codeTypingTimer = setTimeout(updateCodePane, delay); } document.addEventListener('input', ()=>scheduleCodeUpdate(50)); document.addEventListener('change', ()=>scheduleCodeUpdate(50)); // Initial render setTimeout(updateCodePane, 200);