aboutsummaryrefslogtreecommitdiff
path: root/docs/examples-catalog.md
blob: aeee0a0ff2d57dfe00b3c39d0e27d345cf76bf83 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
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 |