Nach dem Tod zählt die Buchhaltung weiter: der gerammte Block wird zum Ausweichmanöver, die Fragmente dahinter kassiert der Leichnam #18

Closed
opened 2026-09-02 15:40:23 +02:00 by gilli · 1 comment
Owner

⁨⁩⁨Nach dem Tod zählt die Buchhaltung weiter: der gerammte Block wird zum Ausweichmanöver, die Fragmente dahinter kassiert der Leichnam⁩ ⁨⁩#⁨18⁩⁨⁩"
By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩
bug
game-feel
gameplay
medium

Komponente: GAME (simStep, die, gameOver, computeScore)
Betroffene Version: main @ 42f3cce, index.html (3.333 Zeilen, MD5 aae5f94c1dc05903f2c214d255d2fe9d),
Zeilen 2786–2795 (Ausweichmanöver), 2796–2809 (Fragmente), 2810–2828 (Gruben), 2829–2834 (Kollision),
2681–2697 (die), 2698–2726 (gameOver), 3023

Beschreibung

Während der Todesanimation (0,95 s bis zum Game-Over-Dialog, update() Zeile 3023) läuft
simStep() weiter – das ist gewollt (Körper gleitet, Partikel, Zeitlupe). S.dying wird in
simStep() aber nur an zwei Stellen abgefragt: beim Kollisionstest (Zeile 2830, damit man
nicht doppelt stirbt) und beim Tintenschweif (Zeile 2836). Die drei Wertungs-Schleifen davor
laufen ungeprüft:

// index.html:2789  – Hindernis passiert?
if (!o.passed && o.x + o.w < body.x - 16) { o.passed = true; S.stats.dodges++; addFocus(0.04); AUDIO.dodge(); }
// index.html:2800  – Fragment kassiert?
… S.stats.frags++; addFocus(0.07); AUDIO.collect();
// index.html:2818  – Grube überquert?
… S.stats.pits++; if (p.usedHook && !p.touchedGround) { S.stats.clean++; flashMessage("Saubere Linie +60"); … }

Der Leichengleit trägt die Figur noch ~120 px weit – genau über das Hindernis hinweg, in das
sie hineingelaufen ist, und bis an die Fragmente dahinter. gameOver() liest die Statistik
danach und schreibt sie in den Dialog und in localStorage.

Messung

Headless-Chromium auf der gebauten Testseite, requestAnimationFrame stillgelegt, Schritt
1/120. Aufbau: Block bei x = 1000, dahinter Fragmente bei 1080/1120/1160.

Zeitpunkt dodges frags score Körper-x
im Moment des Todes (S.dying === true) 0 0 38 988
beim Game-Over-Dialog 1 2 150 1106

Anzeige im Dialog: Punktzahl 150, Ausweichmanöver 1, Energiefragmente 2.

Zweite Messung mit zwei Blöcken (1400, 1700): Tod bei dodges 0 / score 78 → Game Over
dodges 1 / score 140. Getroffenes Hindernis ist jeweils der erste Block.

Hörbar dazu: beim Aufprall laufen AUDIO.crash() und der Ausweich-Ton
(AUDIO.dodge(), 620 → 900 Hz) sowie der Kassier-Ton durcheinander – die Szene meldet Erfolg
beim Sterben.

Auswirkung

  • Nahezu jeder Kollisions-Tod wertet den gerammten Block nachträglich zum erfolgreichen
    Ausweichmanöver hoch (+50 Punkte für jedes Hindernis, das innerhalb der ~120 px
    Gleitstrecke endet). Die Punktzahl hängt damit davon ab, wie weit der Körper rutscht,
    nicht davon, was der Spieler gemacht hat.
  • Fragmente hinter dem Hindernis werden kassiert, obwohl die Figur im Block liegt.
  • Die Dialogzeilen „Ausweichmanöver“ und „Energiefragmente“ sind keine redliche Auskunft
    über den Lauf.
  • Dieselbe Schleife kann im Tod noch „Saubere Linie +60“ einblenden und den Fokusbalken
    laden.
  • Die erhöhten Werte werden dauerhaft geschrieben: d.best = Math.max(d.best, score) in
    gameOver() (Zeile 2704) – auch der „Persönlicher Rekord geknackt“-Toast kann darauf
    basieren.

Lösungsvorschlag

  1. Wertungen an S.dying koppeln – die drei Schleifen (Ausweichmanöver, Fragmente,
    Gruben) hinter dieselbe Bedingung wie der Kollisionstest setzen, z. B. gemeinsamen Block
    if (!S.dying) { … } bilden. Der Körper darf weitergleiten, nur die Buchhaltung nicht.
  2. Oder: beim Tod einfrieren – in die() S.frozenStats = Object.assign({}, S.stats) und
    S.frozenDistance = S.distance setzen; computeScore() und gameOver() lesen
    ausschließlich den eingefrorenen Stand. Das beantwortet auch die Frage „zählt die
    Gleitstrecke?“ ehrlich: nein.
  3. o.passed für das Hindernis, das den Tod ausgelöst hat, darf nie gesetzt werden.

Tests

Neue Prüfung (tests/issue18.mjs, deterministisch über den CORE-Harness plus
PULSE.update(1/120) in der Browser-Variante):

  • Figur auf flachem Boden, ein Block 100 px voraus, zwei Fragmente dahinter; Schritt für
    Schritt bis S.overShown. Schranke: stats.dodges, stats.frags, stats.pits,
    stats.clean und computeScore() sind beim Game Over identisch mit dem Stand des
    Frames, in dem S.dying true wurde. Gegen die Auslieferung rot: dodges 0 → 1,
    frags 0 → 2, score 38 → 150.
  • Gruben-Variante: eine Grube, die erst innerhalb der Gleitstrecke endet, darf nicht mehr
    als pits++ bzw. clean++ zählen.
  • Ton-Pfad: im Todesframe darf kein dodge/collect-Event mehr ausgelöst werden (zählbar am
    Stub von AUDIO in der Testseite).

Dokumentiert in issues/18-punktewertung-laeuft-nach-dem-tod-weiter.md (Commit 42f3cce).

1 comments

⁨⁩⁨Nach dem Tod zählt die Buchhaltung weiter: der gerammte Block wird zum Ausweichmanöver, die Fragmente dahinter kassiert der Leichnam⁩ ⁨⁩#⁨18⁩⁨⁩" By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩ bug game-feel gameplay medium > **Komponente:** GAME (`simStep`, `die`, `gameOver`, `computeScore`) > **Betroffene Version:** `main` @ `42f3cce`, `index.html` (3.333 Zeilen, MD5 `aae5f94c1dc05903f2c214d255d2fe9d`), > Zeilen 2786–2795 (Ausweichmanöver), 2796–2809 (Fragmente), 2810–2828 (Gruben), 2829–2834 (Kollision), > 2681–2697 (`die`), 2698–2726 (`gameOver`), 3023 > > ## Beschreibung > > Während der Todesanimation (0,95 s bis zum Game-Over-Dialog, `update()` Zeile 3023) läuft > `simStep()` weiter – das ist gewollt (Körper gleitet, Partikel, Zeitlupe). `S.dying` wird in > `simStep()` aber **nur an zwei Stellen** abgefragt: beim Kollisionstest (Zeile 2830, damit man > nicht doppelt stirbt) und beim Tintenschweif (Zeile 2836). Die drei Wertungs-Schleifen davor > laufen ungeprüft: > > ```js > // index.html:2789 – Hindernis passiert? > if (!o.passed && o.x + o.w < body.x - 16) { o.passed = true; S.stats.dodges++; addFocus(0.04); AUDIO.dodge(); } > // index.html:2800 – Fragment kassiert? > … S.stats.frags++; addFocus(0.07); AUDIO.collect(); > // index.html:2818 – Grube überquert? > … S.stats.pits++; if (p.usedHook && !p.touchedGround) { S.stats.clean++; flashMessage("Saubere Linie +60"); … } > ``` > > Der Leichengleit trägt die Figur noch ~120 px weit – genau über das Hindernis hinweg, in das > sie hineingelaufen ist, und bis an die Fragmente dahinter. `gameOver()` liest die Statistik > danach und schreibt sie in den Dialog und in `localStorage`. > > ## Messung > > Headless-Chromium auf der gebauten Testseite, `requestAnimationFrame` stillgelegt, Schritt > 1/120. Aufbau: Block bei x = 1000, dahinter Fragmente bei 1080/1120/1160. > > | Zeitpunkt | `dodges` | `frags` | `score` | Körper-x | > |---|---|---|---|---| > | im Moment des Todes (`S.dying === true`) | 0 | 0 | **38** | 988 | > | beim Game-Over-Dialog | **1** | **2** | **150** | 1106 | > > Anzeige im Dialog: Punktzahl **150**, Ausweichmanöver **1**, Energiefragmente **2**. > > Zweite Messung mit zwei Blöcken (1400, 1700): Tod bei `dodges` 0 / `score` 78 → Game Over > `dodges` 1 / `score` 140. Getroffenes Hindernis ist jeweils der erste Block. > > Hörbar dazu: beim Aufprall laufen `AUDIO.crash()` **und** der Ausweich-Ton > (`AUDIO.dodge()`, 620 → 900 Hz) sowie der Kassier-Ton durcheinander – die Szene meldet Erfolg > beim Sterben. > > ## Auswirkung > > * Nahezu jeder Kollisions-Tod wertet den gerammten Block nachträglich zum erfolgreichen > Ausweichmanöver hoch (+50 Punkte für jedes Hindernis, das innerhalb der ~120 px > Gleitstrecke endet). Die Punktzahl hängt damit davon ab, wie weit der Körper rutscht, > nicht davon, was der Spieler gemacht hat. > * Fragmente hinter dem Hindernis werden kassiert, obwohl die Figur im Block liegt. > * Die Dialogzeilen „Ausweichmanöver“ und „Energiefragmente“ sind keine redliche Auskunft > über den Lauf. > * Dieselbe Schleife kann im Tod noch „Saubere Linie +60“ einblenden und den Fokusbalken > laden. > * Die erhöhten Werte werden dauerhaft geschrieben: `d.best = Math.max(d.best, score)` in > `gameOver()` (Zeile 2704) – auch der „Persönlicher Rekord geknackt“-Toast kann darauf > basieren. > > ## Lösungsvorschlag > > 1. **Wertungen an `S.dying` koppeln** – die drei Schleifen (Ausweichmanöver, Fragmente, > Gruben) hinter dieselbe Bedingung wie der Kollisionstest setzen, z. B. gemeinsamen Block > `if (!S.dying) { … }` bilden. Der Körper darf weitergleiten, nur die Buchhaltung nicht. > 2. **Oder:** beim Tod einfrieren – in `die()` `S.frozenStats = Object.assign({}, S.stats)` und > `S.frozenDistance = S.distance` setzen; `computeScore()` und `gameOver()` lesen > ausschließlich den eingefrorenen Stand. Das beantwortet auch die Frage „zählt die > Gleitstrecke?“ ehrlich: nein. > 3. `o.passed` für das Hindernis, das den Tod ausgelöst hat, darf nie gesetzt werden. > > ## Tests > > Neue Prüfung (`tests/issue18.mjs`, deterministisch über den CORE-Harness plus > `PULSE.update(1/120)` in der Browser-Variante): > > * Figur auf flachem Boden, ein Block 100 px voraus, zwei Fragmente dahinter; Schritt für > Schritt bis `S.overShown`. Schranke: `stats.dodges`, `stats.frags`, `stats.pits`, > `stats.clean` und `computeScore()` sind beim Game Over **identisch** mit dem Stand des > Frames, in dem `S.dying` `true` wurde. Gegen die Auslieferung rot: dodges 0 → 1, > frags 0 → 2, score 38 → 150. > * Gruben-Variante: eine Grube, die erst innerhalb der Gleitstrecke endet, darf nicht mehr > als `pits++` bzw. `clean++` zählen. > * Ton-Pfad: im Todesframe darf kein `dodge`/`collect`-Event mehr ausgelöst werden (zählbar am > Stub von `AUDIO` in der Testseite). > > --- > > *Dokumentiert in `issues/18-punktewertung-laeuft-nach-dem-tod-weiter.md` (Commit `42f3cce`).* 1 comments
Author
Owner

Abarbeitet in 7cec3e1 (Doku: 2ee6082), main ist pushed.

Was geändert wurde (index.html, simStep / update)

  1. Die drei Wertungs-Schleifen – Ausweichmanöver, Energiefragmente, Gruben – sitzen jetzt
    gemeinsam hinter if (!S.dying), also hinter derselben Bedingung wie der Kollisionstest.
    Bewusst keine eingefrorene Kopie der Statistik (S.frozenStats): damit entstehen
    o.passed / f.got / p.crossed gar nicht erst (Vorschlag 3 aus dem Befund – das gerammte
    Hindernis wird nie nachträglich zum Ausweichmanöver), und gameOver(), computeScore() und
    das HUD lesen unverändert die lebenden Felder.
  2. S.distance, S.speed und der Sektorwechsel laufen nur, solange das Spiel lebt. Der
    Gleitanteil von ~120 px steht damit weder in der Punktzahl, noch im Dialog, noch in
    d.farthest, und an einer Sektorgrenze kommt kein nachträglicher Sektorbonus (+100) dazu.
    speed hält bei Tod beim letzten lebenden Tempo – Physik, Kamera, Partikel und Zeitlupe
    bleiben unangetastet.
  3. Dieselbe Klammer um die Schwungbuchung bei ev.landed (S.swingPending && !S.dying) und um
    S.focusUsed += dt: eine Landung der Leiche ist kein Schwung, die Zeitlupe des Sterbens ist
    keine eingesetzte Fokuszeit.

Messung vorher → nachher (Block 1000, Fragmente 1080/1120/1160, Grubenkante 1090, Tod bei
988 px, Gleitweg bis 1101 px)

Tod Game Over vorher Game Over nachher
dodges / frags / pits 0 / 0 / 0 1 / 2 / 1 0 / 0 / 0
Score / Strecke 38 / 38 m 149 / 49 m 38 / 38 m
neue Audio-Knoten über die Gleitstrecke – 3 0
Fokuszeit im Dialog 1,7 s 2,7 s 1,7 s

Tests

  • tests/issue18.mjs (neu, 40 Prüfungen, kein Browser): newStats, computeScore, addFocus,
    die, gameOver und simStep per Klammerzähler aus dem Dokument, geschritten mit echtem
    CORE.stepBody gegen AUDIO-/RENDER-/DOM-/STORE-Zählattrappen. Block + Fragmente, Gruben- und
    Sektorgrenz-Variante, Landung im Leichengleit, „nach dem Game Over schreibt nichts mehr“ – und
    der Gegentest, dass lebend Ausweichen, Fragmente, Gruben, Fokusbalken und Strecke weiter zählen.
  • tests/browser_issue18.py (neu): derselbe Fall im echten update()-Pfad – Dialog-DOM
    (#goScore, #goDist, #goDodges, #goFrag, #goClean, #goFocus), HUD ≡ Dialog,
    localStorage (best/farthest) und ein Klang-Spy auf createOscillator/createBufferSource,
    der über die Gleitstrecke keine einzige neue Knotenerzeugung zählt.
  • Gegen main vor dem Fix: 21 bzw. 22 Prüfungen rot, jetzt grün. ./tests/run-all.sh komplett
    grün (13 Node-Suiten + Soak über 5 min + 11 Browser-Suiten). Beide neuen Suiten sind in
    run-all.sh, tests/README.md und AGENTS.md (Invarianten: „Die Buchhaltung endet mit dem
    Leben“) nachgetragen.
Abarbeitet in `7cec3e1` (Doku: `2ee6082`), `main` ist pushed. **Was geändert wurde** (`index.html`, `simStep` / `update`) 1. Die drei Wertungs-Schleifen – Ausweichmanöver, Energiefragmente, Gruben – sitzen jetzt gemeinsam hinter `if (!S.dying)`, also hinter **derselben** Bedingung wie der Kollisionstest. Bewusst keine eingefrorene Kopie der Statistik (`S.frozenStats`): damit entstehen `o.passed` / `f.got` / `p.crossed` gar nicht erst (Vorschlag 3 aus dem Befund – das gerammte Hindernis wird nie nachträglich zum Ausweichmanöver), und `gameOver()`, `computeScore()` und das HUD lesen unverändert die lebenden Felder. 2. `S.distance`, `S.speed` und der Sektorwechsel laufen nur, solange das Spiel lebt. Der Gleitanteil von ~120 px steht damit weder in der Punktzahl, noch im Dialog, noch in `d.farthest`, und an einer Sektorgrenze kommt kein nachträglicher Sektorbonus (+100) dazu. `speed` hält bei Tod beim letzten lebenden Tempo – Physik, Kamera, Partikel und Zeitlupe bleiben unangetastet. 3. Dieselbe Klammer um die Schwungbuchung bei `ev.landed` (`S.swingPending && !S.dying`) und um `S.focusUsed += dt`: eine Landung der Leiche ist kein Schwung, die Zeitlupe des Sterbens ist keine eingesetzte Fokuszeit. **Messung vorher → nachher** (Block 1000, Fragmente 1080/1120/1160, Grubenkante 1090, Tod bei 988 px, Gleitweg bis 1101 px) | | Tod | Game Over vorher | Game Over nachher | |---|---|---|---| | `dodges` / `frags` / `pits` | 0 / 0 / 0 | **1 / 2 / 1** | 0 / 0 / 0 | | Score / Strecke | 38 / 38 m | **149 / 49 m** | 38 / 38 m | | neue Audio-Knoten über die Gleitstrecke | – | **3** | 0 | | Fokuszeit im Dialog | 1,7 s | **2,7 s** | 1,7 s | **Tests** * `tests/issue18.mjs` (neu, 40 Prüfungen, kein Browser): `newStats`, `computeScore`, `addFocus`, `die`, `gameOver` und `simStep` per Klammerzähler aus dem Dokument, geschritten mit echtem `CORE.stepBody` gegen AUDIO-/RENDER-/DOM-/STORE-Zählattrappen. Block + Fragmente, Gruben- und Sektorgrenz-Variante, Landung im Leichengleit, „nach dem Game Over schreibt nichts mehr“ – und der Gegentest, dass lebend Ausweichen, Fragmente, Gruben, Fokusbalken und Strecke weiter zählen. * `tests/browser_issue18.py` (neu): derselbe Fall im echten `update()`-Pfad – Dialog-DOM (`#goScore`, `#goDist`, `#goDodges`, `#goFrag`, `#goClean`, `#goFocus`), HUD ≡ Dialog, `localStorage` (`best`/`farthest`) und ein Klang-Spy auf `createOscillator`/`createBufferSource`, der über die Gleitstrecke keine einzige neue Knotenerzeugung zählt. * Gegen `main` vor dem Fix: 21 bzw. 22 Prüfungen rot, jetzt grün. `./tests/run-all.sh` komplett grün (13 Node-Suiten + Soak über 5 min + 11 Browser-Suiten). Beide neuen Suiten sind in `run-all.sh`, `tests/README.md` und `AGENTS.md` (Invarianten: „Die Buchhaltung endet mit dem Leben“) nachgetragen.
gilli closed this issue 2026-09-03 21:58:49 +02:00
Sign in to join this conversation.
No description provided.