Files
mindflux/src/main/resources/devlab/content.json
T
AliceDarkest b88962da6f 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>
2026-06-30 16:32:03 +02:00

3959 lines
123 KiB
JSON

{
"games": [
{
"slug": "query-builder",
"title": "Query Builder",
"language": "SQL",
"path": "syntax",
"description": "Baue SQL-Abfragen Stück für Stück zusammen und lerne die Reihenfolge der Klauseln.",
"tasks": [
{
"type": "cloze",
"prompt": "Spalten auswählen",
"points": 100,
"narrative": "Jede Abfrage beginnt mit dem richtigen Keyword.",
"codeTemplate": "{{1}} name, stadt\nFROM kunden;",
"gaps": [
{
"position": 1,
"placeholder": "Keyword",
"hint": "Womit wählst du Spalten aus?",
"caseSensitive": false,
"solutions": [
"SELECT"
]
}
],
"hint": "Das allererste Wort jeder Lese-Abfrage.",
"successText": "Perfekt, die Spalten sind ausgewählt!"
},
{
"type": "match",
"prompt": "Klausel und Zweck",
"points": 100,
"instruction": "Ordne jede Klausel ihrer Aufgabe zu.",
"pairs": [
[
"WHERE",
"filtert einzelne Zeilen"
],
[
"GROUP BY",
"fasst Zeilen zu Gruppen zusammen"
],
[
"HAVING",
"filtert ganze Gruppen"
],
[
"ORDER BY",
"sortiert das Ergebnis"
]
],
"hint": "WHERE wirkt VOR der Gruppierung, HAVING danach.",
"successText": "Alle Klauseln richtig zugeordnet!"
},
{
"type": "choice",
"prompt": "Der Durchschnitt",
"points": 100,
"question": "Welche Aggregatfunktion liefert den Durchschnittswert?",
"options": [
"SUM()",
"AVG()",
"COUNT()",
"MAX()"
],
"correctIndex": 1,
"hint": "Average heißt Durchschnitt.",
"successText": "Richtig, AVG() berechnet den Mittelwert."
},
{
"type": "cloze",
"prompt": "Zeilen filtern",
"points": 100,
"narrative": "Du suchst nur Kunden aus Leipzig.",
"codeTemplate": "SELECT name\nFROM kunden\nWHERE stadt {{1}} 'Leipzig';",
"gaps": [
{
"position": 1,
"placeholder": "Operator",
"hint": "Test auf Gleichheit.",
"caseSensitive": false,
"solutions": [
"="
]
}
],
"hint": "In SQL prüft man Gleichheit mit einem einzelnen Zeichen.",
"successText": "Gut gefiltert!"
},
{
"type": "cloze",
"prompt": "Nur Eindeutiges",
"points": 100,
"narrative": "Du willst jede Stadt nur einmal sehen.",
"codeTemplate": "SELECT {{1}} stadt\nFROM kunden;",
"gaps": [
{
"position": 1,
"placeholder": "Keyword",
"hint": "Entfernt doppelte Werte.",
"caseSensitive": false,
"solutions": [
"DISTINCT"
]
}
],
"hint": "Das Wort steht direkt hinter SELECT.",
"successText": "Duplikate verschwunden!"
},
{
"type": "choice",
"prompt": "Alle Zeilen zählen",
"points": 100,
"question": "Wie zählst du die Anzahl aller Zeilen einer Tabelle?",
"options": [
"COUNT(*)",
"SUM(*)",
"TOTAL()",
"ROWS()"
],
"correctIndex": 0,
"hint": "Der Stern steht für alle Spalten.",
"successText": "Genau, COUNT(*) zählt jede Zeile."
},
{
"type": "cloze",
"prompt": "Gruppieren",
"points": 100,
"narrative": "Wie viele Mitarbeiter hat jede Abteilung?",
"codeTemplate": "SELECT abteilung, COUNT(*)\nFROM mitarbeiter\n{{1}} abteilung;",
"gaps": [
{
"position": 1,
"placeholder": "Klausel",
"hint": "Fasst Zeilen je Abteilung zusammen.",
"caseSensitive": false,
"solutions": [
"GROUP BY"
]
}
],
"hint": "Zwei Wörter mit Leerzeichen.",
"successText": "Sauber gruppiert!"
},
{
"type": "order",
"prompt": "Einfache Abfrage bauen",
"points": 100,
"instruction": "Bring die Zeilen in die korrekte SQL-Reihenfolge.",
"items": [
"SELECT name",
"FROM kunden",
"WHERE stadt = 'Berlin'",
"ORDER BY name"
],
"hint": "Erst auswählen, dann woher, dann filtern, dann sortieren.",
"successText": "Die Abfrage steht richtig!"
},
{
"type": "cloze",
"prompt": "Absteigend sortieren",
"points": 100,
"narrative": "Die ältesten Kunden sollen oben stehen.",
"codeTemplate": "SELECT name, alter\nFROM kunden\nORDER BY alter {{1}};",
"gaps": [
{
"position": 1,
"placeholder": "Keyword",
"hint": "Descending = absteigend.",
"caseSensitive": false,
"solutions": [
"DESC"
]
}
],
"hint": "Drei Buchstaben für absteigend.",
"successText": "Größter Wert zuerst!"
},
{
"type": "choice",
"prompt": "Vor der Gruppierung filtern",
"points": 100,
"question": "Welche Klausel filtert Zeilen, BEVOR gruppiert wird?",
"options": [
"HAVING",
"WHERE",
"GROUP BY",
"ORDER BY"
],
"correctIndex": 1,
"hint": "Diese Klausel arbeitet auf einzelnen Zeilen.",
"successText": "Richtig, WHERE filtert vor der Gruppierung."
},
{
"type": "cloze",
"prompt": "Gruppen filtern",
"points": 100,
"narrative": "Nur Abteilungen mit mehr als 5 Leuten interessieren dich.",
"codeTemplate": "SELECT abteilung, COUNT(*)\nFROM mitarbeiter\nGROUP BY abteilung\n{{1}} COUNT(*) > 5;",
"gaps": [
{
"position": 1,
"placeholder": "Klausel",
"hint": "Filtert nach dem Gruppieren.",
"caseSensitive": false,
"solutions": [
"HAVING"
]
}
],
"hint": "WHERE geht hier nicht, weil aggregiert wird.",
"successText": "Nur große Abteilungen bleiben übrig!"
},
{
"type": "choice",
"prompt": "Nach der Gruppierung",
"points": 100,
"question": "Welche Klausel filtert ganze Gruppen NACH GROUP BY?",
"options": [
"WHERE",
"HAVING",
"LIMIT",
"ON"
],
"correctIndex": 1,
"hint": "Sie kann Aggregatfunktionen wie COUNT(*) nutzen.",
"successText": "Korrekt, HAVING filtert Gruppen."
},
{
"type": "match",
"prompt": "Aggregatfunktionen",
"points": 100,
"instruction": "Ordne jede Funktion ihrer Bedeutung zu.",
"pairs": [
[
"COUNT()",
"Anzahl"
],
[
"SUM()",
"Summe"
],
[
"AVG()",
"Durchschnitt"
],
[
"MAX()",
"Maximum"
],
[
"MIN()",
"Minimum"
]
],
"hint": "Englische Wörter helfen: count, sum, average ...",
"successText": "Alle Funktionen sitzen!"
},
{
"type": "cloze",
"prompt": "Ergebnis begrenzen",
"points": 100,
"narrative": "Du brauchst nur die ersten 10 Treffer.",
"codeTemplate": "SELECT name\nFROM kunden\n{{1}} 10;",
"gaps": [
{
"position": 1,
"placeholder": "Klausel",
"hint": "Begrenzt die Zeilenzahl.",
"caseSensitive": false,
"solutions": [
"LIMIT"
]
}
],
"hint": "Englisch für Grenze.",
"successText": "Nur 10 Zeilen, perfekt!"
},
{
"type": "cloze",
"prompt": "Datensatz einfügen",
"points": 100,
"narrative": "Ein neuer Kunde kommt in die Tabelle.",
"codeTemplate": "{{1}} INTO kunden (name, stadt)\n{{2}} ('Anna', 'Berlin');",
"gaps": [
{
"position": 1,
"placeholder": "Keyword",
"hint": "Womit fügst du ein?",
"caseSensitive": false,
"solutions": [
"INSERT"
]
},
{
"position": 2,
"placeholder": "Keyword",
"hint": "Leitet die Werteliste ein.",
"caseSensitive": false,
"solutions": [
"VALUES"
]
}
],
"hint": "INSERT INTO ... VALUES (...).",
"successText": "Anna ist eingetragen!"
},
{
"type": "order",
"prompt": "Vollständige Abfrage",
"points": 100,
"instruction": "Setze die komplette Abfrage in die richtige Reihenfolge.",
"items": [
"SELECT abteilung, COUNT(*)",
"FROM mitarbeiter",
"WHERE gehalt > 3000",
"GROUP BY abteilung",
"HAVING COUNT(*) > 2",
"ORDER BY abteilung"
],
"hint": "SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY.",
"successText": "Die ganze Query steht korrekt!"
}
]
},
{
"slug": "sql-detective",
"title": "SQL Detective",
"language": "SQL",
"path": "debugging",
"description": "Löse IT-Ermittlungsfälle, indem du fehlerhafte und fehlende Teile von SQL-Abfragen aufspürst.",
"tasks": [
{
"type": "spot",
"prompt": "Fall: Falsche Anführungszeichen",
"points": 100,
"narrative": "Die Abfrage liefert einen Fehler statt der Berliner Kunden.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"SELECT",
"*",
"FROM",
"kunden",
"WHERE",
"stadt",
"=",
"\"Berlin\"",
";"
],
"bugIndex": 7,
"fix": "'Berlin'",
"hint": "String-Literale brauchen in SQL einfache Anführungszeichen.",
"successText": "Gelöst! Strings stehen in einfachen Quotes."
},
{
"type": "choice",
"prompt": "Fall: NULL-Werte finden",
"points": 100,
"question": "Welche Bedingung findet Zeilen ohne Stadt (NULL)?",
"options": [
"stadt = NULL",
"stadt IS NULL",
"stadt == NULL",
"stadt NULL()"
],
"correctIndex": 1,
"hint": "Mit = lässt sich NULL nicht vergleichen.",
"successText": "Richtig, NULL prüft man mit IS NULL."
},
{
"type": "cloze",
"prompt": "Fall: Fehlendes JOIN-Schlüsselwort",
"points": 100,
"narrative": "Der Verknüpfungs-Befehl fehlt zwischen den Tabellen.",
"codeTemplate": "SELECT *\nFROM bestellungen b\nJOIN kunden k {{1}} b.kunde_id = k.id;",
"gaps": [
{
"position": 1,
"placeholder": "Keyword",
"hint": "Gibt die Verknüpfungsbedingung an.",
"caseSensitive": false,
"solutions": [
"ON"
]
}
],
"hint": "Zwei Buchstaben.",
"successText": "Die Tabellen sind sauber verbunden!"
},
{
"type": "spot",
"prompt": "Fall: Kaputter Operator",
"points": 100,
"narrative": "Der Vergleichsoperator existiert so nicht.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"SELECT",
"name",
"FROM",
"kunden",
"WHERE",
"alter",
"=>",
"18",
";"
],
"bugIndex": 6,
"fix": ">=",
"hint": "Größer-oder-gleich schreibt man andersherum.",
"successText": "Korrekt, der Operator heißt >=."
},
{
"type": "cloze",
"prompt": "Fall: NULL-Prüfung ergänzen",
"points": 100,
"narrative": "Du suchst Kunden, bei denen keine Stadt hinterlegt ist.",
"codeTemplate": "SELECT name\nFROM kunden\nWHERE stadt {{1}} NULL;",
"gaps": [
{
"position": 1,
"placeholder": "Operator",
"hint": "Nicht = verwenden!",
"caseSensitive": false,
"solutions": [
"IS"
]
}
],
"hint": "Das Schlüsselwort für NULL-Vergleiche.",
"successText": "Sauber, IS NULL funktioniert."
},
{
"type": "choice",
"prompt": "Fall: Der richtige JOIN",
"points": 100,
"question": "Welcher JOIN liefert ALLE Zeilen der linken Tabelle, auch ohne Treffer rechts?",
"options": [
"INNER JOIN",
"LEFT JOIN",
"RIGHT JOIN",
"CROSS JOIN"
],
"correctIndex": 1,
"hint": "Links steht zuerst.",
"successText": "Richtig, LEFT JOIN behält alle linken Zeilen."
},
{
"type": "spot",
"prompt": "Fall: Verlorenes Komma",
"points": 100,
"narrative": "Zwischen zwei Spalten hat sich ein falsches Wort eingeschlichen.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"SELECT",
"name",
"AND",
"stadt",
"FROM",
"kunden",
";"
],
"bugIndex": 2,
"fix": ",",
"hint": "Spalten werden nicht mit AND getrennt.",
"successText": "Genau, Spalten trennt ein Komma."
},
{
"type": "cloze",
"prompt": "Fall: Muster-Suche",
"points": 100,
"narrative": "Du suchst alle Namen, die mit A beginnen.",
"codeTemplate": "SELECT name\nFROM kunden\nWHERE name {{1}} 'A%';",
"gaps": [
{
"position": 1,
"placeholder": "Operator",
"hint": "Für Mustervergleiche mit %.",
"caseSensitive": false,
"solutions": [
"LIKE"
]
}
],
"hint": "Vier Buchstaben für Textmuster.",
"successText": "Alle A-Namen gefunden!"
},
{
"type": "choice",
"prompt": "Fall: Nur gemeinsame Treffer",
"points": 100,
"question": "Welcher JOIN liefert nur Zeilen, die in BEIDEN Tabellen einen Treffer haben?",
"options": [
"LEFT JOIN",
"FULL JOIN",
"INNER JOIN",
"RIGHT JOIN"
],
"correctIndex": 2,
"hint": "Die Schnittmenge beider Tabellen.",
"successText": "Korrekt, INNER JOIN zeigt nur Übereinstimmungen."
},
{
"type": "spot",
"prompt": "Fall: NULL falsch verglichen",
"points": 100,
"narrative": "Diese Abfrage liefert nie ein Ergebnis, obwohl es NULL-Werte gibt.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"SELECT",
"*",
"FROM",
"kunden",
"WHERE",
"stadt",
"=",
"NULL",
";"
],
"bugIndex": 6,
"fix": "IS",
"hint": "NULL lässt sich nicht mit = vergleichen.",
"successText": "Gelöst! IS NULL statt = NULL."
},
{
"type": "cloze",
"prompt": "Fall: Bereich abfragen",
"points": 100,
"narrative": "Gesucht sind Produkte zwischen 10 und 20 Euro.",
"codeTemplate": "SELECT *\nFROM produkte\nWHERE preis {{1}} 10 AND 20;",
"gaps": [
{
"position": 1,
"placeholder": "Keyword",
"hint": "Für einen Wertebereich inklusive Grenzen.",
"caseSensitive": false,
"solutions": [
"BETWEEN"
]
}
],
"hint": "Englisch für 'zwischen'.",
"successText": "Bereich korrekt eingegrenzt!"
},
{
"type": "match",
"prompt": "Fall: JOIN-Typen einordnen",
"points": 100,
"instruction": "Ordne jedem JOIN-Typ seine Bedeutung zu.",
"pairs": [
[
"INNER JOIN",
"nur Treffer in beiden Tabellen"
],
[
"LEFT JOIN",
"alle links + passende rechts"
],
[
"RIGHT JOIN",
"alle rechts + passende links"
],
[
"FULL JOIN",
"alle Zeilen beider Tabellen"
]
],
"hint": "Denk an links, rechts und beide Seiten.",
"successText": "Alle JOIN-Typen aufgeklärt!"
},
{
"type": "spot",
"prompt": "Fall: Tippfehler im Schlüsselwort",
"points": 100,
"narrative": "Ein Schlüsselwort wurde vertippt und blockiert die Abfrage.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"SELECT",
"COUNT(*)",
"FROM",
"mitarbeiter",
"GROPU BY",
"abteilung",
";"
],
"bugIndex": 4,
"fix": "GROUP BY",
"hint": "Lies das Gruppierungs-Keyword genau.",
"successText": "Tippfehler entlarvt: GROUP BY!"
},
{
"type": "cloze",
"prompt": "Fall: Liste von Werten",
"points": 100,
"narrative": "Gesucht sind Kunden aus Berlin oder Leipzig.",
"codeTemplate": "SELECT *\nFROM kunden\nWHERE stadt {{1}} ('Berlin', 'Leipzig');",
"gaps": [
{
"position": 1,
"placeholder": "Operator",
"hint": "Prüft, ob ein Wert in einer Liste steht.",
"caseSensitive": false,
"solutions": [
"IN"
]
}
],
"hint": "Zwei Buchstaben.",
"successText": "Beide Städte erfasst!"
},
{
"type": "choice",
"prompt": "Fall: Kreuzprodukt",
"points": 100,
"question": "Wie viele Zeilen liefert ein CROSS JOIN von 3 und 4 Zeilen?",
"options": [
"7",
"12",
"3",
"4"
],
"correctIndex": 1,
"hint": "Beim Kreuzprodukt wird multipliziert.",
"successText": "Richtig, 3 mal 4 = 12 Zeilen."
},
{
"type": "cloze",
"prompt": "Fall: Eindeutige zählen",
"points": 100,
"narrative": "Wie viele verschiedene Städte kommen vor?",
"codeTemplate": "SELECT COUNT({{1}} stadt)\nFROM kunden;",
"gaps": [
{
"position": 1,
"placeholder": "Keyword",
"hint": "Zählt nur unterschiedliche Werte.",
"caseSensitive": false,
"solutions": [
"DISTINCT"
]
}
],
"hint": "Das gleiche Wort, das Duplikate entfernt.",
"successText": "Nur eindeutige Städte gezählt!"
}
]
},
{
"slug": "debug-hunt",
"title": "Debug Hunt",
"language": "Java",
"path": "debugging",
"description": "Jage echte Bugs in Java-Code: falsche Typen, Vergleiche, Indizes und mehr.",
"tasks": [
{
"type": "spot",
"prompt": "Falscher Datentyp",
"points": 100,
"narrative": "Hier wird einer int-Variablen ein Text zugewiesen.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"int",
"x",
"=",
"\"10\"",
";"
],
"bugIndex": 3,
"fix": "10",
"hint": "Ein int speichert Zahlen, keinen String.",
"successText": "Erwischt! 10 ohne Anführungszeichen."
},
{
"type": "choice",
"prompt": "Was wird ausgegeben?",
"points": 100,
"question": "Was gibt System.out.println(5 + 3 + \"!\"); aus?",
"options": [
"53!",
"8!",
"5 + 3!",
"!8"
],
"correctIndex": 1,
"hint": "Erst werden die Zahlen addiert, dann kommt der Text.",
"successText": "Richtig, 5+3=8, also 8!"
},
{
"type": "cloze",
"prompt": "Vergleich statt Zuweisung",
"points": 100,
"narrative": "In der Bedingung soll verglichen werden, nicht zugewiesen.",
"codeTemplate": "if (x {{1}} 5) {\n System.out.println(\"fünf\");\n}",
"gaps": [
{
"position": 1,
"placeholder": "Operator",
"hint": "Doppeltes Zeichen für Gleichheit.",
"caseSensitive": true,
"solutions": [
"=="
]
}
],
"hint": "Ein einzelnes = würde nur zuweisen.",
"successText": "Korrekt, == vergleicht Werte."
},
{
"type": "spot",
"prompt": "Fehlendes Semikolon",
"points": 100,
"narrative": "Diese Zeile endet mit dem falschen Zeichen.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"int",
"y",
"=",
"5",
","
],
"bugIndex": 4,
"fix": ";",
"hint": "Anweisungen enden in Java nicht mit Komma.",
"successText": "Gut, eine Anweisung endet mit ;"
},
{
"type": "cloze",
"prompt": "Null-Prüfung",
"points": 100,
"narrative": "Vor dem Zugriff soll geprüft werden, ob name nicht null ist.",
"codeTemplate": "if (name {{1}} null) {\n name.length();\n}",
"gaps": [
{
"position": 1,
"placeholder": "Operator",
"hint": "Ungleich.",
"caseSensitive": true,
"solutions": [
"!="
]
}
],
"hint": "Ausrufezeichen und Gleichheitszeichen.",
"successText": "Sicher, != null verhindert den Crash."
},
{
"type": "choice",
"prompt": "String-Vergleich",
"points": 100,
"question": "Welcher Fehler steckt in if (s == \"hallo\") für einen Inhaltsvergleich?",
"options": [
"Vergleicht Referenzen statt Inhalt",
"Syntaxfehler",
"Nichts, ist korrekt",
"s muss großgeschrieben sein"
],
"correctIndex": 0,
"hint": "Für String-Inhalte nutzt man equals().",
"successText": "Richtig, == vergleicht nur Referenzen."
},
{
"type": "spot",
"prompt": "Index außerhalb der Grenzen",
"points": 100,
"narrative": "Diese Schleife greift einen Schritt zu weit zu.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"for",
"(",
"int",
"i",
"=",
"0",
";",
"i",
"<=",
"arr.length",
";",
"i++",
")"
],
"bugIndex": 8,
"fix": "<",
"hint": "Gültige Indizes gehen von 0 bis length-1.",
"successText": "Gelöst! i < arr.length verhindert den Überlauf."
},
{
"type": "cloze",
"prompt": "Rückgabewert",
"points": 100,
"narrative": "Die Methode soll das Quadrat zurückgeben, tut es aber nicht.",
"codeTemplate": "int quadrat(int n) {\n {{1}} n * n;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Keyword",
"hint": "Liefert einen Wert zurück.",
"caseSensitive": true,
"solutions": [
"return"
]
}
],
"hint": "Sechs Buchstaben, kleingeschrieben.",
"successText": "Korrekt, return gibt das Ergebnis zurück."
},
{
"type": "choice",
"prompt": "Ganzzahl-Division",
"points": 100,
"question": "Was ergibt 10 / 3 in Java, wenn beide Werte int sind?",
"options": [
"3.33",
"3",
"4",
"3.0"
],
"correctIndex": 1,
"hint": "Bei int-Division wird der Rest verworfen.",
"successText": "Richtig, das Ergebnis ist 3."
},
{
"type": "spot",
"prompt": "Falscher Variablentyp",
"points": 100,
"narrative": "Ein Wahrheitswert wird im falschen Typ gespeichert.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"int",
"flag",
"=",
"true",
";"
],
"bugIndex": 0,
"fix": "boolean",
"hint": "true und false brauchen einen eigenen Typ.",
"successText": "Erwischt! boolean statt int."
},
{
"type": "cloze",
"prompt": "Typ für Wahrheitswerte",
"points": 100,
"narrative": "Welcher Datentyp speichert true oder false?",
"codeTemplate": "{{1}} aktiv = true;",
"gaps": [
{
"position": 1,
"placeholder": "Datentyp",
"hint": "Für ja/nein-Werte.",
"caseSensitive": true,
"solutions": [
"boolean"
]
}
],
"hint": "Sieben Buchstaben, kleingeschrieben.",
"successText": "Korrekt, boolean speichert Wahrheitswerte."
},
{
"type": "choice",
"prompt": "String-Verkettung",
"points": 100,
"question": "Was gibt System.out.println(\"\" + 5 + 3); aus?",
"options": [
"8",
"53",
"5 3",
"Fehler"
],
"correctIndex": 1,
"hint": "Sobald ein String dabei ist, wird verkettet statt addiert.",
"successText": "Richtig, das Ergebnis ist 53."
},
{
"type": "spot",
"prompt": "Verdrehter Operator",
"points": 100,
"narrative": "Der Ungleich-Operator wurde falsch herum geschrieben.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"if",
"(",
"a",
"=!",
"b",
")"
],
"bugIndex": 3,
"fix": "!=",
"hint": "Erst das Ausrufezeichen, dann das Gleichheitszeichen.",
"successText": "Gelöst! Der Operator heißt !=."
},
{
"type": "cloze",
"prompt": "Kurzschreibweise zum Addieren",
"points": 100,
"narrative": "Der Wert von summe soll um 5 erhöht werden.",
"codeTemplate": "int summe = 0;\nsumme {{1}} 5;",
"gaps": [
{
"position": 1,
"placeholder": "Operator",
"hint": "Kurzform für summe = summe + 5.",
"caseSensitive": true,
"solutions": [
"+="
]
}
],
"hint": "Plus und Gleichheitszeichen.",
"successText": "Korrekt, += addiert und weist zu."
},
{
"type": "choice",
"prompt": "Array-Zugriff",
"points": 100,
"question": "Was passiert bei int[] a = new int[3]; a[3] = 1;?",
"options": [
"Läuft normal durch",
"ArrayIndexOutOfBoundsException",
"NullPointerException",
"Kompilierfehler"
],
"correctIndex": 1,
"hint": "Gültige Indizes sind 0, 1 und 2.",
"successText": "Richtig, Index 3 existiert nicht."
},
{
"type": "cloze",
"prompt": "Schleifenbedingung",
"points": 100,
"narrative": "Die Schleife soll genau zehnmal laufen (0 bis 9).",
"codeTemplate": "for (int i = 0; i {{1}} 10; i++) {\n System.out.println(i);\n}",
"gaps": [
{
"position": 1,
"placeholder": "Operator",
"hint": "Kleiner als, nicht kleiner-gleich.",
"caseSensitive": true,
"solutions": [
"<"
]
}
],
"hint": "Mit <= würde sie elfmal laufen.",
"successText": "Korrekt, i < 10 ergibt 10 Durchläufe."
},
{
"type": "choice",
"prompt": "Was steht in b?",
"points": 100,
"question": "Welchen Wert hat b nach boolean b = 5 > 3;?",
"options": [
"true",
"false",
"1",
"Fehler"
],
"correctIndex": 0,
"hint": "Ist 5 wirklich größer als 3?",
"successText": "Richtig, 5 > 3 ist true."
}
]
},
{
"slug": "git-mission",
"title": "Git Mission",
"language": "Git",
"path": "debugging",
"description": "Meistere typische Repo-Situationen mit den richtigen Git-Befehlen und Reihenfolgen.",
"tasks": [
{
"type": "order",
"prompt": "Mission: Änderung veröffentlichen",
"points": 100,
"instruction": "Bring die Befehle in die richtige Reihenfolge.",
"items": [
"git add .",
"git commit -m \"feat: Login hinzugefügt\"",
"git push"
],
"hint": "Erst vormerken, dann festschreiben, dann hochladen.",
"successText": "Deine Änderung ist veröffentlicht!"
},
{
"type": "cloze",
"prompt": "Befehl: Festschreiben",
"points": 100,
"narrative": "Deine vorgemerkten Änderungen sollen gespeichert werden.",
"codeTemplate": "git {{1}} -m \"fix: Bug behoben\"",
"gaps": [
{
"position": 1,
"placeholder": "Befehl",
"hint": "Schreibt Änderungen in die Historie.",
"caseSensitive": true,
"solutions": [
"commit"
]
}
],
"hint": "Sechs Buchstaben.",
"successText": "Commit erstellt!"
},
{
"type": "choice",
"prompt": "Mission: Historie ansehen",
"points": 100,
"question": "Welcher Befehl zeigt den Commit-Verlauf?",
"options": [
"git status",
"git log",
"git diff",
"git show"
],
"correctIndex": 1,
"hint": "Englisch für Protokoll.",
"successText": "Richtig, git log zeigt die Historie."
},
{
"type": "cloze",
"prompt": "Befehl: Branch wechseln",
"points": 100,
"narrative": "Du willst zurück auf den main-Branch.",
"codeTemplate": "git {{1}} main",
"gaps": [
{
"position": 1,
"placeholder": "Befehl",
"hint": "Moderner Befehl zum Branch-Wechsel.",
"caseSensitive": true,
"solutions": [
"switch",
"checkout"
]
}
],
"hint": "switch oder das ältere checkout.",
"successText": "Du bist jetzt auf main!"
},
{
"type": "order",
"prompt": "Mission: Arbeit zwischenparken",
"points": 100,
"instruction": "Du musst schnell den Branch wechseln, ohne zu committen. Ordne die Schritte.",
"items": [
"git stash",
"git switch main",
"git pull",
"git switch feature",
"git stash pop"
],
"hint": "Erst sichern, Branch-Arbeit erledigen, dann zurückholen.",
"successText": "Deine Arbeit ist wieder da!"
},
{
"type": "choice",
"prompt": "Mission: Lokale Änderung verwerfen",
"points": 100,
"question": "Du willst eine noch NICHT committete Änderung an datei.txt verwerfen. Welcher Befehl?",
"options": [
"git restore datei.txt",
"git revert HEAD",
"git reset --hard origin",
"git rm datei.txt"
],
"correctIndex": 0,
"hint": "restore stellt den letzten committeten Stand wieder her.",
"successText": "Richtig, git restore verwirft die Änderung."
},
{
"type": "cloze",
"prompt": "Befehl: Branch zusammenführen",
"points": 100,
"narrative": "Der Feature-Branch soll in den aktuellen Branch übernommen werden.",
"codeTemplate": "git {{1}} feature/login",
"gaps": [
{
"position": 1,
"placeholder": "Befehl",
"hint": "Führt zwei Branches zusammen.",
"caseSensitive": true,
"solutions": [
"merge"
]
}
],
"hint": "Englisch für verschmelzen.",
"successText": "Branches zusammengeführt!"
},
{
"type": "order",
"prompt": "Mission: Neuer Feature-Branch",
"points": 100,
"instruction": "Starte ein neues Feature und veröffentliche es. Ordne die Schritte.",
"items": [
"git switch -c feature/login",
"git add .",
"git commit -m \"Login umgesetzt\"",
"git push -u origin feature/login"
],
"hint": "Branch anlegen, vormerken, committen, hochladen.",
"successText": "Feature-Branch steht online!"
},
{
"type": "cloze",
"prompt": "Befehl: Einzelnen Commit holen",
"points": 100,
"narrative": "Du brauchst genau einen Commit aus einem anderen Branch.",
"codeTemplate": "git {{1}} a1b2c3d",
"gaps": [
{
"position": 1,
"placeholder": "Befehl",
"hint": "Pflückt einen einzelnen Commit.",
"caseSensitive": true,
"solutions": [
"cherry-pick"
]
}
],
"hint": "Zwei Wörter mit Bindestrich.",
"successText": "Commit übernommen!"
},
{
"type": "choice",
"prompt": "Mission: Gepushten Commit zurücknehmen",
"points": 100,
"question": "Welcher Befehl macht einen bereits gepushten Commit mit einem neuen Commit rückgängig?",
"options": [
"git reset --hard",
"git revert <hash>",
"git checkout",
"git clean"
],
"correctIndex": 1,
"hint": "Er ändert die Historie nicht, sondern kehrt sie um.",
"successText": "Richtig, git revert ist sicher für geteilte Branches."
},
{
"type": "cloze",
"prompt": "Befehl: Commit umkehren",
"points": 100,
"narrative": "Der letzte Commit soll durch einen Gegen-Commit rückgängig gemacht werden.",
"codeTemplate": "git {{1}} HEAD",
"gaps": [
{
"position": 1,
"placeholder": "Befehl",
"hint": "Erzeugt einen umkehrenden Commit.",
"caseSensitive": true,
"solutions": [
"revert"
]
}
],
"hint": "Englisch für rückgängig machen.",
"successText": "Der Commit ist umgekehrt!"
},
{
"type": "order",
"prompt": "Mission: Merge-Konflikt lösen",
"points": 100,
"instruction": "Beim Zusammenführen entsteht ein Konflikt. Ordne die Schritte zur Lösung.",
"items": [
"git merge feature",
"git status",
"git add konflikt.txt",
"git commit"
],
"hint": "Mergen, Konflikt prüfen, gelöste Datei vormerken, abschließen.",
"successText": "Konflikt gelöst und gemergt!"
},
{
"type": "cloze",
"prompt": "Befehl: Arbeit zwischenspeichern",
"points": 100,
"narrative": "Du willst deine Änderungen kurz beiseitelegen, ohne zu committen.",
"codeTemplate": "git {{1}}",
"gaps": [
{
"position": 1,
"placeholder": "Befehl",
"hint": "Legt Änderungen auf einen Stapel.",
"caseSensitive": true,
"solutions": [
"stash"
]
}
],
"hint": "Englisch für Versteck.",
"successText": "Änderungen sicher zwischengespeichert!"
},
{
"type": "choice",
"prompt": "Mission: Remote-Stand holen",
"points": 100,
"question": "Welcher Befehl lädt Änderungen vom Remote UND führt sie zusammen?",
"options": [
"git fetch",
"git pull",
"git push",
"git clone"
],
"correctIndex": 1,
"hint": "fetch holt nur, dieser Befehl mergt zusätzlich.",
"successText": "Richtig, git pull holt und mergt."
},
{
"type": "cloze",
"prompt": "Befehl: Stash zurückholen",
"points": 100,
"narrative": "Deine zwischengespeicherten Änderungen sollen zurück in den Arbeitsbereich.",
"codeTemplate": "git stash {{1}}",
"gaps": [
{
"position": 1,
"placeholder": "Befehl",
"hint": "Holt den letzten Stash und entfernt ihn vom Stapel.",
"caseSensitive": true,
"solutions": [
"pop"
]
}
],
"hint": "Drei Buchstaben.",
"successText": "Deine Änderungen sind zurück!"
},
{
"type": "order",
"prompt": "Mission: Gepushten Commit rückgängig",
"points": 100,
"instruction": "Ein fehlerhafter Commit ist schon auf dem Remote. Ordne die sichere Lösung.",
"items": [
"git log --oneline",
"git revert a1b2c3d",
"git push"
],
"hint": "Hash finden, umkehren, hochladen.",
"successText": "Der fehlerhafte Commit ist neutralisiert!"
}
]
},
{
"slug": "html-architect",
"title": "HTML Architect",
"language": "HTML",
"path": "syntax",
"description": "Baue Seiten mit semantischem HTML: Struktur-Elemente, Attribute und sauberes Markup.",
"tasks": [
{
"type": "cloze",
"prompt": "Kopfbereich auszeichnen",
"points": 100,
"narrative": "Oben auf der Seite stehen Logo und Titel. Welches Element umschliesst diesen Kopfbereich?",
"codeTemplate": "<{{1}}>\n <h1>Mindflux</h1>\n</{{2}}>",
"gaps": [
{
"position": 1,
"placeholder": "Tag",
"hint": "Semantischer Kopfbereich.",
"caseSensitive": false,
"solutions": [
"header"
]
},
{
"position": 2,
"placeholder": "Tag",
"hint": "Gleiches Tag wie das oeffnende.",
"caseSensitive": false,
"solutions": [
"header"
]
}
],
"successText": "Stark! <header> markiert den einleitenden Bereich."
},
{
"type": "choice",
"prompt": "Hauptinhalt",
"points": 100,
"question": "Welches Element kennzeichnet den zentralen Hauptinhalt eines Dokuments?",
"options": [
"<main>",
"<content>",
"<body>",
"<section id=\"main\">"
],
"correctIndex": 0,
"hint": "Es darf pro Seite nur einmal sichtbar vorkommen.",
"successText": "Genau, <main> umschliesst den Hauptinhalt."
},
{
"type": "cloze",
"prompt": "Navigation",
"points": 100,
"narrative": "Die Linkliste fuehrt durch die Seite.",
"codeTemplate": "<{{1}} aria-label=\"Hauptmenue\">\n <a href=\"/\">Start</a>\n</{{2}}>",
"gaps": [
{
"position": 1,
"placeholder": "Tag",
"hint": "Steht fuer Navigation.",
"caseSensitive": false,
"solutions": [
"nav"
]
},
{
"position": 2,
"placeholder": "Tag",
"hint": "Gleiches Tag schliessen.",
"caseSensitive": false,
"solutions": [
"nav"
]
}
],
"successText": "<nav> ist die richtige Wahl fuer Navigationsbloecke."
},
{
"type": "order",
"prompt": "Seitengeruest ordnen",
"points": 100,
"instruction": "Bringe die Grundbausteine einer Seite in die uebliche Reihenfolge von oben nach unten.",
"items": [
"<header>",
"<nav>",
"<main>",
"<footer>"
],
"hint": "Kopf, dann Menue, dann Inhalt, dann Fuss.",
"successText": "Sauberes Dokument-Outline!"
},
{
"type": "cloze",
"prompt": "Eigenstaendiger Beitrag",
"points": 100,
"narrative": "Ein Blogpost soll auch einzeln (z. B. im RSS-Feed) sinnvoll sein.",
"codeTemplate": "<{{1}}>\n <h2>Blogpost</h2>\n <p>Text</p>\n</{{2}}>",
"gaps": [
{
"position": 1,
"placeholder": "Tag",
"hint": "Eigenstaendiger, wiederverwendbarer Inhalt.",
"caseSensitive": false,
"solutions": [
"article"
]
},
{
"position": 2,
"placeholder": "Tag",
"hint": "Gleiches Tag schliessen.",
"caseSensitive": false,
"solutions": [
"article"
]
}
],
"successText": "<article> kapselt einen in sich geschlossenen Beitrag."
},
{
"type": "choice",
"prompt": "Navigationsbereich",
"points": 100,
"question": "Womit zeichnest du eine Navigationsleiste semantisch korrekt aus?",
"options": [
"<nav>",
"<div class=\"nav\">",
"<navigation>",
"<menu role=\"nav\">"
],
"correctIndex": 0,
"hint": "Es gibt ein dediziertes HTML-Element dafuer.",
"successText": "Richtig, <nav> ist standardisiert und barrierearm."
},
{
"type": "spot",
"prompt": "Falsches schliessendes Tag",
"points": 100,
"narrative": "Ein schliessendes Tag passt nicht zum oeffnenden Element.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"<section>",
"Inhalt",
"</div>"
],
"bugIndex": 2,
"fix": "</section>",
"hint": "Welches Tag wurde geoeffnet?",
"successText": "Korrekt, oeffnendes und schliessendes Tag muessen uebereinstimmen."
},
{
"type": "cloze",
"prompt": "Thematischer Abschnitt",
"points": 100,
"narrative": "Ein klar abgegrenztes Kapitel mit eigener Ueberschrift.",
"codeTemplate": "<{{1}}>\n <h2>Kapitel 1</h2>\n</{{2}}>",
"gaps": [
{
"position": 1,
"placeholder": "Tag",
"hint": "Thematischer Block mit Ueberschrift.",
"caseSensitive": false,
"solutions": [
"section"
]
},
{
"position": 2,
"placeholder": "Tag",
"hint": "Gleiches Tag schliessen.",
"caseSensitive": false,
"solutions": [
"section"
]
}
],
"successText": "<section> gruppiert thematisch zusammengehoerigen Inhalt."
},
{
"type": "choice",
"prompt": "Randinhalt",
"points": 100,
"question": "Welches Element eignet sich fuer ergaenzende Randinhalte wie eine Sidebar?",
"options": [
"<aside>",
"<side>",
"<extra>",
"<div class=\"sidebar\">"
],
"correctIndex": 0,
"hint": "Inhalt, der nur indirekt zum Hauptinhalt gehoert.",
"successText": "Genau, <aside> ist fuer nebensaechliche Inhalte gedacht."
},
{
"type": "cloze",
"prompt": "Label verknuepfen",
"points": 100,
"narrative": "Das Label soll beim Klick das passende Eingabefeld fokussieren.",
"codeTemplate": "<label {{1}}=\"mail\">E-Mail</label>\n<input id=\"mail\" {{2}}=\"email\">",
"gaps": [
{
"position": 1,
"placeholder": "Attribut",
"hint": "Verweist auf die id des Inputs.",
"caseSensitive": false,
"solutions": [
"for"
]
},
{
"position": 2,
"placeholder": "Attribut",
"hint": "Bestimmt die Art des Eingabefelds.",
"caseSensitive": false,
"solutions": [
"type"
]
}
],
"successText": "for verknuepft Label und Feld, type setzt die Eingabeart."
},
{
"type": "spot",
"prompt": "Falscher Input-Typ",
"points": 100,
"narrative": "Im type-Wert hat sich ein Tippfehler eingeschlichen.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"<input",
"type=",
"\"emial\"",
">"
],
"bugIndex": 2,
"fix": "\"email\"",
"hint": "Lies den Typ-Wert genau.",
"successText": "type=\"email\" aktiviert die E-Mail-Validierung."
},
{
"type": "order",
"prompt": "Ueberschriften-Hierarchie",
"points": 100,
"instruction": "Ordne die Ueberschriften von der wichtigsten zur untergeordneten Ebene.",
"items": [
"<h1>",
"<h2>",
"<h3>",
"<h4>"
],
"hint": "h1 ist die oberste Ebene.",
"successText": "Eine saubere Hierarchie hilft Screenreadern und SEO."
},
{
"type": "cloze",
"prompt": "Fussbereich",
"points": 100,
"narrative": "Ganz unten stehen Copyright und Kontakt.",
"codeTemplate": "<{{1}}>\n <p>(c) 2026 Mindflux</p>\n</{{2}}>",
"gaps": [
{
"position": 1,
"placeholder": "Tag",
"hint": "Abschliessender Bereich.",
"caseSensitive": false,
"solutions": [
"footer"
]
},
{
"position": 2,
"placeholder": "Tag",
"hint": "Gleiches Tag schliessen.",
"caseSensitive": false,
"solutions": [
"footer"
]
}
],
"successText": "<footer> markiert den abschliessenden Bereich."
},
{
"type": "choice",
"prompt": "Bild mit Beschriftung",
"points": 100,
"question": "Welche Kombination beschreibt ein Bild mit Bildunterschrift semantisch korrekt?",
"options": [
"<figure> + <figcaption>",
"<img> + <caption>",
"<picture> + <legend>",
"<div> + <small>"
],
"correctIndex": 0,
"hint": "Eines kapselt das Bild, das andere die Beschriftung.",
"successText": "Richtig, <figcaption> gehoert in eine <figure>."
},
{
"type": "cloze",
"prompt": "Schaltflaeche",
"points": 100,
"narrative": "Der Button soll ein Formular absenden.",
"codeTemplate": "<button {{1}}=\"submit\">Senden</button>",
"gaps": [
{
"position": 1,
"placeholder": "Attribut",
"hint": "Bestimmt das Verhalten des Buttons.",
"caseSensitive": false,
"solutions": [
"type"
]
}
],
"successText": "type=\"submit\" loest das Absenden aus."
},
{
"type": "spot",
"prompt": "Nicht-semantischer Container",
"points": 100,
"narrative": "Eine Navigation sollte nicht als generischer Container ausgezeichnet sein.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"<div",
"class=",
"\"nav\"",
">"
],
"bugIndex": 0,
"fix": "<nav",
"hint": "Welches dedizierte Element gibt es fuer Navigation?",
"successText": "Mit <nav> brauchst du keine Hilfsklasse fuer die Bedeutung."
},
{
"type": "choice",
"prompt": "Ueberschriften-Ebene",
"points": 100,
"question": "Wie viele <h1>-Hauptueberschriften sind pro Seite ueblicherweise empfohlen?",
"options": [
"Genau eine",
"Beliebig viele",
"Mindestens drei",
"Keine"
],
"correctIndex": 0,
"hint": "Eine klare Hauptueberschrift pro Dokument.",
"successText": "Eine einzige h1 haelt die Struktur eindeutig."
},
{
"type": "cloze",
"prompt": "Seiteninhalt umschliessen",
"points": 100,
"narrative": "Zwischen Header und Footer steht der zentrale Inhaltsbereich.",
"codeTemplate": "<{{1}}>\n <h1>Willkommen</h1>\n</{{2}}>",
"gaps": [
{
"position": 1,
"placeholder": "Tag",
"hint": "Der eine zentrale Hauptinhalt.",
"caseSensitive": false,
"solutions": [
"main"
]
},
{
"position": 2,
"placeholder": "Tag",
"hint": "Gleiches Tag schliessen.",
"caseSensitive": false,
"solutions": [
"main"
]
}
],
"successText": "<main> umschliesst den dominanten Inhalt der Seite."
}
]
},
{
"slug": "css-designer",
"title": "CSS Designer",
"language": "CSS",
"path": "visual",
"description": "Layout mit Flexbox und Grid: display, gap, Ausrichtung und Responsive-Grundlagen.",
"tasks": [
{
"type": "cloze",
"prompt": "Flex-Container aktivieren",
"points": 100,
"narrative": "Die Buttons sollen in einer Reihe nebeneinander liegen.",
"codeTemplate": ".bar {\n {{1}}: {{2}};\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Bestimmt das Layout-Modell.",
"caseSensitive": false,
"solutions": [
"display"
]
},
{
"position": 2,
"placeholder": "Wert",
"hint": "Eindimensionales Reihen-Layout.",
"caseSensitive": false,
"solutions": [
"flex"
]
}
],
"successText": "display: flex macht das Element zum Flex-Container."
},
{
"type": "choice",
"prompt": "Horizontal verteilen",
"points": 100,
"question": "Mit welcher Property verteilst du Flex-Items entlang der Hauptachse?",
"options": [
"justify-content",
"align-items",
"text-align",
"float"
],
"correctIndex": 0,
"hint": "Haupt- vs. Querachse beachten.",
"successText": "justify-content steuert die Verteilung auf der Hauptachse."
},
{
"type": "spot",
"prompt": "Falscher Anzeige-Wert",
"points": 100,
"narrative": "Der Container soll ein Flexbox-Layout nutzen, aber der Wert stimmt nicht.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"display",
":",
"flexbox",
";"
],
"bugIndex": 2,
"fix": "flex",
"hint": "Der CSS-Wert heisst nicht wie das Konzept.",
"successText": "Korrekt, der Wert lautet flex, nicht flexbox."
},
{
"type": "match",
"prompt": "Layout-Properties zuordnen",
"points": 100,
"instruction": "Ordne jede Property ihrer Wirkung zu.",
"pairs": [
[
"display: flex",
"Flex-Layout aktivieren"
],
[
"gap",
"Abstand zwischen Items"
],
[
"justify-content",
"Ausrichtung Hauptachse"
],
[
"align-items",
"Ausrichtung Querachse"
]
],
"hint": "Achte auf Haupt- und Querachse.",
"successText": "Perfekt zugeordnet!"
},
{
"type": "cloze",
"prompt": "Abstand zwischen Items",
"points": 100,
"narrative": "Zwischen den Kacheln soll ein gleichmaessiger Abstand entstehen.",
"codeTemplate": ".grid {\n display: grid;\n {{1}}: 16px;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Funktioniert in Grid und Flex.",
"caseSensitive": false,
"solutions": [
"gap"
]
}
],
"successText": "gap erzeugt Abstaende ohne Rand-Tricks."
},
{
"type": "cloze",
"prompt": "Zentriert verteilen",
"points": 100,
"narrative": "Die Items sollen mittig auf der Hauptachse sitzen.",
"codeTemplate": ".row {\n display: flex;\n justify-content: {{1}};\n}",
"gaps": [
{
"position": 1,
"placeholder": "Wert",
"hint": "Mittige Ausrichtung.",
"caseSensitive": false,
"solutions": [
"center"
]
}
],
"successText": "center buendelt die Items in der Mitte."
},
{
"type": "choice",
"prompt": "Zweidimensionales Layout",
"points": 100,
"question": "Welches Modell eignet sich am besten fuer ein Raster aus Zeilen UND Spalten?",
"options": [
"display: grid",
"display: flex",
"display: block",
"display: inline"
],
"correctIndex": 0,
"hint": "Flex ist eindimensional, das gesuchte ist zweidimensional.",
"successText": "Grid ist fuer zweidimensionale Layouts gemacht."
},
{
"type": "spot",
"prompt": "Tippfehler im Wert",
"points": 100,
"narrative": "Der Verteilungs-Wert ist falsch geschrieben.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"justify-content",
":",
"space-betwen",
";"
],
"bugIndex": 2,
"fix": "space-between",
"hint": "Da fehlt ein Buchstabe.",
"successText": "Richtig, der Wert heisst space-between."
},
{
"type": "cloze",
"prompt": "Spalten definieren",
"points": 100,
"narrative": "Das Grid soll drei gleich breite Spalten bekommen.",
"codeTemplate": ".grid {\n display: grid;\n {{1}}: 1fr 1fr 1fr;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Definiert die Spaltenspur.",
"caseSensitive": false,
"solutions": [
"grid-template-columns"
]
}
],
"successText": "grid-template-columns legt Anzahl und Breite der Spalten fest."
},
{
"type": "match",
"prompt": "justify-content Werte",
"points": 100,
"instruction": "Ordne jeden Wert seiner Wirkung zu.",
"pairs": [
[
"center",
"Items mittig zusammen"
],
[
"space-between",
"Erstes/letztes aussen, Rest verteilt"
],
[
"flex-start",
"Items am Anfang"
],
[
"space-around",
"Gleicher Raum um jedes Item"
]
],
"hint": "Denk an die Position des ersten und letzten Items.",
"successText": "Alle Werte richtig erkannt!"
},
{
"type": "cloze",
"prompt": "Querachse ausrichten",
"points": 100,
"narrative": "Die Items sollen vertikal mittig in der Reihe stehen.",
"codeTemplate": ".row {\n display: flex;\n {{1}}: center;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Wirkt auf der Querachse.",
"caseSensitive": false,
"solutions": [
"align-items"
]
}
],
"successText": "align-items richtet Items auf der Querachse aus."
},
{
"type": "choice",
"prompt": "Responsives Verhalten",
"points": 100,
"question": "Womit passt du Styles ab einer bestimmten Bildschirmbreite an?",
"options": [
"@media (min-width: 600px) { ... }",
"@responsive 600px { ... }",
"@screen > 600 { ... }",
"@width: 600px"
],
"correctIndex": 0,
"hint": "Es ist eine At-Regel mit Bedingung.",
"successText": "Media Queries reagieren auf die Viewport-Breite."
},
{
"type": "cloze",
"prompt": "Eine flexible Spalte",
"points": 100,
"narrative": "Auf schmalen Schirmen soll alles in einer flexiblen Spalte ueber die volle Breite stehen.",
"codeTemplate": "@media (max-width: 600px) {\n .grid { grid-template-columns: {{1}}; }\n}",
"gaps": [
{
"position": 1,
"placeholder": "Wert",
"hint": "Eine flexible Einheit, die den ganzen Platz nimmt.",
"caseSensitive": false,
"solutions": [
"1fr"
]
}
],
"successText": "1fr fuellt den verfuegbaren Raum mit einer Spalte."
},
{
"type": "spot",
"prompt": "Tippfehler in der Property",
"points": 100,
"narrative": "Der Property-Name ist verdreht.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"grid-template-colmuns",
":",
"1fr 1fr",
";"
],
"bugIndex": 0,
"fix": "grid-template-columns",
"hint": "Lies das Wort 'columns' genau.",
"successText": "Richtig geschrieben heisst es grid-template-columns."
},
{
"type": "choice",
"prompt": "Luecke zwischen Zellen",
"points": 100,
"question": "Welche Property erzeugt gleichmaessige Abstaende zwischen Grid- oder Flex-Items?",
"options": [
"gap",
"margin-collapse",
"spacing",
"padding-between"
],
"correctIndex": 0,
"hint": "Funktioniert ohne Rand am letzten Element.",
"successText": "gap ist die moderne Loesung fuer Item-Abstaende."
},
{
"type": "cloze",
"prompt": "Grid-Container aktivieren",
"points": 100,
"narrative": "Die Galerie soll als Raster angeordnet werden.",
"codeTemplate": ".gallery {\n {{1}}: {{2}};\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Bestimmt das Layout-Modell.",
"caseSensitive": false,
"solutions": [
"display"
]
},
{
"position": 2,
"placeholder": "Wert",
"hint": "Zweidimensionales Raster.",
"caseSensitive": false,
"solutions": [
"grid"
]
}
],
"successText": "display: grid eroeffnet das Raster-Layout."
},
{
"type": "match",
"prompt": "Property zu Effekt",
"points": 100,
"instruction": "Ordne die Property dem sichtbaren Effekt zu.",
"pairs": [
[
"flex-direction: column",
"Items untereinander"
],
[
"flex-wrap: wrap",
"Umbruch in neue Zeile"
],
[
"grid-template-columns",
"Spaltenanzahl/-breite"
],
[
"gap",
"Abstand zwischen Items"
]
],
"hint": "Stell dir das Layout bildlich vor.",
"successText": "Sehr gut, alle Effekte richtig zugeordnet!"
},
{
"type": "spot",
"prompt": "Tippfehler im Property-Namen",
"points": 100,
"narrative": "Der Name der Ausrichtungs-Property ist falsch geschrieben.",
"instruction": "Klick den fehlerhaften Token an.",
"tokens": [
"allign-items",
":",
"center",
";"
],
"bugIndex": 0,
"fix": "align-items",
"hint": "Wie viele L hat 'align'?",
"successText": "Korrekt, align-items wird mit einem L geschrieben."
}
]
},
{
"slug": "css-cascade",
"title": "CSS Cascade",
"language": "CSS",
"path": "visual",
"description": "Verstehe Spezifitaet, Reihenfolge, Vererbung und !important - und welche Regel am Ende gewinnt.",
"tasks": [
{
"type": "choice",
"prompt": "Wer gewinnt: ID oder Klasse?",
"points": 100,
"question": "Beide Regeln treffen dasselbe Element:\n#box { color: red; }\n.box { color: blue; }\nWelche setzt sich durch?",
"options": [
"#box (ID ist spezifischer)",
".box (Klasse ist spezifischer)",
"Die zuletzt geschriebene Regel",
"Keine, sie heben sich auf"
],
"correctIndex": 0,
"hint": "ID-Selektoren wiegen schwerer als Klassen.",
"successText": "Richtig, eine ID schlaegt eine Klasse."
},
{
"type": "choice",
"prompt": "Inline gegen ID",
"points": 100,
"question": "Welche Deklaration hat die hoechste Spezifitaet (ohne !important)?",
"options": [
"Ein Inline-Style (style-Attribut)",
"Ein ID-Selektor",
"Ein Klassen-Selektor",
"Ein Element-Selektor"
],
"correctIndex": 0,
"hint": "Denke an die Spezifitaets-Stufen a,b,c,d.",
"successText": "Inline-Styles haben die hoechste normale Spezifitaet."
},
{
"type": "match",
"prompt": "Spezifitaet zuordnen",
"points": 100,
"instruction": "Ordne jeden Selektor seiner Spezifitaet (IDs, Klassen, Elemente) zu.",
"pairs": [
[
"#header",
"1,0,0"
],
[
".nav",
"0,1,0"
],
[
"a",
"0,0,1"
],
[
"#header .nav a",
"1,1,1"
]
],
"hint": "Zaehle IDs, dann Klassen, dann Element-Namen.",
"successText": "Perfekt, du zaehlst Spezifitaet richtig!"
},
{
"type": "cloze",
"prompt": "Spezifischer werden",
"points": 100,
"narrative": "Die Klasse allein verliert gegen eine andere Regel. Erhoehe die Spezifitaet, indem du die ID #card des Containers voranstellst.",
"codeTemplate": "{{1}} .title {\n color: var(--accent);\n}",
"gaps": [
{
"position": 1,
"placeholder": "Selektor",
"hint": "ID-Selektor des Containers.",
"caseSensitive": true,
"solutions": [
"#card"
]
}
],
"successText": "Mit #card davor gewinnt die Regel an Gewicht."
},
{
"type": "choice",
"prompt": "Notbremse !important",
"points": 100,
"question": "Welche Aussage ueber !important ist korrekt?",
"options": [
"Es ueberschreibt normale Deklarationen unabhaengig von der Spezifitaet",
"Es erhoeht die Spezifitaet um genau eine ID",
"Es wirkt nur bei Inline-Styles",
"Es deaktiviert die Vererbung"
],
"correctIndex": 0,
"hint": "!important steht ausserhalb der normalen Spezifitaets-Rechnung.",
"successText": "Genau, !important sticht normale Regeln - sparsam einsetzen."
},
{
"type": "choice",
"prompt": "Gleiche Spezifitaet",
"points": 100,
"question": "Zwei Regeln gleicher Spezifitaet treffen dasselbe Element:\n.btn { color: red; }\n.btn { color: green; }\nWelche gewinnt?",
"options": [
"Die spaeter im Stylesheet stehende (green)",
"Die zuerst stehende (red)",
"Beide gemischt",
"Die kuerzere Regel"
],
"correctIndex": 0,
"hint": "Bei Gleichstand entscheidet die Quellreihenfolge.",
"successText": "Bei gleicher Spezifitaet gewinnt die letzte Regel."
},
{
"type": "cloze",
"prompt": "Farbe als Token",
"points": 100,
"narrative": "Statt einer hartkodierten Hex-Farbe nutzt du eine CSS-Variable, damit Themes durchgreifen koennen.",
"codeTemplate": ".badge {\n color: {{1}};\n}",
"gaps": [
{
"position": 1,
"placeholder": "Wert",
"hint": "Greift auf eine Custom-Property zu: var(--name).",
"caseSensitive": false,
"solutions": [
"var(--accent)"
]
}
],
"successText": "var(--accent) bleibt theme-faehig statt fest verdrahtet."
},
{
"type": "choice",
"prompt": "Element gegen Klasse",
"points": 100,
"question": "p { color: black; } und .lead { color: gray; } treffen dasselbe Element. Welche gewinnt?",
"options": [
".lead (Klasse schlaegt Element)",
"p (Element schlaegt Klasse)",
"Die zuletzt geladene Datei",
"Keine"
],
"correctIndex": 0,
"hint": "Eine Klasse wiegt mehr als ein Element-Name.",
"successText": "Richtig, die Klasse .lead ist spezifischer."
},
{
"type": "match",
"prompt": "Kaskaden-Faktoren ordnen",
"points": 100,
"instruction": "Ordne jeden Faktor seiner Rolle in der Kaskade zu.",
"pairs": [
[
"Spezifitaet",
"Gewicht des Selektors"
],
[
"Reihenfolge",
"Spaeteres ueberschreibt Gleichwertiges"
],
[
"!important",
"Hebt normale Regeln auf"
],
[
"Vererbung",
"Werte fliessen an Kindelemente"
]
],
"hint": "Vier Saeulen entscheiden, welcher Wert greift.",
"successText": "Du kennst die Hebel der Kaskade!"
},
{
"type": "choice",
"prompt": "Spezifitaet zaehlen",
"points": 100,
"question": "Welche Spezifitaet (IDs, Klassen, Elemente) hat der Selektor ul li a.active?",
"options": [
"0,1,3",
"0,2,2",
"1,1,2",
"0,1,2"
],
"correctIndex": 0,
"hint": "Drei Element-Namen und eine Klasse.",
"successText": "Genau: 0 IDs, 1 Klasse, 3 Elemente = 0,1,3."
},
{
"type": "cloze",
"prompt": "Variable definieren",
"points": 100,
"narrative": "Lege das Theme-Token zentral an, damit alle Komponenten darauf zugreifen koennen.",
"codeTemplate": ":root {\n {{1}}: #4f46e5;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Custom-Property",
"hint": "Custom Properties beginnen mit zwei Bindestrichen.",
"caseSensitive": true,
"solutions": [
"--accent"
]
}
],
"successText": "--accent ist nun als wiederverwendbares Token verfuegbar."
},
{
"type": "choice",
"prompt": "Vererbung",
"points": 100,
"question": "Welche Eigenschaft wird typischerweise an Kindelemente vererbt?",
"options": [
"color",
"margin",
"padding",
"border"
],
"correctIndex": 0,
"hint": "Text-bezogene Eigenschaften vererben sich oft.",
"successText": "Richtig, color wird standardmaessig vererbt."
},
{
"type": "cloze",
"prompt": "Per ID treffen",
"points": 100,
"narrative": "Sprich genau das Element mit der ID main an.",
"codeTemplate": "{{1}} {\n padding: 24px;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Selektor",
"hint": "ID-Selektor beginnt mit einer Raute.",
"caseSensitive": true,
"solutions": [
"#main"
]
}
],
"successText": "#main spricht das Element ueber seine ID an."
},
{
"type": "choice",
"prompt": "Universalselektor",
"points": 100,
"question": "Welche Spezifitaet (IDs, Klassen, Elemente) traegt der Universalselektor * ?",
"options": [
"0,0,0",
"0,0,1",
"1,0,0",
"0,1,0"
],
"correctIndex": 0,
"hint": "Der Stern hat kein eigenes Gewicht.",
"successText": "Korrekt, * traegt die Spezifitaet 0,0,0."
},
{
"type": "match",
"prompt": "Selektoren vergleichen",
"points": 100,
"instruction": "Ordne jeden Selektor seiner Spezifitaet zu.",
"pairs": [
[
"*",
"0,0,0"
],
[
"li",
"0,0,1"
],
[
".active",
"0,1,0"
],
[
"#main",
"1,0,0"
]
],
"hint": "Von kein Gewicht bis ID aufsteigend.",
"successText": "Du sortierst Spezifitaet sicher!"
},
{
"type": "choice",
"prompt": "Wer schlaegt wen?",
"points": 100,
"question": "Eine Stylesheet-Regel hat !important, ein Inline-Style hat es nicht. Welche gewinnt?",
"options": [
"Die Regel mit !important",
"Der Inline-Style",
"Die mit der hoeheren Spezifitaet",
"Beide gleich"
],
"correctIndex": 0,
"hint": "!important steht ueber normalen Inline-Styles.",
"successText": "Richtig, !important schlaegt einen normalen Inline-Style."
},
{
"type": "cloze",
"prompt": "Klasse an Element binden",
"points": 100,
"narrative": "Erhoehe die Spezifitaet, indem du die Klasse active direkt an das Element a bindest.",
"codeTemplate": "a{{1}} {\n color: var(--accent);\n}",
"gaps": [
{
"position": 1,
"placeholder": "Selektor-Teil",
"hint": "Klassen-Selektor beginnt mit einem Punkt.",
"caseSensitive": true,
"solutions": [
".active"
]
}
],
"successText": "a.active ist spezifischer als a allein."
}
]
},
{
"slug": "flexbox-factory",
"title": "Flexbox Factory",
"language": "CSS",
"path": "visual",
"description": "Baue Flexbox-Layouts Schritt fuer Schritt: Richtung, Ausrichtung, Umbruch und flexible Items.",
"tasks": [
{
"type": "cloze",
"prompt": "Flexbox starten",
"points": 100,
"narrative": "Erster Schritt jedes Flex-Layouts: den Container aktivieren.",
"codeTemplate": ".container {\n {{1}}: {{2}};\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Bestimmt das Layout-Modell.",
"caseSensitive": false,
"solutions": [
"display"
]
},
{
"position": 2,
"placeholder": "Wert",
"hint": "Eindimensionales Flex-Layout.",
"caseSensitive": false,
"solutions": [
"flex"
]
}
],
"successText": "display: flex macht alle direkten Kinder zu Flex-Items."
},
{
"type": "choice",
"prompt": "Items untereinander",
"points": 100,
"question": "Welcher Wert von flex-direction stapelt die Items vertikal untereinander?",
"options": [
"column",
"row",
"stack",
"vertical"
],
"correctIndex": 0,
"hint": "Spalte statt Reihe.",
"successText": "column dreht die Hauptachse in die Vertikale."
},
{
"type": "cloze",
"prompt": "Richtung festlegen",
"points": 100,
"narrative": "Das Menue soll seine Eintraege untereinander anzeigen.",
"codeTemplate": ".menu {\n display: flex;\n {{1}}: column;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Bestimmt die Hauptachsen-Richtung.",
"caseSensitive": false,
"solutions": [
"flex-direction"
]
}
],
"successText": "flex-direction steuert, wohin die Hauptachse zeigt."
},
{
"type": "match",
"prompt": "Flex-Properties zuordnen",
"points": 100,
"instruction": "Ordne jede Property ihrer Wirkung zu.",
"pairs": [
[
"flex-direction",
"Richtung der Hauptachse"
],
[
"justify-content",
"Verteilung auf der Hauptachse"
],
[
"align-items",
"Ausrichtung auf der Querachse"
],
[
"flex-wrap",
"Umbruch in mehrere Zeilen"
]
],
"hint": "Haupt- vs. Querachse im Kopf behalten.",
"successText": "Alle Flex-Properties sitzen!"
},
{
"type": "cloze",
"prompt": "Auf der Hauptachse verteilen",
"points": 100,
"narrative": "Logo links, Menue rechts - mit maximalem Abstand dazwischen.",
"codeTemplate": ".row {\n display: flex;\n {{1}}: space-between;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Verteilt entlang der Hauptachse.",
"caseSensitive": false,
"solutions": [
"justify-content"
]
}
],
"successText": "justify-content: space-between druckt die Items nach aussen."
},
{
"type": "choice",
"prompt": "Hauptachse bei row",
"points": 100,
"question": "Bei flex-direction: row - in welche Richtung verlaeuft die Hauptachse?",
"options": [
"Horizontal (links nach rechts)",
"Vertikal (oben nach unten)",
"Diagonal",
"Es gibt keine Hauptachse"
],
"correctIndex": 0,
"hint": "row bedeutet Reihe.",
"successText": "Genau, bei row laeuft die Hauptachse horizontal."
},
{
"type": "order",
"prompt": "Zwei-Reihen-Layout bauen",
"points": 100,
"instruction": "Ordne die Schritte, um Items bei Bedarf in mehrere Reihen umbrechen zu lassen.",
"items": [
"display: flex;",
"flex-wrap: wrap;",
"gap: 16px;",
"justify-content: space-between;"
],
"hint": "Erst Flex aktivieren, dann Umbruch erlauben, dann verfeinern.",
"successText": "So entsteht ein sauberes, umbrechendes Layout."
},
{
"type": "cloze",
"prompt": "Umbruch erlauben",
"points": 100,
"narrative": "Passen die Items nicht in eine Zeile, sollen sie umbrechen.",
"codeTemplate": ".grid {\n display: flex;\n {{1}}: wrap;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Erlaubt mehrzeilige Flex-Container.",
"caseSensitive": false,
"solutions": [
"flex-wrap"
]
}
],
"successText": "flex-wrap: wrap bricht volle Zeilen automatisch um."
},
{
"type": "choice",
"prompt": "Gleiche Hoehe",
"points": 100,
"question": "Welcher align-items-Wert streckt alle Items auf die volle Hoehe des Containers?",
"options": [
"stretch",
"center",
"baseline",
"flex-start"
],
"correctIndex": 0,
"hint": "Es ist auch der Standardwert.",
"successText": "stretch dehnt die Items ueber die Querachse."
},
{
"type": "cloze",
"prompt": "Querachse zentrieren",
"points": 100,
"narrative": "Die Icons in der Leiste sollen vertikal mittig sitzen.",
"codeTemplate": ".bar {\n display: flex;\n {{1}}: center;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Wirkt auf der Querachse.",
"caseSensitive": false,
"solutions": [
"align-items"
]
}
],
"successText": "align-items: center zentriert auf der Querachse."
},
{
"type": "match",
"prompt": "Verteilungs-Werte",
"points": 100,
"instruction": "Ordne jeden justify-content-Wert seiner Wirkung zu.",
"pairs": [
[
"center",
"Mittig zusammen"
],
[
"space-between",
"Aussen buendig, Rest verteilt"
],
[
"flex-end",
"Am Ende der Achse"
],
[
"space-evenly",
"Ueberall gleicher Abstand"
]
],
"hint": "Achte auf die Raender.",
"successText": "Du beherrschst die Verteilungs-Werte!"
},
{
"type": "choice",
"prompt": "Verfuegbaren Platz fuellen",
"points": 100,
"question": "Womit laesst du ein einzelnes Flex-Item den verbleibenden freien Platz einnehmen?",
"options": [
"flex-grow: 1",
"flex-shrink: 0",
"order: 1",
"align-self: stretch"
],
"correctIndex": 0,
"hint": "Es geht ums Wachsen, nicht ums Schrumpfen.",
"successText": "flex-grow: 1 laesst das Item expandieren."
},
{
"type": "cloze",
"prompt": "Abstand setzen",
"points": 100,
"narrative": "Zwischen den Werkzeug-Buttons soll ein fester Abstand liegen.",
"codeTemplate": ".toolbar {\n display: flex;\n {{1}}: 12px;\n}",
"gaps": [
{
"position": 1,
"placeholder": "Property",
"hint": "Funktioniert auch in Flex-Containern.",
"caseSensitive": false,
"solutions": [
"gap"
]
}
],
"successText": "gap haelt die Items gleichmaessig auf Abstand."
},
{
"type": "order",
"prompt": "Item perfekt zentrieren",
"points": 100,
"instruction": "Ordne die Schritte, um ein einzelnes Item horizontal UND vertikal zu zentrieren.",
"items": [
"display: flex;",
"justify-content: center;",
"align-items: center;"
],
"hint": "Erst Flex, dann beide Achsen zentrieren.",
"successText": "Der Klassiker fuer perfekte Zentrierung!"
},
{
"type": "choice",
"prompt": "Kurzschreibweise flex",
"points": 100,
"question": "Was bedeutet die Kurzform flex: 1; ?",
"options": [
"flex-grow: 1; flex-shrink: 1; flex-basis: 0%",
"Nur flex-grow: 1",
"flex-basis: 1px",
"flex-wrap: 1"
],
"correctIndex": 0,
"hint": "Die Kurzform setzt drei Eigenschaften auf einmal.",
"successText": "Richtig, flex: 1 entspricht 1 1 0%."
},
{
"type": "cloze",
"prompt": "Reihenfolge umkehren",
"points": 100,
"narrative": "Die Items sollen horizontal liegen, aber von rechts nach links angeordnet sein.",
"codeTemplate": ".row {\n display: flex;\n flex-direction: {{1}};\n}",
"gaps": [
{
"position": 1,
"placeholder": "Wert",
"hint": "Reihe, aber umgekehrt.",
"caseSensitive": false,
"solutions": [
"row-reverse"
]
}
],
"successText": "row-reverse dreht die Hauptachsen-Richtung um."
},
{
"type": "match",
"prompt": "Achsen unterscheiden",
"points": 100,
"instruction": "Ordne jede Property der Achse zu, auf der sie wirkt.",
"pairs": [
[
"justify-content",
"Hauptachse"
],
[
"align-items",
"Querachse"
],
[
"flex-direction",
"Definiert die Hauptachse"
],
[
"align-self",
"Einzelnes Item, Querachse"
]
],
"hint": "justify = Haupt, align = Quer.",
"successText": "Du trennst Haupt- und Querachse sicher!"
},
{
"type": "choice",
"prompt": "Querachse benennen",
"points": 100,
"question": "Bei flex-direction: row verlaeuft die Querachse ...",
"options": [
"vertikal (oben nach unten)",
"horizontal (links nach rechts)",
"diagonal",
"im Kreis"
],
"correctIndex": 0,
"hint": "Die Querachse steht senkrecht zur Hauptachse.",
"successText": "Genau, bei row ist die Querachse vertikal."
}
]
},
{
"slug": "regex-master",
"title": "Regex Master",
"language": "Regex",
"path": "syntax",
"description": "Schreibe reguläre Ausdrücke und lass sie alle Beispiele korrekt klassifizieren.",
"tasks": [
{
"type": "regex",
"prompt": "Nur Ziffern",
"points": 100,
"instruction": "Schreibe ein Muster, das nur aus einer oder mehreren Ziffern besteht.",
"samples": [
{
"text": "12345",
"shouldMatch": true
},
{
"text": "007",
"shouldMatch": true
},
{
"text": "12a",
"shouldMatch": false
},
{
"text": "",
"shouldMatch": false
},
{
"text": "3 4",
"shouldMatch": false
}
],
"pattern": "^\\d+$",
"hint": "\\d steht für eine Ziffer, + für ein oder mehr.",
"successText": "Saubere Ziffernfolge!"
},
{
"type": "regex",
"prompt": "Nur Kleinbuchstaben",
"points": 100,
"instruction": "Akzeptiere ausschließlich Kleinbuchstaben a-z (mindestens einen).",
"samples": [
{
"text": "hallo",
"shouldMatch": true
},
{
"text": "abc",
"shouldMatch": true
},
{
"text": "Hallo",
"shouldMatch": false
},
{
"text": "abc123",
"shouldMatch": false
},
{
"text": "",
"shouldMatch": false
}
],
"pattern": "^[a-z]+$",
"hint": "Eine Zeichenklasse [a-z] deckt genau die Kleinbuchstaben ab.",
"successText": "Alles klein geschrieben."
},
{
"type": "regex",
"prompt": "Binärzahl",
"points": 100,
"instruction": "Erlaube nur die Ziffern 0 und 1.",
"samples": [
{
"text": "1010",
"shouldMatch": true
},
{
"text": "0",
"shouldMatch": true
},
{
"text": "102",
"shouldMatch": false
},
{
"text": "",
"shouldMatch": false
},
{
"text": "10 01",
"shouldMatch": false
}
],
"pattern": "^[01]+$",
"hint": "Nur die beiden Zeichen 0 und 1 sind erlaubt.",
"successText": "0 und 1, mehr braucht es nicht."
},
{
"type": "regex",
"prompt": "Postleitzahl",
"points": 100,
"instruction": "Eine deutsche PLZ besteht aus genau 5 Ziffern.",
"samples": [
{
"text": "12345",
"shouldMatch": true
},
{
"text": "01067",
"shouldMatch": true
},
{
"text": "1234",
"shouldMatch": false
},
{
"text": "123456",
"shouldMatch": false
},
{
"text": "12a45",
"shouldMatch": false
}
],
"pattern": "^\\d{5}$",
"hint": "Mit {5} legst du eine feste Anzahl fest.",
"successText": "Genau 5 Stellen!"
},
{
"type": "regex",
"prompt": "CSS-Datei",
"points": 100,
"instruction": "Dateiname aus Wortzeichen/Bindestrich, der auf .css endet.",
"samples": [
{
"text": "style.css",
"shouldMatch": true
},
{
"text": "main.css",
"shouldMatch": true
},
{
"text": "style.scss",
"shouldMatch": false
},
{
"text": "style.css.map",
"shouldMatch": false
},
{
"text": "styles",
"shouldMatch": false
}
],
"pattern": "^[\\w-]+\\.css$",
"hint": "Den Punkt musst du als \\. maskieren.",
"successText": "Endung .css erkannt."
},
{
"type": "regex",
"prompt": "Hex-Farbe",
"points": 100,
"instruction": "Eine Hex-Farbe: # gefolgt von genau 6 Hex-Ziffern.",
"samples": [
{
"text": "#ff0000",
"shouldMatch": true
},
{
"text": "#A1B2C3",
"shouldMatch": true
},
{
"text": "#fff",
"shouldMatch": false
},
{
"text": "ff0000",
"shouldMatch": false
},
{
"text": "#gggggg",
"shouldMatch": false
}
],
"pattern": "^#[0-9a-fA-F]{6}$",
"hint": "Hex-Ziffern sind 0-9 sowie a-f und A-F.",
"successText": "Perfekte Farbe!"
},
{
"type": "regex",
"prompt": "Eigenname",
"points": 100,
"instruction": "Erster Buchstabe groß, danach nur Kleinbuchstaben.",
"samples": [
{
"text": "Max",
"shouldMatch": true
},
{
"text": "Anna",
"shouldMatch": true
},
{
"text": "max",
"shouldMatch": false
},
{
"text": "MAX",
"shouldMatch": false
},
{
"text": "Max1",
"shouldMatch": false
}
],
"pattern": "^[A-Z][a-z]+$",
"hint": "Ein Großbuchstabe, dann eine Folge Kleinbuchstaben.",
"successText": "Groß angefangen, klein weiter."
},
{
"type": "regex",
"prompt": "Variablenname",
"points": 100,
"instruction": "Beginnt mit Buchstabe oder _, danach Wortzeichen.",
"samples": [
{
"text": "userName",
"shouldMatch": true
},
{
"text": "_count",
"shouldMatch": true
},
{
"text": "user_1",
"shouldMatch": true
},
{
"text": "1user",
"shouldMatch": false
},
{
"text": "user name",
"shouldMatch": false
},
{
"text": "user-name",
"shouldMatch": false
}
],
"pattern": "^[a-zA-Z_]\\w*$",
"hint": "\\w steht für Buchstaben, Ziffern und Unterstrich.",
"successText": "Gültiger Bezeichner!"
},
{
"type": "regex",
"prompt": "Benutzername",
"points": 100,
"instruction": "3 bis 16 Wortzeichen (Buchstaben, Ziffern, Unterstrich).",
"samples": [
{
"text": "max_99",
"shouldMatch": true
},
{
"text": "abc",
"shouldMatch": true
},
{
"text": "ab",
"shouldMatch": false
},
{
"text": "this_is_way_too_long_name",
"shouldMatch": false
},
{
"text": "max mustermann",
"shouldMatch": false
}
],
"pattern": "^\\w{3,16}$",
"hint": "Mit {3,16} gibst du eine Spanne an.",
"successText": "Länge passt!"
},
{
"type": "regex",
"prompt": "URL-Slug",
"points": 100,
"instruction": "Kleinbuchstaben/Ziffern, mit einzelnen Bindestrichen getrennt.",
"samples": [
{
"text": "mein-erster-post",
"shouldMatch": true
},
{
"text": "blog",
"shouldMatch": true
},
{
"text": "abc123",
"shouldMatch": true
},
{
"text": "Mein-Post",
"shouldMatch": false
},
{
"text": "mein--post",
"shouldMatch": false
},
{
"text": "mein-post-",
"shouldMatch": false
}
],
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
"hint": "Kein Bindestrich am Anfang/Ende und keine doppelten.",
"successText": "Sauberer Slug!"
},
{
"type": "regex",
"prompt": "E-Mail (.de)",
"points": 100,
"instruction": "Einfache E-Mail mit @ und Endung .de.",
"samples": [
{
"text": "max@web.de",
"shouldMatch": true
},
{
"text": "info@firma.de",
"shouldMatch": true
},
{
"text": "max@web.com",
"shouldMatch": false
},
{
"text": "maxweb.de",
"shouldMatch": false
},
{
"text": "MAX@WEB.DE",
"shouldMatch": false
}
],
"pattern": "^[\\w.-]+@[\\w.-]+\\.de$",
"hint": "Verankere die Endung mit \\.de am Stringende.",
"successText": "Gültige .de-Adresse!"
},
{
"type": "regex",
"prompt": "Dezimalzahl",
"points": 100,
"instruction": "Ganzzahl mit optionalem Vorzeichen und optionalem Nachkommateil (Punkt).",
"samples": [
{
"text": "3.14",
"shouldMatch": true
},
{
"text": "42",
"shouldMatch": true
},
{
"text": "-0.5",
"shouldMatch": true
},
{
"text": "3.",
"shouldMatch": false
},
{
"text": ".5",
"shouldMatch": false
},
{
"text": "1,5",
"shouldMatch": false
}
],
"pattern": "^-?\\d+(\\.\\d+)?$",
"hint": "Der Nachkommateil ist eine optionale Gruppe (...)?.",
"successText": "Zahl korrekt erkannt."
},
{
"type": "regex",
"prompt": "Uhrzeit HH:MM",
"points": 100,
"instruction": "24-Stunden-Uhrzeit von 00:00 bis 23:59.",
"samples": [
{
"text": "09:30",
"shouldMatch": true
},
{
"text": "23:59",
"shouldMatch": true
},
{
"text": "00:00",
"shouldMatch": true
},
{
"text": "24:00",
"shouldMatch": false
},
{
"text": "9:30",
"shouldMatch": false
},
{
"text": "12:60",
"shouldMatch": false
}
],
"pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
"hint": "Trenne die Stundenbereiche mit einer Alternative |.",
"successText": "Zeit im gültigen Bereich!"
},
{
"type": "regex",
"prompt": "Telefonnummer",
"points": 100,
"instruction": "Beginnt mit +49 oder 0, danach 3 bis 12 Ziffern.",
"samples": [
{
"text": "+4915112345",
"shouldMatch": true
},
{
"text": "017212345",
"shouldMatch": true
},
{
"text": "12345678",
"shouldMatch": false
},
{
"text": "+49",
"shouldMatch": false
},
{
"text": "+1234567",
"shouldMatch": false
},
{
"text": "0",
"shouldMatch": false
}
],
"pattern": "^(\\+49|0)[0-9]{3,12}$",
"hint": "Das + ist ein Sonderzeichen und muss als \\+ maskiert werden.",
"successText": "Nummer im richtigen Format!"
},
{
"type": "regex",
"prompt": "IPv4-Adresse",
"points": 100,
"instruction": "Vier durch Punkte getrennte Zahlengruppen (grob, 1-3 Ziffern).",
"samples": [
{
"text": "192.168.0.1",
"shouldMatch": true
},
{
"text": "8.8.8.8",
"shouldMatch": true
},
{
"text": "255.255.255.255",
"shouldMatch": true
},
{
"text": "192.168.0",
"shouldMatch": false
},
{
"text": "192.168.0.1.5",
"shouldMatch": false
},
{
"text": "1234.0.0.1",
"shouldMatch": false
}
],
"pattern": "^\\d{1,3}(\\.\\d{1,3}){3}$",
"hint": "Eine Gruppe (\\.\\d{1,3}) wiederholt sich genau dreimal.",
"successText": "Vier Oktette erkannt!"
},
{
"type": "regex",
"prompt": "Datum ISO",
"points": 100,
"instruction": "Datum im Format YYYY-MM-DD mit gültigem Monat/Tag-Bereich.",
"samples": [
{
"text": "2026-06-30",
"shouldMatch": true
},
{
"text": "1999-12-31",
"shouldMatch": true
},
{
"text": "2026-13-01",
"shouldMatch": false
},
{
"text": "2026-00-10",
"shouldMatch": false
},
{
"text": "26-06-30",
"shouldMatch": false
},
{
"text": "2026/06/30",
"shouldMatch": false
}
],
"pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$",
"hint": "Begrenze Monat auf 01-12 und Tag auf 01-31 mit Alternativen.",
"successText": "Gültiges ISO-Datum!"
},
{
"type": "regex",
"prompt": "Datum deutsch",
"points": 100,
"instruction": "Datum im Format DD.MM.YYYY mit gültigem Tag/Monat-Bereich.",
"samples": [
{
"text": "30.06.2026",
"shouldMatch": true
},
{
"text": "01.01.2000",
"shouldMatch": true
},
{
"text": "31.13.2026",
"shouldMatch": false
},
{
"text": "1.6.2026",
"shouldMatch": false
},
{
"text": "30/06/2026",
"shouldMatch": false
},
{
"text": "32.06.2026",
"shouldMatch": false
}
],
"pattern": "^(0[1-9]|[12][0-9]|3[01])\\.(0[1-9]|1[0-2])\\.\\d{4}$",
"hint": "Die Trennpunkte musst du als \\. schreiben.",
"successText": "Datum sitzt!"
},
{
"type": "regex",
"prompt": "Webadresse",
"points": 100,
"instruction": "URL mit http oder https, Domain und optionalem Pfad.",
"samples": [
{
"text": "http://example.com",
"shouldMatch": true
},
{
"text": "https://www.google.de/search",
"shouldMatch": true
},
{
"text": "https://test.io/path/to/page",
"shouldMatch": true
},
{
"text": "ftp://example.com",
"shouldMatch": false
},
{
"text": "http//example.com",
"shouldMatch": false
},
{
"text": "example.com",
"shouldMatch": false
}
],
"pattern": "^https?://[\\w.-]+\\.[a-z]{2,}(/.*)?$",
"hint": "Das s in https ist optional: https? .",
"successText": "Gültige URL!"
},
{
"type": "regex",
"prompt": "Kreditkartennummer",
"points": 100,
"instruction": "16 Ziffern in vier Vierergruppen, durch Leerzeichen getrennt.",
"samples": [
{
"text": "1234 5678 9012 3456",
"shouldMatch": true
},
{
"text": "0000 0000 0000 0000",
"shouldMatch": true
},
{
"text": "1234567890123456",
"shouldMatch": false
},
{
"text": "1234 5678 9012",
"shouldMatch": false
},
{
"text": "12 34 56 78",
"shouldMatch": false
}
],
"pattern": "^(\\d{4} ){3}\\d{4}$",
"hint": "Drei Gruppen mit Leerzeichen, dann eine letzte ohne.",
"successText": "Format stimmt!"
},
{
"type": "regex",
"prompt": "Wortgrenze: PHP",
"points": 100,
"instruction": "Der Text enthält das eigenständige Wort PHP (als ganzes Wort).",
"samples": [
{
"text": "Ich liebe PHP sehr",
"shouldMatch": true
},
{
"text": "PHP ist toll",
"shouldMatch": true
},
{
"text": "PHPStorm ist gut",
"shouldMatch": false
},
{
"text": "Ich mag Python",
"shouldMatch": false
},
{
"text": "phpmyadmin laden",
"shouldMatch": false
}
],
"pattern": "^.*\\bPHP\\b.*$",
"hint": "\\b markiert eine Wortgrenze rund um das Wort.",
"successText": "Ganzes Wort erkannt!"
},
{
"type": "regex",
"prompt": "Sicheres Passwort",
"points": 100,
"instruction": "Mindestens 8 Zeichen und enthält mindestens einen Buchstaben und eine Ziffer.",
"samples": [
{
"text": "Passwort1",
"shouldMatch": true
},
{
"text": "abc12345",
"shouldMatch": true
},
{
"text": "Test1234",
"shouldMatch": true
},
{
"text": "password",
"shouldMatch": false
},
{
"text": "12345678",
"shouldMatch": false
},
{
"text": "abc123",
"shouldMatch": false
}
],
"pattern": "^(?=.*[A-Za-z])(?=.*\\d).{8,}$",
"hint": "Mit Lookaheads (?=...) erzwingst du beide Bedingungen.",
"successText": "Stark genug!"
}
]
},
{
"slug": "php-request",
"title": "PHP Request",
"language": "PHP",
"path": "debugging",
"description": "Verarbeite Formulare, Sessions und Datenbanken sicher in PHP.",
"tasks": [
{
"type": "choice",
"prompt": "POST-Formular lesen",
"points": 100,
"question": "Ein Formular wird mit method=\"post\" abgeschickt. Über welche Superglobale liest du die Daten?",
"options": [
"$_GET",
"$_POST",
"$_SESSION",
"$_COOKIE"
],
"correctIndex": 1,
"hint": "Die Superglobale heißt wie die HTTP-Methode.",
"successText": "Richtig, $_POST enthält die Formulardaten."
},
{
"type": "choice",
"prompt": "URL-Parameter lesen",
"points": 100,
"question": "Werte aus der URL wie ?id=5 liest du mit welcher Superglobale?",
"options": [
"$_POST",
"$_GET",
"$_FILES",
"$_ENV"
],
"correctIndex": 1,
"hint": "Query-Parameter gehören zur GET-Methode.",
"successText": "Korrekt, $_GET liest die URL-Parameter."
},
{
"type": "cloze",
"prompt": "POST-Wert auslesen",
"points": 100,
"narrative": "Lies das Feld email aus den gesendeten Formulardaten.",
"codeTemplate": "$email = {{1}}[\"email\"];",
"gaps": [
{
"position": 1,
"placeholder": "Superglobale",
"hint": "Array mit den POST-Daten.",
"caseSensitive": true,
"solutions": [
"$_POST"
]
}
],
"successText": "Stimmt, $_POST liefert die Formularwerte."
},
{
"type": "cloze",
"prompt": "Session starten",
"points": 100,
"narrative": "Bevor du auf $_SESSION zugreifst, musst du die Session starten.",
"codeTemplate": "<?php\n{{1}}();\n$_SESSION[\"user\"] = \"Max\";",
"gaps": [
{
"position": 1,
"placeholder": "Funktion",
"hint": "Startet oder setzt die Session fort.",
"caseSensitive": true,
"solutions": [
"session_start"
]
}
],
"successText": "Genau, session_start() muss zuerst laufen."
},
{
"type": "choice",
"prompt": "Existenz prüfen",
"points": 100,
"question": "Welche Funktion prüft, ob ein Array-Schlüssel existiert und nicht null ist?",
"options": [
"empty",
"isset",
"is_null",
"array_key"
],
"correctIndex": 1,
"hint": "Sie gibt false zurück, wenn die Variable nicht gesetzt ist.",
"successText": "Richtig, isset() ist die passende Wahl."
},
{
"type": "cloze",
"prompt": "Wert absichern",
"points": 100,
"narrative": "Greife nur auf das POST-Feld zu, wenn es wirklich existiert.",
"codeTemplate": "if ({{1}}($_POST[\"email\"])) {\n $email = $_POST[\"email\"];\n}",
"gaps": [
{
"position": 1,
"placeholder": "Prüf-Funktion",
"hint": "Prüft, ob die Variable existiert und nicht null ist.",
"caseSensitive": true,
"solutions": [
"isset"
]
}
],
"successText": "Sauber abgesichert mit isset()."
},
{
"type": "spot",
"prompt": "Falsche Superglobale",
"points": 100,
"instruction": "Das Formular nutzt method=\"post\". Klick den fehlerhaften Token an.",
"tokens": [
"$email",
"=",
"$_GET",
"[",
"'email'",
"]",
";"
],
"bugIndex": 2,
"fix": "$_POST",
"hint": "POST-Daten liegen nicht in $_GET.",
"successText": "Korrekt, hier muss $_POST stehen."
},
{
"type": "spot",
"prompt": "Zuweisung statt Vergleich",
"points": 100,
"instruction": "Hier soll verglichen statt zugewiesen werden. Klick den fehlerhaften Token an.",
"tokens": [
"if",
"(",
"$alter",
"=",
"18",
")"
],
"bugIndex": 3,
"fix": "==",
"hint": "Ein einzelnes = weist zu, statt zu vergleichen.",
"successText": "Richtig, == vergleicht die Werte."
},
{
"type": "choice",
"prompt": "Login speichern",
"points": 100,
"question": "Wo speicherst du, dass ein Nutzer eingeloggt ist, sodass es über mehrere Requests erhalten bleibt?",
"options": [
"$_GET",
"$_POST",
"$_SESSION",
"$_SERVER"
],
"correctIndex": 2,
"hint": "Diese Daten überdauern einzelne Anfragen.",
"successText": "Genau, der Login-Status gehört in $_SESSION."
},
{
"type": "cloze",
"prompt": "Cookie setzen",
"points": 100,
"narrative": "Setze ein Cookie, das eine Stunde lang gültig ist.",
"codeTemplate": "{{1}}(\"theme\", \"dark\", time() + 3600);",
"gaps": [
{
"position": 1,
"placeholder": "Funktion",
"hint": "Sendet ein Cookie an den Browser.",
"caseSensitive": true,
"solutions": [
"setcookie"
]
}
],
"successText": "Cookie gesetzt mit setcookie()."
},
{
"type": "cloze",
"prompt": "Statement vorbereiten",
"points": 100,
"narrative": "Nutze ein Prepared Statement, um SQL-Injection zu verhindern.",
"codeTemplate": "$stmt = $pdo->{{1}}(\"SELECT * FROM users WHERE email = ?\");",
"gaps": [
{
"position": 1,
"placeholder": "PDO-Methode",
"hint": "Bereitet die SQL-Anweisung vor.",
"caseSensitive": true,
"solutions": [
"prepare"
]
}
],
"successText": "Richtig, prepare() erstellt das Statement."
},
{
"type": "cloze",
"prompt": "Statement ausführen",
"points": 100,
"narrative": "Führe das vorbereitete Statement mit dem gebundenen Wert aus.",
"codeTemplate": "$stmt->{{1}}([$email]);",
"gaps": [
{
"position": 1,
"placeholder": "PDO-Methode",
"hint": "Führt das Prepared Statement mit den Parametern aus.",
"caseSensitive": true,
"solutions": [
"execute"
]
}
],
"successText": "Ausgeführt mit execute()."
},
{
"type": "spot",
"prompt": "SQL-Injection",
"points": 100,
"instruction": "SQL-Injection-Gefahr! Klick den Token an, der so nicht direkt in die Query gehört.",
"tokens": [
"$sql",
"=",
"\"... WHERE id = \"",
".",
"$id"
],
"bugIndex": 4,
"fix": "?",
"hint": "Nutze einen Platzhalter und binde den Wert per Prepared Statement.",
"successText": "Richtig, statt $id gehört ein Platzhalter ? in die Query."
},
{
"type": "choice",
"prompt": "Passwort prüfen",
"points": 100,
"question": "Mit welcher Funktion prüfst du ein eingegebenes Passwort gegen einen gespeicherten Hash?",
"options": [
"password_hash",
"password_verify",
"md5",
"strcmp"
],
"correctIndex": 1,
"hint": "Sie vergleicht Klartext mit einem Hash.",
"successText": "Korrekt, password_verify() ist sicher."
},
{
"type": "cloze",
"prompt": "Passwort hashen",
"points": 100,
"narrative": "Speichere niemals Klartext-Passwörter. Erzeuge einen sicheren Hash.",
"codeTemplate": "$hash = {{1}}($passwort, PASSWORD_DEFAULT);",
"gaps": [
{
"position": 1,
"placeholder": "Funktion",
"hint": "Erzeugt einen sicheren Passwort-Hash.",
"caseSensitive": true,
"solutions": [
"password_hash"
]
}
],
"successText": "Sicher gehasht mit password_hash()."
},
{
"type": "cloze",
"prompt": "Weiterleitung",
"points": 100,
"narrative": "Leite den Browser nach dem Login auf das Dashboard um.",
"codeTemplate": "{{1}}(\"Location: /dashboard.php\");\nexit;",
"gaps": [
{
"position": 1,
"placeholder": "Funktion",
"hint": "Sendet einen rohen HTTP-Header.",
"caseSensitive": true,
"solutions": [
"header"
]
}
],
"successText": "Weiterleitung per header() gesetzt."
}
]
},
{
"slug": "api-explorer",
"title": "API Explorer",
"language": "REST",
"path": "visual",
"description": "Lerne HTTP-Methoden, Statuscodes und REST-Routen kennen.",
"tasks": [
{
"type": "match",
"prompt": "Statuscodes zuordnen",
"points": 100,
"instruction": "Ordne die HTTP-Statuscodes ihrer Bedeutung zu.",
"pairs": [
[
"200",
"OK"
],
[
"201",
"Created"
],
[
"404",
"Not Found"
],
[
"500",
"Internal Server Error"
]
],
"hint": "2xx ist Erfolg, 4xx Client-Fehler, 5xx Server-Fehler.",
"successText": "Alle Codes richtig zugeordnet!"
},
{
"type": "match",
"prompt": "Methoden und Zweck",
"points": 100,
"instruction": "Ordne jede HTTP-Methode ihrem Zweck zu.",
"pairs": [
[
"GET",
"Daten lesen"
],
[
"POST",
"Neu anlegen"
],
[
"PUT",
"Ersetzen"
],
[
"DELETE",
"Löschen"
]
],
"hint": "Denk an CRUD: lesen, anlegen, ändern, löschen.",
"successText": "Methoden sitzen!"
},
{
"type": "choice",
"prompt": "Ressource anlegen",
"points": 100,
"question": "Welche HTTP-Methode legt eine neue Ressource an?",
"options": [
"GET",
"POST",
"PUT",
"DELETE"
],
"correctIndex": 1,
"hint": "Sie sendet Daten im Body zum Server.",
"successText": "Richtig, POST erstellt neue Ressourcen."
},
{
"type": "choice",
"prompt": "Daten abrufen",
"points": 100,
"question": "Welche Methode nutzt du, um Daten abzurufen, ohne etwas zu ändern?",
"options": [
"GET",
"POST",
"PATCH",
"DELETE"
],
"correctIndex": 0,
"hint": "Diese Methode ist sicher und verändert nichts.",
"successText": "Korrekt, GET liest nur."
},
{
"type": "cloze",
"prompt": "Nutzer abrufen",
"points": 100,
"narrative": "Du willst den Nutzer mit der ID 7 abrufen.",
"codeTemplate": "{{1}} /api/users/7",
"gaps": [
{
"position": 1,
"placeholder": "HTTP-Methode",
"hint": "Daten lesen, ohne sie zu ändern.",
"caseSensitive": false,
"solutions": [
"GET"
]
}
],
"successText": "Genau, GET liest die Ressource."
},
{
"type": "match",
"prompt": "Code-Klassen",
"points": 100,
"instruction": "Ordne die Statuscode-Klassen ihrer Bedeutung zu.",
"pairs": [
[
"2xx",
"Erfolg"
],
[
"3xx",
"Weiterleitung"
],
[
"4xx",
"Client-Fehler"
],
[
"5xx",
"Server-Fehler"
]
],
"hint": "Die erste Ziffer verrät die Kategorie.",
"successText": "Klassen verstanden!"
},
{
"type": "choice",
"prompt": "Erfolgreich erstellt",
"points": 100,
"question": "Welcher Statuscode signalisiert, dass eine Ressource erfolgreich erstellt wurde?",
"options": [
"200",
"201",
"204",
"404"
],
"correctIndex": 1,
"hint": "Created hat einen eigenen 2xx-Code.",
"successText": "Richtig, 201 Created."
},
{
"type": "cloze",
"prompt": "Nutzer anlegen",
"points": 100,
"narrative": "Du legst einen neuen Nutzer an.",
"codeTemplate": "{{1}} /api/users",
"gaps": [
{
"position": 1,
"placeholder": "HTTP-Methode",
"hint": "Neu anlegen mit Daten im Body.",
"caseSensitive": false,
"solutions": [
"POST"
]
}
],
"successText": "Korrekt, POST legt an."
},
{
"type": "choice",
"prompt": "Nicht gefunden",
"points": 100,
"question": "Eine angeforderte Ressource existiert nicht. Welcher Statuscode passt?",
"options": [
"200",
"301",
"404",
"500"
],
"correctIndex": 2,
"hint": "Der berühmteste Fehlercode im Web.",
"successText": "Richtig, 404 Not Found."
},
{
"type": "cloze",
"prompt": "Statuszeile Erfolg",
"points": 100,
"narrative": "Eine erfolgreiche Antwort beginnt mit dieser Statuszeile.",
"codeTemplate": "HTTP/1.1 {{1}} OK",
"gaps": [
{
"position": 1,
"placeholder": "Statuscode",
"hint": "Der Standard-Code für Erfolg.",
"caseSensitive": false,
"solutions": [
"200"
]
}
],
"successText": "Genau, 200 OK."
},
{
"type": "match",
"prompt": "Weitere Codes",
"points": 100,
"instruction": "Ordne die Statuscodes ihrer Bedeutung zu.",
"pairs": [
[
"301",
"Moved Permanently"
],
[
"400",
"Bad Request"
],
[
"401",
"Unauthorized"
],
[
"403",
"Forbidden"
]
],
"hint": "401 = nicht eingeloggt, 403 = kein Zugriff trotz Login.",
"successText": "Stark, alles richtig!"
},
{
"type": "choice",
"prompt": "Nicht authentifiziert",
"points": 100,
"question": "Der Client ist nicht authentifiziert. Welcher Statuscode ist korrekt?",
"options": [
"400",
"401",
"403",
"404"
],
"correctIndex": 1,
"hint": "Unauthorized meint hier: nicht eingeloggt.",
"successText": "Richtig, 401 Unauthorized."
},
{
"type": "cloze",
"prompt": "Statuszeile Fehler",
"points": 100,
"narrative": "Die Ressource fehlt. Vervollständige die Statuszeile.",
"codeTemplate": "HTTP/1.1 {{1}} Not Found",
"gaps": [
{
"position": 1,
"placeholder": "Statuscode",
"hint": "Client fragt etwas an, das es nicht gibt.",
"caseSensitive": false,
"solutions": [
"404"
]
}
],
"successText": "Korrekt, 404 Not Found."
},
{
"type": "choice",
"prompt": "Teil-Update",
"points": 100,
"question": "Welche Methode aktualisiert nur einzelne Felder einer Ressource?",
"options": [
"POST",
"PUT",
"PATCH",
"GET"
],
"correctIndex": 2,
"hint": "PUT ersetzt komplett, diese Methode nur teilweise.",
"successText": "Richtig, PATCH ändert einzelne Felder."
},
{
"type": "cloze",
"prompt": "Nutzer löschen",
"points": 100,
"narrative": "Du löschst den Nutzer mit der ID 7.",
"codeTemplate": "{{1}} /api/users/7",
"gaps": [
{
"position": 1,
"placeholder": "HTTP-Methode",
"hint": "Entfernt die Ressource.",
"caseSensitive": false,
"solutions": [
"DELETE"
]
}
],
"successText": "Genau, DELETE entfernt sie."
},
{
"type": "cloze",
"prompt": "Antwortformat",
"points": 100,
"narrative": "Die API antwortet mit JSON. Vervollständige den Header.",
"codeTemplate": "Content-Type: application/{{1}}",
"gaps": [
{
"position": 1,
"placeholder": "Format",
"hint": "Das gängige Datenformat moderner APIs.",
"caseSensitive": false,
"solutions": [
"json"
]
}
],
"successText": "Korrekt, application/json."
}
]
}
]
}