From 7de71b0e83d0b369008f68bf55f3449d97bc86a4 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 24 Aug 2026 13:19:21 +0200 Subject: [PATCH] Energiemanager: Blackouts fuer Lehrer sichtbar machen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - game.html: bei Blackout sofort action:'blackout' an /progress feuern (setId/Tag/Block/Frequenz/Ursache, score:0) — Fehlversuch bleibt auch bei Set-Abbruch sichtbar, kein player_progress/Sterne. - progress.php: neuer action:'blackout' -> eigene assessments-Zeile, KEIN XP/plays/best_stars. - live.php: Blackout-Flag (event:'blackout') durch die Tag-Summary reichen (blackout/blackoutCount pro Session). - teacher.html: needsHelp energiemanager (>=2 Blackouts oder Tag <4/8); Blackout-Spalte + '🔌 neu planen'-Status in Aktuell; Tag-Balken als 🔌 Blackout kennzeichnen; Run-Karte: eigenes Blackout-Kaertchen + '🔌 N Blackouts'-Badge im fertigen Set. Co-Authored-By: Claude Opus 4.8 --- App/php/api/live.php | 7 ++++++ App/php/api/progress.php | 20 ++++++++++++++++ App/sims/energiemanager/game.html | 39 +++++++++++++++++++++++++++++++ App/teacher.html | 34 ++++++++++++++++++++++++--- 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/App/php/api/live.php b/App/php/api/live.php index d0bf805..a46eb6e 100644 --- a/App/php/api/live.php +++ b/App/php/api/live.php @@ -211,6 +211,8 @@ if ($method === 'GET') { 'score' => null, 'durationMs' => 0, 'completed' => null, + 'blackout' => false, + 'blackoutCount' => 0, 'rowCount' => 0, ]; } @@ -224,6 +226,9 @@ if ($method === 'GET') { if ($dmv > $currentSess['durationMs']) $currentSess['durationMs'] = $dmv; $cp = $extractCompleted($row); if ($cp !== null) $currentSess['completed'] = $cp; + // Blackout-Fehlversuch (Energiemanager) markieren. + $rEvt = $row['results'] ? json_decode($row['results'], true) : []; + if (($rEvt['event'] ?? '') === 'blackout') { $currentSess['blackout'] = true; $currentSess['blackoutCount']++; } $prevTs = $ts; } if ($currentSess !== null) $sessions[] = $currentSess; @@ -276,6 +281,8 @@ if ($method === 'GET') { 'stars' => $s['stars'], 'score' => $s['score'], 'completed' => $s['completed'], + 'blackout' => $s['blackout'], + 'blackoutCount' => $s['blackoutCount'], 'success' => $success, 'rowCount' => $s['rowCount'], ]; diff --git a/App/php/api/progress.php b/App/php/api/progress.php index 6e6dfc1..73d0103 100644 --- a/App/php/api/progress.php +++ b/App/php/api/progress.php @@ -162,6 +162,26 @@ if ($method === 'POST') { } // === Pre/Post-Antworten speichern === + // === Blackout (Energiemanager): Fehlversuch sofort protokollieren === + // Ein Blackout beendet das Set NICHT — die Schüler:in plant neu. Damit der + // Fehlversuch aber auch bei Abbruch beim Lehrer sichtbar bleibt, schreiben + // wir ihn als eigene assessments-Zeile (score:0 → roter Tag-Balken). + // KEIN player_progress-Update: ein Blackout gibt weder Sterne noch XP. + if ($action === 'blackout') { + $sessionId = Session::requireStudent(); + $simId = $body['simId'] ?? ''; + if (!$simId) Response::error('simId erforderlich'); + $session = $db->fetchOne('SELECT class_id FROM student_sessions WHERE id = ?', [$sessionId]); + if (!defined('GGS_MAX_SESSION_MS')) define('GGS_MAX_SESSION_MS', 7200000); + $durationMs = max(0, min((int)($body['durationMs'] ?? 0), GGS_MAX_SESSION_MS)); + $data = json_encode($body['data'] ?? []); + $db->execute( + 'INSERT INTO assessments (session_id, sim_id, class_id, results, duration_ms, submitted_at) VALUES (?, ?, ?, ?, ?, NOW())', + [$sessionId, $simId, $session['class_id'] ?? null, $data, $durationMs] + ); + Response::ok(['recorded' => 'blackout']); + } + if ($action === 'answer') { $sessionId = Session::requireStudent(); $simId = $body['simId'] ?? ''; diff --git a/App/sims/energiemanager/game.html b/App/sims/energiemanager/game.html index 6fd72d9..f909065 100644 --- a/App/sims/energiemanager/game.html +++ b/App/sims/energiemanager/game.html @@ -1817,6 +1817,9 @@ setRun.blackouts = (setRun.blackouts || 0) + 1; var tooLow = effFreq < FREQ_LOW; + // Blackout sofort ans Lehrer-Backend melden — auch wenn das Set danach + // abgebrochen wird, bleibt der Fehlversuch sichtbar (kein Set-Abschluss nötig). + submitBlackoutToBackend(blockIdx, effFreq, tooLow); var freqTxt = effFreq.toFixed(2).replace('.', ','); var cause = tooLow ? ('Die Netzfrequenz ist auf ' + freqTxt + ' Hz gefallen — es wurde zu wenig Strom erzeugt. ' @@ -2034,6 +2037,42 @@ renderSetSummaryUI(summary); } + // Ein Blackout ist der echte Verlierzustand → sofort als eigenes Fehl-Event + // melden. score:0 färbt den Tag-Balken rot; kein player_progress-Update + // (Blackout gibt keine Sterne/XP). Ursache/Frequenz/Block für die Lehrer-Sicht. + function submitBlackoutToBackend(blockIdx, effFreq, tooLow) { + if (!window.EM_SESSION_ID) return; // Free-Mode: kein Submit + var apiBase = window.EM_API_BASE || '/geograsim/App/api'; + var setMeta = SETS[currentSetIdx]; + try { + fetch(apiBase + '/progress', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ + action: 'blackout', + simId: 'energiemanager', + durationMs: Math.max(0, Date.now() - setRun.startedAt), + data: { + event: 'blackout', + score: 0, + setId: setMeta.id, + setTitle: setMeta.title, + setIcon: setMeta.icon, + day: currentDayIdx + 1, + dayTotal: setMeta.days.length, + dayTitle: DAY.title, + block: blockIdx + 1, + blockTotal: DAY.blocks.length, + freq: Math.round(effFreq * 100) / 100, + cause: tooLow ? 'unterfrequenz' : 'ueberfrequenz', + blackoutSeq: setRun.blackouts || 0 + } + }) + }).catch(function(e){ console.log('[energiemanager] blackout-submit fehlgeschlagen', e && e.message); }); + } catch (_) {} + } + function submitSetToBackend(summary) { if (!window.EM_SESSION_ID) return; // Free-Mode: kein Submit var apiBase = window.EM_API_BASE || '/geograsim/App/api'; diff --git a/App/teacher.html b/App/teacher.html index 2ad17c9..fe885c2 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -784,7 +784,8 @@ var LIVE_PRIMARY_FIELDS = { return v ? icon + v + idxBadge : '—'; }}, {k:'day', label:'Tag', fmt:function(v, row){ var s = row.state||{}; return s.dayTotal ? v+' / '+s.dayTotal+'' : '—'; }}, - {k:'block', label:'Block', fmt:function(v, row){ var s = row.state||{}; if (s.phase === 'plan') return 'plant'; return s.blockTotal ? (v+1)+' / '+s.blockTotal+'' : '—'; }}, + {k:'block', label:'Block', fmt:function(v, row){ var s = row.state||{}; if (s.phase === 'blackout') return '🔌 neu planen'; if (s.phase === 'plan') return 'plant'; return s.blockTotal ? (v+1)+' / '+s.blockTotal+'' : '—'; }}, + {k:'blackouts', label:'🔌', align:'center', fmt:function(v){ if (!v) return ''; var c = v >= 2 ? '#d8453a' : '#c9913a'; return ''+v+''; }}, {k:'balanced', label:'Tag ausg.', fmt:function(v, row){ var s = row.state||{}; if (s.phase !== 'done') return ''; var color = v === s.blockTotal ? '#5a8a5e' : v >= 4 ? '#c9913a' : '#c07a6b'; return ''+v+'/'+s.blockTotal+''; }}, {k:'setBlocksBalanced', label:'Set ausg.', fmt:function(v, row){ var s = row.state||{}; var max = (s.setTotal && s.dayTotal && s.blockTotal) ? s.dayTotal * s.blockTotal : 0; if (!max) return '—'; var color = v >= max*0.87 ? '#5a8a5e' : v >= max*0.5 ? '#c9913a' : '#c07a6b'; return ''+v+'/'+max+''; }}, {k:'upperLake', label:'⬆ See', unit:'GWh'}, @@ -1281,6 +1282,7 @@ function _liveNeedsHelp(simId, s) { case 'farmer': return (s.cash==null?1000:s.cash) < 0 || ((s.attempts||0) >= 6 && s.hit_rate != null && s.hit_rate < 30); case 'logistik': return (s.balance==null?1000:s.balance) < 0; case 'eu-werkstatt': return (s.attemptsThisStep||0) >= 6; + case 'energiemanager': return (s.blackouts||0) >= 2 || (s.phase === 'done' && (s.balanced||0) < 4); case 'tourismustal': return (s.money==null?1:s.money) < 0 || (s.lossStreak||0) >= 8; case 'tourismusregion': return (s.budget==null?1:s.budget) < -400 || (s.env==null?100:s.env) < 30; } @@ -2379,6 +2381,9 @@ async function renderLiveSummary() { var inProgress = !aborted && (sess.isLive || sess.success === 'running'); // Farbe = Erfolg (Ampel). Kein Wert → neutral grau. Abbruch → rot-gestrichelt. var col = aborted ? '#c0392b' : (score != null ? _tagTraffic(score) : '#aab2b8'); + // Energiemanager-Blackout: eigener Fehlversuch (score 0). Als 🔌 kennzeichnen, + // solange die Session nicht doch noch gut abgeschlossen wurde. + var isBlackout = !!sess.blackout && sess.success !== 'good' && (score == null || score < 50); var barH = 18; // EINHEITLICHE Höhe — der Erfolg steckt in der Farbe, nicht in der Höhe // Rahmen kodiert den Zustand: fertig = geschlossen · läuft = rechts gepunktet · Abbruch = gestrichelt var border = aborted ? 'border:1.5px dashed rgba(255,255,255,.7);' @@ -2387,7 +2392,8 @@ async function renderLiveSummary() { // Laufende Spiele: langsam blinkender Block. Pausiert → Hauptbalken ruhig // (der halbhohe Schwanz blinkt stattdessen). var anim = (inProgress && !sess.paused) ? 'animation:_tagblink 1.8s ease-in-out infinite;' : ''; - var stateWord = !inProgress ? (aborted ? 'abgebrochen' : 'fertig') : (sess.paused ? 'pausiert ⏸' : 'läuft noch'); + var stateWord = isBlackout ? 'Blackout 🔌' : (!inProgress ? (aborted ? 'abgebrochen' : 'fertig') : (sess.paused ? 'pausiert ⏸' : 'läuft noch')); + if (isBlackout) icon = '🔌'; var tip = (sess.simTitle||sess.simId) + (sess.lessonLabel?' · '+sess.lessonLabel:'') + ' · ' + _fmtHM(sess.startMs) + '–' + _fmtHM(endMs) + ' · ' + _liveFmtMin(realDurSec) + ' · ' + stateWord @@ -5114,6 +5120,28 @@ function renderSonnensystemRun(dt, dur, r, d) { } function renderEnergiemanagerRun(dt, dur, r, d) { + // Eigener Fehlversuch: Blackout (kein Set-Abschluss). + if (d.event === 'blackout') { + var causeTxt = d.cause === 'ueberfrequenz' + ? 'Überfrequenz — es wurde zu viel Strom erzeugt (pumpen fehlte).' + : 'Unterfrequenz — es wurde zu wenig Strom erzeugt (turbinieren fehlte).'; + var freqTxt = (d.freq != null) ? String(d.freq).replace('.', ',') + ' Hz' : '—'; + return '' + + '
' + + '
' + + '
🔌 Blackout · '+(d.setIcon||'⚡')+' '+esc(d.setTitle||d.setId||'?')+'' + + '
'+dt+' · '+dur+'
' + + '
' + + '
Netz
zusammengebrochen
' + + '
' + + '
' + + 'Tag '+(d.day||'?')+'/'+(d.dayTotal||'?')+(d.dayTitle?' · '+esc(d.dayTitle):'')+'' + + 'Block '+(d.block||'?')+'/'+(d.blockTotal||'?')+'' + + 'Frequenz: '+freqTxt+'' + + '
' + + '
'+causeTxt+'
' + + '
'; + } var stars = Math.max(0, Math.min(5, r.stars || 0)); var starsHtml = ''+'★'.repeat(stars)+''+'★'.repeat(5-stars)+''; var setIcon = d.setIcon || '⚡'; @@ -5140,7 +5168,7 @@ function renderEnergiemanagerRun(dt, dur, r, d) { + '
' + '
' + '
'+setIcon+' Set '+esc(setTitle)+'' - + '
'+dt+' · '+dur+' · '+(d.daysPlayed||0)+' Tage
' + + '
'+dt+' · '+dur+' · '+(d.daysPlayed||0)+' Tage'+((d.blackouts||0)>0 ? ' · 🔌 '+d.blackouts+' Blackout'+(d.blackouts===1?'':'s')+'' : '')+'
' + '
' + '
'+starsHtml+'
'+quote+'% ausgegl.
' + '
'