From ef9c93cb40971e289078f19b638960d6fa65a5c7 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Wed, 22 Apr 2026 02:27:45 +0300 Subject: [PATCH] docs(crew-movement): plan + resume summary --- docs/tasks/crew-movement.md | 84 ++++++++++++++++++++++ docs/tasks/summary-crew-movement-resume.md | 48 +++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 docs/tasks/summary-crew-movement-resume.md diff --git a/docs/tasks/crew-movement.md b/docs/tasks/crew-movement.md index 6f2133a6b788f40f5ab27479d900b2204d829831..983719aa3889f4b20c4e8e04b36c29d70cf2ace6 100644 --- a/docs/tasks/crew-movement.md +++ b/docs/tasks/crew-movement.md @@ -47,3 +47,87 @@ Second milestone of the `ftl-shape` vertical slice. Adds interactive crew: click - Fail fast: unreachable paths return empty, not "closest-so-far". Clicking outside the ship is a no-op, not a teleport. - Separate data from rendering and input — `crew.go` has no `ebiten.` imports; `render.go` never mutates crew state. - Deterministic core: same input sequence and timestep → identical game state. + +## Plan + +Approach: three phases, TDD-first on the two pure units (tiles/pathing, then crew tick), then a single integration phase that wires input + rendering into `main.go` and `render.go`. Greenfield feature, no backwards-compat risk. + +### Phase 1 — Walkability and pathfinding (TDD) + +- **1.1** `tiles_test.go` (create) — tests first, must fail before 1.2. + - `TestWalkableTiles_coversAllRooms` — `walkableTiles(NewPlayerShip())` returns the exact union of the 5 rooms (54 tiles total: 9+12+12+12+9); spot-check tiles inside each room are present and an off-ship tile is absent. + - `TestWalkableTiles_ignoresOutside` — tiles at `(0,0)` and `(20,20)` are absent. + - `TestBFSPath_sameTile` — `bfsPath(walk, {4,10}, {4,10})` → `nil`/empty. + - `TestBFSPath_adjacentTile` — `bfsPath(walk, {4,10}, {5,10})` → `[[2]int{{5,10}}]`. + - `TestBFSPath_unreachable` — from walkable tile to off-ship tile → empty. + - `TestBFSPath_throughShields` — `bfsPath(walk, Pilot-center, Engines-center)` is non-empty, contains only walkable tiles, ends on the Engines tile, and length equals Manhattan distance (7 for `(4,10)→(11,10)`). + - `TestBFSPath_deterministic` — two consecutive calls produce equal slices. + +- **1.2** `tiles.go` (create) + - `package main`; no Ebitengine imports. + - `func walkableTiles(s Ship) map[[2]int]bool` — iterates `s.Rooms`, inserts every `(x, y)` with `GridX ≤ x < GridX+GridW`, `GridY ≤ y < GridY+GridH`. + - `func bfsPath(walk map[[2]int]bool, from, to [2]int) [][2]int` — 4-neighbour BFS with fixed neighbour ordering `{{+1,0},{-1,0},{0,+1},{0,-1}}` (for determinism), visited-set, parent map; returns waypoints excluding `from`, including `to`. Empty on same-tile or unreachable. + - Invariant: pure, no rendering/input deps; `bfsPath` deterministic via fixed neighbour order. + +- Commit: `crew-movement: walkability + BFS pathfinding with tests` + +### Phase 2 — Crew type and movement tick (TDD) + +- **2.1** `crew_test.go` (create) + - `TestNewStartingCrew_count` — returns 3 crew. + - `TestNewStartingCrew_positions` — Alice at `(4,10)` (Pilot), Bob at `(7,10)` (Shields), Carol at `(11,10)` (Engines); initials `'A'`, `'B'`, `'C'`; distinct colours. + - `TestUpdateCrew_idle` — empty `Path`, any `dt` → all fields unchanged. + - `TestUpdateCrew_subTileProgress` — path `[[2]int{{5,10}}]`, `dt = 0.1` (< `TileTime`) → `TileX/TileY` unchanged, `MoveT ≈ 0.1/TileTime`, path unchanged. + - `TestUpdateCrew_tileSnap` — path `[[2]int{{5,10}}]`, `dt = TileTime` → `TileX==5`, `Path` empty, `MoveT == 0`. + - `TestUpdateCrew_multiStepOvershoot` — path `[{5,10},{6,10}]`, `dt = 1.5*TileTime` → `TileX==6`, remaining path `[{6,10}]` wait, check logic. Actually: start `(4,10)`, path `[{5,10},{6,10}]`; `dt=1.5*TileTime` means advance 1.5 tiles. After 1 tile: `(5,10)`, path `[{6,10}]`, `MoveT=0`. Remaining 0.5 tile into next: `MoveT≈0.5`, `TileX==5`, path `[{6,10}]`. Assert that final state. + - `TestUpdateCrew_canonicalIntegral` — after any tick, `TileX` and `TileY` are integers equal to the last crossed waypoint (never fractional); `MoveT` is render-time only. + +- **2.2** `crew.go` (create) + - `package main`; imports `image/color` only (no Ebitengine). + - `const TileTime = 0.35` — seconds per tile. + - `type Crew struct { ID int; Name string; Initial rune; Color color.RGBA; TileX, TileY int; Path [][2]int; MoveT float64 }`. + - `func NewStartingCrew() []Crew` — hard-coded 3 crew: Alice/Bob/Carol at tiles `(4,10)`, `(7,10)`, `(11,10)`, with distinct muted tints (teal, amber, magenta). Initials `'A'`, `'B'`, `'C'`. + - `func updateCrew(c *Crew, dt float64)` — if `len(c.Path) == 0` return. Advance `c.MoveT += dt / TileTime`. While `c.MoveT >= 1 && len(c.Path) > 0`: snap `(c.TileX, c.TileY) = c.Path[0]`, `c.Path = c.Path[1:]`, `c.MoveT -= 1`. If `len(c.Path) == 0`, force `c.MoveT = 0`. + - Invariants: `crew.go` does not import Ebitengine; `TileX/TileY` only mutated on waypoint crossing. + +- Commit: `crew-movement: crew struct, starting roster, tick` + +### Phase 3 — Input and rendering integration + +- **3.1** `render.go` (modify — append, no changes to existing code) + - Add `const CrewRadius = 6` (12 px diameter at `TilePx=16`). + - Add `func drawCrew(screen *ebiten.Image, c Crew, selected bool)`: + - Compute interpolated pixel centre: if `len(c.Path) > 0`, `(cx, cy) = lerp((TileX,TileY), Path[0], MoveT)` in tile units; else `(TileX, TileY)`. Multiply by `TilePx`, add `TilePx/2` for tile centre. + - `vector.DrawFilledCircle(screen, px, py, CrewRadius, c.Color, true)` (antialiased so placeholder doesn't look too harsh; everything else stays non-AA). + - If `selected`, draw a 1-px white ring: `vector.StrokeCircle(screen, px, py, CrewRadius+1, 1, white, true)`. + - Label: `ebitenutil.DebugPrintAt(screen, string(c.Initial), int(px)-2, int(py)-5)`. + - Invariant: reads `Crew`, does not mutate; uses only virtual coords through `TilePx`. + +- **3.2** `main.go` (modify) + - Add imports: `time`. + - Extend `Game`: `walk map[[2]int]bool`, `crew []Crew`, `selectedCrew int`, `lastTime time.Time`. + - `main()`: after `NewPlayerShip()`, compute `walk := walkableTiles(ship)`, `crew := NewStartingCrew()`; initialise `Game{ship, walk, crew, -1, time.Time{}}`. + - `Update()`: + - First call: `lastTime = time.Now()`, return nil. + - Else: `dt := time.Since(g.lastTime).Seconds(); g.lastTime = time.Now()`. Clamp `dt` to a sane ceiling (e.g. `0.1`) to avoid huge jumps on resume. + - For each crew: `updateCrew(&g.crew[i], dt)`. + - Poll left-click: `if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft)` (new import `inpututil`) — read `cx, cy := ebiten.CursorPosition()` (already in virtual coords). Derive tile `tx, ty := cx/TilePx, cy/TilePx`. + - Selection: iterate crew; if cursor is within `CrewRadius` of crew centre (tile-space check is sufficient: same tile or adjacent-and-close), set `g.selectedCrew = i` and return. + - Otherwise, if `g.selectedCrew >= 0` and `g.walk[[2]int{tx,ty}]`: issue move — `from := [2]int{crew.TileX, crew.TileY}; crew.Path = bfsPath(g.walk, from, [2]int{tx,ty}); crew.MoveT = 0`. + - `Draw()`: after `drawShip`, iterate crew and call `drawCrew(screen, c, i == g.selectedCrew)`. + - Invariants: virtual-coord drawing only; single codebase WASM-safe (`time.Since` and `inpututil` both work under `GOOS=js`). + +- Commit: `crew-movement: wire input, tick, and render` + +### Test strategy + +Per task `TDD: yes` — tests from Phase 1 and Phase 2 written first, must fail on the pre-implementation tree, then green after the paired source file. Phase 3 has no automated tests (input and rendering need real mouse/display); acceptance is manual via `go run .` and `make play-web`: +- Click each crew in turn: ring appears, persists until another crew or empty click. +- Click a destination tile on a selected crew: crew walks there tile-by-tile, ring follows them. +- Click an unreachable tile (e.g. outside ship): no-op. +- Clicking a new destination on an already-moving crew: path replaces, no crash. + +### Open questions / risks + +- `inpututil` is a standard Ebitengine subpackage that works under WASM; no conditional imports needed. If it doesn't, Phase 3 falls back to `ebiten.IsMouseButtonPressed` + manual edge detection. +- Mouse coords in the browser: `ebiten.CursorPosition()` is documented to return logical-space coords via `Layout`; this is how WASM-served Ebitengine games handle input. If verify finds the browser returns raw canvas pixels, the fix is a single `(windowW, windowH) := ebiten.WindowSize()` scale step. diff --git a/docs/tasks/summary-crew-movement-resume.md b/docs/tasks/summary-crew-movement-resume.md new file mode 100644 index 0000000000000000000000000000000000000000..ca1b1124ff52542978777b37ac0d682f22805494 --- /dev/null +++ b/docs/tasks/summary-crew-movement-resume.md @@ -0,0 +1,48 @@ +# summary-crew-movement-resume + +Handoff summary written 2026-04-22. Resumes the `crew-movement` milestone after the plan stage. + +## Goal + +Building `ftl-shape`, an FTL-like game in Go + Ebitengine (virtual resolution 640×360 at a 1280×720 window) that compiles to native desktop and browser WASM. Vertical slice scope: one ship, one combat encounter, pixel-art placeholders. + +## Current state + +- `skeleton` milestone **complete and shipped** through the full `up:` workflow (Design → Plan → Execute → Verify → Review). Merge-ready, no findings at confidence ≥80. Native and WASM both verified rendering the 5-room placeholder ship. +- `crew-movement` milestone **in progress**: Design committed at `aa11b70`; Plan written but **uncommitted** in `docs/tasks/crew-movement.md` (git status: `M`). Waiting on user approval before `up:uexecute`. +- 6 commits on `main`; no remote. +- Project memory: `~/.claude/projects/-Users-blikh-data-home-ftl-shape/memory/project_ftl_shape.md`. + +## Infrastructure / Environment + +Purely local. Go 1.26.2 at `/opt/homebrew/Cellar/go/1.26.2/libexec`. No remote work, no running background processes, no tmux. + +## Active blocker + +None. Workflow is paused at "plan presented, awaiting approval". Resume with execute. + +## Key files + +- `docs/tasks/skeleton.md` — done; Design / Plan / Verify / Conclusion all filled. +- `docs/tasks/crew-movement.md` — Design + Plan present; Plan uncommitted. +- `main.go`, `ship.go`, `render.go` — current skeleton code. +- `Makefile` — targets `run`, `build`, `build-wasm`, `serve`, `play-web`. +- `web/index.html`, `.gitignore` — WASM shell and ignore rules. + +## What to do next + +1. `git -C /Users/blikh/data/home/ftl-shape diff docs/tasks/crew-movement.md` → review the Plan section. +2. On approval: `git -C /Users/blikh/data/home/ftl-shape add docs/tasks/crew-movement.md && git commit -m "docs(crew-movement): plan"`. +3. Invoke `up:uexecute`. The plan dispatches 3 phases: tiles + BFS (TDD) → crew struct + tick (TDD) → input & render integration. +4. **Model**: let `up:implementer` and `up:reviewer` use their default (Sonnet 4.6 — what those agents are designed for). If dispatch errors with `"model ... may not exist or you may not have access to it"`, fall back to `model: "opus"`. That fallback worked every time this session after one transient Sonnet failure; Sonnet is still worth trying first. +5. After execute: `up:uverify` → `up:ureview`. +6. Remaining milestones designed but not yet started: `systems-power`, `combat`, `polish`. + +## Gotchas + +- Subagent model: try Sonnet default first; fall back to `model: "opus"` only if dispatch errors with model-access failure. See step 4. +- Go ≥1.24 moved `wasm_exec.js` to `$(go env GOROOT)/lib/wasm/wasm_exec.js`; the Makefile uses the new path. Older toolchains will fail loudly (acceptable per Design's fail-fast principle). +- `go build ./...` from the repo root leaves a bare `./ftl-shape` binary. `.gitignore` already covers it along with `bin/`, `web/main.wasm`, `web/wasm_exec.js`. +- Crew-movement plan uses `inpututil.IsMouseButtonJustPressed` + `ebiten.CursorPosition()`. Ebitengine docs state `CursorPosition` returns logical (post-`Layout`) coords, so no manual window → virtual scaling should be needed. Flagged as a browser verify-time smoke in the plan's "Open questions / risks" section — if it misbehaves, scale via `ebiten.WindowSize()`. +- Reviewer noted during skeleton review that Design prose mentions `text/v2` for labels while code uses `ebitenutil.DebugPrintAt`; sub-threshold, deferred as future polish. Crew labels will follow the same convention for consistency. +- The `skeleton` Verify flow includes a live Chrome smoke via the `mcp__chrome-devtools__*` tools plus `python3 -m http.server 8765` in `web/`. Same pattern works for verifying crew-movement's browser behaviour.