Entwicklung eines Spiels in reinem JavaScript, bei dem eine Figur direkt durch Code-Eingaben gesteuert wird. Mit einem eigenen, reaktiven Renderers, der alle Zustandsänderungen und Aktionen dynamisch auf dem HTML-Canvas darstellt
Zum GitHub Repo
Aller Anfang ist Schwer, das gilt besonders fürs Programmieren. Abstrakte Konzepte wie Kontrollstrukturen oder imperative Logik sind ohne direktes, visuelles Feedback oft schwer nachzuvollziehen. Oft verwenden Schulen und Universitäten beim Unterrichten genau dieser Fähigkeiten den Werkzeug Hamster-Simulator. Dabei schreiben die Lernenden ein kleines Programm, um einen virtuellen Hamster durch ein Territorium zu steuern, Körner einzusammeln und Hindernissen auszuweichen, wodurch sie eine direkte visuelle Rückmeldung auf ihren Code erhalten. Das Problem, der Hamster-Simulator ist mittlerweile technisch veraltet, die Installation stellt mittlerweile eine größere Herausforderung dar, als einige anfängliche Level. Klassischerweise schreibt dort man vereinfachten Java Code.
https://commons.wikimedia.org/wiki/File:JavaHamsterSimulator.png
Fasziniert von der Idee, Programmcode mittels eines „Spielfelds“ zu visualisieren, entstand dieses Projekt als Experiment. Es ist der Versuch, diese Grundidee mit moderner Web-Technik und UX-Design umzusetzen, um in einem begrenzten Rahmen die Funktionsweise eines solchen Systems für mich selbst zu rekonstruieren. Daraus entstand dieses Programm. Statt eines Hamsters wird ein Pirat gesteuert, der sich auf Schatzsuche befindet. Nutzer schreibe in einer browserbasierte Anwendung Java-Scrip Code, um levelbasierte Aufgaben zu lösen und so ein intuitives Verständnis für die Auswirkungen ihres Codes zu entwickeln.
Nach dem laden eines Levels aus der Level-Übersicht können, Nutzer in einer geteilten Ansicht Code schreiben, um einen Piraten auf seinem Weg zum Schatz zu steuern und die Ausführung live zu verfolgen. Das Herzstück ist jedoch der Level-Editor, der es ermöglicht, gänzlich eigene Herausforderungen zu erstellen. Hier können Nutzer eine individuelle Map entwerfen, eine eigene Aufgabenbeschreibung verfassen.

Entgegen den momentanen Trent wurde auf ein umfassendes Frontend-Framework verzichtet, und stattdessen wurde eine ereignisgesteuerte Architektur mit reinen JavaScript-Modulen implementiert. Zum Bündeln der verschiedenen JS-Module wurde das Front-End Tool Vite benutzt. Die GameAPI stellt die Brücke zum System dar, die die Funktionalität zur Verfügung stellt und von den jeweiligen Skripten der verschiedenen Seiten aufgerufen wird. Die Architektur des Systems ist modular aufgebaut und orientiert sich an einer klaren Trennung der Verantwortlichkeiten. Die wichtigsten Module und Klassen sind:
GameController: Er agiert als das Gehirn der Anwendung. Der GameController initialisiert das Spiel, koordiniert die anderen Komponenten und verwaltet den zentralen Spielzustand (z. B. die Position des Piraten). Er nimmt Anfragen vom InputHandler entgegen und steuert den gesamten Lebenszyklus eines Levels.
InputHandler: Diese Komponente ist die Schnittstelle zum Benutzer. Der InputHandler fängt Ereignisse wie Klicks auf den “Ausführen”-Button ab und leitet die entsprechenden Befehle an den GameController weiter.
LevelLoader: Für die Bereitstellung der Spielinhalte ist der LevelLoader zuständig. Er liest die Level-Daten, wie die Kartenstruktur, die Startposition des Piraten und die Aufgabenbeschreibung aus externen Dateien (z. B. JSON) und stellt sie dem GameController zur Verfügung.
CodeParser: Ist die Komponente zum analysieren und parsen des vom Nutzer geschriebenen Code. Er wandelt den Text in ausführbare Befehle um, die der GameController schrittweise abarbeiten kann.
Renderer: Die visuelle Darstellung der Spielwelt ist die alleinige Aufgabe des Renderer. Er empfängt den aktuellen Spielzustand vom GameController und zeichnet die Karte, die Objekte und die Figur auf den HTML-Canvas.
Die initiiert und benutzt wird die API in den einzelnen Skripten: home.js, game.js, editor.js, die in die dazugehörigen HTML-Dateien eingebunden sind. Sie verbinden die HTML-Elemente, die der Nutzer sieht (wie Buttons und Texteditor), mit der gekapselten Spiellogik im Hintergrund (GameController, GameAPI usw.).
Eine der komplexesten und zugleich wichtigsten Komponenten des Projekts ist der CodeParser. Seine Aufgabe ist es, den vom Nutzer eingegebenen JavaScript-Code so zu verarbeiten, dass er sicher, kontrolliert und visuell nachvollziehbar ausgeführt werden kann. Um dies zu erreichen, wurde ein Ansatz gewählt, der über ein simples eval() weit hinausgeht und auf der Analyse der Code-Struktur basiert.
Bevor ein Computer Programmcode ausführen kann, muss er ihn zunächst verstehen. Ein zentraler Schritt in diesem Prozess ist die Umwandlung des geschriebenen Codes (einer Zeichenkette) in eine hierarchische Baumstruktur, den sogenannten Abstract Syntax Tree (AST). Ein AST repräsentiert die logische und syntaktische Struktur des Codes, befreit von überflüssigen Elementen wie Kommas oder Klammern. Jeder Knoten im Baum steht für ein Konstrukt im Code – zum Beispiel eine Variablendeklaration, eine Funktion, eine for-Schleife oder einen Methodenaufruf.
AST Walking bezeichnet den Prozess, diesen Baum systematisch zu durchlaufen, Knoten für Knoten. Indem man den Baum “entlangwandert”, kann man den Code nicht nur verstehen, sondern ihn auch analysieren oder sogar modifizieren, bevor er ausgeführt wird. Man kann beispielsweise jeden Funktionsaufruf finden, jede Schleife inspizieren oder die Verwendung bestimmter Variablen überprüfen.
https://commons.wikimedia.org/wiki/File:Abstract_syntax_tree_for_Euclidean_algorithm.svg
Das Kernproblem der Visualisierung ist die Geschwindigkeit. Eine for-Schleife mit zehn geheVor()-Aufrufen würde in JavaScript in Millisekunden ablaufen – viel zu schnell für das menschliche Auge. Um die Ausführung zu verlangsamen und an die Animationen zu koppeln, muss jeder Schritt einzeln und nacheinander ausgeführt werden.
Die Lösung besteht darin, den gesamten Benutzercode dynamisch in eine async function zu verpacken. Dadurch wird es möglich, vor jedem potenziell animierten Befehl (wie geheVor()) ein await einzufügen. Dieses await pausiert die Ausführung der Funktion, bis die Animation des Piraten abgeschlossen ist. Erst dann wird der nächste Befehl in der Schleife abgearbeitet. Ohne diese Modifikation wäre eine schrittweise, visuelle Darstellung der Programmausführung unmöglich.
Die wahre Stärke eines AST-basierten Ansatzes liegt in der Möglichkeit der statischen Code-Analyse, also der Fähigkeit, den Code zu prüfen, ohne ihn auszuführen. Obwohl dieses Feature im aktuellen Projekt noch nicht implementiert ist, eröffnet der Ansatz ein enormes didaktisches Potenzial. Zukünftig könnte das System den Code statisch analysieren, um dem Nutzer gezielte Hinweise zu geben, warum sein Code fehlerhaft oder ineffizient ist. Darüber hinaus ließe sich dieses Konzept direkt in das Leveldesign integrieren: Aufgaben könnten beispielsweise eine maximale Anzahl an for-Schleifen vorschreiben oder das Verwenden von if-Abfragen gezielt untersagen. Das könnte z.B. beinhalten:
Erkennung von Endlosschleifen: Man könnte den AST auf while(true)-Konstrukte oder Schleifen ohne Abbruchbedingung prüfen und den Nutzer warnen, bevor der Code überhaupt ausgeführt wird und den Browser zum Absturz bringt.
Intelligentes Feedback: Das System könnte erkennen, ob ein Nutzer versucht, eine Variable zu verwenden, die er noch nicht deklariert hat, oder ob er eine for-Schleife falsch strukturiert. Es könnte dann gezielte Hinweise geben, wie z.B.: “Hast du vergessen, die Variable i mit let zu deklarieren?”
Einschränkung des Funktionsumfangs: Für Anfängerlevel könnte man durch die Analyse des ASTs die Verwendung von fortgeschrittenen Konzepten (z.B. Arrays oder Objects) verbieten und sicherstellen, dass nur die bereits gelehrten Funktionen verwendet werden.
Zusammenfassend lässt sich sagen, dass der gewählte Ansatz nicht nur das funktionale Problem der synchronisierten Animationen löst, sondern auch das Fundament für eine zukünftige, tiefergehende Analysen bietet.
Um die abstrakten Spielzustände vom GameConroller visuell darzustellen müssen Daten wie Koordinaten und Objekte in Visuelle Objekte überführt und gerendert werden. Zusätzlich muss sich die Darstellung bei neuen geänderten Daten ebenfalls updaten. Da keine UI-Bibliothek verwendet wird müssen diese Funktionen von Grund auf implementiert werden, der effektivste Weg hierzu ist das rendern mittels HTML-Canvas. Das HTML <canvas> Element ist wie eine digitale Leinwand, auf die mit JavaScript in Echtzeit gezeichnet werden kannst. Es ist möglich alles von einfachen Formen und Linien bis hin zu komplexen Animationen und sogar Spielen direkt im Browser zu erstellen. Dabei ist die HTML-Canvas „stateless“, es ist eine reine “pixelbasierte” Zeichenfläche, die bei jeder Änderung neu bemalt wird. Dadurch kann der gesamte Code zum Rendern in einem Modul gekapselt werden.
Nachdem die Canvas initiiert wurde und ein Content übergeben wurde, können einfache Striche gezeichnet werden:
this.ctx.strokeStyle = "black"; // Farbe der Linien
this.ctx.lineWidth = 1; // Dicke der Linen
this.ctx.beginPath(); // Start des Zeichnen der Line
this.ctx.moveTo(X, Y); // Start der Line mit Input für X und Y-Koordinate
this.ctx.lineTo(X2, Y2); // Ende der Line
this.ctx.stroke(); // Zeichnet die Line
Für das Zeichnen komplexerer Grafiken wie dem Piraten oder der Insel wurde eine Methode namens „Tile-Based“ Rendering genutzt.
Tile-Based Rendering (kachelbasiertes Rendern) ist eine etablierte Technik in der 2D-Spieleentwicklung zur Darstellung von Spielwelten, die auf einem Gitter (Grid) basieren. Anstatt die Spielwelt als ein einziges, großes Bild zu betrachten, wird sie in gleich große, quadratische Kacheln (Tiles) zerlegt, ähnlich wie bei einem Mosaik oder einem Fliesenboden. Das spart Speicher und Ressourcen.
Dieses System besteht aus zwei Kernkomponenten:
Das Tileset (der Kachelsatz): Dies ist eine einzelne Bilddatei, die alle einzigartigen grafischen Elemente enthält, aus denen die Welt aufgebaut werden kann. Jedes Element ist jeweils auf einem Tile Abgebildet.
Die Tilemap (die Kachelkarte): Dies ist eine zweidimensionale Datenstruktur (typischerweise ein zweidimensionales Array), die die eigentliche Level-Architektur beschreibt. Jede Zelle in diesem Array enthält eine ID, die auf eine bestimmte Kachel im Tileset verweist. Der Renderer nutzt diese Karte, um zu wissen, welche Kachel er an welcher Position auf dem Canvas zeichnen muss.
Beispiel einer einfachen Tilemap:
// 0 = Sand, 1 = Wasser
const tilemap = [
[1, 1, 1, 1],
[1, 0, 0, 1],
[1, 0, 0, 1],
[1, 1, 1, 1],
];
Für das korrekte zeichnen einer einzelnen Kachel aus dem geladenen Tileset an die korrekte Position auf dem Canvas ist die Methode drawTile() zuständig. Die Funktion nimmt den Index der Kachel (tileIndex) sowie deren Grid-Koordinaten (x, y) als Parameter entgegen. Danach:
Berechnung der Quell-Koordinaten (sx, sy): Zuerst ermittelt die Funktion, welcher Ausschnitt aus der Tileset-Bilddatei kopiert werden soll. sx = tileIndex * tileSize berechnet die horizontale Startposition im Tileset. Ist tileIndex z.B. 2, springt die Funktion zur dritten Kachel im Bild. sy gibt die vertikale Startposition an, je nachdem ob der Spielhintergrund (Pos: 0) oder Spieler (Pos: 2) gerendert werden soll.
Berechnung der Ziel-Koordinaten (dx, dy): Anschließend werden die Grid-Koordinaten (x, y) in Pixel-Koordinaten für den Canvas umgerechnet. dx = x * this.gridSize bestimmt die exakte horizontale Pixelposition, an der die Kachel platziert werden soll.
Wer das Programm nun austesten möchte, dann das unter hier tun.