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

Closed
opened 2026-09-04 15:46:51 +02:00 by gilli · 2 comments
Owner

Komponente: CORE (buildPattern-Layout von HOOK_CHAIN Zeilen 1206–1240, grabbable 701–705,
findGrab 706–717, autopilot 1364, verifyContent 1553–1601, takeoffWindow 1497–1510,
releaseWindow 1522–1540, simStrategy 1368–1399), K (GRAB_RADIUS 492, CHAIN_* 504–513,
MIN_TAKEOFF_FRAMES 476)
Betroffene Version: main @ 86a481d, index.html (3.684 Zeilen, MD5 777d18c3edbb834aa41fccc7d10a7d18)

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. Ob grabbable() (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: simStrategy prüft jedes Muster ab einem
kanonischen Start auf Boden (content.x0 − clamp(0,45·v, 150, 235)), der Lauf übergibt das
Muster 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.hitTest oder y > K.FALL_DEATH_Y):
sieben Läufe à 5 Minuten, vier Tode, drei davon in einer HOOK_CHAIN — bei
12 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 per
Design breiter ist als jeder Sprung.

Messprotokoll

Node gegen das extrahierte CORE (tests/harness.mjs) — dieselbe Simulation, dieselben
Konstanten, dieselbe Schrittweite wie der Lauf. Weltbau identisch zu resetWorld
(x0 = −400 … START_X + 900, gen.frontierX = START_X + 900), Muster über den echten Pfad
generateAhead (contextAt → pickKind → fallbackChain → buildPattern → verifyContent → verifyAllInputs → commitContent), onCommit nimmt res.best.strategy ins Muster wie updateDemo.

1. Sieben Läufe à 5 Minuten (Seed, Tod, Stelle)

Seed Ergebnis Tod Ort
4242 gestorben HOOK_CHAIN, fell t 276,34 s · x 171 745 · 17 114 m · Sektor 39
777 gestorben HOOK_CHAIN, fell t 202,37 s · 12 482 m · Sektor 28
20260904 gestorben HOOK_CHAIN, fell t 287,35 s · 17 748 m · Sektor 40
555 gestorben Kollision an JUMP siehe Befund #24 (andere Ursache)
31337 · 1 · 2 · 90210 überlebt 300 s – 18 531 … 18 714 m

Alle Ketten-Tode: Strategie der Muster enthält grab:"auto", hang: 0.44; grabs = 1;
verifyContent hatte das Muster mit window ≥ 4 und release ≥ 4 als spielbar in die Welt
geschrieben.

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 × jumpDistance 480),
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 140 und weit über K.HOOK_EDGE_GUARD 135. #22 hält hier
also in jeder Hinsicht.

Kanonischer Solverlauf (Start auf Boden, x0 − 235) Lebendlauf (Ankunft vom vorigen Muster)
Muster-Strategie {grab:"auto", hang:0.44, jumpLead:26, hold:0.1} dieselbe
1. Haken gefasst bei 171 233 gefasst
Loslasspunkt (hangT 0,442) x 171 442, y 428, vx 498, vy −110 x 171 439, y 429, vy −108
kleinster Abstand zu Haken 2 224,2 px → Griff 225,8 px → kein Griff
Schritte im Reichweitenkreis 3 von 120 = 25 ms 0
Ausgang landet 171 910 ✓ fällt bei 171 758 in die Grube, ev.fell bei 171 745

Drei 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):

Tempo Ketten Margen (px)
≈ 430 (Sektor 1–3) 14 −10,4 · −1,3 · −1,2 · −1,1 · −1,0 · −0,9 · −0,8 · −0,7 · −0,6 · −0,6 · −0,5 · −0,4 · −0,2 · −0,1
≈ 520 (Sektor 5–6) 14 −2,8 · −2,7 · −2,1 · −1,8 · −1,6 · −1,5 · −1,4 · −0,7 · −0,6 · −0,3 · −0,2 · −0,1 · −0,1 · 0,0
640 (Sektor 8+, Endlos) 14 −2,7 · −2,1 · −2,0 · −1,8 · −1,5 · −1,5 · −1,2 · −1,0 · −0,9 · −0,9 · −0,8 · −0,7 · −0,4 · −0,3

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:

Prüfung Wert Bedeutung
verifyContent ok: true in die Welt geschrieben
takeoffWindow 13 (≥ K.MIN_TAKEOFF_FRAMES 4) „13 Absprungzeitpunkte treffen“
releaseWindow 11 „11 Haltezeiten treffen“
verifyAllInputs true alle (lead, hold)-Kombinationen schaffbar
1-Px-Scan derselben Achse (jumpLead um ±107 px = ±10 Bildraten, Schritt 1 px) 122 bestanden, 93 gestorben 43 % der Pixel-Positionen im als sicher gemessenen Band sterben

Alle 93 Fehlschläge haben grabs = 1 und Einflugabstände ≥ 227 px — wieder der zweite Haken.
takeoffWindow rastet in 1/60 s (Zeile 1500: step = ctx.speed / 60) und trifft mit seinen
11 Stichproben genau die Glücklichen Pixel; zwischen ihnen liegen die tödlichen.

Ursache im Code

  1. Layout zielt auf „streift den Kreis“, nicht auf „schneidet den Kreis“.
    buildPattern (Zeilen 1209–1239) setzt Grubenbreite w = rng.range(1,40, 1,52) · jd,
    zweiten Haken g1 − rng.range(140, 164) und Höhe chainH2 = 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 true wird (Zeilen 701–705: d <= K.GRAB_RADIUS, eine Stichprobe pro
    Simulationsschritt, keine Marge).
  2. Keine Prüfung kennt den Einflug. simStrategy (1368–1380) startet immer auf Boden bei
    content.x0 − clamp(0,45·v, 150, 235); takeoffWindow (1497) und releaseWindow (1522)
    variieren innerhalb dieses Startes jumpLead bzw. hang. Der Lauf übergibt das Muster in
    einem 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 dem
    lebenden Zustand — aber nur die Menü-Demo ruft sie, der Lauf pfad hat kein Äquivalent.
  3. Die Kette ist ein Verstärker. Der erste Schwung wandelt einen halben Pixel Einflug in
    drei Pixel am Loslasspunkt (§2). Am ersten Seil ist das verzeihend (#22: Fenster bis 733 ms),
    am zweiten entscheidet es über 1,6 px.
  4. Das Raster der Toleranzprüfung ist gröber als die tödliche Struktur. 1/60 s bei 640 px/s
    sind 10,7 px; §4 zeigt Tode innerhalb eines als 13 Bildraten breit gemessenen Fensters.

Folgeverhalten

  • Läufe enden in einer Endlos-Runde (Sektor 28–40) an einem Muster, das die Generierung selbst
    als spielbar eingetragen hat — ohne jede Möglichkeit, es durch Können zu retten: Die Grube ist
    1,40–1,52 × jumpDistance breit (Issue #16: bewusst breiter als jeder Sprung), es gibt nichts
    zu springen, und der zweite Haken ist außerhalb von 225 px.
  • Der Spieler sieht dafür nicht einmal einen Grund: Der Greif-Halo (§ Befund #26) und
    el.grabHint melden den zweiten Haken als erreichbar, solange er im Kreis liegt — die
    Einflugphase, an der es hängt, ist nirgends ablesbar.
  • Die Menü-Demo zeigt den Befund nicht: sie plant an jeder Grenze mit liveStrategy() neu
    und nimmt die erste Strategie, die ab dem lebenden Zustand überlebt. Ein reiner Demo-Gegentest
    lässt ihn weiter durch (dieselbe Blindheit wie bei #22).
  • Der nächtliche Soak sieht es nicht, weil seine Tod-Erkennung seit #21 nicht mehr existiert
    (Befund #25) — node tests/soak.mjs 5 meldet für dieselbe Seed ok 5 Minuten Spielzeit ohne Tod.

Nicht verletzt (bewusst abgegrenzt)

  • Issue #22 hält vollständig: Loslassen ist eine Haltezeit, releaseWindow misst 11, die
    Fenstergrößen der Muster-Strategie liegen mittig, ein Rhythmus trägt beide Seile.
  • Issue #6 hält: zweiter Haken 143 px vor der Landekante (≥ K.CHAIN_EDGE_REST_MIN 140 und
    ≥ K.HOOK_EDGE_GUARD 135), pit.hooks == verlegte Haken, Rückfall entscheidet vor dem Verlegen.
  • Issue #16 hält: Ketten-Gruben bleiben breiter als jede Sprungbahn, verifyContent verlangt
    weiterhin K.MIN_TAKEOFF_FRAMES.
  • Der erste Haken ist und bleibt verzeihend; die Kette stirbt nie am ersten Seil.

Lösungsvorschlag

  1. Marge statt Berührung. Das Layout darf eine Kette nur verlegen, wenn der zweite Haken mit
    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 eine reachMargin(world, content, ctx, s) analog
      releaseWindow, verifyContent verlangt reachMargin ≥ K.CHAIN_GRAB_MARGIN; oder
    • die Verifikation prüft mit verkleinertem Radius (GRAB_RADIUS − MARGE) — dann ist
      „bestanden“ automatisch „mit Luft bestanden“.
      Danach K.CHAIN_EDGE_REST_*, CHAIN_HOOK_H_*, CHAIN_W_OF_JD_* an dieser Marge neu
      ausmessen (wie in #22 an der Fensterbreite gemessen wurde) — nicht an Flächen.
  2. Einflug in die Prüfung aufnehmen. verifyContent (oder eine neue
    verifySequence) prüft jedes Muster zusätzlich ab einer handvoll Erwartungs-Zustände an der
    Grenze: 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.
  3. Raster verschärfen. takeoffWindow/releaseWindow in halben Bildraten (1/120 s) statt
    1/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.
  4. Was der Lauf tut, muss geprüft sein. Der Lauf könnte an jeder Muster-Grenze mit liveStrategy() die überlebende Strategie wählen (die Demo tut es, der
    Lauf nicht). Findet er keine, war das Muster unverlegbar — genau die Auskunft, die die
    Generierung jetzt noch nicht hat.

Erwarteter Test

  • tests/issue23.mjs — extrahiertes CORE, echte Generierung über N Samen:
    (a) jede committed HOOK_CHAIN hat 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_FRAMES bestandene Absprünge bei 1-Px-Raster um die
    Muster-Lead, (c) 5-Minuten-Lebendlauf mit korrekter Tod-Erkennung (siehe #25) über mehrere
    Samen ohne Tod. Gegen git show HEAD:index.html rot: Margen 0,0…2,8 px, 93/215 Tode im
    Scanband, 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 (Commit 86a481d).

**Komponente:** CORE (`buildPattern`-Layout von `HOOK_CHAIN` Zeilen 1206–1240, `grabbable` 701–705, `findGrab` 706–717, `autopilot` 1364, `verifyContent` 1553–1601, `takeoffWindow` 1497–1510, `releaseWindow` 1522–1540, `simStrategy` 1368–1399), `K` (`GRAB_RADIUS` 492, `CHAIN_*` 504–513, `MIN_TAKEOFF_FRAMES` 476) **Betroffene Version:** `main` @ `86a481d`, `index.html` (3.684 Zeilen, MD5 `777d18c3edbb834aa41fccc7d10a7d18`) ## 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. Ob `grabbable()` (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: `simStrategy` prüft jedes Muster ab einem **kanonischen Start auf Boden** (`content.x0 − clamp(0,45·v, 150, 235)`), der Lauf übergibt das Muster 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.hitTest` oder `y > K.FALL_DEATH_Y`): **sieben Läufe à 5 Minuten, vier Tode, drei davon in einer `HOOK_CHAIN`** — bei 12 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 per Design breiter ist als jeder Sprung. ## Messprotokoll Node gegen das extrahierte `CORE` (`tests/harness.mjs`) — dieselbe Simulation, dieselben Konstanten, dieselbe Schrittweite wie der Lauf. Weltbau identisch zu `resetWorld` (`x0 = −400 … START_X + 900`, `gen.frontierX = START_X + 900`), Muster über den echten Pfad `generateAhead` (`contextAt → pickKind → fallbackChain → buildPattern → verifyContent → verifyAllInputs → commitContent`), `onCommit` nimmt `res.best.strategy` ins Muster wie `updateDemo`. ### 1. Sieben Läufe à 5 Minuten (Seed, Tod, Stelle) | Seed | Ergebnis | Tod | Ort | |---|---|---|---| | 4242 | gestorben | `HOOK_CHAIN`, `fell` | t 276,34 s · x 171 745 · 17 114 m · Sektor 39 | | 777 | gestorben | `HOOK_CHAIN`, `fell` | t 202,37 s · 12 482 m · Sektor 28 | | 20260904 | gestorben | `HOOK_CHAIN`, `fell` | t 287,35 s · 17 748 m · Sektor 40 | | 555 | gestorben | Kollision an `JUMP` | siehe Befund #24 (andere Ursache) | | 31337 · 1 · 2 · 90210 | überlebt 300 s | – | 18 531 … 18 714 m | Alle Ketten-Tode: Strategie der Muster enthält `grab:"auto"`, `hang: 0.44`; `grabs = 1`; `verifyContent` hatte das Muster mit `window ≥ 4` und `release ≥ 4` als spielbar in die Welt geschrieben. ### 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 × `jumpDistance` 480), 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 140` und weit über `K.HOOK_EDGE_GUARD 135`. #22 hält hier also in jeder Hinsicht. | | Kanonischer Solverlauf (Start auf Boden, x0 − 235) | Lebendlauf (Ankunft vom vorigen Muster) | |---|---|---| | Muster-Strategie | `{grab:"auto", hang:0.44, jumpLead:26, hold:0.1}` | dieselbe | | 1. Haken | gefasst bei 171 233 | gefasst | | Loslasspunkt (hangT 0,442) | **x 171 442**, y 428, vx 498, vy −110 | **x 171 439**, y 429, vy −108 | | kleinster Abstand zu Haken 2 | **224,2 px** → Griff | **225,8 px** → kein Griff | | Schritte im Reichweitenkreis | **3 von 120 = 25 ms** | 0 | | Ausgang | landet 171 910 ✓ | fällt bei 171 758 in die Grube, `ev.fell` bei 171 745 | Drei 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): | Tempo | Ketten | Margen (px) | |---|---|---| | ≈ 430 (Sektor 1–3) | 14 | −10,4 · −1,3 · −1,2 · −1,1 · −1,0 · −0,9 · −0,8 · −0,7 · −0,6 · −0,6 · −0,5 · −0,4 · −0,2 · −0,1 | | ≈ 520 (Sektor 5–6) | 14 | −2,8 · −2,7 · −2,1 · −1,8 · −1,6 · −1,5 · −1,4 · −0,7 · −0,6 · −0,3 · −0,2 · −0,1 · −0,1 · **0,0** | | 640 (Sektor 8+, Endlos) | 14 | −2,7 · −2,1 · −2,0 · −1,8 · −1,5 · −1,5 · −1,2 · −1,0 · −0,9 · −0,9 · −0,8 · −0,7 · −0,4 · −0,3 | **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: | Prüfung | Wert | Bedeutung | |---|---|---| | `verifyContent` | `ok: true` | in die Welt geschrieben | | `takeoffWindow` | **13** (≥ `K.MIN_TAKEOFF_FRAMES` 4) | „13 Absprungzeitpunkte treffen“ | | `releaseWindow` | **11** | „11 Haltezeiten treffen“ | | `verifyAllInputs` | `true` | alle `(lead, hold)`-Kombinationen schaffbar | | **1-Px-Scan derselben Achse** (`jumpLead` um ±107 px = ±10 Bildraten, Schritt 1 px) | **122 bestanden, 93 gestorben** | 43 % der Pixel-Positionen im als sicher gemessenen Band sterben | Alle 93 Fehlschläge haben `grabs = 1` und Einflugabstände ≥ 227 px — wieder der zweite Haken. `takeoffWindow` rastet in 1/60 s (Zeile 1500: `step = ctx.speed / 60`) und trifft mit seinen 11 Stichproben genau die Glücklichen Pixel; zwischen ihnen liegen die tödlichen. ## Ursache im Code 1. **Layout zielt auf „streift den Kreis“, nicht auf „schneidet den Kreis“.** `buildPattern` (Zeilen 1209–1239) setzt Grubenbreite `w = rng.range(1,40, 1,52) · jd`, zweiten Haken `g1 − rng.range(140, 164)` und Höhe `chainH2 = 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 `true` wird (Zeilen 701–705: `d <= K.GRAB_RADIUS`, eine Stichprobe pro Simulationsschritt, keine Marge). 2. **Keine Prüfung kennt den Einflug.** `simStrategy` (1368–1380) startet immer auf Boden bei `content.x0 − clamp(0,45·v, 150, 235)`; `takeoffWindow` (1497) und `releaseWindow` (1522) variieren *innerhalb* dieses Startes `jumpLead` bzw. `hang`. Der Lauf übergibt das Muster in einem 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 dem **lebenden** Zustand — aber **nur die Menü-Demo ruft sie**, der Lauf pfad hat kein Äquivalent. 3. **Die Kette ist ein Verstärker.** Der erste Schwung wandelt einen halben Pixel Einflug in drei Pixel am Loslasspunkt (§2). Am ersten Seil ist das verzeihend (#22: Fenster bis 733 ms), am zweiten entscheidet es über 1,6 px. 4. **Das Raster der Toleranzprüfung ist gröber als die tödliche Struktur.** 1/60 s bei 640 px/s sind 10,7 px; §4 zeigt Tode *innerhalb* eines als 13 Bildraten breit gemessenen Fensters. ## Folgeverhalten * Läufe enden in einer Endlos-Runde (Sektor 28–40) an einem Muster, das die Generierung selbst als spielbar eingetragen hat — ohne jede Möglichkeit, es durch Können zu retten: Die Grube ist 1,40–1,52 × `jumpDistance` breit (Issue #16: bewusst breiter als jeder Sprung), es gibt nichts zu springen, und der zweite Haken ist außerhalb von 225 px. * Der Spieler sieht dafür nicht einmal einen Grund: Der Greif-Halo (§ Befund #26) und `el.grabHint` melden den zweiten Haken als erreichbar, solange er im Kreis liegt — die Einflugphase, an der es hängt, ist nirgends ablesbar. * **Die Menü-Demo zeigt den Befund nicht**: sie plant an jeder Grenze mit `liveStrategy()` neu und nimmt die erste Strategie, die ab dem lebenden Zustand überlebt. Ein reiner Demo-Gegentest lässt ihn weiter durch (dieselbe Blindheit wie bei #22). * Der nächtliche Soak sieht es nicht, weil seine Tod-Erkennung seit #21 nicht mehr existiert (Befund #25) — `node tests/soak.mjs 5` meldet für dieselbe Seed `ok 5 Minuten Spielzeit ohne Tod`. ## Nicht verletzt (bewusst abgegrenzt) * Issue #22 hält vollständig: Loslassen ist eine Haltezeit, `releaseWindow` misst 11, die Fenstergrößen der Muster-Strategie liegen mittig, ein Rhythmus trägt beide Seile. * Issue #6 hält: zweiter Haken 143 px vor der Landekante (≥ `K.CHAIN_EDGE_REST_MIN` 140 und ≥ `K.HOOK_EDGE_GUARD` 135), `pit.hooks` == verlegte Haken, Rückfall entscheidet vor dem Verlegen. * Issue #16 hält: Ketten-Gruben bleiben breiter als jede Sprungbahn, `verifyContent` verlangt weiterhin `K.MIN_TAKEOFF_FRAMES`. * Der erste Haken ist und bleibt verzeihend; die Kette stirbt nie am ersten Seil. ## Lösungsvorschlag 1. **Marge statt Berührung.** Das Layout darf eine Kette nur verlegen, wenn der zweite Haken mit 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 eine `reachMargin(world, content, ctx, s)` analog `releaseWindow`, `verifyContent` verlangt `reachMargin ≥ K.CHAIN_GRAB_MARGIN`; oder * die Verifikation prüft mit verkleinertem Radius (`GRAB_RADIUS − MARGE`) — dann ist „bestanden“ automatisch „mit Luft bestanden“. Danach `K.CHAIN_EDGE_REST_*`, `CHAIN_HOOK_H_*`, `CHAIN_W_OF_JD_*` an dieser Marge neu ausmessen (wie in #22 an der Fensterbreite gemessen wurde) — nicht an Flächen. 2. **Einflug in die Prüfung aufnehmen.** `verifyContent` (oder eine neue `verifySequence`) prüft jedes Muster zusätzlich ab einer handvoll Erwartungs-Zustände an der Grenze: 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. 3. **Raster verschärfen.** `takeoffWindow`/`releaseWindow` in halben Bildraten (1/120 s) statt 1/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. 4. **Was der Lauf tut, muss geprüft sein.** Der Lauf könnte an jeder Muster-Grenze mit `liveStrategy()` die überlebende Strategie wählen (die Demo tut es, der Lauf nicht). Findet er keine, war das Muster unverlegbar — genau die Auskunft, die die Generierung jetzt noch nicht hat. ## Erwarteter Test * `tests/issue23.mjs` — extrahiertes `CORE`, echte Generierung über N Samen: (a) jede committed `HOOK_CHAIN` hat 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_FRAMES` bestandene Absprünge bei **1-Px-Raster** um die Muster-Lead, (c) 5-Minuten-Lebendlauf mit korrekter Tod-Erkennung (siehe #25) über mehrere Samen ohne Tod. Gegen `git show HEAD:index.html` rot: Margen 0,0…2,8 px, 93/215 Tode im Scanband, 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` (Commit `86a481d`).*
Author
Owner

Behoben in dee2b2c (Doku-Nachtrag a7c7bde), CI-Lauf #11 grün (Node + Browser).

Ursache lag nicht im Kettenlayout. Dieselben Muster lassen sich mit 40 … 90 px Marge
durchfliegen – verifyContent nahm aber die erste Strategie, die überhaupt durchkam, und die
traf im Schnitt mit 11,6 px. Geprüft war nämlich, ob grabbable() in irgendeinem
Simulationsschritt ja sagt: ein Streifen am Kreisrand reichte, entschieden hat die Phase auf dem
1/120-s-Raster. takeoffWindow (13) und releaseWindow (11) meldeten bestanden, weil sie eine
andere 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 des
    Laufs (K.FIXED_TIMESTEP, bewusst nicht K.SIM_DT). Der Griff ist ab dem vermessenen Seil
    ausgeschaltet: 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_RADIUS minus dem ungünstigsten dieser Abstände, über jedes
    Seil des Musters und über ±K.REACH_SCAN_FRAMES (1) Bildraten Lead.
  • verifyContent – nimmt eine Strategie erst ab K.HOOK_GRAB_MARGIN = 40 px (ein Vielfaches des
    Schrittwegs bei K.SPEED_MAX). Das Gate sitzt vor den Fenstern: sechs Bahnen statt
    vierundachtzig, das ist der Preisunterschied. res.best.reach trägt die Zahl als Diagnose.
  • Gilt für alle Hakenmuster, nicht nur Ketten: ein Streifen ist an einem Einzelhaken derselbe
    Wurf (vorher 9,1 px).
  • Keine Layout-Zahl angefasst – Kettenbreite, Hakenhöhen, Rückfall auf ein Seil bleiben wie in
    #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

Größe vorher nachher
Marge beider Seile, generierte Ketten (495 Muster, 6 Samen) 4,1 … 23,6 px (Ø 11,6) 40,2 … 50,4 px (Ø 43,8)
Marge Einzelhaken-Muster 9,1 … 107,3 px 40,9 … 107,3 px
tragendes Band um die Muster-Lead (1-px-Raster) 3,5 Bildraten 12,3 … 20,2 Bildraten
Tode ±1 Bildrate um die Muster-Lead viele 0 von 21 Pixeln je Kette
Seed 4242 / 777 / 20260904 (5 min) Tod bei 276,34 s / 202,37 s / 287,35 s überleben, 18 246 / 18 323 / 18 253 m
Ketten unter 495 Mustern 15 14 (Artenmischung unverändert)
Generierung 495 Muster 363 ms 439 ms
teuerster generateAhead-Aufruf eines Laufs 11,7 ms 16,1 ms

Von 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.hitTest bzw.
y > K.FALL_DEATH_Y und nicht über das tote body.dead (#25).

Tests

  • tests/issue23.mjs – 21 Prüfungen, vorher 13 rot. Misst die Marge unabhängig nach statt sie
    aus dem Solver abzulesen (Nachmessung muss zu CORE.reachMargin auf 0,5 px stimmen), fährt das
    1-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 + takeoffWindow
    • releaseWindow – spielbar und wird von verifyContent verworfen; Gegentest: dieselbe Kette
      wird 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, stillgelegter
    Loop: 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).
  • Beide Suiten hängen in tests/run-all.sh; ./tests/run-all.sh 1 meldet „alle Suiten grün“
    (17 Node-Suiten + Soak, 16 Browser-Suiten).
Behoben in `dee2b2c` (Doku-Nachtrag `a7c7bde`), CI-Lauf #11 grün (Node + Browser). **Ursache lag nicht im Kettenlayout.** Dieselben Muster lassen sich mit 40 … 90 px Marge durchfliegen – `verifyContent` nahm aber die *erste* Strategie, die überhaupt durchkam, und die traf im Schnitt mit 11,6 px. Geprüft war nämlich, ob `grabbable()` in *irgendeinem* Simulationsschritt ja sagt: ein Streifen am Kreisrand reichte, entschieden hat die Phase auf dem 1/120-s-Raster. `takeoffWindow` (13) und `releaseWindow` (11) meldeten bestanden, weil sie eine andere 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 des Laufs (`K.FIXED_TIMESTEP`, bewusst nicht `K.SIM_DT`). Der Griff ist ab dem vermessenen Seil ausgeschaltet: 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_RADIUS` minus dem ungünstigsten dieser Abstände, über **jedes** Seil des Musters und über ±`K.REACH_SCAN_FRAMES` (1) Bildraten Lead. * `verifyContent` – nimmt eine Strategie erst ab `K.HOOK_GRAB_MARGIN` = 40 px (ein Vielfaches des Schrittwegs bei `K.SPEED_MAX`). Das Gate sitzt **vor** den Fenstern: sechs Bahnen statt vierundachtzig, das ist der Preisunterschied. `res.best.reach` trägt die Zahl als Diagnose. * Gilt für **alle** Hakenmuster, nicht nur Ketten: ein Streifen ist an einem Einzelhaken derselbe Wurf (vorher 9,1 px). * **Keine Layout-Zahl angefasst** – Kettenbreite, Hakenhöhen, Rückfall auf ein Seil bleiben wie in #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** | Größe | vorher | nachher | |---|---|---| | Marge beider Seile, generierte Ketten (495 Muster, 6 Samen) | 4,1 … 23,6 px (Ø 11,6) | 40,2 … 50,4 px (Ø 43,8) | | Marge Einzelhaken-Muster | 9,1 … 107,3 px | 40,9 … 107,3 px | | tragendes Band um die Muster-Lead (1-px-Raster) | 3,5 Bildraten | 12,3 … 20,2 Bildraten | | Tode ±1 Bildrate um die Muster-Lead | viele | 0 von 21 Pixeln je Kette | | Seed 4242 / 777 / 20260904 (5 min) | Tod bei 276,34 s / 202,37 s / 287,35 s | überleben, 18 246 / 18 323 / 18 253 m | | Ketten unter 495 Mustern | 15 | 14 (Artenmischung unverändert) | | Generierung 495 Muster | 363 ms | 439 ms | | teuerster `generateAhead`-Aufruf eines Laufs | 11,7 ms | 16,1 ms | Von 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.hitTest` bzw. `y > K.FALL_DEATH_Y` und nicht über das tote `body.dead` (#25). **Tests** * `tests/issue23.mjs` – 21 Prüfungen, **vorher 13 rot**. Misst die Marge unabhängig nach statt sie aus dem Solver abzulesen (Nachmessung muss zu `CORE.reachMargin` auf 0,5 px stimmen), fährt das 1-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 + `takeoffWindow` + `releaseWindow` – spielbar und wird von `verifyContent` verworfen; Gegentest: dieselbe Kette wird 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, stillgelegter Loop: 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). * Beide Suiten hängen in `tests/run-all.sh`; `./tests/run-all.sh 1` meldet „alle Suiten grün“ (17 Node-Suiten + Soak, 16 Browser-Suiten).
Author
Owner

Schließe nach Verifikation: Fix dee2b2c, Tests tests/issue23.mjs (vorher 13 rot) und tests/browser_issue23.py (vorher 5 rot), CI-Lauf #11 grün auf beiden Jobs. Details im Kommentar oben.

Schließe nach Verifikation: Fix `dee2b2c`, Tests `tests/issue23.mjs` (vorher 13 rot) und `tests/browser_issue23.py` (vorher 5 rot), CI-Lauf #11 grün auf beiden Jobs. Details im Kommentar oben.
gilli closed this issue 2026-09-04 22:24:40 +02:00
Sign in to join this conversation.
No description provided.