diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-06-06 20:59:12 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-06-06 20:59:12 +0200 |
| commit | d9a54f1319cc9dcc2861cecc9edfde14cf039c5b (patch) | |
| tree | 74df791d6eda0ffe6c09cb32e5ffd76c4849cbac /docs | |
| parent | 18592fd9f520a33ab307e496b995c1a7bf284893 (diff) | |
Add examples catalog with subcommand dispatch
- Extract game engine from package main to engine/ library package
- Create examples/ package with registry, Entry type, and BuilderDeps
- Register default world as an example (moved from main())
- Add combat example: two teams of units facing each other
- Rewrite main.go as thin dispatch: go run . example [name]
- Update README with examples catalog one-liner
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/examples-catalog.md | 253 |
1 files changed, 253 insertions, 0 deletions
diff --git a/docs/examples-catalog.md b/docs/examples-catalog.md new file mode 100644 index 0000000..aeee0a0 --- /dev/null +++ b/docs/examples-catalog.md @@ -0,0 +1,253 @@ +# Examples Catalog — Architecture + +## 0. Solution (TL;DR) + +Add an `examples/` package with a registry of named scenarios. Each example is a function that populates a `*Game` with entities, positions, etc. The `example` subcommand selects which scenario to load. When no subcommand is given, the `"default"` example runs. No new dependencies, no structural changes to the engine. + +``` +go run . example # list available examples +go run . example combat # launches a combat demo +go run . # launches the default world (via registry) +``` + +## 1. Context & scope + +### Current landscape + +The game has an ECS engine, a camera system, pathfinding, resource gathering, unit spawning, patrol orders, and an Input abstraction layer. But currently, the "world" is hardcoded in `main()`: + +```go +// main.go — hardcoded world setup +ironMineBuilder := NewEntityBuilder()... +game.Append(ironMineBuilder.WithPosition(...).Build()) +townCenterBuilder := NewEntityBuilder()... +townCenter := game.Append(townCenterBuilder.WithPosition(...).Build()) +// ... +``` + +Every feature addition bloats `main()` further. There is no way to: + +- Demonstrate a specific feature in isolation +- Quickly iterate on a scenario without rebuilding everything +- Show the game to someone without them seeing a crowded test world + +### What is being built + +An **examples catalog** — a lightweight framework under `examples/` where each scenario is a self-contained Go file that builds a world. The CLI entry point (`main.go`) dispatches to the chosen example, or runs `"default"` when no subcommand is given. + +Future examples might demonstrate: + +- Combat between two groups of units +- Resource gathering loop (mine → carry → deposit) +- Patrol routes along a border +- Mass spawning from multiple town centers +- A "tech demo" with every entity type on screen + +## 2. Goals & non-goals + +### Goals + +- **CLI dispatch:** `go run . example combat` loads the combat scenario; `go run .` → `"default"` example from the registry +- **Self-contained examples:** Each example file owns its entity setup, positioning, and initial camera position — including `"default"` +- **Feature independence:** Examples live outside `main.go` and don't require changes to core engine files +- **Discoverability:** `go run . example` (no name) prints all registered examples with descriptions +- **Minimal wiring:** Zero boilerplate beyond adding a file and registering it + +### Non-goals + +- Not a scripting system — examples are compiled Go, not JSON/YAML data files +- Not a map editor — the goal is code-driven scenario setup, not a GUI tool +- Not a save/load system — examples are static worlds, not persisted state +- Not a modding API — only Go code, no plugin boundary + +## 3. Design + +### Architecture overview + +```mermaid +flowchart LR + A[main.go] -->|example combat| B{examples.Registry} + B -->|lookup "combat"| C[combat.Build] + C -->|Append entities| D[Game] + D -->|ebiten.RunGame| E[Rendered world] +``` + +### Registry + +A single map that maps example name → builder function: + +``` +examples/registry.go + type Builder func(*Game, BuilderDeps) + type BuilderDeps struct { UnitBuilder EntityBuilder; ... } + var Registry = map[string]struct{ + Name string + Description string + Build Builder + } +``` + +The `BuilderDeps` struct carries shared resources (font, pre-built entity builders) so each example doesn't recreate them. The example only calls `game.Append(...)` and optionally sets `game.Camera`. + +### Example structure (combat) + +```go +// examples/combat.go +package examples + +import "age_of_empires/..." + +func init() { + Registry["combat"] = Entry{ + Name: "Combat", + Description: "Two groups of units fight each other", + Build: buildCombat, + } +} + +func buildCombat(g *Game, deps BuilderDeps) { + // Red team + redBuilder := deps.UnitBuilder.WithSolid(NewFilledCircleImage(100, color.RGBA{0xff,0,0,0xff})) + for x := 0; x < 5; x++ { + g.Append(redBuilder.WithPosition(physics.Point{X: 400 + x*120, Y: 800}).Build()) + } + // Blue team + blueBuilder := deps.UnitBuilder.WithSolid(NewFilledCircleImage(100, color.RGBA{0,0,0xff,0xff})) + for x := 0; x < 5; x++ { + g.Append(blueBuilder.WithPosition(physics.Point{X: 400 + x*120, Y: 1200}).Build()) + } + // Camera position + g.Camera = Camera{X: 800, Y: 1000, Zoom: 1.0} +} +``` + +### CLI dispatch + +In `main.go`, before creating the Game: + +1. Look at `os.Args` for the subcommand pattern: `example` or `example <name>` +2. If `go run . example` (no name), print the registry and exit +3. If `go run . example <name>`, look up the name in `examples.Registry`: + - If found, call the builder with the game pointer before `ebiten.RunGame` + - If not found, print error + available names and exit +4. If no `example` subcommand (`go run .`), dispatch to `"default"` via the same registry + +This avoids mixing concerns: flags like `--profile` are truly cross-cutting, while `example` is a mode selector — naturally a subcommand. + +### Package structure + +``` +go_age_of_empire/ +├── examples/ +│ ├── registry.go # Registry map, Builder type, BuilderDeps +│ ├── combat.go # Combat scenario +│ ├── gathering.go # Resource gathering scenario +│ └── patrol.go # Patrol route scenario +├── main.go # CLI dispatch + default fallback +├── camera.go +├── ... +``` + +### Flow diagram + +```mermaid +flowchart TD + Start([os.Args]) --> Subcmd{os.Args[1] == "example"?} + Subcmd -->|No| Lookup["Look up 'default' in examples.Registry"] + Subcmd -->|Yes| HasName{os.Args[2] exists?} + HasName -->|No| List["Print registry and exit"] + HasName -->|Yes| Lookup["Look up name in examples.Registry"] + Lookup --> Found{Found?} + Found -->|No| Error["Print error + available names, exit"] + Found -->|Yes| Build["Call example.Build(game, deps)"] + Build --> Run["ebiten.RunGame(game)"] + Default --> Run +``` + +### BuilderDeps vs global state + +Shared resources (font, entity templates) are passed through `BuilderDeps` instead of being global variables. This keeps examples testable and independent. + +```go +type BuilderDeps struct { + FaceSource *text.GoTextFaceSource + UnitBuilder EntityBuilder + // future: BuildingBuilder, TreeBuilder, etc. +} +``` + +`main.go` constructs the `BuilderDeps` once and passes it to every example. + +## 4. Alternatives + +### A — Data-driven (JSON/YAML) world definitions + +A JSON file per example describing entities, positions, and properties. A generic loader iterates over the data and calls `game.Append()`. + +| Pro | Con | +|---|---| +| Non-programmers can author scenarios | Can't express logic (patrol routes, timers, AI behavior) | +| Loadable at runtime without recompilation | Type safety requires schema validation | +| Easy to share and version | Entity composition is awkward in flat JSON | + +**Verdict:** Rejected for now. The examples catalog is for developers demonstrating features, and the feature logic (e.g., "spawn 5 units then patrol them") is too procedural for a data format. Data files could be added later for map data. + +### B — Standalone binaries per example + +Each example gets its own `main.go` in a subdirectory: `examples/combat/main.go`, `examples/gathering/main.go`. + +| Pro | Con | +|---|---| +| Truly independent, no dispatch code needed | Massive duplication of engine setup (font, icon, window, Ebiten boilerplate) | +| Each can have its own `go.mod` | Can't share entity builders or utility code without a library package | +| Clean separation | `go run ./examples/combat` is more typing than `go run . example combat` | + +**Verdict:** Rejected. The duplication cost outweighs the isolation benefit. A single binary with CLI dispatch is more ergonomic. + +### C — Plugin system (Go plugins / shared objects) + +Each example is compiled as a `.so` plugin loaded at runtime via `plugin.Open`. + +| Pro | Con | +|---|---| +| Examples can be added without recompiling the main binary | Linux-only, fragile ABI, not supported on all platforms | +| | Debugging is painful | +| | Overkill for first-party examples shipped in the same repo | + +**Verdict:** Rejected. Go's plugin system has too many caveats and zero advantage for in-repo examples. + +## 5. Open questions + +1. **Should the default world (current `main()` code) also be an example?** + - ✅ **Decision:** Yes — move it to `examples/default.go` and register it as `"default"`. + - When `go run .` (no subcommand), dispatch to `"default"` via the same registry. + - Single code path: every mode of execution goes through `examples.Registry`. + - `main.go` has zero world-building logic. + +2. **Should examples be able to add new ECS components?** + - ✅ **Decision:** No. New components belong in `ecs/component.go`. + - Examples only wire existing components into the world. + +3. **How should the camera initial position be set?** + - ✅ **Decision:** Add `Camera.SetCenter(x, y)` for clarity. Examples call `deps.SetCamera(game, x, y)` or set `game.Camera.X/Y` directly. + +4. **Should `example` without an argument or with an unknown name print the list?** + - ✅ **Decision:** No argument → list. Unknown name → error + list. + +5. **How are build resources (entity builders with specific colors/sizes) shared?** + - ✅ **Decision:** `BuilderDeps` holds a base `UnitBuilder`. Examples clone it and call `.WithSolid(...)` to customize color/size. + +## 6. Parties involved + +- **Author:** Martin (implementing the examples) +- **Consumers:** Anyone who wants to quickly see a feature in action (demo purposes, debugging, onboarding) +- **Future contributors:** Adding a new example should be a single-file change with no engine modifications + +## 7. Cross concerns + +| Concern | Mitigation | +|---|---| +| **CLI hygiene** | Use `os.Args` scanning (no external flag dependency needed); subcommand avoids conflating modes with flags. Print usage on unknown subcommand | +| **Example isolation** | Each example is a `func(*Game, BuilderDeps)`. No mutable global state is shared between examples | +| **Testability** | Examples can be tested by calling `example.Build(game, deps)` and asserting on `game.Mask`, `game.Position`, etc. | +| **Observability** | `go run . example` lists examples; unknown names print error + list. The active example name is logged at startup | |
