~bigbes/game-prototype-ftl

b4322f8a40b68082c7ecc92baa53c0876632c96d — Eugene Blikh a month ago
chore: initialize project and task design
2 files changed, 120 insertions(+), 0 deletions(-)

A docs/tasks/skeleton.md
A go.mod
A  => docs/tasks/skeleton.md +117 -0
@@ 1,117 @@
# skeleton

First milestone of the `ftl-shape` vertical slice. Establishes the Go + Ebitengine pipeline and a placeholder ship render that later milestones (crew-movement, systems-power, combat, polish) build on.

## Design

**Purpose.** Prove the Go + Ebitengine pipeline builds and runs on both desktop (macOS/Linux/Windows) and in-browser WASM, rendering a static placeholder player ship with 5 rooms in a ship-like layout. No interactivity. Foundation for every subsequent milestone.

**In scope.** Go module, Ebitengine main loop, virtual-resolution rendering, room data model, rectangle-based room rendering with labels, Makefile for native + WASM builds, minimal `web/index.html` shell, dev server via `wasmserve`, one-command "run in browser" target.

**Out of scope.** Input, crew, animation, enemy ship, sound, combat, menus, save system, disk-loaded assets, WASM size optimization.

**Chosen approach.**

- **Rendering.** Virtual resolution 640×360; window opens at 1280×720 (2× scale). Ebitengine's `Layout` returns 640×360; all drawing in virtual pixels. Resizable window is fine — Ebitengine handles scaling.
- **Project layout.** Single package at module root (`sourcecraft.dev/bigbes/ftl-shape`). Files by concern:
  - `main.go` — entrypoint, `Game` struct, `Update`/`Draw`/`Layout` methods.
  - `ship.go` — `Ship`, `Room` types, placeholder ship constructor.
  - `render.go` — drawing helpers (rect fill, border, label).
- **Ship model.** `Ship{Rooms []Room}` and `Room{ID, Name, GridX, GridY, GridW, GridH, Role}` in tile units. Render layer converts tile→virtual pixels at 16 px/tile.
- **Placeholder ship content.** 5 rooms — Pilot (front/left), Weapons (mid-upper), Shields (mid-center), MedBay (mid-lower), Engines (rear/right). Each rendered as a role-tinted filled rectangle + 1-px border + role label via Ebitengine's default text face from `text/v2`.
- **Build tooling — `Makefile` targets.**
  - `run` — native, `go run .`.
  - `build` — native binary to `bin/ftl-shape`.
  - `build-wasm` — produce `web/main.wasm`; copy `wasm_exec.js` from the installed Go toolchain (`$(go env GOROOT)/lib/wasm/wasm_exec.js` on Go ≥1.24).
  - `play-web` — one-command "run in browser": start `wasmserve` (auto-rebuilds on source change), print `http://localhost:8080`, and auto-open the browser (`open` on macOS, `xdg-open` on Linux, print-only otherwise).
  - `serve` — same as `play-web` without the auto-open step.
- **Browser shell.** `web/index.html` loads `wasm_exec.js` + `main.wasm` using the standard Go WASM snippet. `.gitignore` excludes `bin/` and `web/main.wasm`.

**Tradeoffs that settled the decisions.**
- Grid-based rooms over freeform pixel coords: aligns with FTL's movement model; simplifies future click-to-move milestone.
- Single package over `cmd/ + internal/`: small surface area, minimum friction. Split when a file crosses ~300 lines or a real subsystem boundary emerges.
- Virtual resolution over native drawing: crisp pixel art at any window size; resizable-safe. Cost is a single `Layout` decision — negligible.

**Unknowns.** WASM binary size for Ebitengine builds is typically 10–20 MB. Acceptable for dev; optimization is out of scope.

**TDD: no** — skeleton is a visual + build-pipeline proof. `Ship`/`Room` are trivial data structs with no logic to assert; success is "window opens, rectangles appear, WASM loads in browser." TDD applies at the `crew-movement` milestone where pathfinding has real behavior.

### Invariants

- All drawing uses virtual-resolution coordinates (640×360). No code draws in window-pixel units.
- Single codebase builds unchanged for `GOOS=js GOARCH=wasm` and native targets; no platform-conditional imports (`//go:build`, `GOOS`-suffixed files) in this milestone.
- Room positions and sizes are stored in tile units on `Room`. Pixel conversion happens only at render time.
- No external asset files in milestone 1 — all placeholder art is code-drawn (filled rects + default text face).
- `ship.go` does not import Ebitengine. Data types know nothing about rendering.

### Principles

- YAGNI: no sub-packages, no interfaces, no ECS until a file crosses ~300 lines or a clear boundary emerges.
- Fail fast on init errors (WASM bootstrap, font load). Panic is acceptable; no silent fallbacks.
- Deterministic rendering: same ship state → same pixels. No hidden randomness in this milestone.
- Separate data from rendering: ship/room types never call the renderer and never hold Ebitengine state.

## Plan

Approach: two phases — first gets a native window rendering the placeholder ship, second adds the WASM build and the one-command browser target. Each phase is a self-contained runnable checkpoint.

Backwards-compat: greenfield project, nothing to break.

### Phase 1 — Native app rendering the placeholder ship

- **1.1** `go.mod` (modify)
  - Add dep `github.com/hajimehoshi/ebiten/v2`; run `go mod tidy`.

- **1.2** `ship.go` (create)
  - Package `main`.
  - `type RoomRole int` with constants `RolePilot, RoleWeapons, RoleShields, RoleMedBay, RoleEngines`.
  - `type Room struct { ID int; Name string; GridX, GridY, GridW, GridH int; Role RoomRole }` — tile-unit coords only.
  - `type Ship struct { Rooms []Room }`.
  - `func NewPlayerShip() Ship` — hard-coded 5-room layout: Pilot front-left, Weapons mid-upper, Shields mid-center, MedBay mid-lower, Engines rear-right.
  - Invariants: **no Ebitengine imports in this file**; positions and sizes in tile units only.

- **1.3** `render.go` (create)
  - Package `main`. Constants `TilePx = 16`, `VirtualW = 640`, `VirtualH = 360`.
  - `func roleColor(role RoomRole) color.RGBA` — role → tint mapping (mapping lives here, not in `ship.go`).
  - `func drawShip(screen *ebiten.Image, s Ship)` — iterates rooms, delegates to `drawRoom`.
  - `func drawRoom(screen *ebiten.Image, r Room)` — fills role-tinted rect, draws 1-px border, writes role name via `ebitenutil.DebugPrintAt` positioned inside the room.
  - Invariant: all coordinate math converts tile → virtual pixels through `TilePx`; no window-pixel math.

- **1.4** `main.go` (create)
  - Package `main`. `type Game struct { ship Ship }`.
  - `func (g *Game) Update() error` — returns nil.
  - `func (g *Game) Draw(screen *ebiten.Image)` — fills dark background, calls `drawShip(screen, g.ship)`.
  - `func (g *Game) Layout(int, int) (int, int)` — returns `VirtualW, VirtualH`.
  - `func main()` — `ebiten.SetWindowSize(1280, 720)`, `ebiten.SetWindowTitle("ftl-shape")`, `ebiten.RunGame(&Game{ship: NewPlayerShip()})`, panic on error.
  - Invariant: no platform-conditional imports; single build source for native + WASM.

- Commit: `scaffold game loop and placeholder ship render`

### Phase 2 — WASM build and browser run target

- **2.1** `Makefile` (create)
  - Targets: `run`, `build`, `build-wasm`, `serve`, `play-web`, `.PHONY` line.
  - `build`: `go build -o bin/ftl-shape .`.
  - `build-wasm`: `GOOS=js GOARCH=wasm go build -o web/main.wasm .` then `cp "$$(go env GOROOT)/lib/wasm/wasm_exec.js" web/wasm_exec.js` (Go ≥1.24 path).
  - `serve`: `go run github.com/hajimehoshi/wasmserve@latest .` — wasmserve rebuilds wasm on source change and serves on `:8080`.
  - `play-web`: detect OS (`uname`), background-delay `open`/`xdg-open http://localhost:8080`, then invoke the same wasmserve command.

- **2.2** `web/index.html` (create)
  - ~20-line WASM loader: `<script src="wasm_exec.js">`, instantiate `new Go()`, `WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then(r => go.run(r.instance))`. Canvas set to 1280×720.
  - Used by any static-serve path; wasmserve generates its own index when running the dev loop, so this file is for future release hosting and not on the `play-web` critical path.

- **2.3** `.gitignore` (create)
  - Exclude `bin/`, `web/main.wasm`, `web/wasm_exec.js`.

- Commit: `add Makefile, WASM loader shell, play-web target`

### Test strategy

No automated tests (per `TDD: no`). Manual acceptance:
- Phase 1: `go run .` → window opens showing 5 labeled, colored rooms on a dark background.
- Phase 2: `make play-web` → browser tab at `:8080` shows identical render; `make build` produces a runnable `bin/ftl-shape`.

### Open questions / risks

- `wasm_exec.js` at `$GOROOT/lib/wasm/` — confirmed present for Go 1.26.2 (user's toolchain). Would fail loudly on Go <1.24; acceptable per design's fail-fast principle.
- WASM binary size (~10–20 MB) — known and out of scope per Design.

A  => go.mod +3 -0
@@ 1,3 @@
module sourcecraft.dev/bigbes/ftl-shape

go 1.26.2