diff --git a/.gitignore b/.gitignore
index 497b9bd..bb42f04 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,10 @@
# Lead-Daten — enthalten personenbezogene Daten, gehören nicht ins Repo
leads/
+# GameJam — Einreichungen & hochgeladene Spiele (User-Content)
+gamejam/
+public/gamejam-games/
+
# OS / Editor
.DS_Store
diff --git a/public/gamejam.html b/public/gamejam.html
new file mode 100644
index 0000000..2f34a6c
--- /dev/null
+++ b/public/gamejam.html
@@ -0,0 +1,727 @@
+
+
+
+
+
+AI GameJam — Human in the Loop · TechDay Bruchsal 2026
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TechDay Bruchsal · 27.05.2026
+
+
+
+
+
+
+
+
+
AI GameJam // TechDay Bruchsal 2026
+
Human inthe Loop
+
Ein Spiel bauen mit KI — in zwei Stunden. Der Mensch steuert.
+
+
+
+
Jetzt läuft
+
—
+
—
+
--:--
+
bis zur nächsten Phase
+
+
+
+
+
+
+
+
+
+
+
+
Das Thema
+
Human in the Loop
+
+ Der Mensch bleibt am Steuer — die KI ist Werkzeug, nicht Autopilot.
+ Heute baut ihr ein Spiel, indem ihr die KI führt: Konzept schärfen,
+ Entscheidungen treffen, iterieren. Lasst das Thema auch in euer Spiel
+ einfließen — die beste Interpretation fließt in die Bewertung ein.
+
+
+
+
+
+
+
+
So läuft's
+
+
+
1
+
+
Schritt 1
+
Planen
+
Im Terminal startet ihr den GDD-Dialog. Die KI stellt gezielte Fragen — am Ende steht euer Game-Konzept.
+
+
+
2
+
+
Schritt 2
+
Bauen
+
Dann wird gecodet: ihr beschreibt, die KI baut, ihr testet im Browser und iteriert.
+
+
+
3
+
+
Schritt 3
+
Zeigen
+
Game hochladen, Demo-Runde, die Jury bewertet. Die Besten gewinnen.
+
+
+
+
+
+
+
+
+
Bewertung
+
Vier Kategorien
+
Je 0–10 Punkte. Der Durchschnitt ist die Gesamtnote — die Top 3 gewinnen.
+
+
/01
Spielspaß
Macht es Spaß?
0 – 10 Punkte
+
/02
Kreativität
Wie originell?
0 – 10 Punkte
+
/03
Theme
Human in the Loop
0 – 10 Punkte
+
/04
Look & Feel
Präsentation & Game Feel
0 – 10 Punkte
+
+
+
+
+
+
+
+
Preise
+
+
+
Erster Platz
+
Claude Max 5x
+
Ein Monat
+
+
+
Zweiter Platz
+
Claude Pro
+
Ein Monat
+
+
+
Dritter Platz
+
Claude Pro
+
Ein Monat
+
+
+
+
+
+
+
+
+
Games
+
Spiel einreichen
+
+
+
+ Teilnehmer
+
+
+
+ Game-Name
+
+
+
+
+
HTML-Datei hier ablegen
+
deine fertige .html — klicken zum Auswählen
+
+
+
+ Einreichen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/index.html b/public/index.html
index d0b474d..c514a5a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -21,6 +21,7 @@
Leistungen
Vorgehen
Organisation
+ GameJam
Kontakt aufnehmen
@@ -31,6 +32,7 @@
Leistungen
Vorgehen
Organisation
+ GameJam
Kontakt aufnehmen
diff --git a/server.js b/server.js
index 3974a5c..b42624e 100644
--- a/server.js
+++ b/server.js
@@ -1,7 +1,10 @@
/* ============================================================
ToolCTRL — Landing Page Server
Abhängigkeitsfrei (nur Node-Standardbibliothek).
- Liefert /public und nimmt Leads unter POST /api/lead entgegen.
+ - Liefert /public
+ - POST /api/lead — Kontakt-Leads
+ - POST /api/gamejam/upload — GameJam: Spiel einreichen (Datei-Upload)
+ - GET /api/gamejam/games — GameJam: eingereichte Spiele
============================================================ */
"use strict";
@@ -9,6 +12,7 @@
const http = require("http");
const fs = require("fs");
const path = require("path");
+const crypto = require("crypto");
const ROOT = __dirname;
const PUBLIC_DIR = path.join(ROOT, "public");
@@ -16,6 +20,12 @@ const LEADS_DIR = path.join(ROOT, "leads");
const LEADS_JSON = path.join(LEADS_DIR, "leads.ndjson");
const LEADS_CSV = path.join(LEADS_DIR, "leads.csv");
+/* GameJam-Speicher */
+const GAMEJAM_DIR = path.join(ROOT, "gamejam");
+const SUBMISSIONS_JSON = path.join(GAMEJAM_DIR, "submissions.ndjson");
+const GAMES_DIR = path.join(PUBLIC_DIR, "gamejam-games");
+const MAX_UPLOAD = 5 * 1024 * 1024; /* 5 MB pro eingereichtem Spiel */
+
/* Bevorzugte Ports — der erste freie wird genutzt. */
const PORT_CANDIDATES = [4317, 4318, 4417, 5317, 8317, 0];
const HOST = "127.0.0.1";
@@ -45,6 +55,12 @@ function ensureLeadStore() {
}
}
+/* ---------- GameJam-Speicher vorbereiten ---------- */
+function ensureGamejamStore() {
+ if (!fs.existsSync(GAMEJAM_DIR)) fs.mkdirSync(GAMEJAM_DIR, { recursive: true });
+ if (!fs.existsSync(GAMES_DIR)) fs.mkdirSync(GAMES_DIR, { recursive: true });
+}
+
function csvCell(value) {
const s = String(value == null ? "" : value);
return '"' + s.replace(/"/g, '""').replace(/\r?\n/g, " ") + '"';
@@ -162,6 +178,154 @@ function handleLead(req, res) {
});
}
+/* ---------- Multipart-Parser (abhängigkeitsfrei) ---------- */
+function parseMultipart(buffer, boundary) {
+ const result = { fields: {}, files: [] };
+ const delim = Buffer.from("--" + boundary);
+ let start = buffer.indexOf(delim);
+ if (start < 0) return result;
+ start += delim.length;
+
+ while (start < buffer.length) {
+ /* "--" direkt nach dem Delimiter => Ende */
+ if (buffer[start] === 0x2d && buffer[start + 1] === 0x2d) break;
+ if (buffer[start] === 0x0d && buffer[start + 1] === 0x0a) start += 2;
+
+ const next = buffer.indexOf(delim, start);
+ if (next < 0) break;
+
+ let partEnd = next;
+ if (buffer[partEnd - 2] === 0x0d && buffer[partEnd - 1] === 0x0a) partEnd -= 2;
+ const part = buffer.slice(start, partEnd);
+ start = next + delim.length;
+
+ const headerEnd = part.indexOf("\r\n\r\n");
+ if (headerEnd < 0) continue;
+ const headerStr = part.slice(0, headerEnd).toString("utf8");
+ const content = part.slice(headerEnd + 4);
+
+ const nameM = headerStr.match(/name="([^"]*)"/i);
+ if (!nameM) continue;
+ const fileM = headerStr.match(/filename="([^"]*)"/i);
+
+ if (fileM) {
+ result.files.push({
+ field: nameM[1],
+ filename: fileM[1],
+ data: content
+ });
+ } else {
+ result.fields[nameM[1]] = content.toString("utf8");
+ }
+ }
+ return result;
+}
+
+/* ---------- GameJam: Spiel einreichen ---------- */
+function handleGamejamUpload(req, res) {
+ const ctype = req.headers["content-type"] || "";
+ const bM = ctype.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
+ if (!/multipart\/form-data/i.test(ctype) || !bM) {
+ return sendJSON(res, 400, { error: "Ungültiges Upload-Format." });
+ }
+ const boundary = (bM[1] || bM[2]).trim();
+
+ const chunks = [];
+ let size = 0;
+ let tooLarge = false;
+
+ req.on("data", (chunk) => {
+ size += chunk.length;
+ if (size > MAX_UPLOAD) {
+ tooLarge = true;
+ req.destroy();
+ return;
+ }
+ chunks.push(chunk);
+ });
+
+ req.on("end", () => {
+ if (tooLarge) {
+ return sendJSON(res, 413, { error: "Datei zu groß (max. 5 MB)." });
+ }
+
+ let parsed;
+ try {
+ parsed = parseMultipart(Buffer.concat(chunks), boundary);
+ } catch (e) {
+ return sendJSON(res, 400, { error: "Upload konnte nicht gelesen werden." });
+ }
+
+ const teilnehmer = String(parsed.fields.teilnehmer || "").trim();
+ const game = String(parsed.fields.game || "").trim();
+ const file = parsed.files.find((f) => f.field === "file");
+
+ if (!teilnehmer || !game) {
+ return sendJSON(res, 400, { error: "Teilnehmer und Game-Name angeben." });
+ }
+ if (!file || !file.data || file.data.length === 0) {
+ return sendJSON(res, 400, { error: "Keine Datei ausgewählt." });
+ }
+ if (!/\.html?$/i.test(file.filename || "")) {
+ return sendJSON(res, 400, { error: "Nur einzelne .html-Dateien." });
+ }
+
+ const id = Date.now().toString(36) + "-" + crypto.randomBytes(4).toString("hex");
+ const entry = {
+ id: id,
+ zeitpunkt: new Date().toISOString(),
+ teilnehmer: teilnehmer.slice(0, 200),
+ game: game.slice(0, 200),
+ url: "/gamejam-games/" + id + ".html"
+ };
+
+ try {
+ ensureGamejamStore();
+ fs.writeFileSync(path.join(GAMES_DIR, id + ".html"), file.data);
+ fs.appendFileSync(SUBMISSIONS_JSON, JSON.stringify(entry) + "\n", "utf8");
+ } catch (e) {
+ console.error("[gamejam] Speichern fehlgeschlagen:", e.message);
+ return sendJSON(res, 500, { error: "Speichern fehlgeschlagen." });
+ }
+
+ console.log(
+ "[gamejam] " + entry.zeitpunkt + " — \"" + entry.game +
+ "\" von " + entry.teilnehmer
+ );
+ sendJSON(res, 200, { ok: true, id: id });
+ });
+}
+
+/* ---------- GameJam: eingereichte Spiele ---------- */
+function handleGamejamGames(req, res) {
+ let games = [];
+ try {
+ if (fs.existsSync(SUBMISSIONS_JSON)) {
+ games = fs
+ .readFileSync(SUBMISSIONS_JSON, "utf8")
+ .split("\n")
+ .filter(Boolean)
+ .map((line) => {
+ try {
+ return JSON.parse(line);
+ } catch (e) {
+ return null;
+ }
+ })
+ .filter(Boolean)
+ .map((g) => ({
+ id: g.id,
+ teilnehmer: g.teilnehmer,
+ game: g.game,
+ url: g.url
+ }));
+ }
+ } catch (e) {
+ console.error("[gamejam] Galerie konnte nicht gelesen werden:", e.message);
+ }
+ sendJSON(res, 200, games);
+}
+
function sendJSON(res, code, obj) {
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(obj));
@@ -178,6 +342,20 @@ const server = http.createServer((req, res) => {
return handleLead(req, res);
}
+ if (route === "/api/gamejam/upload") {
+ if (req.method !== "POST") {
+ return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
+ }
+ return handleGamejamUpload(req, res);
+ }
+
+ if (route === "/api/gamejam/games") {
+ if (req.method !== "GET") {
+ return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
+ }
+ return handleGamejamGames(req, res);
+ }
+
if (req.method !== "GET" && req.method !== "HEAD") {
res.writeHead(405).end("Method Not Allowed");
return;
@@ -204,13 +382,15 @@ function listen(candidates) {
server.listen(port, HOST, () => {
const actual = server.address().port;
ensureLeadStore();
+ ensureGamejamStore();
console.log("");
console.log(" ToolCTRL — Landing Page läuft");
console.log(" ----------------------------------------");
- console.log(" URL: http://" + HOST + ":" + actual);
- console.log(" Leads: " + LEADS_CSV);
- console.log(" " + LEADS_JSON);
- console.log(" Stop: Strg + C");
+ console.log(" URL: http://" + HOST + ":" + actual);
+ console.log(" GameJam: http://" + HOST + ":" + actual + "/gamejam.html");
+ console.log(" Leads: " + LEADS_CSV);
+ console.log(" GameJam-Einreichungen: " + SUBMISSIONS_JSON);
+ console.log(" Stop: Strg + C");
console.log("");
});
}