ask-handoff

Duck Hunt Fire v0 — arma + mira

slug duck-hunt-fire-2026-11-02-gun-mira project duck-hunt-fire source cursor updated 11/2/2026, 11:45:32 PM

Duck Hunt Fire v0 — Implementation Plan

Goal: Phone gun APK aims a crosshair on Fire Stick TV APK over LAN WebSocket; static targets + hit flash; Stick serves /gun.apk. Task: TICKET-duck-hunt-v0 Handoff: none Spec: ask-handoff plan slug duck-hunt-fire-2026-11-02-gun-mira-design Architecture: Multi-module Android repo. :shared holds JSON protocol + aim math (JVM unit tests). :tv Leanback app embeds Ktor HTTP+WS, Canvas game, NSD advertise, serves gun APK. :gun phone app uses SensorManager + NSD discovery + WS client + fire button. Tech: Kotlin, AGP, Ktor CIO (tv), Android Canvas, NSD/_duckhunt._tcp, minSdk tv 28 / gun 26, compileSdk 34. Test layout: JUnit 4/5 on JVM for :shared (shared/src/test/java/.../*Test.kt). Android instrumented tests deferred. Verify: ./gradlew :shared:test. Global Constraints:

  • v0 = gun + crosshair + static targets only (no ducks/score/rounds)
  • LAN WebSocket only; no Bluetooth; no internet required after install
  • Stick is runtime host; Mac = build + adb only
  • One gun client; reject extra connections
  • TDD on every task (red → green)
  • Execute via execute (inline only)
  • Final review via review (designated reviewer model, mandatory)

Task 1: Repo scaffold + shared protocol

Files:

  • Create: settings.gradle.kts, build.gradle.kts, gradle.properties, shared/build.gradle.kts, shared/src/main/java/.../Protocol.kt, shared/src/main/java/.../AimMath.kt, shared/src/test/java/.../ProtocolTest.kt, shared/src/test/java/.../AimMathTest.kt, README.md
  • Test: shared/src/test/java/.../ProtocolTest.kt, AimMathTest.kt

Interfaces:

  • Produces: AimMessage, FireMessage, HelloMessage parse/serialize; AimMath.orientationToNormalized(yaw, pitch, cal) -> Pair<Float,Float> clamped 0..1

Acceptance:

  • ./gradlew :shared:test green
  • JSON round-trip for aim / fire / hello

TDD:

  • Red: failing Protocol/AimMath tests
  • Green: minimal impl
  • Verify: ./gradlew :shared:test
  • Expected: BUILD SUCCESSFUL, all tests pass

Risk: mech

Task 2: TV app — Ktor host + connect UI

Files:

  • Create: tv/build.gradle.kts, tv/src/main/AndroidManifest.xml, tv/src/main/java/.../TvActivity.kt, tv/src/main/java/.../GameServer.kt, tv/src/main/java/.../NsdAdvertiser.kt, tv/src/main/res/...
  • Test: shared tests still green; manual: server binds port 8787
  • Also: tv/src/test/java/.../GameServerParseTest.kt if parse helpers extracted to testable JVM; else keep protocol tests in shared only and add tv/src/test for pure Kotlin helpers

Interfaces:

  • Consumes: shared protocol
  • Produces: HTTP GET / health, /gun.apk (404 until Task 5 embeds APK), WS /ws; NSD _duckhunt._tcp name DuckHunt; connect UI shows IP:8787

Acceptance:

  • TV APK builds; on launch shows IP + port
  • WS accepts one hello from gun role; second connection rejected
  • ./gradlew :shared:test still green

TDD:

  • Red/Green: any new pure helpers tested under tv/src/test or shared
  • Verify: ./gradlew :shared:test :tv:test :tv:assembleDebug
  • Expected: assemble success

Risk: integ

Task 3: TV Canvas — crosshair + static targets + fire hit-test

Files:

  • Create: tv/src/main/java/.../GameView.kt, tv/src/main/java/.../HitTester.kt
  • Test: tv/src/test/java/.../HitTesterTest.kt (or shared)

Interfaces:

  • Consumes: latest aim x,y; fire events
  • Produces: draw targets + crosshair; HitTester.hit(x,y,targets) -> Boolean; flash hit/miss

Acceptance:

  • Aim updates move crosshair; fire near target = hit flash
  • HitTester unit tests cover inside/outside circle

TDD:

  • Red: HitTester tests fail
  • Green: impl + wire GameView to WS aim/fire
  • Verify: ./gradlew :tv:test :tv:assembleDebug
  • Expected: tests pass, APK builds

Risk: mech

Task 4: Gun app — sensors, discovery, WS, fire UI

Files:

  • Create: gun/build.gradle.kts, gun/src/main/AndroidManifest.xml, gun/src/main/java/.../GunActivity.kt, gun/src/main/java/.../SensorAim.kt, gun/src/main/java/.../NsdFinder.kt, gun/src/main/java/.../GunClient.kt, layouts
  • Test: gun/src/test/java/.../SensorAimMapTest.kt for mapping/clamp using shared AimMath

Interfaces:

  • Consumes: shared AimMath + protocol; NSD _duckhunt._tcp
  • Produces: aim 30–60 Hz; fire on button; calibrate button; status text; manual IP fallback

Acceptance:

  • Gun APK builds; connects to TV WS; sends aim/fire
  • Calibrate centers current pose to 0.5,0.5

TDD:

  • Red/Green: mapping tests
  • Verify: ./gradlew :gun:test :gun:assembleDebug
  • Expected: BUILD SUCCESSFUL

Risk: integ

Task 5: Wire install link — TV serves gun APK + QR/URL

Files:

  • Modify: tv/.../GameServer.kt, connect UI; Gradle copy task gun release/debug APK → tv/src/main/assets/gun.apk (or build-time copy into assets)
  • Test: document manual verify; optional JVM test that asset path constant exists

Interfaces:

  • Produces: GET /gun.apk returns APK bytes; UI shows http://<ip>:8787/gun.apk + QR (ZXing or simple QR lib)

Acceptance:

  • From phone browser on LAN, download installs gun APK
  • End-to-end: tilt moves crosshair; fire hits static targets; Mac not needed at runtime

TDD:

  • Red/Green: keep unit tests green; E2E manual on Stick + phone
  • Verify: ./gradlew :gun:assembleDebug :tv:assembleDebug then adb install both
  • Expected: E2E acceptance from spec

Risk: risky