Hakenkette: Der zweite Haken trägt nicht über die Grube – Loslassfenster 25–83 ms gegen 83–683 ms am Einzelhaken, 10 von 15 Ketten ohne treffbare Haltezeit #22

Closed
opened 2026-09-03 22:52:29 +02:00 by gilli · 1 comment
Owner

⁨⁩⁨Hakenkette: Der zweite Haken trägt nicht über die Grube – Loslassfenster 25–83 ms gegen 83–683 ms am Einzelhaken, 10 von 15 Ketten ohne treffbare Haltezeit⁩ ⁨⁩#⁨22⁩⁨⁩"
By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩
bug
game-feel
gameplay
high
level-generation
physics

Komponente: CORE (buildPattern-Layout von HOOK_CHAIN, autopilot/attachHook/releaseHook,
verifyContent, takeoffWindow, verifyAllInputs), GAME (playerCmd, Autograb)
Betroffene Version: main @ 2ee6082, index.html (3.589 Zeilen, MD5 2fa3b0d0c18f09d60427121bb2a3eb05),
Zeilen 1185–1220 (Kettenlayout), 1290–1300 (Loslassregel), 1455–1512 (Toleranzprüfung),
1517–1548 (verifyAllInputs), 3082–3090 (Autograb), K Zeilen 475, 487–493

Beschreibung

Eine HOOK_CHAIN-Grube legt zwei Haken über eine Grube (Issue #6: hookCount = 2,
w = rng.range(1.6·jd, 1.85·jd), firstFx = 0.4, spacing = rng.range(252, 286),
Zeilen 1194–1216). Erwartung des Spielgefühls: erst Schwung 1, dann Schwung 2, dann auf der
gegenüberliegenden Kante landen.

Gemessen: Der erste Haken ist wirklich leicht (er trägt den Körper in 483–733 ms breitem
Haltezeit-Fenster an den zweiten Haken). Der zweite Haken trägt nicht mehr über die Kante:
in 10 von 15 generierten Ketten gibt es überhaupt keine Haltezeit, die mit demselben Rhythmus
an beiden Seilen durchbringt; die übrigen fünf schaffen es in einem Fenster von 25–83 ms. Die
Kontrolle mit einem einzelnen Haken über derselben Generierung liegt bei 83–683 ms
(median 425 ms). Das Muster stirbt also nicht an der Geometrie des ersten Schwungs, sondern an
der des zweiten – und zwar knapp: die 82 Fehlwürfe des zweiten Schwungs enden zwischen 88 px
vor der Landekante und 62 px hinter ihrer x-Position, jeweils unterhalb des Bodens – also
im Bruch, direkt an der Kante.

Der Autopilot (autopilot, Zeile 1295) lässt winkelgesteuert los
(b.theta > s.rel && b.omega > 0). Diese Regel existiert nur, weil sie den Scheitelwinkel der
zweiten Schwingung exakt trifft: die zweite Schwingung hat einen Scheitel von +20…+21° und
jede verifizierte Ketten-Strategie trägt rel = 0.35 rad = 20°. Ein Mensch sieht keinen Winkel,
er hält eine Zeit – und die Zeit, die am ersten Seil richtig war, ist am zweiten zu spät.

Messprotokoll

Alle Zahlen sind gefahren, nicht abgelesen: echtes Auslieferungsdokument über die von
tests/testpage.py gebaute Testseite, echte keydown/keyup-Ereignisse für Leertaste,
Simulation im gestellten Loop mit PULSE.update(1/120) (dito Schrittweite des Laufs).
Greifen übernimmt S.settings.autograb (Standard an), die Grube wird von der echten
Generierung bis Sektor 7 verlegt (contextAt → pickKind → buildPattern → verifyContent →
verifyAllInputs → commitContent), der Körper startet auf dem Solver-Anfangspunkt
x0 − clamp(0.45·v, 150, 235) mit vx = speedAt(Entfernung).

1. Echtes Dokument, Kette gegen Einzelhaken (je 156 Haltezeiten, Raster 1/120 s)

Kette HOOK_CHAIN (Seed 20240816) Kontrolle PIT_HOOK (Seed 555)
Grubenbreite 808 px (1,68 × jumpDistance 480 bei 640 px/s) 682 px
Haken +323 und +577, je 284–286 px über Boden; 2. Haken 231 px vor der Landekante +405, 259 px über Boden
Muster-Strategie {grab:"auto", rel:0.35, jumpLead:168, hold:0.1} {grab:"auto", rel:0.55, jumpLead:10, hold:0.18}
Erfolg mit einer Haltezeit an beiden Seilen 5 von 156, längstes zusammenhängendes Fenster 42 ms (0,367–0,400 s) 36 von 156, längstes Fenster 300 ms (0,725–1,017 s)
Misserfolge 68 τ: erster Schwung trägt nicht an den 2. Haken · 82 τ: zweiter Schwung zu kurz (−88 … +62 px quer zur Kante, immer unter dem Boden) 110 τ zu kurz, aber nie im Fensterbereich
Loslassregel des Autopiloten mit echten Tasten rel 0,35 → +7 px hinter die Kante · rel 0,30 → +4 px · rel 0,25/0,20/0,15 → Tod rel 0,45 → +35 px · rel 0,40 → +17 px · rel 0,30 → −82 px

Die Kette wird im echten Dokument also mit 4–7 px Vorsprung über die Kante entschieden – bei
einer Loslassart, die ein Mensch nicht sehen kann.

2. Node-Durchlauf über 15 Ketten und 15 Einzelhaken (gleiches Modell, 1/120 s)

Muster Fenster 1. Seil (2. Haken gefasst) Fenster 2. Seil (Kante erreicht) Treffer
Kette seed 7 517 ms 0 ms 0/157
Kette seed 13 483 ms 0 ms 0/157
Kette seed 42 508 ms 0 ms 0/157
Kette seed 31337 550 ms 0 ms 0/157
Kette seed 5 550 ms 0 ms 0/157
Kette seed 8 567 ms 0 ms 0/157
Kette seed 1234 567 ms 0 ms 0/157
Kette seed 555 (784 px) 592 ms 0 ms 0/157
Kette seed 6 600 ms 0 ms 0/157
Kette seed 88 600 ms 0 ms 0/157
Kette seed 44 558 ms 25 ms 3/157
Kette seed 555 (810 px) 708 ms 67 ms 8/157
Kette seed 8 (781 px) 725 ms 75 ms 9/157
Kette seed 20240816 733 ms 50 ms 6/157
Kette seed 99 708 ms 83 ms 10/157
Einzelhaken, 15 Muster – 83–683 ms (median 425 ms) 10–82/157

Der erste Haken einer Kette ist verzeihend (median 567 ms), der zweite nicht (median 0 ms).
Das ist exakt die Rückmeldung: „den ersten schafft man problemlos, beim zweiten schwingt es
nicht mehr weit genug“.

Zum Vergleich die eigene Toleranzschranke des Projekts: K.MIN_TAKEOFF_FRAMES = 4
(Zeile 475) verlangt für Sprünge ≥ 4 aufeinanderfolgende Absprungzeitpunkte — das sind
67 ms. Die Kette liegt mit 25–83 ms daran vorbei, in 10 von 15 Mustern unter null.

3. Warum der zweite Schwung nicht trägt (Geometrie, 15 Ketten, verifizierte Strategie)

Größe 1. Haken 2. Haken
Einhangwinkel θ −52° … −75° −30° … −63°
Seillänge beim Einhang 92–223 px 222–225 px (von K.GRAB_RADIUS 225)
Scheitel der Schwingung +20…+22° +20…+21°
Loslasspunkt +349…+410 +654…+702
Restweg bis Landekante 383–442 px (wird über den 2. Haken überbrückt) 110–149 px
Höhe der Füße über Boden beim Loslass – 26–58 px
Flugzeit / Strecke in dieser Zeit – 246–322 ms / 128–190 px

Der zweite Haken liegt 191–231 px vor der Landekante (median 204) – näher als der
Greifradius (225). Wer dort einhängt, hängt deshalb praktisch am Ende der Reichweite
(Seil 222–225 px) und hat Vorwärts-Bogen wie Resthöhe gleichzeitig gegen sich:
L · sin 20° ≈ 77 px bringt den Körper über den Haken hinaus, es bleiben 110–149 px,
die aus 26–58 px Höhe in 246–322 ms Flug überbrückt werden müssen. Die Rechnung geht in
5 von 12 Ketten nicht auf (Rechenwert −3 bis −21 px), die gemessenen Landungen derselben
Läufe liegen bei −2,6 px und −0,2 px vor der Kante – sie überleben nur über die
Landetoleranz K.EDGE_TOL 3 / K.EDGE_LIFT 6 (Zeilen 522–523).

Zusätzlich: die Haltezeiten, die an beiden Seilen funktionieren, liegen auseinander.
Matrix über τ1 × τ2 (Raster 1/30 s, Seed 7): τ1 = 0,88 s braucht τ2 ∈ 0,22 … 0,68 s;
Seed 99: τ1 = 0,55 s → τ2 ∈ 0,28 … 0,78 s; echtes Dokument Seed 20240816: τ1 = 0,40 s mit
τ2 ∈ 0,40 … 0,55 s, alle anderen τ1-Werte der Matrix: kein einziger Erfolg. Ein Rhythmus,
zwei Aufgaben – das trägt kein Mensch zweimal hintereinander zuverlässig.

4. Reproduktion (Werkzeug liegt bewusst nicht im Repo)

Die Sonden sind Einmal-Messwerke (wie bei allen bisherigen Befunden); der verbindliche Test
gehört zum Fix. Recipe:

  1. python3 tests/testpage.py → Testseite mit Bridge.
  2. Welt wie in resetWorld aufbauen (x0 = −400 … START_X + 900, gen.frontierX = START_X + 900),
    dann den echten generateAhead-Ablauf kopieren (contextAt → pickKind → fallbackChain → buildPattern → verifyContent → verifyAllInputs → commitContent) und das erste Muster mit
    hooks.length === 2 einsammeln (Samen 20240816, Ziel ≥ 3 600 m – Ketten greifen ab
    sectorAbs ≥ 6, Zeile 1551).
  3. Körper auf content.x0 − clamp(0.45·v, 150, 235) setzen, vx = ctx.speed, Zustand
    PLAYING, Autograb auf Standard.
  4. Mit page.keyboard die Leertaste tasten: Sprung bei pit.x0 − strategy.jumpLead,
    hold = strategy.hold Sekunden gehalten; dann pro Seil nach τ Frames loslassen;
    Klassifikation nach der ersten Bodenberührung (b.x > pit.x1 + K.SOLVER_MARGIN =
    geschafft, b.y > GROUND_Y + 60 = im Bruch).
  5. Kontrolle: dieselbe Prozedur mit dem ersten Muster hooks.length === 1 (Same 555).

Node-Seite (ohne Browser): das identische Modell mit CORE.stepBody und
K.FIXED_TIMESTEP über die Samen 7, 13, 42, 31337, 5, 8, 99, 1234, 555, 6, 44, 88, 77, 101,
2024, 20240816 – Tabelle 2 oben.

Ursache im Code

  1. Das Layout prüft Distanzen, aber keine Schwung-Energie. buildPattern (Zeilen 1193–1216)
    setzt spacing = rng.range(252, 286) und firstFx = 0.4 relativ zur Grubenbreite und
    begrenzt den Haken nur gegen clamp(hx, e0 + 60, g1 − K.HOOK_EDGE_GUARD).
    K.HOOK_EDGE_GUARD = 135 (Zeile 492, die Klammer Zeile 1216 lässt also höchstens
    g1 − 135 zu) schützt seit Issue #6 davor, dass ein Haken über dem Landepunkt hängt – es
    sichert aber nicht, dass der Schwung von diesem Haken aus den Landepunkt erreicht.
    Bei 231 px Abstand und vollem Seil ist genau das der Engpass.
  2. Die Toleranzprüfung kennt das Loslassen nicht. verifyContent (Zeilen 1472–1512)
    verlangt takeoffWindow(…) ≥ K.MIN_TAKEOFF_FRAMES, und takeoffWindow (1455–1467)
    verschiebt ausschließlich jumpLead in Bildratenschritten; für Hakenmuster bleibt
    tol = base (Zeile 1503), also auch rel der Strategie unverändert. verifyAllInputs
    (1517–1548) verlangt für jedes (lead, hold) nur, dass irgendein Wert aus
    RELEASES = [0.35, 0.55, 0.18, 0.8, 1.05, "peak"] (Zeile 1394) durchkommt – sechs
    Stichproben, keine Fensterbreite. Ein 42-ms-Fenster besteht beide Prüfungen.
  3. Der Solver spielt anders als ein Mensch. autopilot lässt bei theta > rel && omega > 0
    los (Zeile 1295), also am steilsten Punkt, den die Schwingung je hat – und trifft damit
    planerisch eine 1°-Kante. Die Scheitelwinkel aller gemessenen zweiten Schwingungen sind
    20–21°, alle verifizierten Ketten-Strategien tragen rel = 0.35 rad = 20°. Die einzige
    verfügbare Toleranz in Zeit ist der Moment kurz vor dem Umkehrpunkt.
  4. attachHook (Zeilen 714–733) wirft bei jedem Einhang die Radialkomponente der
    Geschwindigkeit weg (b.omega = (v·t) / L). Am zweiten Haken kommt der Körper aber fast
    senkrecht auf das Seil zu (Einhang bei 222–225 px von 225 px Radius) – ein Teil des
    Anflugs ist deshalb unwiederbringlich weg, und SWING_DAMPING = 0.55 (Zeile 493) frisst
    auf dem Weg zum Scheitel den Rest.

Folgeverhalten

  • Läufe sterben an einem Muster, das die Generierung als „verifiziert“ in die Welt schreibt –
    ab Sektor 6, also genau in dem Bereich, in dem HOOK_CHAIN mit Gewicht 14–17 gewürfelt wird
    (Zeilen 936–939) und allowChain frei ist (Zeile 1551).
  • Kein Hinweis für den Spieler, wann er loslassen soll: die HUD-Hilfe verspricht
    „Haken anklicken/greifen“ und „halten = höher und weiter“, aber die Kette verlangt ein
    40-ms-Fenster am zweiten Seil.
  • Die Menü-Demo überlebt das (4 × 150 s, 0 Neustarts), weil sie dieselbe Winkelregel fährt wie
    der Solver. Der Befund ist damit nicht demo-sichtbar, sondern nur spielbar – ein reiner
    Autopilot-Gegentest im Testskript würde ihn weiterhin durchlassen.

Nicht verletzt (bewusst abgegrenzt)

  • Issue #6 hält: pit.hooks == tatsächlich verlegte Haken, kein Haken näher als
    K.HOOK_EDGE_GUARD an der Landekante (gemessen 191–231 px), Rückfall auf einen Haken greift
    vor dem Verlegen. Der zweite Haken hängt also nicht über dem Landepunkt – er ist nur zu nah
    dafür, von ihm aus die Kante zu erreichen.
  • verifyContent/verifyAllInputs/takeoffWindow liefern weiterhin echte Ergebnisse: mit der
    Winkelregel des Autopiloten bestehen alle 15 Ketten das Muster: 15/15 bei 1/110 (Solver-
    schritt), 15/15 bei 1/120 (Spielschritt) und 15/15 bei 1/60.
  • Einzelhaken-Gruben (PIT_HOOK, HOOK_TO_ROLL) sind von dem Maß nicht betroffen – Kontrolle
    oben, median 425 ms Fenster.

Lösungsvorschlag

  1. Loslass-Toleranz messen, nicht nur Lösbarkeit. Analog takeoffWindow eine
    releaseWindow(world, content, ctx, s) – Raster über die Haltezeit der letzten Schwingung
    (oder über rel in Bildraten-Schritten), Erfolg = Landung ≥ K.SOLVER_MARGIN hinter der
    Landekante, ohne EDGE_LIFT-Toleranz. verifyContent verlangt für hooks.length ≥ 2
    zusätzlich releaseWindow ≥ K.MIN_TAKEOFF_FRAMES; Muster fallen über das bestehende
    fallbackChain("HOOK_CHAIN") auf PIT_HOOK/PIT/SAFE zurück.
  2. Layout an der Schwinggeometrie fixen statt an Flächenmaßen. Die Kette braucht einen
    zweiten Haken, dessen Bogen die Kante mit Reserve trägt. Zwei Hebel, beide in CORE.K:
    Mindestabstand zweiter Haken → Landekante aus GRAB_RADIUS ableiten (heute nur
    HOOK_EDGE_GUARD 135 px, gemessene Praxis 191–231 px) und Seillänge begrenzen
    (minJump-Logik aus Issue #16: sized an der gemessenen Hüllkurve, hier an der gemessenen
    Schwunghüllkurve statt an der Sprunghüllkurve). Alternativ: w-Bereich unter
    1.6 … 1.85 × jd und/oder spacing oberhalb von ~320 px, damit der zweite Haken höher und
    damit der Restweg kürzer wird.
  3. Was der Solver prüft, muss der Mensch spielen können. verifyAllInputs für Ketten
    zusätzlich mit ein und derselben Haltezeit an beiden Seilen durchrechnen (Modell
    „ein gelernter Rhythmus“). Der Weg fällt genau dort durch, wo er heute noch durchfällt.
  4. Spielbarkeits-Hinweis sichtbar machen (Hilfe/Erststart-Hinweis): an der Kette gilt
    „am zweiten Haken früh loslassen“ – oder, besser, das Fenster so weit machen, dass der Satz
    nicht nötig ist.

Erwarteter Test

  • tests/issue22.mjs – extrahiertes CORE, echte Generierung über N Samen: jede in die Welt
    geschriebene HOOK_CHAIN muss (a) ein Loslassfenster von ≥ K.MIN_TAKEOFF_FRAMES
    Haltezeiten am letzten Seil haben, (b) mit identischer Haltezeit an beiden Seilen bestehen,
    (c) mit ≥ K.SOLVER_MARGIN px Landeklarheit landen (nicht über EDGE_LIFT). Gegen
    git show HEAD:index.html sind (a)–(c) rot: 10/15 Ketten ohne jedes Fenster, Fenster 25–83 ms,
    Landungen bis −2,6 px vor die Kante.
  • tests/browser_issue22.py – echtes Dokument, echte Leertaste, Autograb Standard: τ-Sweep
    0,10–1,40 s in 1/120 s über eine real generierte Kette; Erwartung nach dem Fix ≥ 4
    aufeinanderfolgende τ (und Kontrolle Einzelhaken unverändert grün). Wie bei allen
    Browser-Suiten: Vorher/Nachher über PULSE_HTML=$(git show HEAD:index.html > …).

Dokumentiert in issues/22-hakenkette-zweiter-haken-traegt-nicht-ueber-die-grube.md (Commit 1a9c2f2).

1 comments

⁨⁩⁨Hakenkette: Der zweite Haken trägt nicht über die Grube – Loslassfenster 25–83 ms gegen 83–683 ms am Einzelhaken, 10 von 15 Ketten ohne treffbare Haltezeit⁩ ⁨⁩#⁨22⁩⁨⁩" By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩ bug game-feel gameplay high level-generation physics > **Komponente:** CORE (`buildPattern`-Layout von `HOOK_CHAIN`, `autopilot`/`attachHook`/`releaseHook`, > `verifyContent`, `takeoffWindow`, `verifyAllInputs`), GAME (`playerCmd`, Autograb) > **Betroffene Version:** `main` @ `2ee6082`, `index.html` (3.589 Zeilen, MD5 `2fa3b0d0c18f09d60427121bb2a3eb05`), > Zeilen 1185–1220 (Kettenlayout), 1290–1300 (Loslassregel), 1455–1512 (Toleranzprüfung), > 1517–1548 (`verifyAllInputs`), 3082–3090 (Autograb), `K` Zeilen 475, 487–493 > > ## Beschreibung > > Eine `HOOK_CHAIN`-Grube legt zwei Haken über eine Grube (Issue #6: `hookCount = 2`, > `w = rng.range(1.6·jd, 1.85·jd)`, `firstFx = 0.4`, `spacing = rng.range(252, 286)`, > Zeilen 1194–1216). Erwartung des Spielgefühls: erst Schwung 1, dann Schwung 2, dann auf der > gegenüberliegenden Kante landen. > > Gemessen: **Der erste Haken ist wirklich leicht** (er trägt den Körper in 483–733 ms breitem > Haltezeit-Fenster an den zweiten Haken). **Der zweite Haken trägt nicht mehr über die Kante**: > in 10 von 15 generierten Ketten gibt es überhaupt *keine* Haltezeit, die mit demselben Rhythmus > an beiden Seilen durchbringt; die übrigen fünf schaffen es in einem Fenster von 25–83 ms. Die > Kontrolle mit einem einzelnen Haken über derselben Generierung liegt bei 83–683 ms > (median 425 ms). Das Muster stirbt also nicht an der Geometrie des ersten Schwungs, sondern an > der des zweiten – und zwar knapp: die 82 Fehlwürfe des zweiten Schwungs enden zwischen 88 px > *vor* der Landekante und 62 px *hinter* ihrer x-Position, jeweils unterhalb des Bodens – also > im Bruch, direkt an der Kante. > > Der Autopilot (`autopilot`, Zeile 1295) lässt *winkel*gesteuert los > (`b.theta > s.rel && b.omega > 0`). Diese Regel existiert nur, weil sie den Scheitelwinkel der > zweiten Schwingung exakt trifft: die zweite Schwingung hat einen Scheitel von **+20…+21°** und > jede verifizierte Ketten-Strategie trägt `rel = 0.35 rad = 20°`. Ein Mensch sieht keinen Winkel, > er hält eine Zeit – und die Zeit, die am ersten Seil richtig war, ist am zweiten zu spät. > > ## Messprotokoll > > Alle Zahlen sind gefahren, nicht abgelesen: echtes Auslieferungsdokument über die von > `tests/testpage.py` gebaute Testseite, echte `keydown`/`keyup`-Ereignisse für Leertaste, > Simulation im gestellten Loop mit `PULSE.update(1/120)` (dito Schrittweite des Laufs). > Greifen übernimmt `S.settings.autograb` (Standard **an**), die Grube wird von der **echten** > Generierung bis Sektor 7 verlegt (`contextAt` → `pickKind` → `buildPattern` → `verifyContent` → > `verifyAllInputs` → `commitContent`), der Körper startet auf dem Solver-Anfangspunkt > `x0 − clamp(0.45·v, 150, 235)` mit `vx = speedAt(Entfernung)`. > > ### 1. Echtes Dokument, Kette gegen Einzelhaken (je 156 Haltezeiten, Raster 1/120 s) > > | | Kette `HOOK_CHAIN` (Seed 20240816) | Kontrolle `PIT_HOOK` (Seed 555) | > |---|---|---| > | Grubenbreite | 808 px (1,68 × `jumpDistance` 480 bei 640 px/s) | 682 px | > | Haken | +323 und +577, je 284–286 px über Boden; **2. Haken 231 px vor der Landekante** | +405, 259 px über Boden | > | Muster-Strategie | `{grab:"auto", rel:0.35, jumpLead:168, hold:0.1}` | `{grab:"auto", rel:0.55, jumpLead:10, hold:0.18}` | > | **Erfolg mit einer Haltezeit an beiden Seilen** | **5 von 156**, längstes zusammenhängendes Fenster **42 ms** (0,367–0,400 s) | **36 von 156**, längstes Fenster **300 ms** (0,725–1,017 s) | > | Misserfolge | 68 τ: erster Schwung trägt nicht an den 2. Haken · 82 τ: zweiter Schwung zu kurz (−88 … +62 px quer zur Kante, immer unter dem Boden) | 110 τ zu kurz, aber nie im Fensterbereich | > | Loslassregel des Autopiloten mit echten Tasten | rel 0,35 → **+7 px** hinter die Kante · rel 0,30 → **+4 px** · rel 0,25/0,20/0,15 → **Tod** | rel 0,45 → +35 px · rel 0,40 → +17 px · rel 0,30 → −82 px | > > Die Kette wird im echten Dokument also mit 4–7 px Vorsprung über die Kante entschieden – bei > einer Loslassart, die ein Mensch nicht sehen kann. > > ### 2. Node-Durchlauf über 15 Ketten und 15 Einzelhaken (gleiches Modell, 1/120 s) > > | Muster | Fenster 1. Seil (2. Haken gefasst) | Fenster 2. Seil (Kante erreicht) | Treffer | > |---|---|---|---| > | Kette seed 7 | 517 ms | **0 ms** | 0/157 | > | Kette seed 13 | 483 ms | **0 ms** | 0/157 | > | Kette seed 42 | 508 ms | **0 ms** | 0/157 | > | Kette seed 31337 | 550 ms | **0 ms** | 0/157 | > | Kette seed 5 | 550 ms | **0 ms** | 0/157 | > | Kette seed 8 | 567 ms | **0 ms** | 0/157 | > | Kette seed 1234 | 567 ms | **0 ms** | 0/157 | > | Kette seed 555 (784 px) | 592 ms | **0 ms** | 0/157 | > | Kette seed 6 | 600 ms | **0 ms** | 0/157 | > | Kette seed 88 | 600 ms | **0 ms** | 0/157 | > | Kette seed 44 | 558 ms | 25 ms | 3/157 | > | Kette seed 555 (810 px) | 708 ms | 67 ms | 8/157 | > | Kette seed 8 (781 px) | 725 ms | 75 ms | 9/157 | > | Kette seed 20240816 | 733 ms | 50 ms | 6/157 | > | Kette seed 99 | 708 ms | 83 ms | 10/157 | > | Einzelhaken, 15 Muster | – | 83–683 ms (median 425 ms) | 10–82/157 | > > **Der erste Haken einer Kette ist verzeihend (median 567 ms), der zweite nicht (median 0 ms).** > Das ist exakt die Rückmeldung: „den ersten schafft man problemlos, beim zweiten schwingt es > nicht mehr weit genug“. > > Zum Vergleich die eigene Toleranzschranke des Projekts: `K.MIN_TAKEOFF_FRAMES = 4` > (Zeile 475) verlangt für Sprünge ≥ 4 aufeinanderfolgende Absprungzeitpunkte — das sind > **67 ms**. Die Kette liegt mit 25–83 ms daran vorbei, in 10 von 15 Mustern unter null. > > ### 3. Warum der zweite Schwung nicht trägt (Geometrie, 15 Ketten, verifizierte Strategie) > > | Größe | 1. Haken | 2. Haken | > |---|---|---| > | Einhangwinkel θ | −52° … −75° | −30° … −63° | > | Seillänge beim Einhang | 92–223 px | **222–225 px** (von `K.GRAB_RADIUS` 225) | > | Scheitel der Schwingung | +20…+22° | +20…+21° | > | Loslasspunkt | +349…+410 | **+654…+702** | > | Restweg bis Landekante | 383–442 px (wird über den 2. Haken überbrückt) | **110–149 px** | > | Höhe der Füße über Boden beim Loslass | – | **26–58 px** | > | Flugzeit / Strecke in dieser Zeit | – | 246–322 ms / 128–190 px | > > Der zweite Haken liegt **191–231 px** vor der Landekante (median 204) – *näher* als der > Greifradius (225). Wer dort einhängt, hängt deshalb praktisch am Ende der Reichweite > (Seil 222–225 px) und hat Vorwärts-Bogen wie Resthöhe gleichzeitig gegen sich: > `L · sin 20° ≈ 77 px` bringt den Körper über den Haken hinaus, es bleiben 110–149 px, > die aus 26–58 px Höhe in 246–322 ms Flug überbrückt werden müssen. Die Rechnung geht in > 5 von 12 Ketten **nicht** auf (Rechenwert −3 bis −21 px), die gemessenen Landungen derselben > Läufe liegen bei −2,6 px und −0,2 px *vor* der Kante – sie überleben nur über die > Landetoleranz `K.EDGE_TOL` 3 / `K.EDGE_LIFT` 6 (Zeilen 522–523). > > Zusätzlich: die Haltezeiten, die an beiden Seilen funktionieren, liegen **auseinander**. > Matrix über τ1 × τ2 (Raster 1/30 s, Seed 7): τ1 = 0,88 s braucht τ2 ∈ 0,22 … 0,68 s; > Seed 99: τ1 = 0,55 s → τ2 ∈ 0,28 … 0,78 s; echtes Dokument Seed 20240816: τ1 = 0,40 s mit > τ2 ∈ 0,40 … 0,55 s, alle anderen τ1-Werte der Matrix: kein einziger Erfolg. Ein Rhythmus, > zwei Aufgaben – das trägt kein Mensch zweimal hintereinander zuverlässig. > > ### 4. Reproduktion (Werkzeug liegt bewusst nicht im Repo) > > Die Sonden sind Einmal-Messwerke (wie bei allen bisherigen Befunden); der verbindliche Test > gehört zum Fix. Recipe: > > 1. `python3 tests/testpage.py` → Testseite mit Bridge. > 2. Welt wie in `resetWorld` aufbauen (`x0 = −400 … START_X + 900`, `gen.frontierX = START_X + 900`), > dann den echten `generateAhead`-Ablauf kopieren (`contextAt → pickKind → fallbackChain → > buildPattern → verifyContent → verifyAllInputs → commitContent`) und das erste Muster mit > `hooks.length === 2` einsammeln (Samen 20240816, Ziel ≥ 3 600 m – Ketten greifen ab > `sectorAbs ≥ 6`, Zeile 1551). > 3. Körper auf `content.x0 − clamp(0.45·v, 150, 235)` setzen, `vx = ctx.speed`, Zustand > `PLAYING`, Autograb auf Standard. > 4. Mit `page.keyboard` die Leertaste tasten: Sprung bei `pit.x0 − strategy.jumpLead`, > `hold` = `strategy.hold` Sekunden gehalten; dann pro Seil nach `τ` Frames loslassen; > Klassifikation nach der **ersten** Bodenberührung (`b.x > pit.x1 + K.SOLVER_MARGIN` = > geschafft, `b.y > GROUND_Y + 60` = im Bruch). > 5. Kontrolle: dieselbe Prozedur mit dem ersten Muster `hooks.length === 1` (Same 555). > > Node-Seite (ohne Browser): das identische Modell mit `CORE.stepBody` und > `K.FIXED_TIMESTEP` über die Samen 7, 13, 42, 31337, 5, 8, 99, 1234, 555, 6, 44, 88, 77, 101, > 2024, 20240816 – Tabelle 2 oben. > > ## Ursache im Code > > 1. **Das Layout prüft Distanzen, aber keine Schwung-Energie.** `buildPattern` (Zeilen 1193–1216) > setzt `spacing = rng.range(252, 286)` und `firstFx = 0.4` relativ zur Grubenbreite und > begrenzt den Haken nur gegen `clamp(hx, e0 + 60, g1 − K.HOOK_EDGE_GUARD)`. > `K.HOOK_EDGE_GUARD = 135` (Zeile 492, die Klammer Zeile 1216 lässt also höchstens > `g1 − 135` zu) schützt seit Issue #6 davor, dass ein Haken *über dem Landepunkt* hängt – es > sichert aber nicht, dass der Schwung *von* diesem Haken aus den Landepunkt erreicht. > Bei 231 px Abstand und vollem Seil ist genau das der Engpass. > 2. **Die Toleranzprüfung kennt das Loslassen nicht.** `verifyContent` (Zeilen 1472–1512) > verlangt `takeoffWindow(…) ≥ K.MIN_TAKEOFF_FRAMES`, und `takeoffWindow` (1455–1467) > verschiebt **ausschließlich `jumpLead`** in Bildratenschritten; für Hakenmuster bleibt > `tol = base` (Zeile 1503), also auch `rel` der Strategie unverändert. `verifyAllInputs` > (1517–1548) verlangt für jedes `(lead, hold)` nur, dass *irgendein* Wert aus > `RELEASES = [0.35, 0.55, 0.18, 0.8, 1.05, "peak"]` (Zeile 1394) durchkommt – sechs > Stichproben, keine Fensterbreite. Ein 42-ms-Fenster besteht beide Prüfungen. > 3. **Der Solver spielt anders als ein Mensch.** `autopilot` lässt bei `theta > rel && omega > 0` > los (Zeile 1295), also am steilsten Punkt, den die Schwingung je hat – und trifft damit > planerisch eine 1°-Kante. Die Scheitelwinkel aller gemessenen zweiten Schwingungen sind > 20–21°, alle verifizierten Ketten-Strategien tragen `rel = 0.35` rad = 20°. Die einzige > verfügbare Toleranz in *Zeit* ist der Moment kurz vor dem Umkehrpunkt. > 4. `attachHook` (Zeilen 714–733) wirft bei jedem Einhang die Radialkomponente der > Geschwindigkeit weg (`b.omega = (v·t) / L`). Am zweiten Haken kommt der Körper aber fast > senkrecht auf das Seil zu (Einhang bei 222–225 px von 225 px Radius) – ein Teil des > Anflugs ist deshalb unwiederbringlich weg, und `SWING_DAMPING = 0.55` (Zeile 493) frisst > auf dem Weg zum Scheitel den Rest. > > ## Folgeverhalten > > * Läufe sterben an einem Muster, das die Generierung als „verifiziert“ in die Welt schreibt – > ab Sektor 6, also genau in dem Bereich, in dem `HOOK_CHAIN` mit Gewicht 14–17 gewürfelt wird > (Zeilen 936–939) und `allowChain` frei ist (Zeile 1551). > * Kein Hinweis für den Spieler, *wann* er loslassen soll: die HUD-Hilfe verspricht > „Haken anklicken/greifen“ und „halten = höher und weiter“, aber die Kette verlangt ein > 40-ms-Fenster am zweiten Seil. > * Die Menü-Demo überlebt das (4 × 150 s, 0 Neustarts), weil sie dieselbe Winkelregel fährt wie > der Solver. **Der Befund ist damit nicht demo-sichtbar, sondern nur spielbar** – ein reiner > Autopilot-Gegentest im Testskript würde ihn weiterhin durchlassen. > > ## Nicht verletzt (bewusst abgegrenzt) > > * Issue #6 hält: `pit.hooks` == tatsächlich verlegte Haken, kein Haken näher als > `K.HOOK_EDGE_GUARD` an der Landekante (gemessen 191–231 px), Rückfall auf einen Haken greift > vor dem Verlegen. Der zweite Haken hängt also nicht über dem Landepunkt – er ist nur zu nah > dafür, von ihm aus die Kante zu erreichen. > * `verifyContent`/`verifyAllInputs`/`takeoffWindow` liefern weiterhin echte Ergebnisse: mit der > Winkelregel des Autopiloten bestehen alle 15 Ketten das Muster: 15/15 bei 1/110 (Solver- > schritt), 15/15 bei 1/120 (Spielschritt) und 15/15 bei 1/60. > * Einzelhaken-Gruben (`PIT_HOOK`, `HOOK_TO_ROLL`) sind von dem Maß nicht betroffen – Kontrolle > oben, median 425 ms Fenster. > > ## Lösungsvorschlag > > 1. **Loslass-Toleranz messen, nicht nur Lösbarkeit.** Analog `takeoffWindow` eine > `releaseWindow(world, content, ctx, s)` – Raster über die *Haltezeit* der letzten Schwingung > (oder über `rel` in Bildraten-Schritten), Erfolg = Landung ≥ `K.SOLVER_MARGIN` hinter der > Landekante, ohne `EDGE_LIFT`-Toleranz. `verifyContent` verlangt für `hooks.length ≥ 2` > zusätzlich `releaseWindow ≥ K.MIN_TAKEOFF_FRAMES`; Muster fallen über das bestehende > `fallbackChain("HOOK_CHAIN")` auf `PIT_HOOK`/`PIT`/`SAFE` zurück. > 2. **Layout an der Schwinggeometrie fixen statt an Flächenmaßen.** Die Kette braucht einen > zweiten Haken, dessen Bogen die Kante *mit Reserve* trägt. Zwei Hebel, beide in `CORE.K`: > Mindestabstand zweiter Haken → Landekante aus `GRAB_RADIUS` ableiten (heute nur > `HOOK_EDGE_GUARD` 135 px, gemessene Praxis 191–231 px) **und** Seillänge begrenzen > (`minJump`-Logik aus Issue #16: sized an der gemessenen Hüllkurve, hier an der gemessenen > Schwunghüllkurve statt an der Sprunghüllkurve). Alternativ: `w`-Bereich unter > `1.6 … 1.85 × jd` und/oder `spacing` oberhalb von ~320 px, damit der zweite Haken höher und > damit der Restweg kürzer wird. > 3. **Was der Solver prüft, muss der Mensch spielen können.** `verifyAllInputs` für Ketten > zusätzlich mit *ein und derselben* Haltezeit an beiden Seilen durchrechnen (Modell > „ein gelernter Rhythmus“). Der Weg fällt genau dort durch, wo er heute noch durchfällt. > 4. Spielbarkeits-Hinweis sichtbar machen (Hilfe/Erststart-Hinweis): an der Kette gilt > „am zweiten Haken früh loslassen“ – oder, besser, das Fenster so weit machen, dass der Satz > nicht nötig ist. > > ## Erwarteter Test > > * `tests/issue22.mjs` – extrahiertes `CORE`, echte Generierung über N Samen: jede in die Welt > geschriebene `HOOK_CHAIN` muss (a) ein Loslassfenster von ≥ `K.MIN_TAKEOFF_FRAMES` > Haltezeiten am letzten Seil haben, (b) mit identischer Haltezeit an beiden Seilen bestehen, > (c) mit ≥ `K.SOLVER_MARGIN` px Landeklarheit landen (nicht über `EDGE_LIFT`). Gegen > `git show HEAD:index.html` sind (a)–(c) rot: 10/15 Ketten ohne jedes Fenster, Fenster 25–83 ms, > Landungen bis −2,6 px vor die Kante. > * `tests/browser_issue22.py` – echtes Dokument, echte Leertaste, Autograb Standard: τ-Sweep > 0,10–1,40 s in 1/120 s über eine real generierte Kette; Erwartung nach dem Fix ≥ 4 > aufeinanderfolgende τ (und Kontrolle Einzelhaken unverändert grün). Wie bei allen > Browser-Suiten: Vorher/Nachher über `PULSE_HTML=$(git show HEAD:index.html > …)`. > > --- > > *Dokumentiert in `issues/22-hakenkette-zweiter-haken-traegt-nicht-ueber-die-grube.md` (Commit `1a9c2f2`).* 1 comments
Author
Owner

Abarbeitet in 025a5bc (gepusht, Doku-Eintrag in gitlab-issues/README.md nachgezogen in 2a119df).

Was geändert wurde:

  • Loslassen ist eine Haltezeit: autopilot lässt nach s.hang los (zählt ab Einhang über b.hangT), ein Wert gilt für beide Seile. Der Winkelzweig (rel) und der peak-Zweig sind ersatzlos weg (Flag ohne Setzer), grab:"late" gestrichen.
  • CORE.releaseWindow(): Fenster aufeinanderfolgender Haltezeiten am letzten Seil (Raster 1/60 s, in der Schrittweite des Laufs), Erfolg = Landung ≥ K.SOLVER_MARGIN hinter der Landekante ohne assistierte Landung. verifyContent verlangt es für Hakenmuster an drei Absprungbildraten, verifyAllInputs verlangt dieselbe Seilhaltezeit für jedes (Absprung, Sprunghaltezeit) – ein Rhythmus, zwei Seile, wie der Befund es fordert.
  • Layout an der Schwinggeometrie: Kettenbreite 1,40–1,52 × jd, erster Haken bei 0,40–0,47 der Breite und 332–368 px hoch, zweiter 14–52 px tiefer und 140–164 px vor der Landekante; Rückfall auf einen Haken bei Hakenabstand < 200 oder > 285 px bzw. < K.HOOK_EDGE_GUARD (Issue #6 hält). Alle Zahlen in CORE.K.
  • Hilfe und Erststart-Hinweis beschriften die Haltezeit am Seil (Lösungsvorschlag 4).

Gemessen nach dem Fix: 159/159 generierte Ketten bestehen verifyContent + verifyAllInputs, Haltezeit-Fenster 6–19 Bildraten (Median 13 = 217 ms – vorher 25–83 ms, in 10/15 Ketten gar keins), Landung ≥ 10,4 px hinter der Kante; echtes Dokument mit echter Leertaste: 55 aufeinanderfolgende Haltezeiten bei 1/120 s (vorher 5 von 156, Fenster 42 ms).

Abdeckung: tests/issue22.mjs (23 Prüfungen, gegen den Befund-Stand 17 rot), tests/browser_issue22.py (15 Prüfungen, vorher 7 rot, darunter exakt das 42-ms-Fenster). Soeben noch einmal die komplette Suite ./tests/run-all.sh gefahren: alle Node-Suiten inkl. Soak und alle Browser-Suiten grün.

Abarbeitet in `025a5bc` (gepusht, Doku-Eintrag in `gitlab-issues/README.md` nachgezogen in `2a119df`). Was geändert wurde: * **Loslassen ist eine Haltezeit**: `autopilot` lässt nach `s.hang` los (zählt ab Einhang über `b.hangT`), ein Wert gilt für beide Seile. Der Winkelzweig (`rel`) und der `peak`-Zweig sind ersatzlos weg (Flag ohne Setzer), `grab:"late"` gestrichen. * **`CORE.releaseWindow()`**: Fenster aufeinanderfolgender Haltezeiten am letzten Seil (Raster 1/60 s, in der Schrittweite des Laufs), Erfolg = Landung ≥ `K.SOLVER_MARGIN` hinter der Landekante ohne assistierte Landung. `verifyContent` verlangt es für Hakenmuster an drei Absprungbildraten, `verifyAllInputs` verlangt dieselbe Seilhaltezeit für jedes (Absprung, Sprunghaltezeit) – ein Rhythmus, zwei Seile, wie der Befund es fordert. * **Layout an der Schwinggeometrie**: Kettenbreite 1,40–1,52 × jd, erster Haken bei 0,40–0,47 der Breite und 332–368 px hoch, zweiter 14–52 px tiefer und 140–164 px vor der Landekante; Rückfall auf einen Haken bei Hakenabstand < 200 oder > 285 px bzw. < `K.HOOK_EDGE_GUARD` (Issue #6 hält). Alle Zahlen in `CORE.K`. * **Hilfe und Erststart-Hinweis** beschriften die Haltezeit am Seil (Lösungsvorschlag 4). Gemessen nach dem Fix: 159/159 generierte Ketten bestehen `verifyContent` + `verifyAllInputs`, Haltezeit-Fenster 6–19 Bildraten (Median 13 = 217 ms – vorher 25–83 ms, in 10/15 Ketten gar keins), Landung ≥ 10,4 px hinter der Kante; echtes Dokument mit echter Leertaste: 55 aufeinanderfolgende Haltezeiten bei 1/120 s (vorher 5 von 156, Fenster 42 ms). Abdeckung: `tests/issue22.mjs` (23 Prüfungen, gegen den Befund-Stand 17 rot), `tests/browser_issue22.py` (15 Prüfungen, vorher 7 rot, darunter exakt das 42-ms-Fenster). Soeben noch einmal die komplette Suite `./tests/run-all.sh` gefahren: alle Node-Suiten inkl. Soak und alle Browser-Suiten grün.
gilli closed this issue 2026-09-04 00:36:28 +02:00
Sign in to join this conversation.
No description provided.