ninelands-reddit/PLAN.md
Lukas Müllner 5c6218b09a feat: Devvit Journeys (App.Ready, Start bei der Burg, Sterne als Fortschritt, Ende mit Gold)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 17:24:51 +01:00

228 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PLAN – Ninelands für Reddit
Basic-Version von Ninelands als tägliches Reddit-Spiel (Devvit Web) für den Jam **Reddit Game of the Day 2026**.
Abgabe: **9. Dezember 2026, 03:00 MEZ**. Eingereicht wird in „Best Daily Game“ und als Portierung in
„Best Ported Daily Game“.
## Entscheidungen des Besitzers (2026-11-13)
- **Nur 9×9**, **nur Englisch**.
- **Das Spiel steht im Vordergrund:** kein Menü, kein Footer, kein Feedback-Knopf. Ausnahmen sind ein schlanker
Startbildschirm (Name, Datum, eigene Statistik/Streak) und eine Ergebniskarte am Ende. Beides ist Teil des Spiels
und wird von der Jury ausdrücklich verlangt.
- **Kern: Karte des Tages mit Bestenliste.** PvP höchstens später als KI-Duell-Bonus (wenig Spieler gleichzeitig
online, viel Aufwand, kostet Polish).
- **Freischaltbares:** Themen und Banner aus dem Hauptspiel, gekoppelt an Streak und Sterne statt an Herrscher-Stufen.
- **Lizenz GPL-3.0** (Besitzer ist alleiniger Urheber der Engine). Das Gerüst der Devvit-Vorlage bleibt BSD-3-Clause
(`LICENSE-TEMPLATE.md`).
- **Musik ja**, mit einer kleinen Credits-Stelle (CC BY-SA 4.0 verlangt die Nennung). Alle Sprites stammen aus
„Mini World Sprites“ (CC0). Nachweise in `CREDITS.md`.
## Phase 0 – Grundlagen
1. Sync-Skript `scripts/sync-engine.sh`: holt `src/core` von einem festen Ninelands-Tag (Vorschlag: `v0.11.0`) nach
`src/engine/` und notiert den Tag in `src/engine/VERSION`. Die Kopie wird nie von Hand geändert. Die Campaign- und
Puzzle-JSONs nur mitnehmen, wenn der Import sonst bricht. Besser: prüfen, ob esbuild sie per Tree-Shaking weglässt.
2. Prüfen, ob die Engine mit TypeScript 7, Biome und esbuild der Vorlage baut. `src/engine/` von Biome ausnehmen,
denn fremde Formatierung soll nicht zu Änderungen an der Kopie zwingen.
3. Assets übernehmen: die Tile-Sprites, die das 9×9-Spiel braucht, sowie die Themen und Banner für Phase 2. Bei der
Musik auf die App-Größe achten: das Hauptspiel hat 21 MB (6 Titel, Opus + MP3). Vermutlich nur 1–2 Titel und nur
ein Format. Das Größenlimit von Devvit vorher in der Doku prüfen.
## Phase 1 – Kern (Tagesrätsel)
1. **Tagespost:** Ein Scheduler-Job legt jeden Tag um 00:00 UTC einen neuen Post an („Ninelands #N – <Datum>“).
Jeder Tag bekommt so eigene Kommentare, in denen über die Karte diskutiert wird.
2. **Tageskarte:** Seed aus dem Datum, 9×9, mit den drei Sternzielen wie im Hauptspiel (★ Gold, ★ Länder, ★ beides),
berechnet aus Bot-Partien auf derselben Karte.
3. **Spiel:** Brett im Post, Ziele-Zeile live, Rückgängig wie im Hauptspiel.
4. **Abgabe und Prüfung:** Der Client schickt die Züge, der Server spielt sie mit derselben Engine nach und speichert
nur selbst berechnetes Gold (wie im Hauptspiel). So kann niemand die Bestenliste mit erfundenen Werten fälschen.
5. **Bestenliste** je Tag in Redis (Sorted Set): Sortierung wie bei den Ninelands-Ranglisten, also **Sterne ↓,
Gold ↓, früher abgegeben zuerst**. Gewertet wird nur der erste Versuch; danach darf man weiterspielen, das zählt
aber nicht. _Entscheidung Besitzer._
6. **Streak:** aufeinanderfolgende Tage mit gespielter Tageskarte.
7. **Startbildschirm:** Name, Nummer und Datum, eigene Streak, „Play“.
8. **Ergebniskarte:** Sterne, Gold, Länder, Platz, Streak, Teilen-Zeile (`Ninelands #N ★★☆ · 9/9 lands · 85 gold`)
und der Anstoß, sie in die Kommentare zu posten.
9. **Devvit Journeys:** Ereignisse für Start, Abgabe und Streak (Jury-Kriterium).
10. **Credits:** ein kleines „ⓘ“ auf dem Startbildschirm mit Musik- und Sprite-Nachweis.
11. **Erstes Spielen:** Hinweise im Zusammenhang statt Textwand (Jury-Kriterium „Intuitive Onboarding“).
## Phase 2 – Wiederkommen
1. Themen freischalten über die Streak (z. B. 3 / 7 / 14 Tage), Banner über gesammelte Sterne. Die Auswahl erfolgt
direkt auf dem Startbildschirm, ohne eigenes Menü.
2. **User-Flair** im Subreddit (z. B. „🔥 12-day streak“, Tagessieger). Vorher in der Doku prüfen, wie Devvit Web
Flair setzt.
3. Winter- bzw. Winterfest-Thema passend zur Bewertung im Dezember.
## Phase 3 – optional
- KI-Duell gegen den Computer als Bonus nach dem Tagesrätsel. Nur, wenn Phase 1 und 2 poliert sind.
## Phase 4 – Einreichung (alles nach Rückfrage beim Besitzer)
- Eigenes Subreddit für das Spiel; öffentlicher Demo-Post (der einzige öffentliche Schritt).
- App-Listing auf developers.reddit.com (Upload, **kein** `devvit publish`).
- Auf Devpost angeben, dass es eine Portierung ist, und erklären, was seit dem 11. Nov. neu entstanden ist (Pflicht
für bestehende Projekte); optional die Feedback-Umfrage.
- **`README.md` (Englisch) ersetzen:** Spiel und Spielanleitung – Pflicht laut offiziellen Regeln.
- Subreddit öffentlich (oder „Restricted“: lesbar für alle) und < 200 Mitglieder.
- Video (< 1 min) und Screenshots für die Beschreibung – die Jury darf allein danach urteilen.
- **Bewertung 9.–16. Dez.:** Tagesposts müssen weiterlaufen; in der Zeit keine riskanten Uploads.
## Grober Zeitplan
| Bis | Was |
| :------- | :--------------------------------- |
| 16. Nov. | Phase 0 |
| 29. Nov. | Phase 1 spielbar im Test-Subreddit |
| 4. Dez. | Phase 2, Polish |
| 8. Dez. | Puffer, Demo-Post, Einreichung |
## Weitere Entscheidungen (2026-11-13)
- **Eigener Seed für Reddit** (Präfix ungleich `hivedoku-daily-`): Sonst wäre die Tageskarte dieselbe wie auf der
Website, und „Watch the best game“ dort verriete die Lösung.
- **Nur der erste Versuch zählt** für die Bestenliste; Weiterspielen ist erlaubt, wird aber nicht gewertet.
- **Subreddit:** Entwickelt wird im Test-Subreddit, das `devvit playtest` selbst anlegt. `r/Ninelands` (falls frei)
legt der Besitzer erst für den Demo-Post an. Hochgeladene, nicht veröffentlichte Apps lassen sich nur in Subreddits
mit < 200 Mitgliedern installieren – für die Jam-Demo reicht das.
## Devvit-Vorgaben, die das Design betreffen (Doku, 2026-11-13)
- **Ton:** erst nach einer Nutzeraktion abspielen, **Stumm-Knopf** im Spiel, bei `visibilitychange` stumm schalten.
- **Gesten:** nur Tippen/Klicken; kein Scroll-Abfangen, kein Zoom/Pan – man muss am Post vorbeiscrollen können.
- **Expanded Mode** nur auf ausdrückliche Aktion (Knopf), nie automatisch; die Standardansicht bleibt im Post.
- **Responsive**, die meisten Spieler sind mobil.
- **`localStorage` wird bei jedem App-Update gelöscht** → Streak, Freischaltungen, gewähltes Thema, Stumm-Schalter in
Redis speichern.
- Server: serverlos, max. 30 s je Anfrage, 4 MB Anfrage, 10 MB Antwort; kein `fs`. Client: keine externen Abrufe.
## Risiken
- Devvit ändert sich schnell: Scheduler, Redis, Flair und Journeys vor dem Bauen in der aktuellen Doku lesen
(`https://developers.reddit.com/docs/llms.txt`, die Seiten gibt es als `.md`, per `curl` abrufbar).
- App-Größe durch die Musik.
- Die Engine kennt viele Modi (Kampagne, Puzzles, Duell). Unnötiges darf nicht ins Bundle wandern
(Metafile von esbuild prüfen).
## As built
### 2026-11-13 – Repo, CI, Lizenz
- Repo `DieLustigeTierwesen/ninelands-reddit` (privat) angelegt.
- CI: Die `parallel:`-Stufe der Vorlage kennt der Forgejo-Runner nicht („Invalid run/uses syntax“). Ersetzt durch
einzelne Schritte nacheinander.
- Lint der Vorlage war rot: Das Biome-Schema 2.6.4 passte nicht zur installierten CLI 2.6.6, und `package.json` hatte
keinen Zeilenumbruch am Ende.
- Lizenz auf GPL-3.0 umgestellt (`LICENSE` aus dem Hauptspiel, `package.json` → `GPL-3.0-only`). Der BSD-Text der
Vorlage bleibt als `LICENSE-TEMPLATE.md`, denn BSD verlangt, dass der Hinweis erhalten bleibt. `CREDITS.md` angelegt.
### 2026-11-13 – Phase 0: Engine und Assets
- `scripts/sync-engine.sh <tag>` holt per `git archive` (nur lesend) `src/core`, `src/campaign`, `src/puzzles` nach
`src/engine/` und Sprites (Tiles, Brett-Themen) sowie Musik nach `public/`. Stand in `src/engine/VERSION`
(`v0.11.0`). Avatare nicht übernommen (braucht die Basic-Version nicht).
- `src/engine/` hat eine eigene `tsconfig.json` (strict, `noUncheckedIndexedAccess` aus, `resolveJsonModule`,
`lib` WebWorker für `crypto`/`structuredClone`). **`erasableSyntaxOnly` aus**, weil `duel.ts` Parameter-Properties
nutzt. Biome ignoriert `src/engine/`.
- **Fallstrick:** Die Engine importiert ohne Dateiendung (`'./Board'`). `node --experimental-strip-types` kann das
nicht auflösen. Deshalb bündelt `test:unit` die Tests jetzt erst mit esbuild nach `dist/test/` (`--packages=external`,
damit `@devvit/web` als ein Modul geladen wird und die Redis-Attrappen im Server-Test greifen), dann `node --test`.
Der Glob `src/**/*.test.ts` läuft unter `sh` ohne globstar = nur eine Ebene tief (`src/*/…`).
- `src/shared/daily.ts`: Tageskarte mit Seed `fnv1a('ninelands-reddit-daily-' + UTC-Tag)`, Nummer ab 2026-11-13 (#1),
Welt-Regel wie im Hauptspiel `rolledTrait(seed)`. **Präfix und `FIRST_DAY` nie ändern** (sonst andere Karten und
Nummern rückwirkend); Schutztests in `daily.test.ts`.
- Ziele (`computeGoals`, 7 Bot-Partien) plus Nachspielen dauern in Node ≈ 80 ms – unproblematisch für den Server.
Für die Prüfung **nicht** `verifyProgress` aus `progress.ts` nehmen: das zieht Kampagnen-JSONs ins Bundle und kennt
nur die Website-Tageskarte. Eigene Prüfung aus `applyMoves` + `computeGoals` + `starsOf`.
- Musik: ein Titel („Peaceful Village“), vom Sync-Skript aus der Opus-Datei des Hauptspiels als **MP3 64 kbit/s**
kodiert (1,0 statt 1,6 MB; Besitzer hat Varianten verglichen). MP3 läuft auch in iOS-Webviews. Ein Größenlimit für
Webview-Assets steht nicht in der Doku; weitere Titel in `scripts/sync-engine.sh` (`musik=(…)`) ergänzen.
- Biome: Ordner-Ausschluss als `"!src/engine"` schreiben – `"!src/engine/"` mit Schrägstrich griff nicht.
### 2026-11-13 – Phase 1, Server
- Endpunkte: `GET api/day` (Karte, Ziele, eigenes Ergebnis, Streak, Top 10, Spielerzahl) und `POST api/submit`
(Züge). Der Tag eines Posts steht in den Post-Daten (`postData.day`); ohne Daten gilt heute (UTC).
- **Prüfung:** Der Server spielt die Züge nach (`src/server/verify.ts`) und wertet nur **fertige** Partien (kein Zug
mehr möglich) – sonst ließe sich früh abbrechen. Gold, Länder und Sterne rechnet er selbst; der Client schickt nur
Züge.
- **Erster Versuch:** `hSetNX` auf `results:{day}` ist atomar; ein zweiter Versuch liefert `counted: false` und das
alte Ergebnis.
- **Bestenliste:** Sorted Set mit `Sterne·100 000 + Gold + 1/(seq+1)` – `seq` ist die laufende Nummer der Abgabe,
dadurch gewinnt bei Gleichstand die frühere, und die Werte sind eindeutig (Platz = `zCard − zRank`).
- **Streak** zählt nur, wenn der Post von heute gespielt wird; alte Posts kann man nachspielen (Bestenliste ja,
Streak nein). Angezeigt wird 0, wenn der letzte Tag älter als gestern ist.
- **Tagespost:** Scheduler `1 0 * * *` (00:01 UTC), dazu bei Installation und per Moderator-Menü. Hash `posts`
verhindert doppelte Posts.
- `devvit.json`: **`redis: true` muss gesetzt sein** (Standard ist `false`). Startbildschirm inline (`height: tall`),
Spiel als Entry `game` (Expanded).
- **Fallstrick:** `hMGet` ist laut Doku nicht für jede App freigeschaltet → einzelne `hGet`.
- Offen: Löschungen von Nutzern respektieren (Devvit-Regel) – Ergebnis und Name bei Kontolöschung entfernen.
### 2026-11-13 – Phase 1, Client
- **Brett aus dem Hauptspiel:** `BoardView.ts`, `atlas.ts`, `geometry.ts`, `board.css` und `env.ts` kommen per
Sync-Skript nach `src/engine/app/` (unverändert). `import.meta.env` und `__API_ORIGIN__` setzt esbuild per
`--define`; Typen dafür in `src/types/ninelands-env.d.ts`. Das Symbol `i-warn` (Warn-Badge der Vorschau) steht
direkt in `public/game.html`.
- **Startbildschirm** (`splash`, inline, `height: tall`): Name, Nummer, Datum, Mini-Karte, Gesetz, Ziele bzw. eigenes
Ergebnis, Spielen-Knopf, Streak, Top 3, „i“ mit Credits.
- **Spiel** (`game`, Expanded): Ziele-Leiste mit Sternen live, Gesetz (antippbar → Regeln), Brett, Hinweiszeile,
Figuren mit Kosten, Rückgängig (1 Schritt wie im Hauptspiel). Touch: erster Tipp zeigt Vorschau und Grund, zweiter
setzt. Nach der Burg automatisch Bauer. Ende → Abgabe → Ergebniskarte (Sterne, Gold, Länder, Platz, Streak, Top 10,
Teilen über `showShareSheet`, „Play again“ als Übung, „Done“ schließt den Expanded Mode).
- Gäste spielen ohne Wertung (Hinweis + `showLoginPrompt`); wer schon gespielt hat, sieht „Practice“.
- **Layout-Fallstrick:** Brett und Hinweis liegen in einem Container mit `container-type: size`; das Brett ist
`min(100cqw, 100cqh − 3.2em)` breit. Mit `flex: 1` + `aspect-ratio` allein blieb viel Leerraum zwischen Brett und
Hinweis.
- Schrift Pixelify Sans über `@fontsource/pixelify-sans` (OFL), von esbuild nach `public/fonts/` kopiert – externe
Schrift-Abrufe sind im Devvit-Client gesperrt.
- Musik: Start beim ersten Tippen, Stumm-Knopf, Pause bei `visibilitychange`. Stumm-Schalter vorerst in
`localStorage` (geht bei App-Updates verloren – vertretbar; Redis erst, wenn es stört).
- Lokale Vorschau ohne Reddit: kleine Server-Attrappe + Playwright-Screenshots (Skripte im Scratchpad, nicht im Repo).
- Biome ergänzt bei `--write` Dateiendungen an Engine-Importen (`'../engine/core/Game.ts'`) – ist so gewollt.
### 2026-11-13 – Spielende und Ziel-Hervorhebung (Rückmeldung Besitzer nach erstem Playtest)
- Problem: Man wusste nicht, wie man die Partie beendet – sie endete nur, wenn kein Zug mehr möglich war (Bauern
sind gratis, das dauert).
- **„End game“-Knopf** wie im Hauptspiel, mit zweitem Tipp zur Bestätigung (3 s). Der Server wertet jetzt auch früh
beendete Partien; abgelehnt wird nur eine leere (`EMPTY`). Kein Vorteil durch frühes Aufhören: Gold zählt vor der
Abgabezeit.
- **Kein automatisches Ende bei 3 Sternen** – unter Spielern mit 3 Sternen entscheidet das Gold; stattdessen Hinweis
„All 3 stars! Keep going for more gold – or end the game.“
- Erreichte Ziele werden **golden** hervorgehoben (wie `goal-chip.done` im Hauptspiel).
- `devvit playtest` trägt `dev.subreddit` (`ninelands_game_dev`) in `devvit.json` ein.
- Rückmeldung Besitzer: Bei „One farm per land“ bleiben am Ende oft nur Ritter, die Gold kosten. Darum prüft der
Client nach jedem Zug, ob noch ein Zug Gold bringt (`gainLeft`, alle gültigen Züge per `preview`, < 1 ms). Wenn
nicht: Hinweis „None of your remaining moves earns gold – end the game to keep your gold.“ und „End game“ leuchtet golden.
- Fix: Im goldenen Ziel-Feld waren Stern und „/ Ziel“ ebenfalls gold (Gold auf Gold, unsichtbar) – beim Umsortieren
der CSS-Regeln für Biome (`noDescendingSpecificity`) waren die Deklarationen vertauscht. Jetzt alles in Tinte.
### 2026-11-13 – Katalog-Assets (Entwurf)
- `npm run capture` (`scripts/capture/`): baut eine Aufnahmeseite nur mit dem Brett, ein Bot (`smart`) spielt eine
Partie mit Vorschau, Playwright nimmt auf, ffmpeg macht daraus `dist/marketing/featuring.mp4` (1500×1000, 30 fps,
≈ 11 s, ≈ 1,7 MB), `fallback.gif` (1000×667, ≤ 2 MB), `fallback.png` (1500×1000) und `tile.png` (560×256 mit
Schriftzug). Maße laut Developer Portal; Größen werden bei Bedarf automatisch heruntergeregelt.
- **Karte vom 2026-10-10** (vor dem ersten Spieltag): läuft nie als Tageskarte – eine künftige Tageskarte im Video
wäre ein Spoiler. Der Bot holt dort in 26 Zügen alle 3 Sterne.
- Playwright als devDependency in der Version des Hauptspiels (1.55.1), damit die vorhandenen Browser im Cache passen.
- Vor der Einreichung neu aufnehmen (z. B. mit Winter-Thema aus Phase 2).
### 2026-11-13 – Devvit Journeys
- `@devvit/analytics` 0.15.3 (passend zu Devvit 0.15.3), `permissions.journeys: true`.
- **Fallstrick:** Der fertige `createTelemetryRouter` ist ein Express-Router; unser Server hat kein Framework. Darum
bauen `src/server/journeys.ts` die fünf Endpunkte (`api/telemetry/journey/start|progress|interaction|end|app-ready`)
nach und reichen an `telemetry` aus `@devvit/analytics/server/reddit` weiter. Das Paket zieht Express als Peer mit
ins Server-Bundle (nur importiert, nicht benutzt).
- Ereignisse: `App.Ready` in Start- und Spielansicht; `Journey.Start` beim **Setzen der Burg** (bewusste Aktion, nicht
das Öffnen – Vorgabe der Doku); `Journey.Progress` je erreichtem Stern (1/3, 2/3, 3/3); `Journey.End` mit
`win` = mindestens ein Stern, `score` = Gold. Alles „feuern und vergessen“.
- Reddit leitet Ereignisse erst nach Freischaltung in der App-Review weiter (Allowlist); bis dahin „dropped“.
- Katalog (Besitzer): App-Icon aus dem Hauptspiel, Texte wie vorgeschlagen, Kategorie Game → Strategy.