- commit
- 523a8cdf4146e5ede2fba75bf944ccbca2c830df
- parent
- c81e5f467a56c016f951e7588dc3d5e26abc83e5
- author
- Dami <dami.etoile@gmail.com>
| M | AGENTS.md | 188 | +++++++++++++++--------------- |
@@ -1,6 +1,6 @@
11 # AGENTS.md - PuzzleTea Development Guide22 3-## Build, Test, and Quality Commands3+## Commands44 55 ### Build & Run66 ```bash@@ -35,51 +35,86 @@ just fmt # gofumpt -w .
3535 just tidy # go mod tidy3636 ```3737 38-**Always run `just fmt` and `just lint` before committing.**38+Always run `just fmt` and `just lint` before committing.3939 4040 Enabled linters (`.golangci.yml`): errcheck, gofumpt (extra-rules), gosimple, govet, ineffassign, misspell (US locale), staticcheck, unused.4141 4242 ---4343 44-## Project Structure45-```46-puzzletea/47-├── main.go # Entry point: wires cmd package48-├── app/ # Root TUI model (Elm architecture)49-│ ├── model.go, update.go, view.go, keys.go, spawn.go, debug.go50-├── cmd/ # CLI commands (Cobra)51-│ ├── root.go, new.go, continue.go, list.go52-├── config/ # Persistent JSON config (~/.puzzletea/config.json)53-├── theme/ # Color theming (WCAG-compliant palettes, contrast utils)54-├── stats/ # XP/level math, streaks, card rendering55-├── game/ # Plugin interfaces, cursor, keys, style, border helpers56-├── store/ # SQLite persistence (~/.puzzletea/history.db)57-├── ui/ # Shared UI: menu list, main menu, table, panel, styles58-├── daily/ # Daily puzzle seeding, RNG, mode selection59-├── resolve/ # CLI argument resolution (category/mode name matching)60-├── namegen/ # Adjective-noun name generator61-├── hashiwokakero/, hitori/, lightsout/, nonogram/62-├── shikaku/, sudoku/, takuzu/, wordsearch/ # Puzzle game packages63-└── vhs/ # VHS tape files for GIF previews64-```65-66-Each puzzle package follows a consistent file structure:44+## Architecture45+46+PuzzleTea is a terminal puzzle game collection built with Go using the **Bubble Tea TUI framework** (Elm architecture: Model-Update-View).47+48+### Technology Stack49+- **TUI**: Bubble Tea v2 (`charm.land/bubbletea/v2`, always aliased as `tea`) + Bubbles + Lip Gloss50+- **CLI**: Cobra51+- **PDF generation**: go-pdf/fpdf (half-letter size: 139.7mm × 215.9mm)52+- **Persistence**: SQLite (`~/.puzzletea/history.db`)53+54+### Control Flow55+```56+main() → cmd.RootCmd (Cobra)57+ ├─ Default: Launch TUI (app.InitialModel → Elm loop)58+ ├─ --new / --continue / --set-seed: direct game launch59+ └─ Subcommands: new, continue, list, export-pdf60+```61+62+### Key Packages63+64+| Package | Role |65+|---------|------|66+| `app/` | Root TUI model; 9 puzzle categories wired at startup |67+| `cmd/` | Cobra CLI commands including `export-pdf` |68+| `game/` | Plugin interfaces (`Gamer`, `Mode`, `Spawner`, `PrintAdapter`), registry |69+| `pdfexport/` | PDF pipeline: JSONL parsing → per-game rendering → cover art |70+| `store/` | SQLite persistence |71+| `theme/` | 365 WCAG-compliant color themes |72+| `stats/` | XP/level/streak system |73+| `config/` | Persistent JSON config (`~/.puzzletea/config.json`) |74+| `resolve/` | CLI argument matching for game/mode names |75+| `daily/` | Deterministic daily puzzle seeding |76+| `ui/` | Shared TUI components (menus, tables, panels) |77+78+### Puzzle Packages79+Eight printable games: `nonogram`, `sudoku`, `nurikabe`, `shikaku`, `wordsearch`, `hashiwokakero`, `hitori`, `takuzu`. One game without PDF export: `lightsout`.80+81+Each puzzle package exposes: `Modes`, `DailyModes`, `HelpContent`, `NewMode(...)`, `New(...)`, `ImportModel([]byte)`, `DefaultKeyMap`, and registers itself via `init()` in `Gamemode.go`.82+83+### Plugin Registration84+```go85+// In Gamemode.go init():86+game.Register("Nonogram", func(data []byte) (game.Gamer, error) {87+ return ImportModel(data)88+})89+// Optional PDF export registration:90+game.RegisterPrintAdapter(adapter)91+```92+93+### PDF Export Pipeline94+```95+export-pdf command96+ → ParseJSONLFiles() # parse schema puzzletea.export.v197+ → adapter.BuildPDFPayload() # game-specific save → typed struct98+ → pdfexport.OrderPuzzlesForPrint() # difficulty-based ordering99+ → pdfexport.WritePDF() # cover + title pages + puzzle bodies + back100+```101+102+### File Layout per Puzzle Package67103 - **Capitalized**: `Model.go`, `Gamemode.go`, `Export.go`68104 - **Lowercase**: `grid.go`, `keys.go`, `style.go`, `generator.go`, `mouse.go`, `<game>_test.go`69105 - **Docs**: `help.md` (embedded via `//go:embed`), `README.md`70106 71107 ---72108 73-## Code Style Guidelines109+## Code Style74110 75111 ### Formatting76-- Use `gofumpt` (stricter than gofmt, extra-rules enabled) -- run `just fmt`77-- No comments required unless explaining non-obvious logic112+- Use `gofumpt` (stricter than gofmt, extra-rules enabled) — run `just fmt`78113 - Keep lines under ~100 characters79114 - US English spelling enforced by misspell linter80115 81116 ### Imports82-Two groups separated by a blank line: stdlib, then everything else (internal + external sorted together). When there are many internal imports, a third group separating internal from external is acceptable.117+Two groups: stdlib, then everything else (internal + external sorted together). Three groups acceptable when there are many internal imports.83118 ```go84119 import (85120 "errors"@@ -91,9 +126,8 @@ import (
91126 "charm.land/lipgloss/v2"92127 )93128 ```94-Note: always alias bubbletea as `tea`.95129 96-### Naming Conventions130+### Naming97131 - **Types**: PascalCase (`Model`, `NonogramMode`, `Entry`)98132 - **Unexported types**: camelCase or lowercase (`grid`, `state`, `menuItem`)99133 - **Variables/Fields**: camelCase (`rowHints`, `currentHints`)@@ -103,8 +137,7 @@ Note: always alias bubbletea as `tea`.
103137 ### Type Declarations104138 Prefer grouped type blocks: `type ( grid [][]rune; state string )`105139 106-### Interface Compliance107-Use compile-time checks in grouped var blocks:140+### Interface Compliance (compile-time checks)108141 ```go109142 var (110143 _ game.Mode = NonogramMode{}@@ -113,56 +146,11 @@ var (
113146 )114147 ```115148 116-### Error Handling117-- Return descriptive errors: `errors.New("puzzle width does not support row tomography definition")`118-- Check errors immediately; use `fmt.Errorf` with `%w` only when wrapping adds context119-- No assertion libraries in tests -- use `t.Errorf` / `t.Fatalf` only120-121149 ### Styling122-Use the `theme` package for colors (`theme.Current().Accent`, etc.) and `game/style.go` shared accessors (`CursorFG()`, `CursorBG()`, `ConflictFG()`). Use `compat.AdaptiveColor` from `charm.land/lipgloss/v2/compat` for adaptive light/dark colors.123-124----125-126-## Plugin Architecture127-128-### Gamer Interface (game/gamer.go)129-Every puzzle `Model` must implement:130-```go131-type Gamer interface {132- Init() tea.Cmd133- Update(msg tea.Msg) (Gamer, tea.Cmd)134- View() string135- GetDebugInfo() string136- GetFullHelp() [][]key.Binding137- GetSave() ([]byte, error)138- IsSolved() bool139- Reset() Gamer140- SetTitle(string) Gamer141-}142-```143-144-### Mode/Spawner Interfaces145-```go146-type Spawner interface { Spawn() (Gamer, error) }147-type SeededSpawner interface {148- Spawner149- SpawnSeeded(rng *rand.Rand) (Gamer, error)150-}151-```150+Use `theme.Current().Accent` etc. from the `theme` package, and `game/style.go` shared accessors (`CursorFG()`, `CursorBG()`, `ConflictFG()`). Use `compat.AdaptiveColor` from `charm.land/lipgloss/v2/compat` for adaptive colors.152151 153-Every mode type embeds `game.BaseMode` via `game.NewBaseMode(title, description)`.154-155-### Puzzle Package Exports156-Every puzzle package exports: `Modes`, `DailyModes` (`[]list.Item`), `HelpContent` (`string`, from `//go:embed help.md`), `NewMode(...)`, `New(...)`, `ImportModel([]byte) (*Model, error)`, and `DefaultKeyMap`.157-158-Each package registers itself via `init()` in `Gamemode.go`:159-```go160-func init() {161- game.Register("Nonogram", func(data []byte) (game.Gamer, error) {162- return ImportModel(data)163- })164-}165-```152+### Error Handling153+Return descriptive errors; use `fmt.Errorf` with `%w` only when wrapping adds context. No assertion libraries in tests — use `t.Errorf`/`t.Fatalf` only.166154 167155 ---168156 @@ -170,10 +158,10 @@ func init() {
170158 171159 ### Section Comments with Priority172160 ```go173-// --- generateTomography (P0) --- // P0 = critical174-// --- Grid serialization (P1) --- // P1 = important175-// --- generateRandomState (P2) --- // P2 = generators/slow176-// --- TitleBarView (P3) --- // P3 = low-priority UI161+// --- generateTomography (P0) --- // P0 = critical162+// --- Grid serialization (P1) --- // P1 = important163+// --- generateRandomState (P2) --- // P2 = generators/slow164+// --- TitleBarView (P3) --- // P3 = low-priority UI177165 ```178166 179167 ### Table-Driven Tests with Subtests@@ -188,12 +176,19 @@ func TestGenerateTomography(t *testing.T) {
188176 }189177 for _, tt := range tests {190178 t.Run(tt.name, func(t *testing.T) {191- // test logic using t.Errorf / t.Fatalf only (no assertion libraries)179+ // test logic using t.Errorf / t.Fatalf only192180 })193181 }194182 }195183 ```196184 185+### Slow Test Gating186+```go187+if testing.Short() {188+ t.Skip("skipping slow generator test in short mode")189+}190+```191+197192 ### Save/Load Round-Trip Pattern198193 ```go199194 data, err := m.GetSave()@@ -203,14 +198,25 @@ if err != nil { t.Fatal(err) }
203198 // verify state preserved204199 ```205200 206-### Slow Test Gating201+---202+203+## Gamer Interface204+Every puzzle `Model` must implement (from `game/gamer.go`):207205 ```go208-if testing.Short() {209- t.Skip("skipping slow generator test in short mode")206+type Gamer interface {207+ Init() tea.Cmd208+ Update(msg tea.Msg) (Gamer, tea.Cmd)209+ View() string210+ GetDebugInfo() string211+ GetFullHelp() [][]key.Binding212+ GetSave() ([]byte, error)213+ IsSolved() bool214+ Reset() Gamer215+ SetTitle(string) Gamer210216 }211217 ```212218 213----219+Every mode type embeds `game.BaseMode` via `game.NewBaseMode(title, description)`.214220 215221 ## Global Keybindings216-`Ctrl+N` Main menu | `Ctrl+C` Quit (saves abandoned) | `Ctrl+E` Debug overlay | `Ctrl+H` Full help | `Ctrl+R` Reset puzzle | `Enter` Select | `Escape` Back222+`Ctrl+N` Main menu | `Ctrl+C` Quit | `Ctrl+E` Debug overlay | `Ctrl+H` Full help | `Ctrl+R` Reset puzzle | `Enter` Select | `Escape` Back