Dev Lab: server-autoritatives Entwicklerlabor mit 6 Mechaniken + 189 Aufgaben
Dev Lab bündelt mehrere Disziplinen (SQL, Java/Debugging, Git, HTML, CSS, Regex, PHP, REST) in 11 Spielen mit zusammen 189 Aufgaben. Im Gegensatz zur ersten, rein clientseitigen Skizze jetzt server-autoritativ, DB-gestützt und im Admin pflegbar – mit echter Mechanik-Vielfalt statt überall Lückentext: cloze (Lücke tippen), choice (Multiple Choice), order (Bausteine zusammensetzen), spot (Bug anklicken), match (Paare zuordnen), regex (Muster schreiben mit Live-Treffer-Vorschau). Prüfung und Score ausschließlich serverseitig – die Lösungen verlassen nie das Backend; Eingabe-Platzhalter sind neutrale Labels (Lösungs-Leak behoben). Fehler-Mechanik mit Versuchen + Hinweis + Auflösung, kein Game-Over; Score über die generische game_scores-Tabelle (Kennung devlab, deck=Spiel-Slug). - Entities DevLabGame/DevLabTask (JSON-payload) + Flyway V8__devlab.sql - DevLabService mit sechs Mechanik-Checkern; API /api/devlab/** (CSRF-ausgenommen, devlab in KNOWN_GAMES) - Admin /admin/devlab (Spiele + Aufgaben pflegen); polymorphe Frontend-Engine devlabgame.js + DB-getriebener Hub devlab.js - Inhalte aus devlab/content.json (idempotenter Seeder); ersetzt die erste clientseitige Dev-Lab-Skizze (devlab.js/devlabgame.js/-game.html/-game.css neu) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
/* Dev Lab Hub – lädt den Spiele-Katalog aus /api/devlab/catalog (DB-gestützt) und gruppiert
|
||||
die Spiele nach Trainingspfad. Spielkarten verlinken auf /devlab/games/{slug}. */
|
||||
(function () {
|
||||
const PATHS = [
|
||||
{ id: 'syntax', marker: '{}', title: 'Syntax & Grundlagen', type: 'Grundlagenpfad',
|
||||
copy: 'Sprachsyntax, Strukturen und Muster verstehen, bevor der Code komplex wird.',
|
||||
mechanics: ['Lücken füllen', 'Zusammensetzen', 'Multiple Choice', 'Regex live'], badge: 'Syntax Core' },
|
||||
{ id: 'debugging', marker: '</>', title: 'Debugging & Problemlösung', type: 'Ermittlungspfad',
|
||||
copy: 'Fehler finden, Ursache erkennen und den passenden Reparaturschritt wählen.',
|
||||
mechanics: ['Bug finden', 'Lücken füllen', 'Befehlsfolgen', 'Storyfälle'], badge: 'Bug Hunter' },
|
||||
{ id: 'visual', marker: '[]', title: 'Praxis & Visualisierung', type: 'Werkstattpfad',
|
||||
copy: 'Direkt sehen, was Regeln, Layouts, APIs und Kaskaden im System bewirken.',
|
||||
mechanics: ['Zuordnen', 'Multiple Choice', 'Statuscodes', 'Kaskade'], badge: 'Visual Builder' }
|
||||
];
|
||||
|
||||
const pathRoot = document.getElementById('dl-stations');
|
||||
const gameRoot = document.getElementById('dl-games');
|
||||
const mechanicsRoot = document.getElementById('dl-mechanics');
|
||||
const ui = {
|
||||
title: document.getElementById('dl-world-title'), type: document.getElementById('dl-world-type'),
|
||||
icon: document.getElementById('dl-world-icon'), copy: document.getElementById('dl-world-copy'),
|
||||
levels: document.getElementById('dl-levels'), xp: document.getElementById('dl-xp'),
|
||||
badge: document.getElementById('dl-badge'), gameCount: document.getElementById('dl-game-count')
|
||||
};
|
||||
|
||||
const esc = (v) => String(v).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
let byPath = {};
|
||||
|
||||
function renderStations(activeId) {
|
||||
pathRoot.innerHTML = PATHS.map((p) => {
|
||||
const games = byPath[p.id] || [];
|
||||
return `<button type="button" class="dl-station${p.id === activeId ? ' active' : ''}" data-path="${p.id}">
|
||||
<span class="dl-station-icon">${p.marker}</span>
|
||||
<span><h3>${esc(p.title)}</h3><p>${esc(p.copy)}</p>
|
||||
<span class="dl-station-meta"><span class="dl-chip">${games.length} Spiele</span><span class="dl-chip">${esc(p.type)}</span></span>
|
||||
</span></button>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderPath(p) {
|
||||
const games = byPath[p.id] || [];
|
||||
const totalTasks = games.reduce((sum, g) => sum + (g.taskCount || 0), 0);
|
||||
ui.title.textContent = p.title; ui.type.textContent = p.type; ui.icon.textContent = p.marker;
|
||||
ui.copy.textContent = p.copy; ui.levels.textContent = games.length; ui.xp.textContent = totalTasks; ui.badge.textContent = p.badge;
|
||||
ui.gameCount.textContent = `${games.length} Spiele`;
|
||||
mechanicsRoot.innerHTML = p.mechanics.map((m) => `<span class="dl-chip">${esc(m)}</span>`).join('');
|
||||
gameRoot.innerHTML = games.length ? games.map((g) => `
|
||||
<article class="dl-game playable">
|
||||
<div><h4>${esc(g.title)}</h4><p>${esc(g.description || '')}</p></div>
|
||||
<div class="dl-game-actions">
|
||||
<span class="dl-game-state">${g.taskCount} Aufgaben</span>
|
||||
<a class="mf-btn" href="/devlab/games/${encodeURIComponent(g.slug)}">Starten</a>
|
||||
</div>
|
||||
</article>`).join('') : '<p class="dl-empty">Für diesen Pfad sind noch keine Spiele freigeschaltet.</p>';
|
||||
renderStations(p.id);
|
||||
}
|
||||
|
||||
pathRoot.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.dl-station');
|
||||
if (!btn) return;
|
||||
const p = PATHS.find((x) => x.id === btn.dataset.path);
|
||||
if (p) renderPath(p);
|
||||
});
|
||||
|
||||
fetch('/api/devlab/catalog')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
byPath = {};
|
||||
(data.games || []).forEach((g) => {
|
||||
const key = PATHS.some((p) => p.id === g.path) ? g.path : 'syntax';
|
||||
(byPath[key] = byPath[key] || []).push(g);
|
||||
});
|
||||
renderPath(PATHS[0]);
|
||||
})
|
||||
.catch(() => renderPath(PATHS[0]));
|
||||
})();
|
||||
Reference in New Issue
Block a user