Wordle auf Deutsch: ein Build-Bericht

13.05.2026 20:44

Ein Spiel an einem Nachmittag — drei Bausteine: eine Wortliste, ein Frontend, ein Bot der mitspielt. Was hier dokumentiert ist, ist die nüchterne Reihenfolge, in der die Stücke entstanden sind, plus die zwei Stellen an denen es nicht von selbst lief.

Ausgangslage

Ziel war ein deutsches Wordle als spielbare Seite unter https://pyground.de/wordle/ . Klassische Regeln: 6 Versuche, 5 Buchstaben pro Wort, Ä/Ö/Ü/ß zählen als einzelne Buchstaben (nicht expandiert zu AE/OE/UE/SS). Daily-Modus: alle Spieler bekommen am selben Tag dasselbe Wort. Persistenz über LocalStorage, keine Server-Stats.

1. Die Wortliste

Das war der erste interessante Punkt. Es gibt für englisches Wordle eine offizielle Liste (~2300 Lösungen, ~13000 erlaubte Rateversuche, gepflegt von der NYT, längst auf GitHub gespiegelt). Für Deutsch gibt es nichts Offizielles — NYT-Wordle ist Englisch-only.

Erster Versuch: Frequenzen aus den Leipzig Corpora Collection (deu_news_2024_1M, CC-BY-4.0) ziehen, mit einer flachen Wortliste (ngerman aus dem Debian-Paket, GPL-3.0) auf gültige deutsche Wörter filtern. Ergebnis nach den Filterstufen:

  • 743k unique Token-Formen im Korpus
  • 21k davon mit exakt 5 Buchstaben aus dem deutschen Alphabet
  • 19k nach Case-Aggregation (Großschreib-Varianten zusammengezogen)
  • 3659 davon im ngerman-Wörterbuch

Drei Probleme mit dieser Liste:

  1. Top-Wörter sind Funktionswörter. nicht, einem, einer, haben, gegen — alle drin. Als Wordle-Lösung langweilig.
  2. Englisch-Lehnwörter fehlen (event, match, music), weil ngerman sie nicht listet. Aus Wordle-Sicht aber gerne gerätselt.
  3. Eigennamen kommen durch. apple, paris, china, peter stehen im ngerman und wurden nicht gefiltert. Der Filter war case-insensitiv und unterscheidet nicht zwischen Substantiv und Eigennamen.

Stattdessen: die fertige Wortliste von 6mal5.com (deutscher Wordle-Klon, de-facto-Standard) aus dem JavaScript-Bundle scrapen. Das Spiel lädt main.<hash>.js (~230 KB), darin als JavaScript-Arrays:

  • 1070 Lösungswörter in Spielreihenfolge (chronologisch, nicht alphabetisch)
  • 3506 Erlaubt-Wörter alphabetisch sortiert
  • Union nach Dedup: 3604 gültige 5-Buchstaben-Wörter

Erkennung der zwei Arrays per Heuristik: clustern von aufeinanderfolgenden 5-Buchstaben-Quoted-Strings im Bundle, prüfen ob alphabetisch sortiert. Das größte nicht-sortierte Cluster ist die Lösungsliste. Eigennamen sind hier sauber raus (trump, biden, kevin werden korrekt nicht akzeptiert), die Kuration kommt mit.

Lizenz: 6mal5 hat keine explizite Lizenz publiziert. Einzelne Wörter sind nicht urheberrechtlich geschützt, aber die kuratierte Auswahl — insbesondere die Lösungs-Reihenfolge — könnte als Datenbank-Werk nach §87a UrhG geschützt sein. Für private Nutzung pragmatisch akzeptiert.

Die Wortliste landet als data/wordlist-de-5.json mit Schema [{"word": "haben", "freq": 40142, "is_solution": false}, ...]. Die freq-Spalte kommt weiterhin aus der LCC-Annotation — ein Ranking-Signal, falls man den Lösungs-Pool später kleiner machen will.

2. Das Spiel

Eine Django-App wordle, dünn:

  • views.py mit einer View, die das Template rendert. Heutiges Wort: date.toordinal() % len(solutions) aus der alphabetisch sortierten Lösungsliste. Deterministisch für alle Clients, keine Session, keine DB.
  • templates/wordle/play.html erbt von core/base.html, sitzt in der mittleren Drei-Spalten-Layout-Spalte.
  • static/wordle/wordle.{css,js} für Spielmechanik. Reines Vanilla JS.
  • data/wordlist-de-5.json wird beim Render via {{ allowed_words|json_script:"wordle-allowed" }} ins DOM eingebettet.

Spielmechanik: bei jedem Versuch zweistufige Auswertung (erst Grün an exakten Positionen, dann Gelb im verbleibenden Pool — wichtig für Doppelbuchstaben). Deutsche Bildschirmtastatur QWERTZ + Ä/Ö/Ü/ß als eigene Tasten. Die deutsche Großschreibung von ß ist „SS" laut .toUpperCase() — das zerschießt 5-Buchstaben-Anzeige. Workaround: eine eigene upperGerman(ch)-Funktion, die ß als ß lässt und den Rest normal hochstellt.

State und Stats in LocalStorage. State-Key pro Tag (wordle-de-state-YYYY-MM-DD), Stats-Key global. So kann man pro Tag genau einmal spielen und seine Versuchsverteilung aufbauen.

Admin-Modus: Superuser bekommt bei jedem Reload ein zufälliges Wort statt des Daily-Worts, plus die LocalStorage-Schreibens werden geskippt. Sichtbar als Banner oben. Praktisch zum Testen, ohne die Daily-Stats zu verschmutzen.

3. Der Solver

Information Gain: für jedes mögliche Rate-Wort g und die Menge der noch verbliebenen Lösungs-Kandidaten C, partitioniert man C nach Feedback-Pattern (3^5 = 243 mögliche Muster). Das beste Rate-Wort ist das mit der höchsten Entropie über diese Verteilung — es zerlegt C am gleichmäßigsten:

\[H(g) = -\sum_{p} \Pr(p \mid g, C) \cdot \log_2 \Pr(p \mid g, C)\]

Die Implementierung ist etwa 100 Zeilen Python: pattern(guess, target) baut den Wordle-Feedback-Code (ternäre Codierung, int), expected_entropy(guess, candidates) summiert die Entropy, top_n_hints(candidates, allowed, n) sortiert.

Für die initiale Lösungsmenge (1070) und den allowed-Pool (3604) sind das 3.86 Millionen Pattern-Berechnungen pro Run. In Python ohne Optimierung läuft das in 6 Sekunden — schnell genug, dass man nicht cachen muss. Nach dem ersten Versuch sind die Kandidaten meist auf 50–200 reduziert, dann läuft die Berechnung unter 1 Sekunde.

Top-3 Startwörter für die 6mal5-Lösungsliste:

# Wort Bits Lösungs-Kandidat?
1 SARTE 5.89 nein
2 RATEN 5.82 ja
3 TALER 5.77 ja

5.89 Bits heißt: nach einem Versuch eliminiert man im Schnitt 98% der 1070 Kandidaten (etwa 18 bleiben). SARTE ist mathematisch optimal — auch wenn es kaum jemand kennt (es bezeichnet im Duden eine Volksgruppe). RATEN ist die praktische Empfehlung: fast genauso gut und gleichzeitig selbst Lösungskandidat.

4. Der Bot soll mitspielen

Die pyground-Seite hat einen KI-Tutor (Claude Haiku via pydantic-ai) als rechte Sidebar. Heutiger Stand: er bekommt nur die URL der aktuellen Seite und einen optionalen Slug für Playground-Code-Runs. Vom Wordle-Spielstand: nichts.

Plan war eine generische Page-Context-Bridge: jede Seite kann eine window.PYGROUND_PAGE_CONTEXT = {app, summary}-Variable setzen, das HTMX-Form des Tutor-Chats schickt das als JSON-Feld bei jedem Submit mit, das Backend validiert (Whitelist app, Max-Länge) und reicht den Summary an den System-Prompt durch. Wordle setzt nach jedem Versuch einen menschen-lesbaren Spielstand-Text in diese Variable.

Lösungswort bleibt geheim, solange das Spiel läuft — der Bot soll nicht spoilern können. Erst nach Gewinn oder Verlust wird das echte Wort offengelegt.

Wo es nicht von selbst lief

Erster Versuch: System-Prompt wird ergänzt um einen Block „der User ist gerade auf folgender Seite aktiv:

". Container-Test bestätigt: Block landet im Prompt, mit einem Test-Marker erkennbar.

Im Browser dann: „Nein, ich sehe keinen Wordle-Spielstand."

Reload-Test mit anderer Frame im System-Prompt: keine Änderung. Memory-Hinweis „Haiku-Tool-Use unzuverlässig" — analog auch für System-vs-History-Konflikte: das Modell folgte der etablierten Konversationshistorie. Vor dem Fix war fünfmal hintereinander geantwortet worden „ich sehe nichts". Diese Antworten standen im message_history-Parameter der Anthropic-API. Das Modell wählte die Konsistenz mit seinen früheren Antworten über die neue Info im System-Prompt.

Fix: den page_context.summary zusätzlich als Prefix in die aktuelle User-Message packen — direkt vor agent.run_sync. Damit ist der Live-Kontext Teil der aktuellen Aufgabe und nicht durch History-Bias überlagert. Die DB-Speicherung des Chat-Logs bleibt dabei unverändert: der Prefix existiert nur im LLM-API-Call, das User-Message-Objekt in der ChatMessage-Tabelle enthält weiter den blanken Text.

Beim nächsten Test antwortete der Bot dann konkret:

Dein Versuch 1: RATEN R grau, A grau, T grau, E gelb, N gelb Tipp für Versuch 2: Such nach einem Wort mit E und N an anderen Positionen.

5. Solver als Hint-Button

Schnitt nach dem Page-Context-Erfolg: der Solver gehört in die Seite, nicht in die Konsole. Eine kleine Endpoint-Route /wordle/hint/ nimmt die bisherigen Versuche + Farb-Feedbacks als POST entgegen, filtert die Lösungs-Kandidaten, ruft die Solver-Logik auf, gibt Top-3 zurück ({word, entropy, is_candidate}).

Frontend: ein dezenter Knopf vor der Tastatur, gestricheltes Border, blasse Farbe. Beim Klick wird ein Flag usedHint=true in den State geschrieben (persistent in LocalStorage), ein roter „Dieses Spiel zählt nicht für die Statistik"-Banner erscheint, und die Top-3 werden in einer kleinen Box unter dem Spielfeld gerendert. updateStats() skippt das Spiel, sobald usedHint=true ist — die Statistik bleibt unverfälscht.

Lessons Learned

Drei Punkte, in der Reihenfolge in der sie aufgetaucht sind:

  1. Wortlisten-Quelle entscheidet alles. Eine selbst aus Korpus + Standardwörterbuch zusammengebaute Liste hat Junk-Probleme (Eigennamen, Lehnwörter, Funktionswort-Bias). Eine kuratierte Liste aus einem etablierten Klon ist trotz Lizenz-Grauzone das pragmatisch Beste. Im Zweifel: kuratieren statt filtern.

  2. Bei mehreren LLM-Provider-Pfaden System-Prompt nicht überbewerten. Anthropic-API mit Haiku 4.5 gewichtet Conversation-History stärker als den System-Prompt — wenn vor einer System-Prompt-Änderung etwas Falsches im History steht, bleibt das hängen. Live-Kontext gehört in die aktuelle User-Message gepackt, nicht (nur) in den System-Prompt.

  3. Cheating-Mechanik einbauen. Solver-Hilfe ist ein scharfes Schwert. Wenn man Statistik-Persistenz hat, sollte der Hint-Button automatisch markieren dass das Spiel nicht mehr zählt — sonst ist es schwer, mit echten Spielern eine Verteilung zu sammeln.

Quellen und Code: das Wortlisten-Tool plus Solver liegen unter wordle-de/, das Spiel selbst als Django-App wordle/ im pyground-Repo.

Kategorien

Vibe Coding

Stichworte

Vibe Coding

Kommentare

Noch keine Kommentare. Schreib den ersten.

Melde dich an, um zu kommentieren.