Skip to content

Read the fishing gauge at native resolution, and correct the darts wind ratio - #9

Merged
averagenative merged 5 commits into
mainfrom
fishing-gauge-native-read
Sep 9, 2026
Merged

averagenative merged 5 commits into
mainfrom
fishing-gauge-native-read

Conversation

@averagenative

Copy link
Copy Markdown
Owner

Suite goes to 1.18, fishing to 2.4 (calVer 7), darts to 1.5.

Both fixes came out of the game's own shipped bundle rather than off more
footage, and both were checked against the existing recordings with
tools/replay.

Fishing: the gauge was read through the 4x downscale

The game quantises the cast gauge — it charges with power = 1 - |sin(theta)|,
theta stepping a degree a tick from 90, and sets the fill sprite's scaleY to
round(64 * power). The track and fill attach from one anchor at -87 and -23,
and 87 - 23 = 64, so a full fill reaches exactly the track's top edge and
fill/track really is the power.

readMeter was being handed the downscaled frame, which put the whole gauge at
~21 rows — a rung was under a third of a row, so the reading could not resolve
the game's own quantum. A one-row error was ~5% of the gauge, and with the aim
curve's slope near full charge at ~1.18 lane-fractions per unit of power it
arrived as ~6% of the lane. That is why long casts were far off while short
ones looked fine: the charge law is flattest at the bottom.

The gauge now gets its own native-resolution grab (~1/40th of the frame, only
once a lane exists), and its geometry is settled once and held rather than
re-derived every frame.

Replayed over five fishing clips: at the plateau the old code reads 0.727 —
16/22, the only vocabulary it had — where native reads 0.778. No clip reads
worse, one improves 84 -> 88 frames measured. Confirmed in live play.

Two things this turned up:

  • the column scan had to start bridging gaps. Its "longest unbroken run" test
    only ever passed because the downscale blurred the pole's texture and the
    fill/track seam into pixels that matched; at native resolution a strict run
    measured 16-32 rows against the 38 demanded, and every frame above about a
    third charge failed outright.
  • on the live canvas totalPx is 64 and never anything else, so a rung is
    exactly one pixel and readings land on exact rungs by construction. The
    recordings disagreed only because they captured the canvas at its CSS size
    (750 tall vs the game's 540), making a rung 1.4px — which H.264 destroys.
    The snap to the ladder is therefore computed and reported but left off: an
    identity where it would be safe, unreliable where it would have to earn its
    place.

Darts: the wind pushes sideways 1.25x harder than it pushes down

The wind is one vector, but the game does not push equally hard along both axes
with it — it builds 30*cos(phi) and 30*sin(phi) into two slots, then adds
the horizontal over 600 and the vertical over 750. predict() applied a
single coefficient to both.

windK was solved from the vertical acceleration difference between two wind
clusters, so 0.0158 is the /750 term and stays; the horizontal is the one that
was never independently confirmed, and it gets the 1.25.

This is a ratio inside one model, which is why it can be taken from the game at
all — vN and gN would need the tick rate and design resolution to convert,
so those measured values are untouched.

It should also account for the residual recorded against landN ("splits
+-20px WITH the wind sign"): a horizontal wind error changes how long the dart
takes to reach the board, so it lands at the wrong point on an otherwise correct
vertical curve, sign-flipping with the wind. landN is carrying some of that
and wants re-measuring on throws recorded after this.

Unverified against footage — both darts recordings replay identically before
and after, because neither has any detected cyan wind for the term to act on.
That confirms no regression, not the fix.

Note on the commit list

This branch also carries fd54875 ("Also record that the correlation behind it
is not stable"), a comment-only hoops commit. It was pushed to
hoops-anchor-note after that branch's PR (#8) had already merged, so nothing
else would ever carry it to main. It rides along here rather than staying
stranded.

Not included

The lane is untouched, and is now the larger of the two remaining errors.
stableLane still takes min(x0)/max(x1) over 1.5s — a monotone envelope that
can only grow — and the blue run is still the wrong reference for a cast the
game measures from the launch point.

🤖 Generated with Claude Code

averagenative and others added 4 commits September 1, 2026 19:00
Two more captures in, the platform-height coupling that motivates all of
this is itself unreliable. Across three captures it measures -0.13,
-0.46 and -0.82; within the two halves of the third, -0.81 and -0.96.
That is not one effect measured three times, it is what a correlation
looks like at n = 6 to 12.

Curvature is the one parameter the anchor cannot touch mathematically --
shotA is a*W straight off the fit, platform position never enters it --
so a coupling there is either real physics or estimator bias. The time
domain rules out the first: across those flights vx holds to 1.3%, and
neither vx nor fitted g tracks platform height, -0.11 and -0.14. The
physical curvature g/2vx^2 is constant, so the wobble is noise in medA.

A slope cap was tried on the strength of it -- refit y(x) using only the
part of the arc below some |dy/dx|, since the steep tail is where the
parameterisation degenerates and how much tail is tracked depends on how
high the platform was. On its own capture it looked excellent, 12.3%
down to 5.1%. It does not replicate: no effect on a second capture, and
on a third the single whole-segment fit underneath it blows up to 92%
spread, that fit being precisely what the gated median exists to avoid.
Not shipped.

What is stable is the thing already merged: the gated median's spread
runs 4-12% by session against 47% before the screens. Anything finer
needs more than a dozen flights per session to see, and every correction
derived from a dozen has failed on the next dozen.
The game quantises the cast gauge: _event_Minigames1 charges with
power = 1 - |sin(theta)|, theta stepping one degree a tick from 90, and
sets the fill sprite's scaleY to round(64 * power). So the fill has 65
heights and one rung is one game unit. The track and fill sprites attach
from one anchor at -87 and -23, and 87 - 23 = 64, so a full fill reaches
exactly the track's top edge and fill/track really is the power.

readMeter was being handed the 4x-downscaled frame. That put the whole
gauge at ~21 rows, so a rung was under a third of a row and the reading
could not resolve the game's own quantum at all. A one-row error was ~5%
of the gauge, and since the aim curve's slope near full charge is ~1.18
lane-fractions per unit of power, it arrived as ~6% of the lane -- four
times the error of the curve it was feeding. That is why long casts were
far off while short ones looked fine: the charge law is flattest at the
bottom, so the same row is worth very little near zero power.

The gauge now gets its own native-resolution grab, about a fortieth of
the frame and only once a lane exists. Replayed against five fishing
clips: at the plateau the old code reads 0.727 -- 16/22, the only
vocabulary it had -- where native reads 0.778. No clip reads worse than
before and one improves 84 -> 88 frames measured.

Two things this turned up. The column scan had to start bridging gaps:
its "longest unbroken run" test only ever passed because the downscale
blurred the pole's texture, the fill/track seam and the marker line into
pixels that matched. At native resolution those gaps are real, a strict
run measured 16-32 rows against the 38 demanded, and every frame above
about a third charge failed outright. And the gauge geometry is now
settled once and held rather than re-derived every frame -- a
denominator that drifts is a slow scale error on every prediction, not
noise the aim marker can average away.

Snapping to the 64-rung ladder is computed and reported but deliberately
not used. A rung is ~1.4px at this canvas size and a fill edge resolves
to maybe half a pixel, so snapping misassigns rungs often enough to give
back most of what it wins. 585 readings across the five clips sit no
closer to the rungs than random. Consistent with a real ladder buried in
measurement noise, which is what the pixel arithmetic predicts.

calVer 7: samples paired with a gauge read through the downscale carry
that error in the power axis, and refitting on them fits the error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wind is one vector, but the game does not push equally hard along
both axes with it. The minigame builds the wind as 30*cos(phi) and
30*sin(phi) into two slots, then each flight tick adds the horizontal
slot over 600 and the vertical slot over 750. Same vector, different
divisors, so the horizontal acceleration is 750/600 = 1.25x the
vertical. predict() was applying one coefficient to both.

Which axis moves is settled by how windK was measured: it was solved
from the vertical acceleration difference between two wind clusters, so
0.0158 is the /750 term and it stays. The horizontal is the one that was
never independently confirmed -- the per-throw x-fits scattered
+-300px/s^2 -- and it is the one that was wrong.

This is a ratio between two terms in one model, which is why it can be
taken from the game at all. vN and gN would need the game's tick rate
and design resolution to convert into these units, and neither is
recoverable from the bundle, so those measured values are left alone.

It should also account for the residual recorded against landN, "the
unexplained leftover splits +-20px WITH the wind sign". A horizontal
wind error does exactly that: it changes how long the dart takes to
reach the board, so the dart lands at the wrong point on an otherwise
correct vertical curve, and the error flips sign when the wind does.
landN was fitted with the horizontal term 20% light and is carrying some
of it, so it wants re-measuring on throws recorded after this.

Unverified against footage. Both darts recordings replay identically
before and after, because neither has any detected cyan wind for the
term to act on -- which confirms no regression, not the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The snap comment said this had to be answered on a live canvas rather
than a recording, and it now has been. Watched through tools/chrome over
a session of real casts, totalPx reads 64 and never anything else: the
backing store is the game's own resolution, so a rung is exactly one
pixel and fillPx/64 IS k/64 by construction. Fifteen distinct locked
charges all came back whole rungs -- 0.063, 0.094, 0.141, 0.156 through
0.813 -- with the largest departure the 0.05% that three decimal places
of printout account for on their own.

That also explains the replay result the previous commit recorded, where
585 readings sat no closer to the rungs than random at any spacing. The
recordings captured the canvas at its CSS size, 750 tall against the
game's 540, so a unit spanned 1.389px and the gauge measured the ~89px
we saw. A 1.4px rung, through H.264, against a fill edge the colour
masks resolve to about half a pixel, does not survive being measured.
The ladder was there and the capture destroyed it -- which was one of
the two explanations offered, now decided.

The snap stays off, and the pair of measurements makes the case better
than either did alone: it is an identity at 64px, exactly where it would
be safe, and unreliable at 89px, exactly where it would have to earn its
place. There is no canvas size at which it is worth having. `snapped`
stays in the probe as a tripwire -- if it ever diverges from rawFrac on
a live canvas, the gauge is being read at a scale nobody has considered.

Comment and provenance only; no behaviour changes, so no @Version bump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@averagenative averagenative self-assigned this Sep 9, 2026
fd54875 edited idleon-hoops.user.js without bumping @Version, so
Tampermonkey would never offer the update. It was pushed to
hoops-anchor-note after that branch's PR had already merged, so no CI
run ever saw it and the failure sat latent until this branch carried the
commit into a PR against main.

Comment-only or not, the check does not care and neither should it: a
file that differs from the installed one and claims the same version is
a file nobody receives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@averagenative
averagenative merged commit f3b85af into main Sep 9, 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