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,HelloMessageparse/serialize;AimMath.orientationToNormalized(yaw, pitch, cal) -> Pair<Float,Float>clamped 0..1
Acceptance:
./gradlew :shared:testgreen- 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:
sharedtests still green; manual: server binds port 8787 - Also:
tv/src/test/java/.../GameServerParseTest.ktif parse helpers extracted to testable JVM; else keep protocol tests in shared only and addtv/src/testfor pure Kotlin helpers
Interfaces:
- Consumes: shared protocol
- Produces: HTTP GET
/health,/gun.apk(404 until Task 5 embeds APK), WS/ws; NSD_duckhunt._tcpnameDuckHunt; connect UI shows IP:8787
Acceptance:
- TV APK builds; on launch shows IP + port
- WS accepts one
hellofrom gun role; second connection rejected ./gradlew :shared:teststill green
TDD:
- Red/Green: any new pure helpers tested under
tv/src/testor 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.ktfor 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 taskgunrelease/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.apkreturns APK bytes; UI showshttp://<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:assembleDebugthenadb installboth - Expected: E2E acceptance from spec
Risk: risky