313 lines
14 KiB
JavaScript
313 lines
14 KiB
JavaScript
/* 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;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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
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);
|