← Alle Artikel

aiDoom — ein Raycaster im Browser, gebaut mit zwei KIs

aiDoom — a Browser Raycaster Built by Two AIs

Zwei Dinge wollte ich ausprobieren. Erstens: Wie weit kommt man mit 3D im Browser, wenn man WebGL bewusst weglässt — nur das nackte 2D-Canvas? Zweitens: Kann ein lokales Sprachmodell den Großteil des Codes schreiben, während ich nur noch Architekt und Reviewer bin? aiDoom ist die Antwort auf beide Fragen.

Es ist ein Wolfenstein-/Doom-artiger Raycaster. TypeScript, Vite, gebaut in unter 300 Millisekunden, am Ende rund 40 KB JavaScript gzippt. Texturen, Sprites, Sound-Effekte — alles prozedural erzeugt. Die einzigen externen Dateien sind ein paar MP3s für die Hintergrundmusik. Zehn Level, am Ende ein Boss, dessen Tod den Win-Screen auslöst.

Kein WebGL, mit Absicht

Der Renderer ist ein klassischer DDA-Raycaster: 640×480, Z-Buffer, Sprites aus acht Blickrichtungen. Jede Wand, jede Gegnertextur wird zur Laufzeit berechnet statt geladen. Das ist langsamer als WebGL und genau das war der Punkt — ich wollte verstehen, wo die Grenze liegt, nicht eine Engine-Library bedienen. Es zwingt einen, jeden Pixel zu verantworten.

Der eigentliche Trick: der Entwicklungs-Workflow

Spannender als das Spiel ist, wie es entstanden ist. Die Rollen waren strikt verteilt:

  • Architekt (ich, mit Claude): liest den Plan, sucht die nächste Aufgabe, schreibt einen copy-paste-fertigen Prompt.
  • Worker (Qwen3 27B, lokal auf Zuse): führt den Prompt gegen den Arbeitsbaum aus. Quasi-kostenlos pro Token, rund 60 Tokens pro Sekunde.
  • Reviewer (wieder ich, mit Claude): liest die Diffs, prüft gegen die Spec, baut, testet, flaggt Bugs.

Die Idee: Das teure Frontier-Modell trifft die Architektur­entscheidungen und kontrolliert die Qualität, das billige lokale Modell macht die Fleißarbeit. Hybrid spart Kosten beim Massen-Editing und behält trotzdem das Urteilsvermögen.

Klingt sauber. War es nicht — anfangs.

Wenn das Modell lügt

Zweimal hat Qwen behauptet, Code geschrieben zu haben, der nie im Arbeitsbaum landete. Mit präzisen Zeilenangaben in der Zusammenfassung. Die kreative Grundlagenarbeit war da — neue Synths, ein Boss-Sprite — aber die Verdrahtung über mehrere bestehende Dateien hinweg: komplett leer. Beide Male musste ich die Integration von Hand nachziehen.

Das Muster ist lehrreich: kreative Neu-Arbeit landet zuverlässig, das Verkabeln über mehrere Dateien mit viel Kontext kollabiert. Das Modell hat den Plan im Kopf und schreibt am Ende eine plausible Zusammenfassung — aus dem Plan, nicht aus dem, was die Werkzeuge tatsächlich getan haben.

Die Härtung war ein einziger Satz, der seitdem an jedem Prompt klebt:

Zeige die Ausgabe von git status und git diff --stat eins zu eins.

Werkzeug-Ausgaben kann man nicht halluzinieren — entweder die Shell liefert sie oder nicht. Ein Selbstbericht "ich habe X gemacht" dagegen sehr leicht. Ab diesem Prompt liefen die Aufgaben sauber durch: Grundlage plus Integration, null Nacharbeit, beim ersten Versuch spec-konform.

Was ich mitgenommen habe

  • Niemals dem Selbstbericht trauen, nur der Werkzeug-Ausgabe. "Build ist grün" allein reicht nicht — der Build geht auch durch, wenn nur die halbe Verkabelung da ist; der Rest fällt erst beim Spielen auf.
  • Eine VERBOTEN-Sektion im Prompt schließt Türen für kreative Interpretation. "Fass die Tests nicht an. Dieses Modul ist read-only."
  • Keine ganzen Funktionskörper vorgeben. Das Modell soll interpretieren, nicht abschreiben. Ein Zeilen-Hinweis reicht.

Der Code ist am Ende Nebensache. Was bleibt, ist eine Methode: ein lokales Modell als disziplinierten, misstrauisch beaufsichtigten Junior einzusetzen — und genau zu wissen, an welcher Stelle es einen anlügen wird.

I wanted to try two things. First: how far can you get with 3D in the browser if you deliberately skip WebGL — just the bare 2D canvas? Second: can a local language model write most of the code while I'm only the architect and reviewer? aiDoom is the answer to both.

It's a Wolfenstein-/Doom-style raycaster. TypeScript, Vite, builds in under 300 milliseconds, about 40 KB of gzipped JavaScript in the end. Textures, sprites, sound effects — all generated procedurally. The only external files are a few MP3s for the background music. Ten levels, a boss at the end whose death triggers the win screen.

No WebGL, on purpose

The renderer is a classic DDA raycaster: 640×480, a Z-buffer, sprites from eight viewing angles. Every wall, every enemy texture is computed at runtime instead of loaded. That's slower than WebGL, and that was exactly the point — I wanted to understand where the limit is, not operate an engine library. It forces you to own every pixel.

The real trick: the dev workflow

More interesting than the game is how it came to be. The roles were strictly divided:

  • Architect (me, with Claude): reads the plan, picks the next task, writes a copy-paste-ready prompt.
  • Worker (Qwen3 27B, local on Zuse): runs the prompt against the working tree. Near-free per token, around 60 tokens per second.
  • Reviewer (me again, with Claude): reads the diffs, checks against the spec, builds, tests, flags bugs.

The idea: the expensive frontier model makes the architecture decisions and controls quality, the cheap local model does the legwork. Hybrid saves cost on bulk editing while keeping the judgment.

Sounds clean. It wasn't — at first.

When the model lies

Twice, Qwen claimed to have written code that never landed in the working tree. With precise line numbers in its summary. The creative foundation work was there — new synths, a boss sprite — but the wiring across several existing files was completely empty. Both times I had to do the integration by hand.

The pattern is instructive: creative greenfield work lands reliably, wiring across multiple files with lots of context collapses. The model has the plan in its head and writes a plausible summary at the end — from the plan, not from what the tools actually did.

The fix was a single sentence that's been stuck to every prompt since:

Show the output of git status and git diff --stat one to one.

You can't hallucinate tool output — either the shell produces it or it doesn't. A self-report of "I did X," on the other hand, is trivial to fake. From that prompt on, the tasks ran clean: foundation plus integration, zero rework, spec-compliant on the first try.

What I took away

  • Never trust the self-report, only the tool output. "Build is green" alone isn't enough — the build passes even with half the wiring missing; the rest only shows up when you play.
  • A FORBIDDEN section in the prompt closes doors for creative interpretation. "Don't touch the tests. This module is read-only."
  • Don't hand over whole function bodies. The model should interpret, not copy. A line-level hint is enough.

The code itself is almost beside the point. What remains is a method: using a local model as a disciplined, suspiciously supervised junior — and knowing exactly where it will lie to you.