Menü-Demo rammt jeden Block und startet alle paar Sekunden neu: jumpLead 26 gehört nicht zur getesteten Strategie #15

Closed
opened 2026-09-01 16:40:48 +02:00 by gilli · 1 comment
Owner

⁨⁩⁨Menü-Demo rammt jeden Block und startet alle paar Sekunden neu: jumpLead 26 gehört nicht zur getesteten Strategie⁩ ⁨⁩#⁨15⁩⁨⁩"
By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩
bug
game-feel
medium

Labels: ~bug ~game-feel ~medium
Komponente: GAME (startDemo, simStepDemo), CORE (autopilot, verifyContent)
Betroffene Version: main @ 552557e, index.html Zeilen 1189–1214, 1245–1271, 1296–1301, 2599–2615

Beschreibung

Der Attract-Hintergrund des Hauptmenüs fährt mit einer fest eingetragenen Strategie durch
die generierte Welt:

// index.html:2611  startDemo()
strat: { grab: "auto", rel: 0.42, jumpLead: 26, hold: K.MAX_JUMP_HOLD_TIME, reel: false, rollLead: 108 }

jumpLead: 26 heißt: gesprungen wird, wenn die Blockkante 26 px vor der Figur liegt
(autopilot, Zeile 1210–1211). Bei den in der Demo anliegenden Geschwindigkeiten (350–370 px/s
in den ersten hundert Metern) sind das 70–75 ms vor der Kollision – die Figur ist beim Absprung
schon in der Blockwand. Die Generierung prüft jedes Muster nur gegen Strategien aus
strategyList() mit JUMP_LEADS = [10, 26, 52, 86, 124, 168] und verwirft das Ergebnis:
verifyContent() liefert { ok, best: { strategy, result } }, generateAhead() wertet aber nur
res.ok aus (Zeile 1425) – die geprüfte Strategie landet nirgends. Die Demo muss deshalb mit
einem Wert vorliebnehmen, der für ihre Muster nicht passt.

Zwei Nebenbefunde derselben Stelle:

  • strat.holdUntil ist mit K.MAX_JUMP_HOLD_TIME (0,18) initialisiert. autopilot liest
    if (t < s.holdUntil) cmd.jumpHeld = true (Zeile 1189) – die Demo hält die Sprungtaste in den
    ersten 0,18 s Demo-Zeit immer, unabhängig davon, ob gesprungen wurde. Der Solver hält dieses
    Feld dagegen in einem separaten st = { holdUntil: -1 } (Zeile 1229); die Demo übergibt d
    für beide Stellen, ihr Feld d.holdUntil: -1 (Zeile 2612) wird nie gelesen.
  • grab: "auto" ist damit faktisch wirkungslos: die Demo erlebt in ihrem kurzen Leben nie eine
    Grube (Tod bei mittleren 1455 px, Gruben liegen dahinter) – 0 Greifvorgänge, 0
    HANGING-Schritte in 30 s.

Messung

Headless-Chromium, requestAnimationFrame für die deterministischen Messungen stillgelegt,
Schritt 1/120, Zähler = Same von startDemo() nachgebaut.

Echtes Spiel, echter Loop, 30 s Menü: 5 Neustarts. Kamerasprung bei jedem Neustart:
camX 1213 → 318, 2543 → 318, 4178 → 318 – der komplette Hintergrund snappt zurück an den
Anfang. 167 Schritte Rollen, 9 Schritte Luft, 0 Schritte am Seil.

Deterministisch, Same 1…30, 25 s, je Demo-Strategie:

jumpLead gestorben mittlere Strecke Todesursache
26 (ausgeliefert) 30 / 30 1 455 px (≈ 2,5 s) hit gegen BLOCK, 30/30
52 17 / 30 8 208 px hit gegen BLOCK
86 0 / 30 9 207 px –
124 2 / 30 8 752 px hit gegen BLOCK
168 11 / 30 6 679 px hit gegen BLOCK

Die Muster sind lösbar, nur die Demo-Strategie nicht. Mini-Solver an der konkreten
Todesstelle (Start 0,45 · Tempo vor dem Hindernis, wie simStrategy, alle 54 Einträge aus
strategyList(false, true)):

Same Hindernis Tempo Strategien, die es schaffen Demo-Strategie
1 BLOCK x 2253, 38 × 54 369 px/s 36 / 54 hit bei x 2241
4 BLOCK x 1918, 56 × 53 360 px/s 36 / 54 hit bei x 1906
7 BLOCK x 2170, 55 × 58 366 px/s 36 / 54 hit bei x 2158
12 BLOCK x 1556, 34 × 64 350 px/s 36 / 54 hit bei x 1545

Ein größerer Wert allein ist keine Lösung (Tabelle oben: 168 px stirbt öfter als 124 px) –
es braucht die zum Muster gehörende Strategie. 60 s mit jumpLead: 86, Same 1…20: 5/20 gestorben,
dabei 19 Greifvorgänge, 2 080 HANGING-Schritte, 14 passierte Gruben, 9 passierte Haken. Die
Seilmechanik, die das Menü zeigen soll, erscheint also erst, wenn die Demo überhaupt soweit kommt.

Auswirkung

  • Der Hauptmenü-Hintergrund ist kein Lauf, sondern ein Flackern: alle 2–6 s springt die Szene
    zurück an den Startpunkt.
  • Die Demo zeigt nie, was das Spiel ausmacht – Sprünge über Gruben und Schwünge an Haken kommen
    im Menü praktisch nie vor (hang = 0).
  • Irreführende Qualitätsanzeige: „Der Autopilot beherrscht die Welt“ ist die naheliegende
    Annahme beim Blick ins Menü; gemessen stirbt er an jedem vierten Muster.
  • Die Demo taugt auch nicht als Regressionsfänger: Sie erreicht die Stellen nicht, an denen
    Seil-, Gruben- und Landungslogik greifen.

Lösungsvorschlag

  1. Strategie pro Muster durchreichen. generateAhead() kennt res.best.strategy bereits;
    über onCommit(placed, ctx, world) (oder ein Feld placed.strategy) in die Welt tragen und
    der Demo beim jeweils nächsten Muster geben. Dann fährt die Demo exakt die Strategie, gegen
    die das Muster verifiziert wurde – dieselbe, die soak.mjs schon verwendet.
  2. Oder: vor jedem Muster verifyContent()/strategyList() für den Ausschnitt laufen lassen
    und die erste bestehende Strategie verwenden (teurer, aber ohne Änderung an der Generierung).
  3. Auf jeden Fall strat.holdUntil auf -1 stellen und die Haltezeit nicht in das geteilte
    Strategieobjekt schreiben lassen (st wie im Solver), damit cmd.jumpHeld nicht aus der
    Initialisierung heraus dauerhaft true ist.
  4. d.holdUntil (Zeile 2612) ist toter Code und kann weg.

Tests

Eine neue Prüfung muss nicht die Physik, sondern das Überleben messen: Demo über N Samen ×
30 s, gezählt werden Neustarts, hit-Ereignisse und erreichte Strecke. Schranken:
0 Neustarts pro 30 s und mindestens ein Seilkontakt (HANGING > 0) in derselben Frist.
Gegen die Auslieferung gemessen: 5 Neustarts in 30 s, HANGING = 0.


Dokumentiert in issues/15-menue-demo-rammt-block-und-laeuft-neu.md (Commit e7185b8).

1 comments

⁨⁩⁨Menü-Demo rammt jeden Block und startet alle paar Sekunden neu: jumpLead 26 gehört nicht zur getesteten Strategie⁩ ⁨⁩#⁨15⁩⁨⁩" By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩ bug game-feel medium > > **Labels:** ~bug ~game-feel ~medium > **Komponente:** GAME (`startDemo`, `simStepDemo`), CORE (`autopilot`, `verifyContent`) > **Betroffene Version:** `main` @ `552557e`, `index.html` Zeilen 1189–1214, 1245–1271, 1296–1301, 2599–2615 > > ## Beschreibung > > Der Attract-Hintergrund des Hauptmenüs fährt mit **einer** fest eingetragenen Strategie durch > die generierte Welt: > > ```js > // index.html:2611 startDemo() > strat: { grab: "auto", rel: 0.42, jumpLead: 26, hold: K.MAX_JUMP_HOLD_TIME, reel: false, rollLead: 108 } > ``` > > `jumpLead: 26` heißt: gesprungen wird, wenn die Blockkante **26 px** vor der Figur liegt > (`autopilot`, Zeile 1210–1211). Bei den in der Demo anliegenden Geschwindigkeiten (350–370 px/s > in den ersten hundert Metern) sind das 70–75 ms vor der Kollision – die Figur ist beim Absprung > schon in der Blockwand. Die Generierung prüft jedes Muster nur gegen Strategien aus > `strategyList()` mit `JUMP_LEADS = [10, 26, 52, 86, 124, 168]` und verwirft das Ergebnis: > `verifyContent()` liefert `{ ok, best: { strategy, result } }`, `generateAhead()` wertet aber nur > `res.ok` aus (Zeile 1425) – die geprüfte Strategie landet nirgends. Die Demo muss deshalb mit > einem Wert vorliebnehmen, der für ihre Muster nicht passt. > > Zwei Nebenbefunde derselben Stelle: > > * `strat.holdUntil` ist mit `K.MAX_JUMP_HOLD_TIME` (0,18) initialisiert. `autopilot` liest > `if (t < s.holdUntil) cmd.jumpHeld = true` (Zeile 1189) – die Demo hält die Sprungtaste in den > ersten 0,18 s Demo-Zeit immer, unabhängig davon, ob gesprungen wurde. Der Solver hält dieses > Feld dagegen in einem separaten `st = { holdUntil: -1 }` (Zeile 1229); die Demo übergibt `d` > für beide Stellen, ihr Feld `d.holdUntil: -1` (Zeile 2612) wird nie gelesen. > * `grab: "auto"` ist damit faktisch wirkungslos: die Demo erlebt in ihrem kurzen Leben nie eine > Grube (Tod bei mittleren 1455 px, Gruben liegen dahinter) – **0** Greifvorgänge, **0** > HANGING-Schritte in 30 s. > > ## Messung > > Headless-Chromium, `requestAnimationFrame` für die deterministischen Messungen stillgelegt, > Schritt 1/120, Zähler = Same von `startDemo()` nachgebaut. > > **Echtes Spiel, echter Loop, 30 s Menü:** 5 Neustarts. Kamerasprung bei jedem Neustart: > `camX` 1213 → 318, 2543 → 318, 4178 → 318 – der komplette Hintergrund snappt zurück an den > Anfang. 167 Schritte Rollen, **9** Schritte Luft, **0** Schritte am Seil. > > **Deterministisch, Same 1…30, 25 s, je Demo-Strategie:** > > | `jumpLead` | gestorben | mittlere Strecke | Todesursache | > |---|---|---|---| > | 26 (ausgeliefert) | **30 / 30** | 1 455 px (≈ 2,5 s) | `hit` gegen BLOCK, 30/30 | > | 52 | 17 / 30 | 8 208 px | `hit` gegen BLOCK | > | 86 | **0 / 30** | 9 207 px | – | > | 124 | 2 / 30 | 8 752 px | `hit` gegen BLOCK | > | 168 | 11 / 30 | 6 679 px | `hit` gegen BLOCK | > > **Die Muster sind lösbar, nur die Demo-Strategie nicht.** Mini-Solver an der konkreten > Todesstelle (Start 0,45 · Tempo vor dem Hindernis, wie `simStrategy`, alle 54 Einträge aus > `strategyList(false, true)`): > > | Same | Hindernis | Tempo | Strategien, die es schaffen | Demo-Strategie | > |---|---|---|---|---| > | 1 | BLOCK x 2253, 38 × 54 | 369 px/s | 36 / 54 | `hit` bei x 2241 | > | 4 | BLOCK x 1918, 56 × 53 | 360 px/s | 36 / 54 | `hit` bei x 1906 | > | 7 | BLOCK x 2170, 55 × 58 | 366 px/s | 36 / 54 | `hit` bei x 2158 | > | 12 | BLOCK x 1556, 34 × 64 | 350 px/s | 36 / 54 | `hit` bei x 1545 | > > **Ein größerer Wert allein ist keine Lösung** (Tabelle oben: 168 px stirbt öfter als 124 px) – > es braucht die zum Muster gehörende Strategie. 60 s mit `jumpLead: 86`, Same 1…20: 5/20 gestorben, > dabei 19 Greifvorgänge, 2 080 HANGING-Schritte, 14 passierte Gruben, 9 passierte Haken. Die > Seilmechanik, die das Menü zeigen soll, erscheint also erst, wenn die Demo überhaupt soweit kommt. > > ## Auswirkung > > * Der Hauptmenü-Hintergrund ist kein Lauf, sondern ein Flackern: alle 2–6 s springt die Szene > zurück an den Startpunkt. > * Die Demo zeigt nie, was das Spiel ausmacht – Sprünge über Gruben und Schwünge an Haken kommen > im Menü praktisch nie vor (`hang = 0`). > * Irreführende Qualitätsanzeige: „Der Autopilot beherrscht die Welt“ ist die naheliegende > Annahme beim Blick ins Menü; gemessen stirbt er an jedem vierten Muster. > * Die Demo taugt auch nicht als Regressionsfänger: Sie erreicht die Stellen nicht, an denen > Seil-, Gruben- und Landungslogik greifen. > > ## Lösungsvorschlag > > 1. **Strategie pro Muster durchreichen.** `generateAhead()` kennt `res.best.strategy` bereits; > über `onCommit(placed, ctx, world)` (oder ein Feld `placed.strategy`) in die Welt tragen und > der Demo beim jeweils nächsten Muster geben. Dann fährt die Demo exakt die Strategie, gegen > die das Muster verifiziert wurde – dieselbe, die `soak.mjs` schon verwendet. > 2. **Oder:** vor jedem Muster `verifyContent()`/`strategyList()` für den Ausschnitt laufen lassen > und die erste bestehende Strategie verwenden (teurer, aber ohne Änderung an der Generierung). > 3. Auf jeden Fall `strat.holdUntil` auf `-1` stellen und die Haltezeit nicht in das geteilte > Strategieobjekt schreiben lassen (`st` wie im Solver), damit `cmd.jumpHeld` nicht aus der > Initialisierung heraus dauerhaft `true` ist. > 4. `d.holdUntil` (Zeile 2612) ist toter Code und kann weg. > > ## Tests > > Eine neue Prüfung muss nicht die Physik, sondern das Überleben messen: Demo über N Samen × > 30 s, gezählt werden Neustarts, `hit`-Ereignisse und erreichte Strecke. Schranken: > **0 Neustarts pro 30 s** und mindestens ein Seilkontakt (`HANGING` > 0) in derselben Frist. > Gegen die Auslieferung gemessen: 5 Neustarts in 30 s, `HANGING` = 0. > > --- > > *Dokumentiert in `issues/15-menue-demo-rammt-block-und-laeuft-neu.md` (Commit `e7185b8`).* 1 comments
Author
Owner

Behoben in 8a4d04b (Doku 42f3cce).

Geändert:

  1. generateAhead() trägt die Strategie, gegen die verifyContent bestanden hat, als placed.strategy ins Muster; startDemo/updateDemo sammeln sie in d.plans und schalten bei x0 − 0,45 · Tempo (dem Solver-Startpunkt) um.
  2. Da die Muster-Strategie gegen den Solver-Start auf Boden verifiziert ist, die Demo aber oft mitten im Flug mit mitgebrachtem Schwung ankommt: CORE.liveStrategy() entscheidet am Grenzpunkt per CORE.simProbe() (Haken geklont, weil releaseHook coolT schreibt) über die erste Strategie, die ab dem lebenden Zustand bis hinter das Muster überlebt. Ohne diesen Schritt starben die Samen ~14–29 kpx an Musterübergängen (Ankunftsphase weicht vom Solver-Plan ab).
  3. Demo-Tempo jetzt speedAt() ohne den 0,9-Abschlag: die Generierung verifiziert gegen ctx.speed = speedAt, bei 0,9× sprang die Figur zu kurz (Same 1: Tod am ersten Block hinter dem Hook-Gate bei 2 047 m).
  4. autopilot liest/schreibt holdUntil im Solver-st, nicht im geteilten Strategieobjekt; d.holdUntil (toter Code) ist weg, die Demo führt ein eigenes d.hold.

Verifikation: tests/issue15.mjs — vorher rot (Nachbau der Auslieferung: 69 Neustarts in 10×30 s, HANGING 0, Mitteldistanz 515 px), jetzt grün: 0 Neustarts in 10×60 s, HANGING 3 346 Schritte (erster Seilkontakt ~35–40 s, Haken sind erst ab Sektor 5 frei — daher 60 s statt 30 s als Messfrist), Mitteldistanz 34,6 kpx. Zusätzlich: content.strategy hängt an allen verifizierten Mustern (SAFE-Notfall ausgenommen), Muster-Strategien bleiben nach einem Demo-Lauf frei von holdUntil. tests/browser_issue10.py folgt dem neuen startDemo-Aufbau; gesamte Suite (./tests/run-all.sh) grün.

Nebenbefund zur Test-Historie: soak.mjs prüft Tod nur über body.dead (Sturz), nicht über hitTest — sein „5 Minuten ohne Tod“ hat Blocktreffer nie gesehen. Nicht Gegenstand dieses Fixes.

Behoben in `8a4d04b` (Doku `42f3cce`). **Geändert:** 1. `generateAhead()` trägt die Strategie, gegen die `verifyContent` bestanden hat, als `placed.strategy` ins Muster; `startDemo`/`updateDemo` sammeln sie in `d.plans` und schalten bei `x0 − 0,45 · Tempo` (dem Solver-Startpunkt) um. 2. Da die Muster-Strategie gegen den Solver-Start auf Boden verifiziert ist, die Demo aber oft mitten im Flug mit mitgebrachtem Schwung ankommt: `CORE.liveStrategy()` entscheidet am Grenzpunkt per `CORE.simProbe()` (Haken geklont, weil `releaseHook` `coolT` schreibt) über die erste Strategie, die ab dem lebenden Zustand bis hinter das Muster überlebt. Ohne diesen Schritt starben die Samen ~14–29 kpx an Musterübergängen (Ankunftsphase weicht vom Solver-Plan ab). 3. Demo-Tempo jetzt `speedAt()` ohne den 0,9-Abschlag: die Generierung verifiziert gegen `ctx.speed = speedAt`, bei 0,9× sprang die Figur zu kurz (Same 1: Tod am ersten Block hinter dem Hook-Gate bei 2 047 m). 4. `autopilot` liest/schreibt `holdUntil` im Solver-`st`, nicht im geteilten Strategieobjekt; `d.holdUntil` (toter Code) ist weg, die Demo führt ein eigenes `d.hold`. **Verifikation:** `tests/issue15.mjs` — vorher rot (Nachbau der Auslieferung: 69 Neustarts in 10×30 s, `HANGING` 0, Mitteldistanz 515 px), jetzt grün: 0 Neustarts in 10×60 s, `HANGING` 3 346 Schritte (erster Seilkontakt ~35–40 s, Haken sind erst ab Sektor 5 frei — daher 60 s statt 30 s als Messfrist), Mitteldistanz 34,6 kpx. Zusätzlich: `content.strategy` hängt an allen verifizierten Mustern (SAFE-Notfall ausgenommen), Muster-Strategien bleiben nach einem Demo-Lauf frei von `holdUntil`. `tests/browser_issue10.py` folgt dem neuen `startDemo`-Aufbau; gesamte Suite (`./tests/run-all.sh`) grün. Nebenbefund zur Test-Historie: `soak.mjs` prüft Tod nur über `body.dead` (Sturz), nicht über `hitTest` — sein „5 Minuten ohne Tod“ hat Blocktreffer nie gesehen. Nicht Gegenstand dieses Fixes.
gilli closed this issue 2026-09-02 13:56:07 +02:00
Sign in to join this conversation.
No description provided.