edu-boardgame-generator/codegen.js

313 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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" # 16 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;i<s.fieldCount-1;i++){
const fId=(s.fields||[])[i];
if(fId){
lines.push({t:'ind', v:`felder.append(Field(index=${i}, typ="minigame", spiel="${MG_PY[fId]||fId}"))`,
hi: hi('fields') && (s.fields||[])[i]===fId});
} else {
lines.push({t:'ind', v:`felder.append(Field(index=${i}, typ="leer"))`});
}
}
lines.push({t:'ind', v:`felder.append(Field(index=${s.fieldCount-1}, typ="ziel")) # 🏁`});
lines.push({t:'ind', v:'return felder'});
lines.push({t:'bl'});
}
// ── Mini-Game Funktionen ──
const usedGames=[...new Set((s.fields||[]).filter(Boolean))];
if(usedGames.length>0){
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
let r='';
switch(line.t){
case 'cm': r=`<span class="py-cm">${esc(line.v)}</span>`; break;
case 'bl': return '';
case 'ph': r=`<span class="py-var">${esc(line.var)}</span><span class="py-punc"> = </span><span class="py-cm">"???" <span style="color:#e06c75;font-style:italic">${esc(line.hint||'')}</span></span>`; break;
case 'kw': r=`<span class="py-kw">${esc(line.v)}</span><span class="py-val">${esc(line.rest||'')}</span>`; break;
case 'ass': r=`<span class="py-var">${esc(line.var)}</span><span class="py-punc"> = </span><span class="py-str">${esc(String(line.val))}</span>`; break;
case 'ass-end':r=`<span class="py-punc">${esc(line.v)}</span>`; break;
case 'fn-def': r=`<span class="py-kw">def </span><span class="py-fn">${esc(line.name)}</span><span class="py-punc">(${esc(line.args||'')})</span><span class="py-punc">:</span>`; break;
case 'ind': r=` <span class="py-val">${esc(line.v)}</span>`; break;
case 'ind2': r=` <span class="py-val">${esc(line.v)}</span>`; break;
case 'ind-cm': r=` <span class="py-cm">${esc(line.v)}</span>`; break;
default: return esc(line.v||'');
}
return line.hi ? `<span class="py-hi">${r}</span>` : 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='<span class="py-cm"># Klicke ein Feld an — dein Code erscheint hier ✨</span>';
numsEl.innerHTML='<div>1</div>';
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=>`<div>${n}</div>`).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);