Hakenkette hängt an einem Unter-Pixel-Griff: Einflug-Abstand 222–225 px von 225 px Reichweite – 3 von 7 Läufen enden unverschuldet in einer verifizierten Kette #23
Labels
No labels
accessibility
bug
cleanup
consistency
crash
critical
game-feel
gameplay
high
input
level-generation
low
maintainability
medium
pause
physics
responsive
security
spec-gap
ui
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gilli/pulse-sprint#23
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Komponente: CORE (
buildPattern-Layout vonHOOK_CHAINZeilen 1206–1240,grabbable701–705,findGrab706–717,autopilot1364,verifyContent1553–1601,takeoffWindow1497–1510,releaseWindow1522–1540,simStrategy1368–1399),K(GRAB_RADIUS492,CHAIN_*504–513,MIN_TAKEOFF_FRAMES476)Betroffene Version:
main@86a481d,index.html(3.684 Zeilen, MD5777d18c3edbb834aa41fccc7d10a7d18)Beschreibung
Issue #22 hat die Kette von der Winkelregel auf die Haltezeit umgestellt und dabei gemessen:
159/159 generierte Ketten bestehen die Toleranzprüfungen, Fenster 6–19 Bildraten. Das ist
nach wie vor wahr — und der Lauf stirbt trotzdem an der Kette.
Der Grund ist eine Achse, die keine der Prüfungen anfasst: die Kette fängt den zweiten Haken
nur, weil die Loslassbahn den Reichweitenkreis streift. Über 42 gemessene Ketten liegt der
kleinste Abstand dieser Bahn zum zweiten Haken bei 222,2 … 225,0 px von 225 px
K.GRAB_RADIUS— die Marge ist also 0,0 … 2,8 px. Obgrabbable()(Zeile 701) ja sagt,entscheidet die Phase, in der die Einflugbahn auf das 1/120-s-Raster der Simulation fällt.
Genau diese Phase liefert das Spiel nicht:
simStrategyprüft jedes Muster ab einemkanonischen Start auf Boden (
content.x0 − clamp(0,45·v, 150, 235)), der Lauf übergibt dasMuster aber in dem Zustand, den das vorherige Muster hinterlässt. Eine Kette ist der Verstärker
dieses Unterschieds: der erste Schwung macht aus einem halben Pixel Einflug drei Pixel am
Loslasspunkt — und drei Pixel entscheiden am zweiten Seil über Leben und Tod.
Gemessen im Lebendmodell (Autopilot fährt die ins Muster geschriebene, verifizierte Strategie,
Spielschrittweite
K.FIXED_TIMESTEP, Tod =CORE.hitTestodery > K.FALL_DEATH_Y):sieben Läufe à 5 Minuten, vier Tode, drei davon in einer
HOOK_CHAIN— bei12 482 m (Sektor 28), 17 114 m (Sektor 39), 17 748 m (Sektor 40). Alle drei mit
grabs = 1: erster Haken gefasst, zweiter nicht, Fall in eine 672–730 px breite Grube, die perDesign breiter ist als jeder Sprung.
Messprotokoll
Node gegen das extrahierte
CORE(tests/harness.mjs) — dieselbe Simulation, dieselbenKonstanten, dieselbe Schrittweite wie der Lauf. Weltbau identisch zu
resetWorld(
x0 = −400 … START_X + 900,gen.frontierX = START_X + 900), Muster über den echten PfadgenerateAhead(contextAt → pickKind → fallbackChain → buildPattern → verifyContent → verifyAllInputs → commitContent),onCommitnimmtres.best.strategyins Muster wieupdateDemo.1. Sieben Läufe à 5 Minuten (Seed, Tod, Stelle)
HOOK_CHAIN,fellHOOK_CHAIN,fellHOOK_CHAIN,fellJUMPAlle Ketten-Tode: Strategie der Muster enthält
grab:"auto",hang: 0.44;grabs = 1;verifyContenthatte das Muster mitwindow ≥ 4undrelease ≥ 4als spielbar in die Weltgeschrieben.
2. Obduktion Seed 4242 (Muster
x0 = 170 651,kind = HOOK_CHAIN, ctx.speed 640)Geometrie (echte Generierung): Grube 171 084 … 171 758 = 674 px (1,40 ×
jumpDistance480),Haken 1 bei (171 380 / y 212), Haken 2 bei (171 615 / y 238) — 143 px vor der Landekante, also
innerhalb von
K.CHAIN_EDGE_REST_MIN 140und weit überK.HOOK_EDGE_GUARD 135. #22 hält hieralso in jeder Hinsicht.
{grab:"auto", hang:0.44, jumpLead:26, hold:0.1}ev.fellbei 171 745Drei Pixel Unterschied im Loslasspunkt, 1,6 px Unterschied im Einflugabstand — das eine Mal
Landung, das andere Mal Tod. Das ist kein Timing-Problem eines Spielers, das ist die Geometrie.
3. Margen über 42 generierte Ketten (je 14 bei ~430 / ~520 / 640 px/s)
Kleinster Abstand der freien Bahn nach dem ersten Loslassen zum zweiten Haken,
Marge = Abstand −
K.GRAB_RADIUS(negativ = innerhalb, also greifbar):42 von 42 Ketten streifen den Kreis statt ihn zu schneiden. Eine einzige Kette hat den
Krümmungsmittelpunkt exakt auf der Kreislinie (Marge 0,0).
4. Die eigenen Toleranzmaße sehen das nicht — und zwar am selben Muster
Für das Muster aus §2 (Seed 4242), jeweils mit dessen Muster-Strategie:
verifyContentok: truetakeoffWindowK.MIN_TAKEOFF_FRAMES4)releaseWindowverifyAllInputstrue(lead, hold)-Kombinationen schaffbarjumpLeadum ±107 px = ±10 Bildraten, Schritt 1 px)Alle 93 Fehlschläge haben
grabs = 1und Einflugabstände ≥ 227 px — wieder der zweite Haken.takeoffWindowrastet in 1/60 s (Zeile 1500:step = ctx.speed / 60) und trifft mit seinen11 Stichproben genau die Glücklichen Pixel; zwischen ihnen liegen die tödlichen.
Ursache im Code
buildPattern(Zeilen 1209–1239) setzt Grubenbreitew = rng.range(1,40, 1,52) · jd,zweiten Haken
g1 − rng.range(140, 164)und HöhechainH2 = chainH1 − rng.range(14, 52).Alles Flächenmaße. Gemessen wird an diesen Maßen nie, mit welcher Marge die Loslassbahn
den Reichweitenkreis des zweiten Hakens schneidet — verlangt wird nur, dass
grabbable()in einem Schritt
truewird (Zeilen 701–705:d <= K.GRAB_RADIUS, eine Stichprobe proSimulationsschritt, keine Marge).
simStrategy(1368–1380) startet immer auf Boden beicontent.x0 − clamp(0,45·v, 150, 235);takeoffWindow(1497) undreleaseWindow(1522)variieren innerhalb dieses Startes
jumpLeadbzw.hang. Der Lauf übergibt das Muster ineinem beliebigen Zustand — für die Kette entscheidet genau der. Das Gegenstück existiert
bereits:
simProbe()/liveStrategy()(1408–1468, aus #15/#17) prüfen eine Strategie ab demlebenden Zustand — aber nur die Menü-Demo ruft sie, der Lauf pfad hat kein Äquivalent.
drei Pixel am Loslasspunkt (§2). Am ersten Seil ist das verzeihend (#22: Fenster bis 733 ms),
am zweiten entscheidet es über 1,6 px.
sind 10,7 px; §4 zeigt Tode innerhalb eines als 13 Bildraten breit gemessenen Fensters.
Folgeverhalten
als spielbar eingetragen hat — ohne jede Möglichkeit, es durch Können zu retten: Die Grube ist
1,40–1,52 ×
jumpDistancebreit (Issue #16: bewusst breiter als jeder Sprung), es gibt nichtszu springen, und der zweite Haken ist außerhalb von 225 px.
el.grabHintmelden den zweiten Haken als erreichbar, solange er im Kreis liegt — dieEinflugphase, an der es hängt, ist nirgends ablesbar.
liveStrategy()neuund nimmt die erste Strategie, die ab dem lebenden Zustand überlebt. Ein reiner Demo-Gegentest
lässt ihn weiter durch (dieselbe Blindheit wie bei #22).
(Befund #25) —
node tests/soak.mjs 5meldet für dieselbe Seedok 5 Minuten Spielzeit ohne Tod.Nicht verletzt (bewusst abgegrenzt)
releaseWindowmisst 11, dieFenstergrößen der Muster-Strategie liegen mittig, ein Rhythmus trägt beide Seile.
K.CHAIN_EDGE_REST_MIN140 und≥
K.HOOK_EDGE_GUARD135),pit.hooks== verlegte Haken, Rückfall entscheidet vor dem Verlegen.verifyContentverlangtweiterhin
K.MIN_TAKEOFF_FRAMES.Lösungsvorschlag
Reserve einfangbar ist. Messgröße: kleinster Abstand der Loslassbahn zum zweiten Haken über
das Einflugfenster hinweg. Zwei Wege, beide in
CORE.K:K.CHAIN_GRAB_MARGIN(z. B. 40 px) und einereachMargin(world, content, ctx, s)analogreleaseWindow,verifyContentverlangtreachMargin ≥ K.CHAIN_GRAB_MARGIN; oderGRAB_RADIUS − MARGE) — dann ist„bestanden“ automatisch „mit Luft bestanden“.
Danach
K.CHAIN_EDGE_REST_*,CHAIN_HOOK_H_*,CHAIN_W_OF_JD_*an dieser Marge neuausmessen (wie in #22 an der Fensterbreite gemessen wurde) — nicht an Flächen.
verifyContent(oder eine neueverifySequence) prüft jedes Muster zusätzlich ab einer handvoll Erwartungs-Zustände an derGrenze: auf Boden (heute) und fliegend mit den Bahnen, die die Vorgänger-Strategien
erzeugen. Der Baustein dafür ist schon im Haus:
simProbe()(Zeile 1408) rechnet genau das —Haken geklont, lebender Zustand, endliches Ziel.
takeoffWindow/releaseWindowin halben Bildraten (1/120 s) statt1/60 s abtasten, oder — billiger und ehrlicher — das gesamte Scanband verlangen statt der
längsten Folge von 1/60-s-Stichen. §4 zeigt, dass 1/60 s bei 640 px/s 10,7 px übergeht.
liveStrategy()die überlebende Strategie wählen (die Demo tut es, derLauf nicht). Findet er keine, war das Muster unverlegbar — genau die Auskunft, die die
Generierung jetzt noch nicht hat.
Erwarteter Test
tests/issue23.mjs— extrahiertesCORE, echte Generierung über N Samen:(a) jede committed
HOOK_CHAINhat am zweiten Seil eine Einflug-Marge ≥K.CHAIN_GRAB_MARGIN(gemessen als kleinster Abstand der Loslassbahn zum zweiten Haken, über das ganze
Einflugfenster), (b) ≥
K.MIN_TAKEOFF_FRAMESbestandene Absprünge bei 1-Px-Raster um dieMuster-Lead, (c) 5-Minuten-Lebendlauf mit korrekter Tod-Erkennung (siehe #25) über mehrere
Samen ohne Tod. Gegen
git show HEAD:index.htmlrot: Margen 0,0…2,8 px, 93/215 Tode imScanband, Lebendtod bei 276,34 s (Seed 4242).
tests/browser_issue23.py— echtes Dokument, echte Leertaste an einer real generierten Kette:Sweep über Absprungpositionen im ±10-Bildraten-Band mit 1-px-Schritt; Erwartung nach dem Fix:
kein Tod im Band, und die Haltezeit der Muster-Strategie liegt mit Marge im Fenster.
Dokumentiert in
issues/23-hakenkette-zweiter-haken-streift-nur-die-reichweite.md(Commit86a481d).body.deadexistiert seit #21 nicht mehr – „5 Minuten Spielzeit ohne Tod“ kann nicht mehr fehlschlagen #25Behoben in
dee2b2c(Doku-Nachtraga7c7bde), CI-Lauf #11 grün (Node + Browser).Ursache lag nicht im Kettenlayout. Dieselben Muster lassen sich mit 40 … 90 px Marge
durchfliegen –
verifyContentnahm aber die erste Strategie, die überhaupt durchkam, und dietraf im Schnitt mit 11,6 px. Geprüft war nämlich, ob
grabbable()in irgendeinemSimulationsschritt ja sagt: ein Streifen am Kreisrand reichte, entschieden hat die Phase auf dem
1/120-s-Raster.
takeoffWindow(13) undreleaseWindow(11) meldeten bestanden, weil sie eineandere Achse messen – Absprungtoleranz und Haltezeit, nicht die Tiefe des Einflugs.
Geändert
CORE.corridorDepth()– tiefster Abstand der freien Bahn zum nächsten Haken, in Schrittweite desLaufs (
K.FIXED_TIMESTEP, bewusst nichtK.SIM_DT). Der Griff ist ab dem vermessenen Seilausgeschaltet: sonst endet die Bahn schon mit dem Schritt, der als erster im Kreis liegt, und die
Tiefe bleibt unsichtbar. Genau dieser Messfehler hat den Befund zunächst versteckt – die erste
Messung meldete „alle Bahnen tief“.
CORE.reachMargin()–K.GRAB_RADIUSminus dem ungünstigsten dieser Abstände, über jedesSeil des Musters und über ±
K.REACH_SCAN_FRAMES(1) Bildraten Lead.verifyContent– nimmt eine Strategie erst abK.HOOK_GRAB_MARGIN= 40 px (ein Vielfaches desSchrittwegs bei
K.SPEED_MAX). Das Gate sitzt vor den Fenstern: sechs Bahnen stattvierundachtzig, das ist der Preisunterschied.
res.best.reachträgt die Zahl als Diagnose.Wurf (vorher 9,1 px).
#22 gemessen. Die Schranke wählt die robuste Bahn aus, statt Geometrie nachzuwürfeln; angenommene
Haltezeiten rücken von 0,44 auf 0,56/0,7.
Gemessen nach dem Fix
generateAhead-Aufruf eines LaufsVon 20 Stichläufen à 5 Minuten stirbt keiner mehr in einer Grube. Offener Rest: Seed 555 stirbt bei
8 192 m an einer Kollision kurz nach der Muster-Grenze – das ist #24 (Reaktionsraum), nicht die
Kette; die Tod-Erkennung des neuen Tests läuft deshalb über
CORE.hitTestbzw.y > K.FALL_DEATH_Yund nicht über das totebody.dead(#25).Tests
tests/issue23.mjs– 21 Prüfungen, vorher 13 rot. Misst die Marge unabhängig nach statt sieaus dem Solver abzulesen (Nachmessung muss zu
CORE.reachMarginauf 0,5 px stimmen), fährt das1-px-Raster um die Muster-Lead, und die Schranke ist als tragend bewiesen: eine künstlich
gekippte Kette (zweiter Haken höher) bleibt nach dem alten Maß – Varianten +
takeoffWindowreleaseWindow– spielbar und wird vonverifyContentverworfen; Gegentest: dieselbe Kettewird nach altem Maß mit 13,6 px Marge angenommen, neu sind es 53,9 px.
tests/browser_issue23.py– vorher 5 rot. Echtes Dokument, echte Leertaste, stillgelegterLoop: Korridor 57,9 / 51,8 px am zweiten bzw. ersten Seil (Befund 0,0 … 2,8 px), Absprungband
209 px = 19,6 Bildraten (vorher 117 px bzw. 37 px), Kontrolle Einzelhaken 43,3 px (vorher 18,2).
tests/run-all.sh;./tests/run-all.sh 1meldet „alle Suiten grün“(17 Node-Suiten + Soak, 16 Browser-Suiten).
Schließe nach Verifikation: Fix
dee2b2c, Teststests/issue23.mjs(vorher 13 rot) undtests/browser_issue23.py(vorher 5 rot), CI-Lauf #11 grün auf beiden Jobs. Details im Kommentar oben.