Musteranfang ohne Reaktionsraum: Das erste Hindernis sitzt 50–120 px hinter der Muster-Grenze, aber jeder zehnte Übergang kommt fliegend an – 89 davon landen innerhalb der Reaktionsdistanz #24

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

Komponente: CORE (buildPattern Zeilen 1101, 1112, 1119, 1127, 1140 — erste Hindernisse;
minReaction/runup/landing 1084–1086; normalizeContent 1258–1274; simStrategy 1368–1380;
verifyContent 1553–1601), GAME (simStep → CORE.hitTest, Zeile 3338)
Betroffene Version: main @ 86a481d, index.html (3.684 Zeilen, MD5 777d18c3edbb834aa41fccc7d10a7d18)

Beschreibung

Jedes Muster wird einzeln verifiziert, und zwar ab einem kanonischen Start: simStrategy
setzt den Körper auf Boden bei content.x0 − clamp(0,45·v, 150, 235) (Zeilen 1370–1373). Die
Generierung sichert außerdem den Nachlauf eines Musters zu — normalizeContent verlängert
den Boden hinter dem letzten Hindernis auf needTail = max(215, 0,58·v) (371 px bei 640 px/s,
Zeile 1259).

Was niemand zusichert, ist der Kopf: Das erste Hindernis eines Musters liegt

Muster Abstand erstes Hindernis → content.x0 Zeile
JUMP 50–90 px 1101
DOUBLE_JUMP 52–90 px 1140
JUMP_ROLL / ROLL_JUMP 50–90 px / 50–84 px 1127
ROLL 60–110 px 1112
MOVING_GATE 71–120 px 1119
Gruben (PIT, PIT_HOOK, PIT_PLATFORM, PIT_DOUBLE, HOOK_*) pad = runup(397 px) + rng(0, 80/90) ✓ 1158, 1169, 1186, 1200, 1205

runup (Zeile 1085, 397 px bei 640) und minReaction (Zeile 1084, 608 px bei 640 px/s und
K.SOLVER_MIN_REACTION 0,95) existieren — runup wird nur vor Grubenkanten benutzt,
minReaction genau einmal: als Innenabstand zwischen den zwei Hindernissen von
JUMP_ROLL/ROLL_JUMP (Zeile 1130).

Ein Fliegen über die Grenze hinweg ist Normalbetrieb: Ein gehaltenes Sprung ist 321 px lang
(#16), der Nachlauf des Vorgängers 371 px — wer kurz vor dem letzten Hindernis des Vorgängers
abspringt, landet hinter der Grenze. Dann ist die verfügbare Reaktionsstrecke das, was vom
Kopf-Abstand übrig bleibt: bei 640 px/s sind 60 px Kopf 94 ms.

Gemessen über fünf Läufe à 5 Minuten: 1 131 Muster-Übergänge, 116 davon fliegend, 89 davon
landen näher als minReaction an einem Hindernis
(Median der freien Strecke: 117 px).

Messprotokoll

Node gegen das extrahierte CORE (Same 4242, 777, 555, 2, 90210; je 300 s,
K.FIXED_TIMESTEP), Muster über den echten generateAhead-Pfad, onCommit notiert
min(o.x − x0) über die Hindernisse und min(p.x0 − x0) über die Gruben. Beim Wechsel des
aktuellen Musters wird der Körperzustand notiert, bei der nächsten Bodenberührung die freie
Strecke bis zum nächsten Hindernis.

1. Übergänge

Anzahl
Muster-Übergänge gesamt 1 131
davon fliegend (Körper bei x0 nicht grounded) 116 (10 %)
davon mit freier Strecke < minReaction (608 px) 89
freie Strecke nach der Landung: min / Median / max −291 / 117 / groß

−291 heißt: Die Landung liegt hinter dem ersten Hindernis — überlebt hat der Lauf das nur,
weil derselbe Anflugbogen schon darüber hinweggetragen hat. Direkt dahinter wartet bei
DOUBLE_JUMP das zweite Hindernis.

Beispiele (alle Seed 4242, v = 640 px/s, minReaction = 608 px):

Muster erstes Hindernis hinter x0 Landung freie Strecke
MOVING_GATE 105 px 2 px hinter der Grenze 103 px
JUMP 69 px 4 px vor der Grenze 73 px
ROLL_JUMP 69 px 16 px vor der Grenze 85 px
ROLL 92 px 30 px vor der Grenze 122 px
DOUBLE_JUMP 60 px 348 px hinter der Grenze −288 px

2. Konkreter Todesfall (Seed 555, t = 134,69 s, 8 192 m, Sektor 19)

  • Vorgänger ROLL_JUMP 81 298 … 82 483 (Tor bei 81 350, Block bei 82 074, w 38 h 43)
  • Folgemuster JUMP 82 483 … 83 092, ein Block bei 82 539 (w 41 h 44) — 56 px hinter der Grenze
  • Muster-Strategie {grab:"none", jumpLead:86, hold:0,18, rollLead:52}, verifyContent dieses
    Musters: ok: true, takeoffWindow 13, simStrategy bestanden bei 1/110 und bei 1/120

Live-Trace (1/120 s):

t 134,617  x 82 481  y 503  JUMPING   vy  887      (Anflug aus dem Vorgänger)
t 134,642  x 82 497  y 530  RUNNING   vy    0   ev[landed]
t 134,667  x 82 513  y 508  JUMPING   vy -867   cmd[jumpHeld]   ← erster Sprungschritt nach der Landung
t 134,692  x 82 529  y 487  JUMPING   vy -834   → Kollision

Landung 14 px innerhalb des neuen Musters, Block bei 82 539 — der Autopilot springt im ersten
möglichen Schritt (26 px vor dem Block) und steigt nicht hoch genug: Hitbox 82 516…82 542 × 457…517 gegen Block 82 539…82 580 × 516…560 → Überschneidung 3 × 1 px an der
Unteren linken Blockecke. Ein Muster also, das der Solver mit 13 Bildraten Fenster als bequem
eintragbar bewertet, ist in der echten Reihenfolge um ein Pixel verloren.

Ursache im Code

  1. Der Kopf ist unreguliert. Zeilen 1101/1112/1119/1127/1140 legen das erste Hindernis mit
    x0 + rng.range(50, 90) bzw. (60, 110) / (70, 120). runup (1085) und minReaction
    (1084) werden dort nicht angefasst; normalizeContent (1258–1274) reguliert nur den
    Nachlauf (needTail) und die Mindestlänge c.x1 ≥ c.x0 + 240.
  2. Verifiziert wird der kanonische Start, nicht die Übergabe. simStrategy (1370–1373)
    beginnt auf Boden; dass der Körper eine Muster-Grenze im Flug überquert (gemessen 10 %), ist
    demSolver unbekannt. verifyContent wiederholt das nur mit Tempo-Varianten
    (factors/leadJitters, Zeilen 1556–1558), nie mit anderen Anfangszuständen.
  3. Der Nachlauf des Vorgängers trägt seine eigene Landung. Bei 640 px/s sind
    needTail 371 px lang, ein gehaltener Sprung 321 px, gemessene Flugstrecke des konkreten
    Falls 385 px — die Landung fällt per Konstruktion in den Kopf des Folgemusters.

Folgeverhalten

  • Läufe sterben an einem JUMP-/DOUBLE_JUMP-/ROLL-/MOVING_GATE-Muster, das als verifiziert
    in der Welt steht. Betroffen ist der ganze Bereich ab ~Sektor 4, weil ab dort Tempo und
    Musterdichte steigen; gemessen bei 8,2 km (Sektor 19, Endlos).
  • Für den Spieler ist das nicht abwendbar: Er sieht den Block erst nach der Landung (Kamera folgt
    mit K.PLAYER_SCREEN_X), und 60 px sind 94 ms.
  • Die Menü-Demo fährt denselben Pfad, plant aber an jeder Grenze mit liveStrategy() neu
    (#15/#17) und überlebt dadurch — der Befund ist demo-seitig unsichtbar.

Nicht verletzt (bewusst abgegrenzt)

  • Issue #16 hält: Blockhöhen an der gemessenen Hüllkurve, Mulden in Landeweite der kürzesten
    Bahn, K.MIN_TAKEOFF_FRAMES auf allen Mustern.
  • Grubenmuster sind nicht betroffen — sie haben pad = runup + rng(0, 80/90) vor der Kante.
  • Das Muster für sich ist lösbar und bequem: verifyContent, takeoffWindow,
    verifyAllInputs und simStrategy (beide Schrittweiten) bestehen alle.

Lösungsvorschlag

  1. Kopffreiraum wie Nachlauf. In buildPattern für alle Musterfamily:
    const head = Math.max(runup, minReaction · K.HEAD_OF_REACTION) statt rng.range(50, 90) —
    die Zahl gehört in CORE.K, gemessen an der Strecke, die ein über die Grenze tragender
    Anflug übrig lässt (Vorschlag: so, dass nach einer Landung bei x0 + 385 − needTail noch
    runup frei bleibt).
  2. Übergabe verifizieren. verifyContent (oder verifySequence(world, prevContent, content, ctx)) prüft zusätzlich ab dem Zustand, den die Vorgänger-Strategie an der Grenze hinterlässt —
    simProbe() (Zeile 1408) kann genau das bereits. Scheitert die Übergabe, greift die
    bestehende fallbackChain, oder der Kopf wird verlängert.
  3. Reihenfolge-Soak. Der Lauf-Teil von tests/soak.mjs (nach Fix von #25) ist der natürliche
    Ort: mehrere Samen × 5 Minuten mit korrekter Tod-Erkennung; ein Tod in einem verifizierten
    Muster ist rot.

Erwarteter Test

  • tests/issue24.mjs — extrahiertes CORE, echte Generierung:
    (a) jedes committed Muster lässt ab jedem plausiblen Übergangszustand (Boden plus
    Anflugbahnen der Vorgänger-Strategie) ≥ K.SOLVER_MIN_REACTION · v freie Strecke bis zum
    ersten Hindernis, (b) Reihenfolge-Lauf über N Samen ohne Tod. Gegen git show HEAD:index.html
    rot: 89 von 116 fliegenden Übergängen unter der Reaktionsdistanz, Kopfabstände 50–90 px,
    Kollision Seed 555 bei 82 529.
  • tests/browser_issue24.py — echtes Dokument, reale Sequenz zweier Muster (ROLL_JUMP → JUMP
    wie Seed 555): Leertaste im ersten möglichen Schritt nach der Landung → Erwartung nach dem Fix
    keine Kollision (Headroom ≥ runup), vorher 3 × 1 px Überschneidung.

Dokumentiert in issues/24-musteranfang-ohne-reaktionsraum-bei-fliegendem-uebergang.md (Commit 86a481d).

**Komponente:** CORE (`buildPattern` Zeilen 1101, 1112, 1119, 1127, 1140 — erste Hindernisse; `minReaction`/`runup`/`landing` 1084–1086; `normalizeContent` 1258–1274; `simStrategy` 1368–1380; `verifyContent` 1553–1601), GAME (`simStep` → `CORE.hitTest`, Zeile 3338) **Betroffene Version:** `main` @ `86a481d`, `index.html` (3.684 Zeilen, MD5 `777d18c3edbb834aa41fccc7d10a7d18`) ## Beschreibung Jedes Muster wird **einzeln** verifiziert, und zwar ab einem kanonischen Start: `simStrategy` setzt den Körper auf Boden bei `content.x0 − clamp(0,45·v, 150, 235)` (Zeilen 1370–1373). Die Generierung sichert außerdem den **Nachlauf** eines Musters zu — `normalizeContent` verlängert den Boden hinter dem letzten Hindernis auf `needTail = max(215, 0,58·v)` (371 px bei 640 px/s, Zeile 1259). Was niemand zusichert, ist der **Kopf**: Das erste Hindernis eines Musters liegt | Muster | Abstand erstes Hindernis → `content.x0` | Zeile | |---|---|---| | `JUMP` | **50–90 px** | 1101 | | `DOUBLE_JUMP` | **52–90 px** | 1140 | | `JUMP_ROLL` / `ROLL_JUMP` | **50–90 px** / 50–84 px | 1127 | | `ROLL` | **60–110 px** | 1112 | | `MOVING_GATE` | **71–120 px** | 1119 | | Gruben (`PIT`, `PIT_HOOK`, `PIT_PLATFORM`, `PIT_DOUBLE`, `HOOK_*`) | `pad = runup(397 px) + rng(0, 80/90)` ✓ | 1158, 1169, 1186, 1200, 1205 | `runup` (Zeile 1085, 397 px bei 640) und `minReaction` (Zeile 1084, **608 px** bei 640 px/s und `K.SOLVER_MIN_REACTION` 0,95) existieren — `runup` wird nur vor Grubenkanten benutzt, `minReaction` genau einmal: als *Innenabstand* zwischen den zwei Hindernissen von `JUMP_ROLL`/`ROLL_JUMP` (Zeile 1130). Ein Fliegen über die Grenze hinweg ist Normalbetrieb: Ein gehaltenes Sprung ist 321 px lang (#16), der Nachlauf des Vorgängers 371 px — wer kurz vor dem letzten Hindernis des Vorgängers abspringt, landet **hinter der Grenze**. Dann ist die verfügbare Reaktionsstrecke das, was vom Kopf-Abstand übrig bleibt: bei 640 px/s sind 60 px Kopf **94 ms**. Gemessen über fünf Läufe à 5 Minuten: **1 131 Muster-Übergänge, 116 davon fliegend, 89 davon landen näher als `minReaction` an einem Hindernis** (Median der freien Strecke: **117 px**). ## Messprotokoll Node gegen das extrahierte `CORE` (Same 4242, 777, 555, 2, 90210; je 300 s, `K.FIXED_TIMESTEP`), Muster über den echten `generateAhead`-Pfad, `onCommit` notiert `min(o.x − x0)` über die Hindernisse und `min(p.x0 − x0)` über die Gruben. Beim Wechsel des aktuellen Musters wird der Körperzustand notiert, bei der nächsten Bodenberührung die freie Strecke bis zum nächsten Hindernis. ### 1. Übergänge | | Anzahl | |---|---| | Muster-Übergänge gesamt | 1 131 | | davon fliegend (Körper bei `x0` nicht `grounded`) | **116 (10 %)** | | davon mit freier Strecke < `minReaction` (608 px) | **89** | | freie Strecke nach der Landung: min / Median / max | **−291** / **117** / groß | `−291` heißt: Die Landung liegt *hinter* dem ersten Hindernis — überlebt hat der Lauf das nur, weil derselbe Anflugbogen schon darüber hinweggetragen hat. Direkt dahinter wartet bei `DOUBLE_JUMP` das zweite Hindernis. Beispiele (alle Seed 4242, v = 640 px/s, `minReaction` = 608 px): | Muster | erstes Hindernis hinter `x0` | Landung | freie Strecke | |---|---|---|---| | `MOVING_GATE` | 105 px | 2 px hinter der Grenze | **103 px** | | `JUMP` | 69 px | 4 px *vor* der Grenze | **73 px** | | `ROLL_JUMP` | 69 px | 16 px vor der Grenze | **85 px** | | `ROLL` | 92 px | 30 px vor der Grenze | **122 px** | | `DOUBLE_JUMP` | 60 px | 348 px hinter der Grenze | **−288 px** | ### 2. Konkreter Todesfall (Seed 555, t = 134,69 s, 8 192 m, Sektor 19) * Vorgänger `ROLL_JUMP` `81 298 … 82 483` (Tor bei 81 350, Block bei 82 074, `w` 38 `h` 43) * Folgemuster `JUMP` `82 483 … 83 092`, **ein Block bei 82 539** (`w` 41 `h` 44) — 56 px hinter der Grenze * Muster-Strategie `{grab:"none", jumpLead:86, hold:0,18, rollLead:52}`, `verifyContent` dieses Musters: `ok: true`, `takeoffWindow` **13**, `simStrategy` **bestanden bei 1/110 und bei 1/120** Live-Trace (1/120 s): ``` t 134,617 x 82 481 y 503 JUMPING vy 887 (Anflug aus dem Vorgänger) t 134,642 x 82 497 y 530 RUNNING vy 0 ev[landed] t 134,667 x 82 513 y 508 JUMPING vy -867 cmd[jumpHeld] ← erster Sprungschritt nach der Landung t 134,692 x 82 529 y 487 JUMPING vy -834 → Kollision ``` Landung 14 px innerhalb des neuen Musters, Block bei 82 539 — der Autopilot springt im ersten möglichen Schritt (26 px vor dem Block) und steigt nicht hoch genug: Hitbox `82 516…82 542 × 457…517` gegen Block `82 539…82 580 × 516…560` → **Überschneidung 3 × 1 px** an der Unteren linken Blockecke. Ein Muster also, das der Solver mit 13 Bildraten Fenster als bequem eintragbar bewertet, ist in der echten Reihenfolge um ein Pixel verloren. ## Ursache im Code 1. **Der Kopf ist unreguliert.** Zeilen 1101/1112/1119/1127/1140 legen das erste Hindernis mit `x0 + rng.range(50, 90)` bzw. `(60, 110)` / `(70, 120)`. `runup` (1085) und `minReaction` (1084) werden dort nicht angefasst; `normalizeContent` (1258–1274) reguliert nur den *Nachlauf* (`needTail`) und die Mindestlänge `c.x1 ≥ c.x0 + 240`. 2. **Verifiziert wird der kanonische Start, nicht die Übergabe.** `simStrategy` (1370–1373) beginnt auf Boden; dass der Körper eine Muster-Grenze im Flug überquert (gemessen 10 %), ist demSolver unbekannt. `verifyContent` wiederholt das nur mit Tempo-Varianten (`factors`/`leadJitters`, Zeilen 1556–1558), nie mit anderen Anfangszuständen. 3. **Der Nachlauf des Vorgängers trägt seine eigene Landung.** Bei 640 px/s sind `needTail` 371 px lang, ein gehaltener Sprung 321 px, gemessene Flugstrecke des konkreten Falls 385 px — die Landung fällt per Konstruktion in den Kopf des Folgemusters. ## Folgeverhalten * Läufe sterben an einem `JUMP`-/`DOUBLE_JUMP`-/`ROLL`-/`MOVING_GATE`-Muster, das als verifiziert in der Welt steht. Betroffen ist der ganze Bereich ab ~Sektor 4, weil ab dort Tempo und Musterdichte steigen; gemessen bei 8,2 km (Sektor 19, Endlos). * Für den Spieler ist das nicht abwendbar: Er sieht den Block erst nach der Landung (Kamera folgt mit `K.PLAYER_SCREEN_X`), und 60 px sind 94 ms. * Die Menü-Demo fährt denselben Pfad, plant aber an jeder Grenze mit `liveStrategy()` neu (#15/#17) und überlebt dadurch — der Befund ist demo-seitig unsichtbar. ## Nicht verletzt (bewusst abgegrenzt) * Issue #16 hält: Blockhöhen an der gemessenen Hüllkurve, Mulden in Landeweite der kürzesten Bahn, `K.MIN_TAKEOFF_FRAMES` auf allen Mustern. * Grubenmuster sind nicht betroffen — sie haben `pad = runup + rng(0, 80/90)` vor der Kante. * Das Muster für sich ist lösbar und bequem: `verifyContent`, `takeoffWindow`, `verifyAllInputs` und `simStrategy` (beide Schrittweiten) bestehen alle. ## Lösungsvorschlag 1. **Kopffreiraum wie Nachlauf.** In `buildPattern` für *alle* Musterfamily: `const head = Math.max(runup, minReaction · K.HEAD_OF_REACTION)` statt `rng.range(50, 90)` — die Zahl gehört in `CORE.K`, gemessen an der Strecke, die ein über die Grenze tragender Anflug übrig lässt (Vorschlag: so, dass nach einer Landung bei `x0 + 385 − needTail` noch `runup` frei bleibt). 2. **Übergabe verifizieren.** `verifyContent` (oder `verifySequence(world, prevContent, content, ctx)`) prüft zusätzlich ab dem Zustand, den die Vorgänger-Strategie an der Grenze hinterlässt — `simProbe()` (Zeile 1408) kann genau das bereits. Scheitert die Übergabe, greift die bestehende `fallbackChain`, oder der Kopf wird verlängert. 3. **Reihenfolge-Soak.** Der Lauf-Teil von `tests/soak.mjs` (nach Fix von #25) ist der natürliche Ort: mehrere Samen × 5 Minuten mit korrekter Tod-Erkennung; ein Tod in einem verifizierten Muster ist rot. ## Erwarteter Test * `tests/issue24.mjs` — extrahiertes `CORE`, echte Generierung: (a) jedes committed Muster lässt ab **jedem** plausiblen Übergangszustand (Boden plus Anflugbahnen der Vorgänger-Strategie) ≥ `K.SOLVER_MIN_REACTION · v` freie Strecke bis zum ersten Hindernis, (b) Reihenfolge-Lauf über N Samen ohne Tod. Gegen `git show HEAD:index.html` rot: 89 von 116 fliegenden Übergängen unter der Reaktionsdistanz, Kopfabstände 50–90 px, Kollision Seed 555 bei 82 529. * `tests/browser_issue24.py` — echtes Dokument, reale Sequenz zweier Muster (`ROLL_JUMP` → `JUMP` wie Seed 555): Leertaste im ersten möglichen Schritt nach der Landung → Erwartung nach dem Fix keine Kollision (Headroom ≥ `runup`), vorher 3 × 1 px Überschneidung. --- *Dokumentiert in `issues/24-musteranfang-ohne-reaktionsraum-bei-fliegendem-uebergang.md` (Commit `86a481d`).*
Author
Owner

Behoben in 37ee45f (Kopf) und b10e09f (Dichte-Füllung), Doku in f1fa9d5.

Was geändert wurde

  1. Der Kopf ist eine Größe, keine Hausnummer. CORE.headClearance(v) = K.HEAD_OF_MAX_ARC · maxJumpDistance(v) (+ K.HEAD_JITTER als Würfel darüber) gilt für alle Familien: Hindernismuster über x0 + head, Gruben über pad = head. Der Einzelwert runup ist ersatzlos gestrichen – ein Wert ohne Setzer ist keine Funktion. Die Zahl ist an der Bahn sized, nicht an einem Flächenmaß: 1,25 × die längste Bahn, die über eine Grenze tragen kann (#16: 570 px bei 640 px/s) = 713 px bei 640 px/s, 401 px beim Starttempo.
  2. Zwei Folgen der längeren Muster: K.SIM_TIME_MARGIN – das Solver-Budget ist eine Strecke (simBudget()), keine feste Zeit mehr, sonst laufen lange Muster als „timeout“ in die Tonne (tests/issue17.mjs wurde rot: 17 von 163 Demo-Grenzen fielen zurück). Und probeSegments in simProbe – die Weltkante ist keine Grube: seit Muster länger als der Verlegehorizont werden können, lag ihr Prüfziel jenseits des erzeugten Bodens und jede Sonde fiel dort hinunter.
  3. Der Kopf kostet Grund, zurückgeholt wird er mit Inhalt, nicht mit Geometrie (b10e09f): K.DENSITY_FILL lässt JUMP zwei Blöcke, ROLL zwei Tore und MOVING_GATE einen Block dahinter tragen. Der Abstand der Füllung ist keine neue Zahl: innerGap() in buildPattern ist derselbe Abstand, der zwei Muster trennt (minReaction, darunter rollDuration · v für die Rolle bzw. 1,15 · minJumpDistance für den Sprung, plus Würfel) – die Ausdrücke standen schon doppelt in JUMP_ROLL/ROLL_JUMP und sind dort jetzt dieselbe Funktion. Der Nachlauf konnte den Grund nicht zurückgeben: er trägt den kanonischen Solver-Start des Folgemusters (x0 − clamp(0,45·v, 150, 235)); ein Versuch, ihn auf 240 px zu kürzen, ließ verifyContent SAFE-Muster verwerfen („Start blockiert“).

Warum nicht der Lösungsvorschlag 2 (verifySequence)? Die Zusage ist geometrisch billiger zu haben und billiger zu prüfen: wenn der Kopf per Definition länger ist als die längste Bahn, die über die Grenze tragen kann, kann keine Landung hinter dem ersten Hindernis liegen. Eine Übergabe-Verifikation hätte für jedes Musterpaar denselben Beweis führen wollen, den jetzt eine Zahl führt – und sie hätte die Verlegungskosten vervielfacht. Verifiziert wird die Naht trotzdem lebend, nicht als Vermutung: acht Sequenzläufe à 180 s mit echtem generateAhead, echtem Autopilot auf der ins Muster geschriebenen Strategie und Schrittweite des Laufs.

Messung nach dem Fix

vorher nachher
Kopf (640 px/s) 50–120 px = 0,09 × längste Bahn 694 … 783 px (1,25 × + Würfel)
fliegende Handüber, 8 Läufe à 180 s 73, davon 61 unter minReaction, tiefster −287 px 48, jedes ≥ 608 px, engstes 706 px, Median 759 px
Seed 555 Tod bei 136,79 s / 8 193 m lebt, 18 528 m in 300 s
echtes Dokument, echte Leertaste 75/99 Anflüge, engste Landung 17 px vor dem Block, tiefste −481 px 99/99 Anflüge leben, keine Landung hinter der ersten Gefahr, engste 629 px Anlauf bzw. 159 px über den ganzen Nachlauf
Demo-Neuplanung 146/163 Grenzen bestätigt 163/163
Dichte (Gefahren pro 1 000 px Musterboden) 1,22 0,75 mit Kopf, 0,85 mit Füllung (43 von 80 Mustern der besetzten Familien gefüllt, 0 erzwungene SAFE-Fallbacks, 0 von 232 Kandidaten abgelehnt, alle 12 Familien verlegt)

Die Dichte ist der bewusste Kompromiss: maxJumpDistance + minReaction (2,07 ×) ließ tests/issue16.mjs auf 276 von geforderten > 300 Mustern fallen, bei 1,25 × bleiben nach der tiefsten denkbaren Landung noch 0,25 × Bahn Anlauf.

Tests

  • tests/issue24.mjs – 32 Prüfungen: Zahlen in CORE.K, Quelle statt Hausnummern (keine Familie würfelt einen eigenen Kopf, neun Kopfstellen an einer Zahl, runup weg), jedes committed Muster hält den Kopf ein und ist länger als 1,2 × längste Bahn, 48 fliegende Handüber in acht Sequenzläufen, Seed 555 überlebt 5 Minuten, Solver-Budget als Strecke, probeSegments (inkl. Beißprobe: an einem echten Block stirbt die Sonde weiter), Dichte A/B gegen K.DENSITY_FILL = 0. Gegen die Fassung vor dem Kopf 23 von 32 rot, gegen den Kopf ohne die Füllung 5 rot.
  • tests/browser_issue24.py – echtes Dokument, echte Leertaste, echtes Musterpaar: Kopf ≥ 1,2 × längste Bahn, 99 Anflüge über dieselbe Grenze (Absprungpixel ±6 Bildraten, drei Haltezeiten), keine Landung hinter der ersten Gefahr, Würfe über den ganzen Nachlauf. Vorher 6 von 7 rot.
  • tests/issue23.mjs verliert seine Ausnahme für Tode außerhalb einer Grube – das war dieser Befund. Volle Suite (./tests/run-all.sh 1) grün.

Nebenbefunde aus der Arbeit

  • 2b4c132: tests/browser_issue23.py machte den Anteil getragener Absprungpixel über ±10 Bildraten an einer Kette fest (≥ 90 %). Über 16 generierte Ketten gemessen ist das eine Verteilung (Median 100 %, kleinste Kette 73 % – bei ihr endet die Toleranz außerhalb des Suchradius, ohne dass ein Streifen entsteht); die Zahl hängt jetzt dort, wo eine Verteilung gemessen werden kann, das Dokument hält Bandbreite, ±1-Bildraten-Fenster und Todesursache. Der Dichte-Wechsel hatte die gepinnte Kette nur von 91,6 % auf 89,8 % gerückt – die Prüfung war vorher schon Glück.
  • 673b0be: browser_smoke las seine Pixel in einem anderen evaluate als es renderte – unter Last lag zwischen beidem ein clear und die Zeilen waren leer.
  • Offen bleibt #25: tests/soak.mjs prüft das seit #21 gestrichene body.dead und kann einen Tod nicht melden. Deshalb zählen die Sequenzläufe in tests/issue24.mjs den Tod selbst über CORE.hitTest/y > K.FALL_DEATH_Y.
**Behoben** in `37ee45f` (Kopf) und `b10e09f` (Dichte-Füllung), Doku in `f1fa9d5`. ## Was geändert wurde 1. **Der Kopf ist eine Größe, keine Hausnummer.** `CORE.headClearance(v) = K.HEAD_OF_MAX_ARC · maxJumpDistance(v)` (+ `K.HEAD_JITTER` als Würfel darüber) gilt für **alle** Familien: Hindernismuster über `x0 + head`, Gruben über `pad = head`. Der Einzelwert `runup` ist ersatzlos gestrichen – ein Wert ohne Setzer ist keine Funktion. Die Zahl ist an der **Bahn** sized, nicht an einem Flächenmaß: 1,25 × die längste Bahn, die über eine Grenze tragen kann (#16: 570 px bei 640 px/s) = 713 px bei 640 px/s, 401 px beim Starttempo. 2. **Zwei Folgen der längeren Muster:** `K.SIM_TIME_MARGIN` – das Solver-Budget ist eine Strecke (`simBudget()`), keine feste Zeit mehr, sonst laufen lange Muster als „timeout“ in die Tonne (`tests/issue17.mjs` wurde rot: 17 von 163 Demo-Grenzen fielen zurück). Und `probeSegments` in `simProbe` – die **Weltkante ist keine Grube**: seit Muster länger als der Verlegehorizont werden können, lag ihr Prüfziel jenseits des erzeugten Bodens und jede Sonde fiel dort hinunter. 3. **Der Kopf kostet Grund, zurückgeholt wird er mit Inhalt, nicht mit Geometrie** (`b10e09f`): `K.DENSITY_FILL` lässt `JUMP` zwei Blöcke, `ROLL` zwei Tore und `MOVING_GATE` einen Block dahinter tragen. Der Abstand der Füllung ist keine neue Zahl: `innerGap()` in `buildPattern` ist **derselbe Abstand, der zwei Muster trennt** (`minReaction`, darunter `rollDuration · v` für die Rolle bzw. `1,15 · minJumpDistance` für den Sprung, plus Würfel) – die Ausdrücke standen schon doppelt in `JUMP_ROLL`/`ROLL_JUMP` und sind dort jetzt dieselbe Funktion. Der Nachlauf konnte den Grund nicht zurückgeben: er trägt den kanonischen Solver-Start des Folgemusters (`x0 − clamp(0,45·v, 150, 235)`); ein Versuch, ihn auf 240 px zu kürzen, ließ `verifyContent` SAFE-Muster verwerfen („Start blockiert“). **Warum nicht der Lösungsvorschlag 2 (`verifySequence`)?** Die Zusage ist geometrisch billiger zu haben und billiger zu prüfen: wenn der Kopf per Definition länger ist als die längste Bahn, die über die Grenze tragen kann, *kann* keine Landung hinter dem ersten Hindernis liegen. Eine Übergabe-Verifikation hätte für jedes Musterpaar denselben Beweis führen wollen, den jetzt eine Zahl führt – und sie hätte die Verlegungskosten vervielfacht. Verifiziert wird die Naht trotzdem **lebend**, nicht als Vermutung: acht Sequenzläufe à 180 s mit echtem `generateAhead`, echtem Autopilot auf der ins Muster geschriebenen Strategie und Schrittweite des Laufs. ## Messung nach dem Fix | | vorher | nachher | |---|---|---| | Kopf (640 px/s) | 50–120 px = 0,09 × längste Bahn | 694 … 783 px (1,25 × + Würfel) | | fliegende Handüber, 8 Läufe à 180 s | 73, davon 61 unter `minReaction`, tiefster **−287 px** | 48, **jedes** ≥ 608 px, engstes 706 px, Median 759 px | | Seed 555 | Tod bei 136,79 s / 8 193 m | lebt, 18 528 m in 300 s | | echtes Dokument, echte Leertaste | 75/99 Anflüge, engste Landung 17 px vor dem Block, tiefste −481 px | **99/99** Anflüge leben, keine Landung hinter der ersten Gefahr, engste 629 px Anlauf bzw. 159 px über den ganzen Nachlauf | | Demo-Neuplanung | 146/163 Grenzen bestätigt | 163/163 | | Dichte (Gefahren pro 1 000 px Musterboden) | 1,22 | 0,75 mit Kopf, **0,85** mit Füllung (43 von 80 Mustern der besetzten Familien gefüllt, 0 erzwungene SAFE-Fallbacks, 0 von 232 Kandidaten abgelehnt, alle 12 Familien verlegt) | Die Dichte ist der bewusste Kompromiss: `maxJumpDistance + minReaction` (2,07 ×) ließ `tests/issue16.mjs` auf 276 von geforderten > 300 Mustern fallen, bei 1,25 × bleiben nach der tiefsten denkbaren Landung noch 0,25 × Bahn Anlauf. ## Tests * `tests/issue24.mjs` – 32 Prüfungen: Zahlen in `CORE.K`, **Quelle statt Hausnummern** (keine Familie würfelt einen eigenen Kopf, neun Kopfstellen an einer Zahl, `runup` weg), jedes committed Muster hält den Kopf ein und ist länger als 1,2 × längste Bahn, 48 fliegende Handüber in acht Sequenzläufen, Seed 555 überlebt 5 Minuten, Solver-Budget als Strecke, `probeSegments` (inkl. Beißprobe: an einem echten Block stirbt die Sonde weiter), Dichte A/B gegen `K.DENSITY_FILL = 0`. **Gegen die Fassung vor dem Kopf 23 von 32 rot**, gegen den Kopf ohne die Füllung 5 rot. * `tests/browser_issue24.py` – echtes Dokument, echte Leertaste, echtes Musterpaar: Kopf ≥ 1,2 × längste Bahn, 99 Anflüge über dieselbe Grenze (Absprungpixel ±6 Bildraten, drei Haltezeiten), keine Landung hinter der ersten Gefahr, Würfe über den ganzen Nachlauf. Vorher 6 von 7 rot. * `tests/issue23.mjs` verliert seine Ausnahme für Tode außerhalb einer Grube – das war dieser Befund. Volle Suite (`./tests/run-all.sh 1`) grün. ## Nebenbefunde aus der Arbeit * `2b4c132`: `tests/browser_issue23.py` machte den Anteil getragener Absprungpixel über ±10 Bildraten an **einer** Kette fest (≥ 90 %). Über 16 generierte Ketten gemessen ist das eine Verteilung (Median 100 %, kleinste Kette 73 % – bei ihr endet die Toleranz außerhalb des Suchradius, ohne dass ein Streifen entsteht); die Zahl hängt jetzt dort, wo eine Verteilung gemessen werden kann, das Dokument hält Bandbreite, ±1-Bildraten-Fenster und Todesursache. Der Dichte-Wechsel hatte die gepinnte Kette nur von 91,6 % auf 89,8 % gerückt – die Prüfung war vorher schon Glück. * `673b0be`: `browser_smoke` las seine Pixel in einem anderen `evaluate` als es renderte – unter Last lag zwischen beidem ein clear und die Zeilen waren leer. * **Offen bleibt #25**: `tests/soak.mjs` prüft das seit #21 gestrichene `body.dead` und kann einen Tod nicht melden. Deshalb zählen die Sequenzläufe in `tests/issue24.mjs` den Tod selbst über `CORE.hitTest`/`y > K.FALL_DEATH_Y`.
gilli closed this issue 2026-09-08 21:40:24 +02:00
Sign in to join this conversation.
No description provided.