Take the fishing cast curve from the game, and lead the fish instead of aiming at it - #15
Merged
Merged
Conversation
…it implies
The helper marked a spot and a power, and the complaint about it was that the
spot is not where the catch is: release over the mark and you sail past,
release under it and you land the fish. Both halves of that turn out to be
true, and neither was going to be fixed by fitting the curve harder.
The catch window has been in the code since 2.4 and has never once drawn. That
version added `catchN` to SPECIES and drew a bar as wide as it, but the field
stayed behind on the species -- the fish objects are built by hand, field by
field, and it was not on the list. So the draw read undefined, `(f.catchN || 0)
* laneW` came out 0, and the lane bar and the gauge band both collapsed to
nothing every frame for two versions. It looked exactly like a helper that
only draws a line, because that is what it was. That commit said the bars had
not been seen rendered and wanted a live spot to confirm; a replay would have
said so at any point.
The curve is no longer fitted at all. The minigame was pulled out of the client
and reimplemented offline (scripts.ActorEvents_229._event_Minigames1) and the
cast is a dozen lines: the hold advances an angle one degree every 10 ms update
from 90, the gauge shows 1 - |sin(angle)|, the release launches the bobber at
(0.4 + 2.06p, -1.4 - 1.45p) under gravity 0.05, and the update that would carry
it past y = 5 does not move it at all -- so it rests on the last x it reached,
not where the parabola crosses the water. Transcribed here it reproduces the
offline module's own landings to 4e-13 of a lane unit over all 91 casts, and
sits 0.58% of the lane from the parabola fitted to the 19 measured casts,
inside that parabola's own 1.1% residual.
Two things fall out of it that no fit could give.
The gauge is a LADDER of 91 rungs, one per update of the hold, and they are not
evenly spaced: |cos| is the rate, so one rung is 0.8 lane units at 6% fill and
6.6 at full. So a catch window is countable, and small. Over every position
each species can spawn at:
fish tol 15 5..21 rungs 50..210 ms mean 90
eel tol 16 5..8 rungs 50..80 ms mean 61
squid tol 18 3..7 rungs 30..70 ms mean 56
whale tol 23 3..9 rungs 30..90 ms mean 69
And that is the other half of the complaint. A release 30 ms late lands 3 lane
units further out at the bottom of the gauge and 18 at the top, against windows
of 15 to 23 -- so a lead smaller than a frame and a half is a whole window at
the far end, and it can only ever push you long. cfg.lead carries it: the marks
are drawn that early and the live arrow looks that far ahead. Seeded at 0,
because it is the player's number and not the game's. The status line measures
what is left of it, attributing each cast to the mark it came closest to, and
refuses to guess -- the runner-up mark has to be more than twice as far off or
the cast is dropped, and it says nothing at all under four casts. It never
moves the setting on its own: a mark that chases your own misses is a mark that
shifts under you exactly while you are learning the timing.
Calibration keeps the samples and learns two numbers instead of three -- where
the lane starts in the game's units and how wide it is -- so the shape can no
longer be got wrong and there is no fallback path left. NOT a calVer bump: a
sample is still a gauge reading paired with a landing, measured the same way,
and discarding them would cost the user their calibration to buy nothing.
Replayed over a fishing recording, 679 frames:
580 with a lane, 577 with fish, no exceptions
575/577 of those had a catch window; the 2 without are real -- a
zero-power cast still flies 24 lane units, so the first 3% of the
lane is short of the shortest cast there is, and the ring draws
with no power beside it rather than clamping to an empty gauge
win.n 1..21 rungs, 21 being the model's own maximum for a fish
aimU0 refit 6.155..11.174 (seed 8.96), aimUW 282.474..296.850 (seed
296.85), which brackets the independent route in the catch-size
comment -- lane ends at game x 11 and 311 -- from a third direction
catchWindow was checked against the offline ladder at 1024 positions across
four tolerances and agrees exactly at every one. refitAim recovers a synthetic
lane (u0 20, uW 310) to three decimals. The lead learner records +6 rungs for a
release six rungs late and -6 for six early, drops a cast 30 rungs off the only
mark, and drops an ambiguous pair 6 and 7 rungs away.
Still measured from where the fish IS, not where it will be. The lane bob keeps
running through the flight, and after cast 17 its amplitude reaches 36 lane
units -- more than a whale's whole window -- so leading the fish through its
own sinusoid is the next real source of error. The frequency is known (G16
advances 1.3 degrees per 20 ms, so 65 deg/s); the phase and amplitude would
have to be fitted per fish from the frames.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…f aimed at
The catch window drawn in 2.6 was placed where the fish IS. From cast 6 the
lane bobs and from 17 it widens -- bob = A * sin(G16), G16 advancing 1.3
degrees every 20 ms, so 65 deg/s and a 5.54 s period, with A at 13 lane units
to cast 16, 23 by cast 30 and 30 by cast 60. A cast is in the air 600 to 1160
ms. So a fish can cover up to 35 lane units while the bobber is flying, which
is more than a whale's entire catch window, and marking where it sits is close
to worthless late in a run. Simulated over 60 lanes at casts 18-42:
where the fish is now 450 casts called, 53% really catch
where it will be on landing 437 casts called, 99% really catch
53% is a coin flip. That is what every version before this one drew.
Each rung is in the air for a different length of time, so there is no single
position of the fish to aim at and no single band to draw around it. hitRungs
asks where the fish will be at THAT rung's landing moment, rung by rung, and
returns the set of casts that connect -- which is the thing worth showing. It
is drawn as bands of contiguous rungs rather than one span. The set has never
been observed to split and probably cannot: a rung is worth 0.8 lane units at
the bottom of the gauge and 6.6 at the top, against a fish covering at most
0.41 of a unit in the 10 ms between one rung's landing and the next's, so the
landing always outruns the fish. Ten lines to draw runs anyway, because a band
that quietly spans a gap is exactly the thing you would need to know about.
The bar deliberately does NOT span the drift. A bar across everywhere the fish
might be is mostly wrong at any given moment, and it is the opposite of the
question. The band goes where the fish will be; a separate hairline leader --
no fill, dotted, visibly not the same object -- joins it to the sprite, because
at a median 15 and worst 29 lane units of offset the band is nowhere near the
fish it belongs to and without the leader it just looks broken.
The fit is the honest one. The frequency is known, so only centre, amplitude
and phase are unknown and x = c + a sin(wt) + b cos(wt) is linear in all three.
What it needs is TIME, not samples:
history prediction error 900 ms out, p50 / p90 / worst
0.5 s 3.6 / 8.6 / 16.0 (useless)
1.0 s 1.0 / 2.3 / 4.5
1.5 s 0.5 / 1.1 / 2.5
2.5 s 0.2 / 0.5 / 1.0
And the residual CANNOT separate those. Fits that went on to miss by more than
6 units had an rms of 0.28 against 0.30 for the ones that did not -- a short arc
fits its own noise perfectly and extrapolates into nonsense regardless. So the
gate is the span and nothing else, 1.2 s for margin over the 1.0 s where the
failures stop. Under it the band draws dashed and the status line says how many
fish are tracked, instead of guessing. History is dropped every time the bobber
lands, because the bob freezes while it sits in the water and the fish are
moved the moment it is reeled in; the fit is therefore cold for 1.2 s after
every catch, by design. The pufferfish is skipped by the game's own bob, so its
band needs no prediction and gets no leader.
Verified end to end, the shipped code fed fish positions out of the offline
module frame by frame, its plans scored against the game's own bob advanced to
each rung's landing moment -- 60 lanes, 461 casts called hittable:
97.6% really catch at 50 fps, 97.6% at 30, no plan still cold after
2.0 s of frames, none of the 60 split into more than one run
The one assumption is that the page's clock and the game's agree, since w is
fixed and the timestamps are performance.now(). Injected deliberately, 5% slow
costs 93.3% and 20% slow costs 82.5% -- still well clear of 53%, so a laggy
page degrades this rather than inverting it. Found by getting the clock wrong
in the test rig first, which read 77%.
Replayed over the fishing recording, 408 frames at 6 fps: 348 with fish, 347
with a plan (the one without is the near-end case 2.6 documented), tracking
live on 144, up to 4 tracks at once, every plan a single run, no exceptions.
Tracking is on fewer frames than it will be live -- the harness samples 167 ms
apart, so the eight-sample floor binds as well as the span; in a browser at 60
fps only the span does.
Lead the fish turns it off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits on the fishing helper, 2.5 → 2.7 (suite 1.48 → 1.57). The complaint
that started it: "it's not actually on the line — if we go over the line we
tend to miss, but if we go under we can often catch." Both halves turned out to
be true, and neither was going to be fixed by fitting the curve harder.
The catch window had been in the code for two versions and never once drew
v2.4 added the tolerance to the species table and drew a bar as wide as it. The
field was never copied onto the detected fish, so the draw read
undefined,(f.catchN || 0) * laneWcame out0, and both the lane bar and the gauge bandcollapsed to nothing every frame. It looked exactly like a helper that only
draws a line, because that is what it was.
The cast curve is no longer fitted
The minigame was pulled out of the client and reimplemented offline
(
scripts.ActorEvents_229._event_Minigames1), and the cast is a dozen lines:the hold advances an angle one degree every 10 ms update from 90, the gauge
shows
1 - |sin(angle)|, the release launches at(0.4 + 2.06p, -1.4 - 1.45p)under gravity
0.05, and the update that would carry the bobber past the waterdoes not move it at all.
Transcribed, it reproduces the offline module's landings to 4e-13 of a lane
unit over all 91 casts, and sits 0.58% of the lane from the parabola fitted to
19 measured casts — inside that parabola's own 1.1% residual. The parabola's
error was never noise, it was shape: −1.9% of the lane through the middle,
against a fish window only ±5% wide. No refit removes that.
Calibration now learns two numbers (where the lane starts, how wide it is)
instead of three. Not a
calVerbump — a sample is still a gauge readingpaired with a landing, so they are kept and refitted.
Two things fall out that a fit could not give. The gauge is a ladder of 91
rungs, unevenly spaced (0.8 lane units per rung at 6% fill, 6.6 at full), so a
catch window is countable: 30 to 210 ms depending on species and distance. And a
release 30 ms late lands 3 lane units further out at the bottom of the gauge
and 18 at the top, against windows of 15–23 — which is the whole of "aim at
the mark and I sail past it".
tuning > Release leadcarries that; the statusline measures what is left of it and refuses to guess when a cast cannot be
attributed to a single mark.
The fish do not hold still
From cast 6 the lane bobs and from 17 it widens. A cast is in the air 600–1160
ms, so a fish can cover up to 35 lane units while the bobber flies — more
than a whale's entire catch window.
53% is a coin flip, and it is what every version before this drew. Each rung is
airborne for a different length of time, so there is no single position of the
fish to aim at: every rung is now tested against its own landing moment, and
what is drawn is the set of casts that actually connect, as bands of contiguous
rungs rather than one span.
The band goes where the fish will be, never across the stretch it drifts over —
a bar spanning the drift is mostly wrong at any moment and is the opposite of
the question. A separate dotted hairline, no fill, joins the band to the sprite,
because at a median 15 and worst 29 lane units of offset the band sits nowhere
near the fish it belongs to.
The fit needs time, not samples (0.5 s of history predicts 900 ms out with a
worst case of 16 lane units; 1.2 s with 4.5), and the residual cannot tell a
good fit from a bad one — fits that went on to miss by more than 6 units had an
rms of 0.28, against 0.30 for the ones that did not. So the gate is the span and
nothing else. Below it the band draws dashed and says it is not tracking.
Verifying
Against the offline module: 1024/1024 positions agree exactly on the catch
window across four tolerances;
refitAimrecovers a synthetic lane (u0 20, uW310) to three decimals; the lead learner records +6 rungs for a release six
rungs late, −6 for six early, and drops both unattributable and ambiguous casts.
The shipped tracker, fed fish positions from the offline module frame by frame
and scored against the game's own bob, calls casts that catch 97.6% of the
time at both 50 and 30 fps. A 5% slow page clock costs 93.3%, a 20% slow one
82.5% — still well clear of 53%, so lag degrades this rather than inverting it.
Replayed over the fishing recording, 408 frames at 6 fps: 348 with fish, 347
with a plan, tracking live on 144, up to 4 tracks at once, every plan a single
run, no exceptions.