Skip to content

Only learn the hoops shot from flights that actually show it - #7

Merged
averagenative merged 3 commits into
mainfrom
hoops-calibration-screens
Sep 1, 2026
Merged

averagenative merged 3 commits into
mainfrom
hoops-calibration-screens

Conversation

@averagenative

Copy link
Copy Markdown
Owner

The README carried this as an open question: per-shot curvature ranging 1.71–3.01, a ±30% swing from shots taken the same way, and "either the shot genuinely varies or the single-shot fit is noisy".

It is the fit. Fitting x(t) and y(t) separately across 15 flights read out of the running game over the DevTools protocol — which needs no release instant, and cannot degenerate the way y as a function of x does — puts the horizontal release speed at 536–541 px/s on every well-tracked flight. The shot is the same shot to half a percent. The fixed three-parameter model was right; what was wrong is what got fed into it.

Two things were being learned from that shouldn't have been:

Short tracks. fitXY returns a curve from 40px of horizontal spread, and curvature error goes as 1/spread², so a fit over a short arc is a guess wearing a number. The three wildest calibrations in the sample — 2.941, 1.865, 2.716 against a true 2.23 — are exactly the three shortest tracks, spans of 200, 143 and 140px.

Bounces. Coming off the backboard or the far lip of the rim are both ordinary ways to score, and both send the ball back over x it has already crossed. The curve is fitted as y of x, so that isn't a hard fit but an impossible one — two y for one x. This is where curvature reached 55. A projectile's x is monotonic, so a retreat from the furthest point reached detects it outright.

Span gate swept over those flights:

gate commits spread + bounce cut
none (shipped) 15 47.4% (1.865–2.941)
0.15 W 10 8.2% 4.3%
0.20 W 10 7.7% 4.2%
0.25 W 10 7.2% 4.0%
0.30 W 10 5.1%
0.35 W 7 2.0%
0.40 W 2 0.9%

Commits hold flat from .15 to .30 and fall off a cliff above, so .20 is the middle of the plateau rather than an edge. 47.4% → 4.2%, for two commits out of ten — and a commit is cheap because the calibration averages. The 40px floor stays inside fitXY so the live arc still draws early in a flight; only learning waits.

calVer 6, because anything learned under the old policy is contaminated rather than stale (a live config caught mid-session held 2.486) and averaging more shots into it doesn't wash it out. The seed moves to the medians of the 8 flights passing both screens: A 2.233 (was 2.103), L −0.119 (was −0.179), R 0.547 (was 0.557 — which the old five-shot seed already had right). R is also the only one of the three the tracked points actually cover; the upward crossing sits behind where tracking starts, so every estimate of it is an extrapolation, and its spread across shots is a quarter of its value. Left flagged as unsettled rather than claimed as fixed.

node --check passes on both files; suite regenerated from the standalone script (hoops 1.8 → 1.9, suite 1.6 → 1.7). Shipped build injected into a live session loads and runs clean.

Not yet observed end-to-end: the gating is verified by replaying the live rule over 15 recorded trajectories, but I haven't watched a fresh session commit under the new code and confirm the values stay in 2.19–2.29.

The README carried this as an open question: per-shot curvature ranging
1.71-3.01, a +/-30% swing from shots taken the same way, and "either the
shot genuinely varies or the single-shot fit is noisy". It is the fit.

Fitting x(t) and y(t) separately across 15 flights read out of the
running game over the DevTools protocol -- which needs no release
instant, and cannot degenerate the way y-as-a-function-of-x does -- puts
the horizontal release speed at 536-541 px/s on every well-tracked
flight. The shot is the same shot to half a percent. The fixed
three-parameter model was right all along; what was wrong is what got
fed into it.

Two things were being learned from that should never have been:

SHORT TRACKS. fitXY returns a curve from 40px of horizontal spread, and
curvature error goes as 1/spread^2, so a fit over a short arc is a guess
wearing a number. The three wildest calibrations in the sample -- 2.941,
1.865 and 2.716 against a true 2.23 -- are exactly the three shortest
tracks, spans of 200, 143 and 140px.

BOUNCES. Coming off the backboard or the far lip of the rim are both
ordinary ways to score, and both send the ball back over x it has
already crossed. The curve is fitted as y of x, so that is not a hard
fit but an impossible one, two y for one x. This is where curvature
reached 55. A projectile's x is monotonic, so a retreat from the
furthest point reached detects it outright; the margin is 1% of width
because the tracker's own x jitter measured 3-7px rms.

Sweeping the span gate over those flights, as commits kept and spread of
the committed curvature:

  none (shipped)  15 commits  47.4%   1.865-2.941
  0.15 W          10 commits   8.2%   + bounce cut  4.3%
  0.20 W          10 commits   7.7%   + bounce cut  4.2%
  0.25 W          10 commits   7.2%   + bounce cut  4.0%
  0.30 W          10 commits   5.1%
  0.35 W           7 commits   2.0%
  0.40 W           2 commits   0.9%

Commits hold flat from .15 to .30 and fall off a cliff above, so .20 is
the middle of the plateau rather than an edge. 47.4% to 4.2%, for two
commits out of ten, and a commit is cheap because the calibration
averages. The 40px floor stays in fitXY so the live arc still draws
early in a flight; only learning waits.

calVer 6, because anything learned under the old policy is contaminated
rather than stale -- a live config caught mid-session held 2.486 -- and
averaging more shots into it does not wash it out. The seed moves to the
medians of the 8 flights that pass both screens: A 2.233 (was 2.103, 6%
low), L -0.119 (was -0.179), R 0.547 (was 0.557, which the old five-shot
seed already had right). R is also the only one of the three the tracked
points actually cover; the upward crossing sits behind where tracking
starts, so every estimate of it is an extrapolation and its spread
across shots is a quarter of its value. Noted as unsettled rather than
fixed.
The anchor is stable and precisely detected -- width 177px in 2565 of
2566 logged frames, x never observed to move at all -- and it is still
the biggest error left, because anchoring to it does not produce a
shot-invariant frame the way the model assumes.

shotL and shotR describe the shot, so they should not care where the
platform is. Measured over two independent runs off the live game:

              corr(platY, shotL)   corr(platY, shotR)
  8 flights         -0.79                +0.71
  5 flights         -0.86                +0.77

Curvature barely moves, which is the tell. shotA is set by g/2vx^2 and
neither cares how high the platform is, while shotL and shotR are where
the arc meets platform HEIGHT. That is half the variance in the two
weakest numbers, ~40px each, the same order as the leave-one-out landing
error -- so the anchor is the next thing to fix, not the fitting.

Three explanations measured, none survived, all written down so the next
person does not spend the afternoon re-testing them:

  - Stale anchor. flightPlat is sampled a frame or two after release and
    the platform IS moving then (26 of 32 releases, ~135px/s, biased
    upward). But recomputing L and R against the platform at release+dt
    across -200..+400ms produces no minimum; the spread falls
    monotonically and is still falling at -200ms, before release.
  - The detector picking a different row as the platform moves. The
    detected width was 177px in 2565 of 2566 frames.
  - The ball inheriting the platform's velocity. platV correlates worse
    than platY, -0.68 against -0.86 for L, though on an oscillating
    platform the two are confounded and five flights cannot separate
    them.

Also recorded: the anchor y snaps to a 5.5px grid because findPlatform
runs on the downscaled readback, which works out at ~4% of the observed
spread and is not worth chasing; and the platform has never been seen to
move horizontally, so that half of the anchor is untested and first
matters when the platform starts looping.

No behaviour change. Numbers only.
The last commit called the platform anchor "the biggest error left" on
the strength of shotL and shotR correlating with platform height. That
correlation is real and replicates, but the conclusion drawn from it was
wrong, and it would have sent the next person down a dead end.

Modelling shotL as linear in platform height does predict it much better
out of sample -- leave-one-out mean error 51.6px to 29.6px, a 43% gain,
with shotR going 40.8 to 37.7px. So the correction works on the
parameters. But the number that decides a make is the height of the arc
where it crosses the RIM, and measured there:

  constant, as shipped        mean 54.7px   worst 117.5px
  linear in platform height   mean 53.0px   worst 113.9px

Three percent. The errors in A, L and R are correlated and very nearly
cancel by the time the curve reaches the rim, so a correction that
plainly improves two of three parameters is worth almost nothing where
it counts. Not shipped: an unexplained empirical correction fitted on 11
flights from one player has to clear more than 3%.

What this does establish is where the ceiling actually is. ~55px of arc
height at the rim, and it is per-shot noise in the two crossings, not
the anchor. Averaging across shots is what removes it, which is what the
commit weighting already does -- so the useful advice is the one the
README already gives: do not reset between shots.
@averagenative
averagenative merged commit 5e4a74a into main Sep 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant