Sprung hat keine Untergrenze nach unten: 165 px Mindesthöhe lassen bei Hindernisfolgen nur ein Absprungfenster ab 17 ms #16

Closed
opened 2026-09-02 15:40:22 +02:00 by gilli · 2 comments
Owner

⁨⁩⁨Sprung hat keine Untergrenze nach unten: 165 px Mindesthöhe lassen bei Hindernisfolgen nur ein Absprungfenster ab 17 ms⁩ ⁨⁩#⁨16⁩⁨⁩"
By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩
bug
game-feel
gameplay
high
level-generation
physics

Komponente: CORE (K, doJump, integrateFree, buildPattern, verifyContent), UI (Hilfe, Erststart-Hinweis)
Betroffene Version: main @ 42f3cce, index.html (3.333 Zeilen, MD5 aae5f94c1dc05903f2c214d255d2fe9d),
Zeilen 448–450, 725–726, 788, 920, 979–980, 1273, 1351, 327, 399

Beschreibung

Der Sprung ist nur nach oben abstufbar. doJump() gibt immer volles K.JUMP_VELOCITY
(−900), integrateFree() senkt die Schwerkraft für höchstens K.MAX_JUMP_HOLD_TIME (0,18 s)
auf 55 %, solange die Taste gehalten wird (Zeile 788):

// index.html:788
if (b.vy < 0 && b.holdT > 0 && cmd.jumpHeld) { g *= (1 - K.JUMP_HOLD_BOOST); b.holdT = Math.max(0, b.holdT - dt); }

Ein Loslassen verkürzt den Sprung nicht – es nimmt nur den Bonus weg. Die kürzeste
spielbare Bahn ist also die ungebremste Vollparabel: 165 px Scheitelhöhe, 0,73 s Luftzeit,
265 px Strecke
bei Starttempo. Der höchste generierte Block ist 74 px hoch; der kleinste
Sprung ist damit 2,2 × so hoch wie das höchste Hindernis. Es gibt weder Jump-Cut noch
Schnellfall – einmal ausgelöst, ist die Bahn für 0,73 … 0,89 s fest.

Das wirkt direkt auf das Layout zurück:

  • Die Mulde zwischen zwei Cluster-Blöcken ist unlandbar. buildPattern("JUMP") setzt
    Cluster-Abstände auf rng.range(58, 104) px (Zeile 980) – der kürzeste Sprung trägt aber
    265 px. Ein Block-hoch-hüpfen und in der Mulde landen ist unmöglich; ein Cluster muss
    mit einem einzigen Sprung überbrückt werden, und dafür gibt es nur das eine Zeitfenster.
  • Das Überbrückungsmaß ist knapp bemessen. Über 74 px Blockhöhe bleibt der Tipp-Sprung
    nur 219 px weit frei (inkl. Körperbreite), der weiteste generierte Cluster ist aber
    254 px breit. Nur der gehaltene Sprung (285 px frei) hat Reserve.
  • verifyContent() prüft Lösbarkeit, nicht Toleranz. Akzeptiert wird eine Strategie,
    wenn ein einziger Absprungzeitpunkt durch das Muster kommt (Zeile 1351) – und das kann
    ausgerechnet die mit hold: K.MAX_JUMP_HOLD_TIME sein (Zeile 1273). Die Breite des
    Zeitfensters, das ein Mensch braucht, ist nirgends ein Kriterium.
  • Der Eingabepuffer hilft hier nicht: INPUT.consume() löst den gepufferten Sprung nur
    aus, wenn die Figur landen kann (canJump). „Zu früh gesprungen“ lässt sich nicht
    korrigieren, „knapp vor der Landung“ ist bei einem Cluster gar keine Option.

Messung

Node gegen den extrahierten CORE-Block, Schritt K.SIM_DT, Figur auf flachem Boden,
Absprung als ein Schritt cmd.jump, Haltedauer wie angegeben.

Sprunghüllkurve in Abhängigkeit von der Haltedauer (Tempo 360 px/s = Start):

Taste gehalten Scheitelhöhe Luftzeit Strecke
0 ms (Tippen = Minimum) 165 px 0,736 s 265 px
17 ms (1 Bildrate) 172 px 0,755 s 272 px
50 ms 186 px 0,791 s 285 px
120 ms 211 px 0,845 s 304 px
180 ms = MAX_JUMP_HOLD_TIME 228 px 0,891 s 321 px
260 ms 228 px 0,891 s 321 px

Die ganze abstufbare Bandbreite liegt zwischen 165 und 228 px – Verhältnis 1,38. Nach unten
ist nichts verfügbar. Bei K.SPEED_MAX (640) fliegt derselbe Tipp-Sprung 471 px, der
gehaltene 570 px.

Strecke, die über Blockhöhe frei überbrückt wird (Tempo 360, inkl. STAND_W):

geforderte Höhe frei überflogen Tipp-Sprung gehaltener Sprung
40 px (kleinster Block) 255 px 311 px
55 px 242 px 298 px
74 px (größter Block) 219 px 285 px

Zeitfenster in echten generierten Mustern. 20 Samen × 30 LOGICAL_WIDTH, Generierung wie
im Spiel (generateAhead, minReactionSec: K.SOLVER_MIN_REACTION); alle Muster mit zwei
Blöcken und < 150 px Abstand (= ein Sprung muss beide überdecken). Absprungzeitpunkte auf das
60-Hz-Bildraster gelegt, Treffer = Sprung löst in dieser Bildrate das Muster sauber aus:

Tempo zu überbrückende Breite höchster Block Fenster Tipp Fenster gehalten
430 228 px 67 px 17 ms 183 ms
387 197 px 52 px 67 ms 233 ms
387 187 px 70 px 67 ms 217 ms
475 238 px 56 px 67 ms 217 ms
472 216 px 62 px 83 ms 250 ms
447 195 px 62 px 117 ms 283 ms
411 181 px 56 px 133 ms 300 ms
640 212 px 74 px 200 ms 333 ms
640 233 px 66 px 200 ms 367 ms

Gesamt über 23 Cluster: Tipp-Fenster 17 / 133 / 283 ms (min / Median / max),
gehalten 183 / 317 / 417 ms. 5 von 23 Clustern liegen unter 100 ms, der engste
(228 px bei Tempo 430) lässt genau eine Bildrate – 17 ms. Halten ist an denselben
Stellen 2 … 10 ×mütlicher.

Zum Vergleich: zwei weit auseinanderliegende Blöcke (Abstand ≥ 150 px, zwei Sprünge
nötig). Dort ist der engste gemessene Zeitraum für den zweiten Sprung 317 / 367 / 383 /
400 / 417 ms – machbar, aber ohne jede Korrekturmöglichkeit, weil der erste Sprung nicht
abgekürzt werden kann.

Die Mechanik ist nirgends erklärt. Die Hilfetabelle sagt zu Leer/↑ nur
„Springen · in der Luft: erreichbaren Haken greifen“ (Zeile 399); „halten = länger“ steht
ausschließlich für das Rollen (Zeile 400) und im Erststart-Hinweis (Zeile 327). Ein Spieler
erfährt also nicht, dass genau das Halten die verzeihende Variante ist.

Auswirkung

  • Der erste Eindruck des Spiels ist unnachgiebig: schon im Starttempo hängen die engsten
    Cluster an einem 17 … 83 ms-Fenster – unter jeder menschlichen Treffergenauigkeit (~150 ms
    Reaktion, ~50 ms Streuung bei gut trainierter Zeitnahme).
  • Es gibt keine fühlbare Abstufung: ob kurz getippt oder lange gehalten – in der Regel sieht
    der Sprung gleich aus (165 → 228 px, 265 → 321 px), und die knappe Variante ist im
    entscheidenden Muster die schlechtere.
  • Der Autopilot/Solver verschleift das: verifyContent() nimmt ein Muster an, solange eine
    Strategie durchkommt – gemessen ist die verzeihende Strategie fast immer die mit
    hold = K.MAX_JUMP_HOLD_TIME, während HOLDS auch 0 enthält und damit die harte Variante
    als „machbar“ gilt.
  • Spielgefühl: Ein zu früh gelassener Sprung ist sicherer Tod, ein zu später auch – ohne
    Möglichkeit, irgendwo dazwischen zu landen.

Lösungsvorschlag

Reihenfolge 1 + 3 ist der Kern, 2 ist die Alternative zu 1, 4 gehört in jeden Fall dazu.
Alle Zahlen gehören gemäß AGENTS.md (Regel 4) als Konstanten in CORE.K.

  1. Sprungabbruch beim Loslassen (Jump-Cut). Einmalig, wenn die Taste während des
    Aufstiegs losgelassen wird: if (!cmd.jumpHeld && b.vy < 0 && !b.jumpCut) { b.vy *= K.JUMP_CUT; b.jumpCut = true; }
    mit K.JUMP_CUT ≈ 0,55 … 0,7; Flag in doJump() zurücksetzen. Damit wird aus
    „165 px Minimum“ ein echter Hüpfball (~90 … 110 px) und die abstufbare Bandbreite wächst
    von 1,38 auf ~2,2.
  2. Oder: K.JUMP_VELOCITY auf ~−700 senken (Mindesthöhe ~102 px) und
    K.MAX_JUMP_HOLD_TIME/K.JUMP_HOLD_BOOST so anheben, dass der gehaltene Sprung bei
    ~225 px bleibt. Günstiger, aber die Untergrenze bleibt ein Vollkommitment.
  3. Generierung gegen die gemessene Hüllkurve statt gegen die Formel. jumpDistance(v)
    (Zeile 920) beschreibt den Tipp-Sprung, buildPattern legt
    between = jd * 0.95 + rng.range(10, 130) (Zeile 980) – 5 % Luft auf genau die Bahn, die
    am wenigsten verzeiht. Konkret: Cluster-Höhen und -Abstände so begrenzen, dass der
    Tipp-Sprung den Cluster mit Reserve überfliegt, und in verifyContent() ein
    Toleranzkriterium ergänzen: eine Strategie wird nur akzeptiert, wenn mindestens
    K.MIN_TAKEOFF_FRAMES (z. B. 3 … 4 Bildraten à 1/60 s) aufeinanderfolgende
    Absprungzeitpunkte das Muster passieren. Muster mit 17-ms-Fenster würden dann verworfen
    und über fallbackChain() zu JUMP/SAFE absteigen.
  4. Mechanik beschriften: in der Hilfetabelle (Zeile 399) und im Erststart-Hinweis
    (Zeile 327) „Leer halten = höher und weiter springen“ ergänzen.

Tests

Eine neue Node-Prüfung (tests/issue16.mjs), die die Hüllkurve und das Fenster misst statt
nur die Kollision:

  • Hüllkurve: Scheitelhöhe des kürzesten Sprungs ≤ K.JUMP_MIN_APEX_MAX (z. B. 120 px),
    Scheitelhöhe des längsten Sprungs ≥ K.JUMP_MAX_APEX_MIN (~220 px) – die Auslieferung ist
    hier rot (165 px / 228 px, Verhältnis 1,38).
  • Toleranz: für N Samen × 30 Bühnenbreiten generierte Muster mit ≥ 2 Blöcken: das Fenster
    gültiger Absprungzeitpunkte (Raster 1/60 s) ≥ 3 Bildraten. Auslieferung: engstes Fenster
    1 Bildrate (17 ms), 5/23 Cluster unter 100 ms.
  • Die bestehenden Suiten tests/issue2.mjs (Landung, Abstieg, Coyote) und soak.mjs
    müssen grün bleiben; tests/issue2.mjs misst die Abweichung der realen Sprungweite von
    jumpDistance(v) bereits absichtlich – die Zahl ändert sich mit jeder der Optionen 1/2 und
    muss im Dreierpack (Konstante, Test, Commit-Notiz) nachgezogen werden.

Dokumentiert in issues/16-sprung-keine-abstufbare-untergrenze-cluster-fenster-ab-17-ms.md (Commit 42f3cce).

⁨2⁩ comments

⁨⁩⁨Sprung hat keine Untergrenze nach unten: 165 px Mindesthöhe lassen bei Hindernisfolgen nur ein Absprungfenster ab 17 ms⁩ ⁨⁩#⁨16⁩⁨⁩" By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩ bug game-feel gameplay high level-generation physics > **Komponente:** CORE (`K`, `doJump`, `integrateFree`, `buildPattern`, `verifyContent`), UI (Hilfe, Erststart-Hinweis) > **Betroffene Version:** `main` @ `42f3cce`, `index.html` (3.333 Zeilen, MD5 `aae5f94c1dc05903f2c214d255d2fe9d`), > Zeilen 448–450, 725–726, 788, 920, 979–980, 1273, 1351, 327, 399 > > ## Beschreibung > > Der Sprung ist nur **nach oben** abstufbar. `doJump()` gibt immer volles `K.JUMP_VELOCITY` > (−900), `integrateFree()` senkt die Schwerkraft für höchstens `K.MAX_JUMP_HOLD_TIME` (0,18 s) > auf 55 %, solange die Taste gehalten wird (Zeile 788): > > ```js > // index.html:788 > if (b.vy < 0 && b.holdT > 0 && cmd.jumpHeld) { g *= (1 - K.JUMP_HOLD_BOOST); b.holdT = Math.max(0, b.holdT - dt); } > ``` > > Ein Loslassen **verkürzt den Sprung nicht** – es nimmt nur den Bonus weg. Die kürzeste > spielbare Bahn ist also die ungebremste Vollparabel: **165 px Scheitelhöhe, 0,73 s Luftzeit, > 265 px Strecke** bei Starttempo. Der höchste generierte Block ist 74 px hoch; der *kleinste* > Sprung ist damit 2,2 × so hoch wie das höchste Hindernis. Es gibt weder Jump-Cut noch > Schnellfall – einmal ausgelöst, ist die Bahn für 0,73 … 0,89 s fest. > > Das wirkt direkt auf das Layout zurück: > > * **Die Mulde zwischen zwei Cluster-Blöcken ist unlandbar.** `buildPattern("JUMP")` setzt > Cluster-Abstände auf `rng.range(58, 104)` px (Zeile 980) – der kürzeste Sprung trägt aber > 265 px. Ein Block-hoch-hüpfen und in der Mulde landen ist unmöglich; ein Cluster **muss** > mit einem einzigen Sprung überbrückt werden, und dafür gibt es nur das eine Zeitfenster. > * **Das Überbrückungsmaß ist knapp bemessen.** Über 74 px Blockhöhe bleibt der Tipp-Sprung > nur 219 px weit frei (inkl. Körperbreite), der weiteste generierte Cluster ist aber > 254 px breit. Nur der gehaltene Sprung (285 px frei) hat Reserve. > * **`verifyContent()` prüft Lösbarkeit, nicht Toleranz.** Akzeptiert wird eine Strategie, > wenn *ein einziger* Absprungzeitpunkt durch das Muster kommt (Zeile 1351) – und das kann > ausgerechnet die mit `hold: K.MAX_JUMP_HOLD_TIME` sein (Zeile 1273). Die Breite des > Zeitfensters, das ein Mensch braucht, ist nirgends ein Kriterium. > * Der **Eingabepuffer hilft hier nicht**: `INPUT.consume()` löst den gepufferten Sprung nur > aus, wenn die Figur landen kann (`canJump`). „Zu früh gesprungen“ lässt sich nicht > korrigieren, „knapp vor der Landung“ ist bei einem Cluster gar keine Option. > > ## Messung > > Node gegen den extrahierten CORE-Block, Schritt `K.SIM_DT`, Figur auf flachem Boden, > Absprung als ein Schritt `cmd.jump`, Haltedauer wie angegeben. > > **Sprunghüllkurve in Abhängigkeit von der Haltedauer (Tempo 360 px/s = Start):** > > | Taste gehalten | Scheitelhöhe | Luftzeit | Strecke | > |---|---|---|---| > | 0 ms (Tippen = Minimum) | **165 px** | 0,736 s | 265 px | > | 17 ms (1 Bildrate) | 172 px | 0,755 s | 272 px | > | 50 ms | 186 px | 0,791 s | 285 px | > | 120 ms | 211 px | 0,845 s | 304 px | > | 180 ms = `MAX_JUMP_HOLD_TIME` | **228 px** | 0,891 s | 321 px | > | 260 ms | 228 px | 0,891 s | 321 px | > > Die ganze abstufbare Bandbreite liegt zwischen 165 und 228 px – Verhältnis 1,38. Nach unten > ist nichts verfügbar. Bei `K.SPEED_MAX` (640) fliegt derselbe Tipp-Sprung **471 px**, der > gehaltene 570 px. > > **Strecke, die über Blockhöhe frei überbrückt wird (Tempo 360, inkl. `STAND_W`):** > > | geforderte Höhe frei überflogen | Tipp-Sprung | gehaltener Sprung | > |---|---|---| > | 40 px (kleinster Block) | 255 px | 311 px | > | 55 px | 242 px | 298 px | > | 74 px (größter Block) | **219 px** | 285 px | > > **Zeitfenster in echten generierten Mustern.** 20 Samen × 30 `LOGICAL_WIDTH`, Generierung wie > im Spiel (`generateAhead`, `minReactionSec: K.SOLVER_MIN_REACTION`); alle Muster mit zwei > Blöcken und < 150 px Abstand (= ein Sprung muss beide überdecken). Absprungzeitpunkte auf das > 60-Hz-Bildraster gelegt, Treffer = Sprung löst in dieser Bildrate das Muster sauber aus: > > | Tempo | zu überbrückende Breite | höchster Block | Fenster Tipp | Fenster gehalten | > |---|---|---|---|---| > | 430 | 228 px | 67 px | **17 ms** | 183 ms | > | 387 | 197 px | 52 px | 67 ms | 233 ms | > | 387 | 187 px | 70 px | 67 ms | 217 ms | > | 475 | 238 px | 56 px | 67 ms | 217 ms | > | 472 | 216 px | 62 px | 83 ms | 250 ms | > | 447 | 195 px | 62 px | 117 ms | 283 ms | > | 411 | 181 px | 56 px | 133 ms | 300 ms | > | 640 | 212 px | 74 px | 200 ms | 333 ms | > | 640 | 233 px | 66 px | 200 ms | 367 ms | > > Gesamt über 23 Cluster: **Tipp-Fenster 17 / 133 / 283 ms** (min / Median / max), > **gehalten 183 / 317 / 417 ms**. **5 von 23 Clustern liegen unter 100 ms**, der engste > (228 px bei Tempo 430) lässt genau **eine** Bildrate – 17 ms. Halten ist an denselben > Stellen 2 … 10 ×mütlicher. > > **Zum Vergleich: zwei weit auseinanderliegende Blöcke** (Abstand ≥ 150 px, zwei Sprünge > nötig). Dort ist der engste gemessene Zeitraum für den *zweiten* Sprung 317 / 367 / 383 / > 400 / 417 ms – machbar, aber ohne jede Korrekturmöglichkeit, weil der erste Sprung nicht > abgekürzt werden kann. > > **Die Mechanik ist nirgends erklärt.** Die Hilfetabelle sagt zu `Leer`/`↑` nur > „Springen · in der Luft: erreichbaren Haken greifen“ (Zeile 399); „halten = länger“ steht > ausschließlich für das Rollen (Zeile 400) und im Erststart-Hinweis (Zeile 327). Ein Spieler > erfährt also nicht, dass genau das Halten die verzeihende Variante ist. > > ## Auswirkung > > * Der erste Eindruck des Spiels ist unnachgiebig: schon im Starttempo hängen die engsten > Cluster an einem 17 … 83 ms-Fenster – unter jeder menschlichen Treffergenauigkeit (~150 ms > Reaktion, ~50 ms Streuung bei gut trainierter Zeitnahme). > * Es gibt keine fühlbare Abstufung: ob kurz getippt oder lange gehalten – in der Regel sieht > der Sprung gleich aus (165 → 228 px, 265 → 321 px), und die *knappe* Variante ist im > entscheidenden Muster die *schlechtere*. > * Der Autopilot/Solver verschleift das: `verifyContent()` nimmt ein Muster an, solange eine > Strategie durchkommt – gemessen ist die verzeihende Strategie fast immer die mit > `hold = K.MAX_JUMP_HOLD_TIME`, während `HOLDS` auch `0` enthält und damit die harte Variante > als „machbar“ gilt. > * Spielgefühl: Ein zu früh gelassener Sprung ist sicherer Tod, ein zu später auch – ohne > Möglichkeit, irgendwo dazwischen zu landen. > > ## Lösungsvorschlag > > Reihenfolge 1 + 3 ist der Kern, 2 ist die Alternative zu 1, 4 gehört in jeden Fall dazu. > Alle Zahlen gehören gemäß `AGENTS.md` (Regel 4) als Konstanten in `CORE.K`. > > 1. **Sprungabbruch beim Loslassen (Jump-Cut).** Einmalig, wenn die Taste während des > Aufstiegs losgelassen wird: `if (!cmd.jumpHeld && b.vy < 0 && !b.jumpCut) { b.vy *= K.JUMP_CUT; b.jumpCut = true; }` > mit `K.JUMP_CUT ≈ 0,55 … 0,7`; Flag in `doJump()` zurücksetzen. Damit wird aus > „165 px Minimum“ ein echter Hüpfball (~90 … 110 px) und die abstufbare Bandbreite wächst > von 1,38 auf ~2,2. > 2. **Oder:** `K.JUMP_VELOCITY` auf ~−700 senken (Mindesthöhe ~102 px) und > `K.MAX_JUMP_HOLD_TIME`/`K.JUMP_HOLD_BOOST` so anheben, dass der gehaltene Sprung bei > ~225 px bleibt. Günstiger, aber die Untergrenze bleibt ein Vollkommitment. > 3. **Generierung gegen die gemessene Hüllkurve statt gegen die Formel.** `jumpDistance(v)` > (Zeile 920) beschreibt den *Tipp*-Sprung, `buildPattern` legt > `between = jd * 0.95 + rng.range(10, 130)` (Zeile 980) – 5 % Luft auf genau die Bahn, die > am wenigsten verzeiht. Konkret: Cluster-Höhen und -Abstände so begrenzen, dass der > Tipp-Sprung den Cluster mit Reserve überfliegt, und in `verifyContent()` ein > **Toleranzkriterium** ergänzen: eine Strategie wird nur akzeptiert, wenn mindestens > `K.MIN_TAKEOFF_FRAMES` (z. B. 3 … 4 Bildraten à 1/60 s) aufeinanderfolgende > Absprungzeitpunkte das Muster passieren. Muster mit 17-ms-Fenster würden dann verworfen > und über `fallbackChain()` zu `JUMP`/`SAFE` absteigen. > 4. **Mechanik beschriften:** in der Hilfetabelle (Zeile 399) und im Erststart-Hinweis > (Zeile 327) „Leer halten = höher und weiter springen“ ergänzen. > > ## Tests > > Eine neue Node-Prüfung (`tests/issue16.mjs`), die die Hüllkurve und das Fenster misst statt > nur die Kollision: > > * **Hüllkurve:** Scheitelhöhe des kürzesten Sprungs ≤ `K.JUMP_MIN_APEX_MAX` (z. B. 120 px), > Scheitelhöhe des längsten Sprungs ≥ `K.JUMP_MAX_APEX_MIN` (~220 px) – die Auslieferung ist > hier rot (165 px / 228 px, Verhältnis 1,38). > * **Toleranz:** für N Samen × 30 Bühnenbreiten generierte Muster mit ≥ 2 Blöcken: das Fenster > gültiger Absprungzeitpunkte (Raster 1/60 s) ≥ 3 Bildraten. Auslieferung: engstes Fenster > 1 Bildrate (17 ms), 5/23 Cluster unter 100 ms. > * Die bestehenden Suiten `tests/issue2.mjs` (Landung, Abstieg, `Coyote`) und `soak.mjs` > müssen grün bleiben; `tests/issue2.mjs` misst die Abweichung der realen Sprungweite von > `jumpDistance(v)` bereits absichtlich – die Zahl ändert sich mit jeder der Optionen 1/2 und > muss im Dreierpack (Konstante, Test, Commit-Notiz) nachgezogen werden. > > --- > > *Dokumentiert in `issues/16-sprung-keine-abstufbare-untergrenze-cluster-fenster-ab-17-ms.md` (Commit `42f3cce`).* ⁨2⁩ comments
Author
Owner

Behoben in 249db31 (Doku-Nachlauf 45a2f87), alle 12 Node-Suiten und alle 9 Browser-Suiten grün.

Was geändert wurde

1 · Sprungabbruch beim Loslassen (Kern, Vorschlag 1)

integrateFree kappt die Steiggeschwindigkeit beim Loslassen im Aufstieg einmalig:

if (!cmd.jumpHeld && !b.jumpCut && b.state === "JUMPING" && b.vy < 0) { b.vy *= K.JUMP_CUT; b.jumpCut = true; b.holdT = 0; }
  • K.JUMP_CUT = 0.78, Flag b.jumpCut fällt in doJump() zurück.
  • Die Zustandsprüfung ist nicht Dekoration: ohne state === "JUMPING" würde jeder Aufstieg aus
    dem Schwingen (SLACK nach releaseHook) halbiert.
  • autopilot setzt cmd.jumpHeld jetzt auch im Absprungschritt (Parität zum echten
    Tastendruck). Fehlt das, greift der Cut im Solver sofort und s.hold ist wirkungslos –
    gemessen: alle Haltezeiten flogen identisch.

Gemessene Hüllkurve bei 360 px/s (Scheitel / Luftzeit / Strecke):

Taste vorher jetzt
Tippen 165 px / 0,73 s / 265 px 99,5 px / 0,56 s / 206 px
100 ms 202 px / 0,82 s / 298 px 155 px / 0,69 s / 252 px
180 ms 228 px / 0,89 s / 321 px 194 px / 0,78 s / 285 px
bis über den Scheitel 228 px / 0,89 s / 321 px 228,5 px / 0,88 s / 321 px

Verhältnis kürzeste → längste Bahn: 1,38 → 2,30 (Strecke 1,21 → 1,56).

2 · Generierung gegen die gemessene Hüllkurve statt gegen die Formel (Vorschlag 3)

jumpEnvelope() fährt Kürzest- und Längstbahn einmalig aus (Vorrat über die beteiligten
Konstanten, minJumpDistance(v)/maxJumpDistance(v)), jumpDistance() bleibt nur als Maßstab.
Layout wird an der unteren Grenze gemacht:

  • maxGapJumpable = K.GAP_OF_MIN_ARC (0,86) × minJumpDistance → Grubenwürfe ≤ 177 px bei
    Starttempo statt ≤ 194 px Formelware.
  • Enger Block-Cluster (Mulde 58–104 px) ersatzlos gestrichen; Mulden jetzt
    ≥ K.HOP_TROUGH_OF_MIN_ARC (1,25) × minJumpDistance → hüpfend und fliegend fahrbar.
    Die Mulde ist damit landbar, der Cluster-„eine Bahn für alles“-Zwang weg.
  • Blockhöhe ≤ maxBlockHeight(v, w) (analytisch aus der Hüllkurve: Parabelstrecke oberhalb
    h + Marge), Breiten/Höhen als K.BLOCK_* in K. Die Spanne 40–74 px bleibt erhalten –
    gedeckelt wird nur, was die losgelassene Bahn nicht überfliegt (beispielhaft: 62 px bei
    Breite 48, 53 px bei Breite 62 im Starttempo).
  • Trittsteine ≤ K.PLATFORM_H_MAX (86 px), sonst steht die Plattform über der Tipp-Scheitelhöhe.

3 · Toleranz statt Lösbarkeit in verifyContent()

takeoffWindow() zählt aufeinanderfolgende Absprungzeitpunkte auf dem 1/60-s-Raster
(Verschub über jumpLead), akzeptiert wird eine Strategie erst ab
K.MIN_TAKEOFF_FRAMES = 4 – gemessen mit der losgelassenen Bahn, denn das ist der Sprung
eines Spielers, der nicht hält. Muster mit 17-ms-Tor fallen zurück und die Kette bedient
JUMP/SAFE.

An Hakenmustern misst die Strategie-Schwingbahn: Hakengruben sind bewusst breiter als jeder
Sprung (1,04–1,42 × jumpDistance), die losgelassene Bahn trägt dort per Design nicht an den
Haken. verifyAllInputs() prüft deshalb ab 0,09 s Haltezeit statt ab 0 – sonst fiele die
Kette von 231 auf 69 pro 10 000 Muster, weil ein Tipp an einer 300-px-Grube ins Leere führt.

4 · Mechanik beschriftet (Vorschlag 4)

Hilfetabelle: „Springen (halten = höher und weiter, im Aufstieg loslassen = kürzer) · in der
Luft: erreichbaren Haken greifen“. Erststart-Hinweis: „Springen: linke Bildschirmhälfte
(halten = höher und weiter, früh loslassen = flacher)“.

Messung nach dem Fix

10 Samen × 30 Bühnenbreiten, 484 Muster ohne Haken, Fenster mit hold = 0:

  • engstes Fenster 4 Bildraten (67 ms), Median 21 Bildraten (0,35 s) – vorher engstes
    Cluster-Fenster 1 Bildrate (17 ms), 5 von 23 Clustern unter 100 ms.
  • Muster mit ≥ 2 Blöcken: engstes Fenster 83 ms, 3 von 48 unter 100 ms.
  • Artenliste unverändert (SAFE 1870, JUMP 1797, ROLL 1477, ENERGY_LINE 1452, PIT_HOOK 725,
    PIT 706, … bei 10 000 Mustern), forcedSafe 0, HOOK_CHAIN 174.

Tests

  • tests/issue16.mjs (neu, 33 Prüfungen): gegen HEAD vor dem Fix rot (19 Prüfungen,
    u. a. Tipp-Scheitel 164,7 px, Cluster-Mulden 72–99 px, engstes Fenster 1 Bildrate, alter
    Cluster wird von verifyContent akzeptiert), jetzt grün. Misst die Hüllkurve durch eigene
    Flüge statt über die ausgelieferten Helfer, dazu: Cut einmalig und nur im JUMPING-Aufstieg,
    hold im Solver keine Attrappe, Blockhöhen/Mulden/Grubenwürfe gegen die Hüllkurve,
    Fenstertest über verlegte Muster, alter Cluster (228 px überbrückt, 67 px hoch, 430 px/s)
    ist lösbar und wird trotzdem verworfen, Beschriftung in Hilfe und Erststart-Hinweis.
  • tests/issue2.mjs auf die neue Halte-Semantik nachgezogen (Dreierpack aus AGENTS.md Regel 4):
    Loslassen nach dem Haltebudget ist jetzt die gekappte Bahn, Differenz loser/gehaltener
    Sprung ≥ 60 px.
  • tests/browser_issue14.py und tests/browser_issue8.py messen die losgelassene Bahn gegen
    die aus K.JUMP_CUT rechnerische Tipp-Bahn (statt starrer 165 px) und prüfen den gehaltenen
    Sprung über den echten Eingabeweg auf ≥ 220 px.
  • Grün außerdem: issue1/3/4/6/9/11/12/13/15, soak.mjs (10 000 Muster + 5 min Autopilot),
    browser_smoke/flow/issue5/issue7/issue9/issue10/issue11.
Behoben in `249db31` (Doku-Nachlauf `45a2f87`), alle 12 Node-Suiten und alle 9 Browser-Suiten grün. ## Was geändert wurde **1 · Sprungabbruch beim Loslassen (Kern, Vorschlag 1)** `integrateFree` kappt die Steiggeschwindigkeit beim Loslassen im Aufstieg einmalig: ```js if (!cmd.jumpHeld && !b.jumpCut && b.state === "JUMPING" && b.vy < 0) { b.vy *= K.JUMP_CUT; b.jumpCut = true; b.holdT = 0; } ``` * `K.JUMP_CUT = 0.78`, Flag `b.jumpCut` fällt in `doJump()` zurück. * Die Zustandsprüfung ist nicht Dekoration: ohne `state === "JUMPING"` würde jeder Aufstieg aus dem Schwingen (`SLACK` nach `releaseHook`) halbiert. * `autopilot` setzt `cmd.jumpHeld` jetzt **auch im Absprungschritt** (Parität zum echten Tastendruck). Fehlt das, greift der Cut im Solver sofort und `s.hold` ist wirkungslos – gemessen: alle Haltezeiten flogen identisch. Gemessene Hüllkurve bei 360 px/s (Scheitel / Luftzeit / Strecke): | Taste | vorher | jetzt | |---|---|---| | Tippen | 165 px / 0,73 s / 265 px | **99,5 px / 0,56 s / 206 px** | | 100 ms | 202 px / 0,82 s / 298 px | 155 px / 0,69 s / 252 px | | 180 ms | 228 px / 0,89 s / 321 px | 194 px / 0,78 s / 285 px | | bis über den Scheitel | 228 px / 0,89 s / 321 px | **228,5 px / 0,88 s / 321 px** | Verhältnis kürzeste → längste Bahn: **1,38 → 2,30** (Strecke 1,21 → 1,56). **2 · Generierung gegen die gemessene Hüllkurve statt gegen die Formel (Vorschlag 3)** `jumpEnvelope()` fährt Kürzest- und Längstbahn einmalig aus (Vorrat über die beteiligten Konstanten, `minJumpDistance(v)`/`maxJumpDistance(v)`), `jumpDistance()` bleibt nur als Maßstab. Layout wird an der **unteren** Grenze gemacht: * `maxGapJumpable = K.GAP_OF_MIN_ARC (0,86) × minJumpDistance` → Grubenwürfe ≤ 177 px bei Starttempo statt ≤ 194 px Formelware. * Enger Block-Cluster (Mulde 58–104 px) ersatzlos gestrichen; Mulden jetzt ≥ `K.HOP_TROUGH_OF_MIN_ARC (1,25) × minJumpDistance` → hüpfend **und** fliegend fahrbar. Die Mulde ist damit landbar, der Cluster-„eine Bahn für alles“-Zwang weg. * Blockhöhe ≤ `maxBlockHeight(v, w)` (analytisch aus der Hüllkurve: Parabelstrecke oberhalb `h + Marge`), Breiten/Höhen als `K.BLOCK_*` in `K`. Die Spanne 40–74 px bleibt erhalten – gedeckelt wird nur, was die losgelassene Bahn nicht überfliegt (beispielhaft: 62 px bei Breite 48, 53 px bei Breite 62 im Starttempo). * Trittsteine ≤ `K.PLATFORM_H_MAX` (86 px), sonst steht die Plattform über der Tipp-Scheitelhöhe. **3 · Toleranz statt Lösbarkeit in `verifyContent()`** `takeoffWindow()` zählt aufeinanderfolgende Absprungzeitpunkte auf dem 1/60-s-Raster (Verschub über `jumpLead`), akzeptiert wird eine Strategie erst ab `K.MIN_TAKEOFF_FRAMES = 4` – **gemessen mit der losgelassenen Bahn**, denn das ist der Sprung eines Spielers, der nicht hält. Muster mit 17-ms-Tor fallen zurück und die Kette bedient JUMP/SAFE. An Hakenmustern misst die Strategie-Schwingbahn: Hakengruben sind bewusst breiter als jeder Sprung (1,04–1,42 × `jumpDistance`), die losgelassene Bahn trägt dort per Design nicht an den Haken. `verifyAllInputs()` prüft deshalb ab 0,09 s Haltezeit statt ab 0 – sonst fiele die Kette von 231 auf 69 pro 10 000 Muster, weil ein Tipp an einer 300-px-Grube ins Leere führt. **4 · Mechanik beschriftet (Vorschlag 4)** Hilfetabelle: „Springen (halten = höher und weiter, im Aufstieg loslassen = kürzer) · in der Luft: erreichbaren Haken greifen“. Erststart-Hinweis: „Springen: linke Bildschirmhälfte (halten = höher und weiter, früh loslassen = flacher)“. ## Messung nach dem Fix 10 Samen × 30 Bühnenbreiten, 484 Muster ohne Haken, Fenster mit `hold = 0`: * engstes Fenster **4 Bildraten (67 ms)**, Median 21 Bildraten (0,35 s) – vorher engstes Cluster-Fenster 1 Bildrate (17 ms), 5 von 23 Clustern unter 100 ms. * Muster mit ≥ 2 Blöcken: engstes Fenster **83 ms**, 3 von 48 unter 100 ms. * Artenliste unverändert (SAFE 1870, JUMP 1797, ROLL 1477, ENERGY_LINE 1452, PIT_HOOK 725, PIT 706, … bei 10 000 Mustern), `forcedSafe` 0, HOOK_CHAIN 174. ## Tests * `tests/issue16.mjs` (neu, 33 Prüfungen): gegen `HEAD` vor dem Fix **rot** (19 Prüfungen, u. a. Tipp-Scheitel 164,7 px, Cluster-Mulden 72–99 px, engstes Fenster 1 Bildrate, alter Cluster wird von `verifyContent` akzeptiert), jetzt grün. Misst die Hüllkurve durch eigene Flüge statt über die ausgelieferten Helfer, dazu: Cut einmalig und nur im `JUMPING`-Aufstieg, `hold` im Solver keine Attrappe, Blockhöhen/Mulden/Grubenwürfe gegen die Hüllkurve, Fenstertest über verlegte Muster, alter Cluster (228 px überbrückt, 67 px hoch, 430 px/s) ist lösbar und wird trotzdem verworfen, Beschriftung in Hilfe und Erststart-Hinweis. * `tests/issue2.mjs` auf die neue Halte-Semantik nachgezogen (Dreierpack aus AGENTS.md Regel 4): Loslassen nach dem Haltebudget ist jetzt die gekappte Bahn, Differenz loser/gehaltener Sprung ≥ 60 px. * `tests/browser_issue14.py` und `tests/browser_issue8.py` messen die losgelassene Bahn gegen die aus `K.JUMP_CUT` rechnerische Tipp-Bahn (statt starrer 165 px) und prüfen den gehaltenen Sprung über den echten Eingabeweg auf ≥ 220 px. * Grün außerdem: issue1/3/4/6/9/11/12/13/15, `soak.mjs` (10 000 Muster + 5 min Autopilot), `browser_smoke/flow/issue5/issue7/issue9/issue10/issue11`.
Author
Owner

Abgearbeitet in 249db31: Jump-Cut (K.JUMP_CUT) gibt dem Sprung eine Untergrenze (99,5 → 228,5 px Scheitel, Verhältnis 2,30), Generierung sized an der gemessenen minJumpDistance() (enger Block-Cluster weg, Gruben- und Muldenmaße, Blockhöhen-Deckel), verifyContent verlangt K.MIN_TAKEOFF_FRAMES = 4 aufeinanderfolgende Absprungzeitpunkte und die Mechanik ist in Hilfe + Erststart-Hinweis beschriftet. Abgedeckt von tests/issue16.mjs (gegen HEAD rot, jetzt 33 ok); alle übrigen Node- und Browser-Suiten grün. Details im Kommentar.

Abgearbeitet in 249db31: Jump-Cut (K.JUMP_CUT) gibt dem Sprung eine Untergrenze (99,5 → 228,5 px Scheitel, Verhältnis 2,30), Generierung sized an der gemessenen minJumpDistance() (enger Block-Cluster weg, Gruben- und Muldenmaße, Blockhöhen-Deckel), verifyContent verlangt K.MIN_TAKEOFF_FRAMES = 4 aufeinanderfolgende Absprungzeitpunkte und die Mechanik ist in Hilfe + Erststart-Hinweis beschriftet. Abgedeckt von tests/issue16.mjs (gegen HEAD rot, jetzt 33 ok); alle übrigen Node- und Browser-Suiten grün. Details im Kommentar.
gilli closed this issue 2026-09-02 18:07:56 +02:00
Sign in to join this conversation.
No description provided.