idleon-automation

Hoops findings — the June 2026 miss investigation (#37)

Settled conclusions from the 2026-06-09/10 investigation into why the hoops bot’s make rate was low (~25%) and inconsistent. Kept here so the “why” survives outside the GitHub thread; the per-session evidence lives in issue #37’s comments. Follow-up model work: issue #38.

The headline reframe: misses are not aim error

Shots whose ball arrives within ±120px of the hoop make at ~64% in the make_prob model-aim regime (model in-band ~58%); the pooled in-band rate across all eras and aim sources is only ~34% (n≈1.2k), because explore and legacy shots arrive in-band and still clank. Arrival proximity is necessary but not sufficient — the predictor is the lever, not the ±120px window. Clean makes cluster within a ~30px standard deviation (the broader made population is wider, scattered by lucky bounce-ins). The bot does not lose to near-misses — it loses to two categorically different failure modes that both masqueraded as “way short” in the data:

Velocity controls the launch — through arc shape, not just range

The ball inherits the platform’s vertical velocity at click time. platform_vy (least-squares slope over the ~1.5s of platform samples before the click; positive = downward) orders same-hoop outcomes monotonically. Notes on interpretation:

The main-loop sample cadence is ~200–1000ms per tick (not the 15–30ms the POLL_INTERVAL comment suggests) — anything derived from the sample buffer must tolerate that.

Direction is a per-hoop policy

_required_direction_for(hoop_y, hoop_x); one predictor fitted per direction (training rows are direction-filtered).

Make detection is multi-signal; OCR alone loses ~10% of makes

Post-shot score OCR drops out often enough that two safety nets exist:

Perturbation sweep: small, capped, hold-aware, cycling

Free-shot exploration

While the start prompt is up, misses cost no lives. The first shot at a fresh hoop stays on predictor aim; after it misses, targets are sampled uniformly across the observed bob range (target_source='explore'). One 56-shot grind produced a complete dir=up landing map of a hoop — the single most informative session of the investigation.

Training-data hygiene

Trajectory predictors train on bounce-aware arrival, not raw landing (fetch_clean_trajectories): floor-bounced shorts keep their row with peak_x as the honest reach; hoop clanks are dropped as reach-censored. The legacy launcher-distance filter both discarded the short-mislaunch population (the velocity signal) and admitted bogus post-bounce landings.

Schema columns added during the investigation: platform_vy, made_source, prompt_up, target_source.

Outcome

The measurement gate passed on 2026-06-10: 132 post-policy attempts (exploration excluded), 46 makes — 34.8%, Wilson 95% CI [0.273, 0.433], lower bound clear of the 25.5% baseline. #37 closed with that verdict.

Open at time of writing

Post-investigation addendum (2026-06-10): fire-latency regression

Every shot from 2026-05-09 (cc42529) to 2026-06-10 fired ~100ms after its fire decision: the start-prompt template match (104ms, full-frame) sat between the crossing detector and the click — re-introduced five days after the original click-latency fix. Launch y therefore trailed the sampled platform_y by roughly platform_vy * 0.1s (tens of px on fast bobs), which is the same mechanism as the velocity-driven over/undershoots documented above and inflated their magnitude during the whole #37 window. Fixed by computing the prompt match after the click from the already-held frame (identical result, zero latency). Interpretation caveats: rows before/after the fix embed systematically different decision→launch latencies, so velocity-coupled effects fitted on pre-fix data (and the predictors trained on it) will run slightly hot until post-fix sessions accumulate.

The back-rim notch (2026-06-26, #97)

The user observed the bot clanking the back of the rim — the ball reaches just past the hoop, bounces backward and falls out of the play area. Diagnosis against shots.db (n=840 clean shots) reframed it:

Fix (make_prob argmax byte-for-byte untouched):

  1. Asymmetric sweep gate (_sweep_should_advance): advance off a back-rim overshoot (residual > BACK_RIM_LO_PX=15) instead of the old symmetric |resid|≤60 hold, which re-fired back-rim clanks as “almost made” (304/604 in-band shots were back-rim clanks). Short/center side keeps the generous hold.
  2. Directional recovery nudge, shipped as a logged 50/50 A/B (back_rim_recovery column, arm by shot-index parity à la darts #48): after a _is_back_rim_bounceout (backward bounce AND peak 15..110px past center — direction-safe, excludes front-rim clanks needing more reach and far-bank banks that score), force the next shot’s perturbation to BACK_RIM_NUDGE_PX=−16. Validate nudge-vs-control on recovery-shot make rate / recurrence (Wilson CIs) before flipping the nudge to default; the #38 model-pick make rate must not regress.

Open: preventive first-shot avoidance via a candidate prefilter classifier (#98, the only route that can reach the deep front-side notch slice the nudge can’t safely touch); lives_diff is unusable so the cost asymmetry of a back-rim miss can’t be measured from the DB (#99) — the user confirmed in-game it’s just one miss, so #97 optimizes make-rate only.