/* codegen.js — Live-Python-Code-Vorschau (EINZIGE Quelle, aus editor.html ausgelagert) */ /* ══════════════════════════════════════════════ PYTHON CODE GENERATOR — Live-Vorschau (kompakt) ══════════════════════════════════════════════ */ // ── Progressive unlock tracking ───────────────────────────── const unlocked = new Set(); let lastKey = null; // welcher Bereich wurde zuletzt angefasst → gelbes Highlight function setCodeFocus(key){ unlocked.add(key); lastKey = key; scheduleCodeUpdate(50); } // Focus-Listener für Text-Eingaben 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'); }); // Mini-Game IDs → Python-Funktionsnamen 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', snake2p:'snake_duell', flappy2p:'flappy_duell', }; // ───────────────────────────────────────────── function buildPythonCode(){ const s = ST; const fig = FIGURES.find(f=>f.id===s.figure); const bg = BACKGROUNDS.find(b=>b.id===s.background); // Leer bis zur ersten Interaktion if(unlocked.size===0 && !s.devName && !s.name && !s.figure) return []; const lines = []; const hi = key => lastKey === key; // ── Imports ── 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'}); // ── Grundeinstellungen ── const anyBasic = s.devName||s.name||s.desc||s.figure||s.background ||unlocked.has('dev')||unlocked.has('gamename')||unlocked.has('gamedesc') ||unlocked.has('figure')||unlocked.has('background')||unlocked.has('movement') ||unlocked.has('failmode')||unlocked.has('lives')||unlocked.has('pts')||unlocked.has('fieldcount'); if(anyBasic){ lines.push({t:'cm', v:'# ── Grundeinstellungen ──'}); if(unlocked.has('dev') || s.devName){ 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'}); } if(unlocked.has('gamename') || s.name){ 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'}); } if(unlocked.has('gamedesc') || s.desc){ 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'}); } if(unlocked.has('figure') || s.figure){ 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'}); } if(unlocked.has('background') || s.background){ 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'}); } if(unlocked.has('movement')){ lines.push({t:'ass', var:'movement_type', val: s.rules.movement==='step' ? '"schritt" # 1 Feld pro Runde' : '"wuerfeln" # 1–6 Felder', hi:hi('movement')}); } if(unlocked.has('failmode')){ 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')}); } } if(unlocked.has('fieldcount')){ lines.push({t:'ass', var:'anzahl_felder', val:String(s.fieldCount), hi:hi('fieldcount')}); } lines.push({t:'bl'}); } // ── Konsequenz nach Mini-Game ── if(unlocked.has('consequences')){ const c = s.consequences || {}; const win = c.win || 'nothing'; const lose = c.lose || 'nothing'; if(win !== 'nothing' || lose !== 'nothing'){ lines.push({t:'cm', v:'# ── Konsequenz nach Mini-Game ──'}); lines.push({t:'kw', v:'if', rest:' ergebnis == "gewonnen":'}); if(win === 'forward') lines.push({t:'ind', v:`spieler.position += ${c.winVal??2} # ${c.winVal??2} Felder vor`, hi:hi('consequences')}); else if(win === 'points') lines.push({t:'ind', v:`spieler.punkte += ${c.winPts??10} # +${c.winPts??10} Punkte`, hi:hi('consequences')}); else if(win === 'again') lines.push({t:'ind', v:'spieler.nochmal_wuerfeln = True', hi:hi('consequences')}); else lines.push({t:'ind', v:'pass', hi:hi('consequences')}); lines.push({t:'kw', v:'else', rest:':'}); if(lose === 'back') lines.push({t:'ind', v:`spieler.position -= ${c.loseVal??1} # ${c.loseVal??1} Felder zurück`, hi:hi('consequences')}); else if(lose === 'life') lines.push({t:'ind', v:'spieler.leben -= 1', hi:hi('consequences')}); else if(lose === 'points') lines.push({t:'ind', v:`spieler.punkte -= ${c.losePts??5} # −${c.losePts??5} Punkte`, hi:hi('consequences')}); else if(lose === 'skip') lines.push({t:'ind', v:'spieler.aussetzen = True', hi:hi('consequences')}); else lines.push({t:'ind', v:'pass', hi:hi('consequences')}); lines.push({t:'bl'}); } } // ── Spielfeld aufbauen ── const hasAnyField = (s.fields||[]).some(Boolean); if(unlocked.has('fields') || hasAnyField){ lines.push({t:'cm', v:'# ── Spielfeld aufbauen ──'}); lines.push({t:'fn-def', name:'setup_spielfeld', args:''}); lines.push({t:'ind', v:'felder = [Field(index=0, typ="start")]'}); for(let i=1;i0){ lines.push({t:'cm', v:'# ── Mini-Game Funktionen ──'}); usedGames.forEach(id=>{ const pyName=MG_PY[id]||id; lines.push({t:'fn-def', name:`spiele_${pyName}`, args:'spieler'}); lines.push({t:'ind', v:`return minigames.${pyName}.starten(spieler).gewonnen`}); }); lines.push({t:'bl'}); } // ── Quiz-Fragen ── const quizItems=(s.quizData||[]).filter(q=>q&&q.question); if(quizItems.length>0 || unlocked.has('quiz')){ lines.push({t:'cm', v:'# ── Quiz-Fragen ──'}); 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'}); } // ── Erzähltexte ── const storyList = (s.storyItems||[]).filter(st=>st&&st.text&&st.text.trim()); if(storyList.length>0){ lines.push({t:'cm', v:'# ── Erzähltexte ──'}); lines.push({t:'ass', var:'erzaehltexte', val:'[', hi:false}); storyList.forEach(st=>{ const txt = st.text.replace(/"/g,"'").slice(0,60); const pos = st.position==='before' ? `vor Feld ${st.fieldIndex}` : `nach Feld ${st.fieldIndex}`; lines.push({t:'ind', v:`{"text": "${txt}", "position": "${pos}"},`}); }); lines.push({t:'ass-end', v:']'}); lines.push({t:'bl'}); } // ── Spiel starten ── lines.push({t:'cm', v:'# ── Spiel starten ──'}); lines.push({t:'kw', v:'if', rest:" __name__ == '__main__':"}); 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,'}); if(storyList.length) lines.push({t:'ind2', v:'stories=erzaehltexte,'}); lines.push({t:'ind', v:')'}); 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; function updateCodePane(){ const lines = buildPythonCode(); const codeEl = document.getElementById('codeContent'); const numsEl = document.getElementById('codeLineNums'); const linesEl= document.getElementById('codeLines'); const scroll = document.getElementById('codeScroll'); if(!codeEl) return; // Dateiname 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'; if(scroll) scroll.scrollTop = 0; 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; 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); } // Nach Layout: scrollTop auf gültigen Bereich begrenzen (verhindert "ins Leere gescrollt") // UND optional zum Highlight scrollen (nur bei grow + neuem Key, damit nichts thrasht) if(scroll){ requestAnimationFrame(()=>{ const maxScroll = Math.max(0, scroll.scrollHeight - scroll.clientHeight); if(scroll.scrollTop > maxScroll) scroll.scrollTop = maxScroll; if(grew && lastKey !== lastKey_scroll){ lastKey_scroll = lastKey; const hiEl = codeEl.querySelector('.py-hi'); if(hiEl){ const preTop = codeEl.getBoundingClientRect().top; const hiTop = hiEl.getBoundingClientRect().top; const offset = hiTop - preTop; let target = scroll.scrollTop + offset - 80; target = Math.max(0, Math.min(maxScroll, target)); scroll.scrollTo({top: target, behavior:'smooth'}); } } }); } } function scheduleCodeUpdate(delay=80){ clearTimeout(codeTypingTimer); codeTypingTimer = setTimeout(updateCodePane, delay); } function downloadPy(){ const codeEl=document.getElementById('codeContent'); if(!codeEl)return; const text=codeEl.innerText||codeEl.textContent||''; const dev=(ST.devName||'dev').toLowerCase().replace(/[^a-z0-9]/g,'_'); const gn=(ST.name||'boardgame').toLowerCase().replace(/[^a-z0-9]/g,'_').slice(0,20); const fname=`${gn}_von_${dev}.py`; const blob=new Blob([text],{type:'text/plain'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=fname; a.click(); URL.revokeObjectURL(a.href); } document.addEventListener('input', ()=>scheduleCodeUpdate(50)); document.addEventListener('change', ()=>scheduleCodeUpdate(50)); // Initial render setTimeout(updateCodePane, 200);