Lichtschranke (GATE) hat keine Kollisionsbox und wird als 22-px-Stummel am oberen Bildschirmrand gezeichnet #1

Closed
opened 2026-08-29 01:41:01 +02:00 by G1LL1 · 5 comments
G1LL1 commented 2026-08-29 01:41:01 +02:00 (Migrated from gitlab.g1ll1.com)

Komponente: CORE (Level-Generator, Kollision), RENDER
Betroffene Version: main @ 238ed66 (übernommen aus kelpie @ 014b986), index.html

Beschreibung

Der Hindernistyp GATE (Lichtschranke – „unter hohen Hindernissen hindurchrollen“) hat im
spielbaren Zustand keine wirksame Hitbox. Der Kollisionsrechteck liegt bei y = 0 … 22,
also ganz oben am Bildschirmrand, statt bei der sichtbaren Balkenunterkante (bottom ≈ 514).

Die Figur kann in normaler Körperhaltung ungehindert jede Lichtschranke durchlaufen.
Zusätzlich wird das Hindernis beim Passieren als erfolgreiches Ausweichmanöver gewertet.

Das Visualisierungsproblem ist dieselbe Ursache: drawObstacle() leitet die sichtbare
Balkenunterkante aus demselben Rechteck ab (bottom = r.y + r.h) und zeichnet die Lichtschranke
dadurch als ca. 22 px hohen Stummel direkt am oberen Rand – nicht als bis auf Kopfhöhe
abgehängendes Tor.

Ursache

In mkObs() werden Höhe und Anker für GATE/MGATE gesetzt, aber die Y-Position auf 0
belassen:

// index.html:979-986
function mkObs(type, x, baseY, w, h) {
  const o = makeObstacle(type, x, baseY, {});
  o.w = w;
  if (type === "BLOCK") { o.h = h; o.y = baseY - h; }
  else if (type === "GATE")  { o.h = 22; o.bottom = baseY - K.GATE_CLEAR;  o.y = 0; }   // <-- o.y bleibt 0
  else if (type === "MGATE") { o.h = 22; o.bottom = baseY - K.MGATE_CLEAR; o.y = 0; }   // <-- egal, wird überschrieben
  return o;
}

obstacleRect() kennt nur für MGATE eine Neu-Berechnung aus o.bottom; für GATE wird
o.y unverwendet weitergereicht:

// index.html:506-512
function obstacleRect(o, tSec, worstCase) {
  if (o.type === "MGATE") { /* ... korrekt: y = o.bottom - osc - o.h */ }
  return { x: o.x, y: o.y, w: o.w, h: o.h };   // GATE -> { y: 0, h: 22 }
}

makeObstacle() (Zeile 499–503) berechnet o.y zunächst korrekt, wird aber von mkObs()
wieder überschrieben. Der MGATE-Zweig funktioniert nur, weil obstacleRect() ihn aus
o.bottom rekonstruiert.

Reproduktion

Isoliert (Node, ohne Browser)

Der CORE-Block ist über die Marker /*__CORE_START__*/ / /*__CORE_END__*/ extrahierbar:

python3 - <<'PY'
src = open('index.html', encoding='utf-8').read()
s, e = src.index('/*__CORE_START__*/'), src.index('/*__CORE_END__*/')
open('core.mjs','w').write(src[s:e] + "\nexport default CORE;\n")
PY
import C from './core.mjs';
const K = C.K, world = C.makeWorld(), gen = C.createGenerator(7);
const ctx = { speed: 400, minReactionSec: 1.0, sectorAbs: 3, sectorIdx: 3, distanceM: 1500, difficulty: 0.5, endless: 0, sectorDef: C.SECTORS[3] };
C.commitContent(world, gen, C.buildPattern('ROLL', gen, ctx), ctx);
const o = world.obstacles[0];
console.log(C.obstacleRect(o, 0, false));            // { y: 0, h: 22 }  <-- statt y ≈ 492

const b = C.makeBody(o.x + o.w / 2, K.GROUND_Y, 400); // stehende Figur direkt in der Tor-Säule
console.log(C.bodyHitbox(b));                         // { y: 500, h: 60 }
console.log(C.hitTest(b, world.obstacles, 0, false)); // null  <-- keine Kollision

Ausgabe:

collision rect: { x: 60.58, y: 0,  w: 41.49, h: 22 }
standing body:  { x: 68.33, y: 500, w: 26,   h: 60 }
STANDING body inside gate -> collision? NO

Im echten Spiel (Playwright, window.PULSE)

Eine einzelne GATE auf flachem Boden gesetzt, 1,2 s ohne Eingabe gelaufen:

{
  "gateRectRuntime": { "x": 1135.2, "y": 0, "w": 50, "h": 22 },
  "drawnBottom": 22,
  "bodyPassedGate": true,
  "dying": false,
  "dodges": 1,
  "dodgesBefore": 0
}

Erwartetes Verhalten

  • Stehende Figur berührt den Lichtschranken-Balken → Kollision → „Lauf beendet / Kollision“.
  • Rollende Figur (34 × 34, ROLL_OFF = 17) passiert den Balken mit lichter Höhe GATE_CLEAR = 46.
  • Sichtbarer Balkenende-Punkt bei bottom = baseY - GATE_CLEAR = 514.

Auswirkung

  1. Kernmechanik fehlt: Eine der drei Grundaktionen (Rollen) ist für das Überleben unnötig.
    ROLL, JUMP_ROLL, ROLL_JUMP und HOOK_TO_ROLL sind ohne Rollpflicht.
  2. Punkte- und Fokus-Inflation: Jedes Durchlaufen zählt als Ausweichmanöver
    (simStep, Zeile 2477–2483: +50 Punkte, +4 % Fokus).
  3. Solver prüft gegen ein Geister-Hindernis: verifyContent()/simStrategy() nutzen
    dieselbe hitTest()/testBox()-Funktion. Damit verifiziert die Lösbarkeitsprüfung
    Lichtschranken-Muster gegen eine nicht existende Kollision. Die Pflicht aus AGENTS.md
    Kein Haken darf so platziert werden, dass die Figur … unter die Unterkante einer
    Lichtschranke schlägt
    “ ist dadurch nicht durchsetzbar – der Solver kann diesen Konflikt
    nicht sehen.
  4. Visualisierung kaputt: Lichtschranke erscheint als 22-px-Markierung am oberen Rand.

Lösungsvorschlag

Entweder Y-Position beim Erzeugen setzen:

else if (type === "GATE")  { o.h = 22; o.bottom = baseY - K.GATE_CLEAR;  o.y = o.bottom - o.h; }
else if (type === "MGATE") { o.h = 22; o.bottom = baseY - K.MGATE_CLEAR; o.y = o.bottom - o.amp - o.h; }

oder – robuster, weil o.y dann nie mehr von Hand gepflegt werden muss – einen GATE-Zweig
in obstacleRect() ergänzen:

if (o.type === "GATE") return { x: o.x, y: o.bottom - o.h, w: o.w, h: o.h };

Danach ist auch drawObstacle() (Zeile 1669) wieder konsistent, und die Lösbarkeitsprüfung
erfasst Lichtschranken automatisch korrekt. Zusätzlich sollte ein Regressionstest die drei
Fälle abdecken: stehend = Treffer, rollend = kein Treffer, Sprung über bottom = kein Treffer.


Dokumentiert in gitlab-issues/01-gate-lichtschranke-keine-kollision.md (Commit 238ed66). Alle Befunde ausgeführt und gemessen; Repro-Schritte im Text.

**Komponente:** CORE (Level-Generator, Kollision), RENDER **Betroffene Version:** `main` @ `238ed66` (übernommen aus `kelpie` @ `014b986`), `index.html` ## Beschreibung Der Hindernistyp `GATE` (Lichtschranke – „unter hohen Hindernissen hindurchrollen“) hat im spielbaren Zustand **keine wirksame Hitbox**. Der Kollisionsrechteck liegt bei `y = 0 … 22`, also ganz oben am Bildschirmrand, statt bei der sichtbaren Balkenunterkante (`bottom ≈ 514`). Die Figur kann in normaler Körperhaltung ungehindert jede Lichtschranke durchlaufen. Zusätzlich wird das Hindernis beim Passieren als erfolgreiches Ausweichmanöver gewertet. Das Visualisierungsproblem ist dieselbe Ursache: `drawObstacle()` leitet die sichtbare Balkenunterkante aus demselben Rechteck ab (`bottom = r.y + r.h`) und zeichnet die Lichtschranke dadurch als ca. 22 px hohen Stummel direkt am oberen Rand – nicht als bis auf Kopfhöhe abgehängendes Tor. ## Ursache In `mkObs()` werden Höhe und Anker für `GATE`/`MGATE` gesetzt, aber die Y-Position auf `0` belassen: ```js // index.html:979-986 function mkObs(type, x, baseY, w, h) { const o = makeObstacle(type, x, baseY, {}); o.w = w; if (type === "BLOCK") { o.h = h; o.y = baseY - h; } else if (type === "GATE") { o.h = 22; o.bottom = baseY - K.GATE_CLEAR; o.y = 0; } // <-- o.y bleibt 0 else if (type === "MGATE") { o.h = 22; o.bottom = baseY - K.MGATE_CLEAR; o.y = 0; } // <-- egal, wird überschrieben return o; } ``` `obstacleRect()` kennt nur für `MGATE` eine Neu-Berechnung aus `o.bottom`; für `GATE` wird `o.y` unverwendet weitergereicht: ```js // index.html:506-512 function obstacleRect(o, tSec, worstCase) { if (o.type === "MGATE") { /* ... korrekt: y = o.bottom - osc - o.h */ } return { x: o.x, y: o.y, w: o.w, h: o.h }; // GATE -> { y: 0, h: 22 } } ``` `makeObstacle()` (Zeile 499–503) berechnet `o.y` zunächst korrekt, wird aber von `mkObs()` wieder überschrieben. Der `MGATE`-Zweig funktioniert nur, weil `obstacleRect()` ihn aus `o.bottom` rekonstruiert. ## Reproduktion ### Isoliert (Node, ohne Browser) Der CORE-Block ist über die Marker `/*__CORE_START__*/` / `/*__CORE_END__*/` extrahierbar: ```bash python3 - <<'PY' src = open('index.html', encoding='utf-8').read() s, e = src.index('/*__CORE_START__*/'), src.index('/*__CORE_END__*/') open('core.mjs','w').write(src[s:e] + "\nexport default CORE;\n") PY ``` ```js import C from './core.mjs'; const K = C.K, world = C.makeWorld(), gen = C.createGenerator(7); const ctx = { speed: 400, minReactionSec: 1.0, sectorAbs: 3, sectorIdx: 3, distanceM: 1500, difficulty: 0.5, endless: 0, sectorDef: C.SECTORS[3] }; C.commitContent(world, gen, C.buildPattern('ROLL', gen, ctx), ctx); const o = world.obstacles[0]; console.log(C.obstacleRect(o, 0, false)); // { y: 0, h: 22 } <-- statt y ≈ 492 const b = C.makeBody(o.x + o.w / 2, K.GROUND_Y, 400); // stehende Figur direkt in der Tor-Säule console.log(C.bodyHitbox(b)); // { y: 500, h: 60 } console.log(C.hitTest(b, world.obstacles, 0, false)); // null <-- keine Kollision ``` Ausgabe: ```text collision rect: { x: 60.58, y: 0, w: 41.49, h: 22 } standing body: { x: 68.33, y: 500, w: 26, h: 60 } STANDING body inside gate -> collision? NO ``` ### Im echten Spiel (Playwright, `window.PULSE`) Eine einzelne `GATE` auf flachem Boden gesetzt, 1,2 s ohne Eingabe gelaufen: ```json { "gateRectRuntime": { "x": 1135.2, "y": 0, "w": 50, "h": 22 }, "drawnBottom": 22, "bodyPassedGate": true, "dying": false, "dodges": 1, "dodgesBefore": 0 } ``` ## Erwartetes Verhalten * Stehende Figur berührt den Lichtschranken-Balken → Kollision → „Lauf beendet / Kollision“. * Rollende Figur (34 × 34, `ROLL_OFF = 17`) passiert den Balken mit lichter Höhe `GATE_CLEAR = 46`. * Sichtbarer Balkenende-Punkt bei `bottom = baseY - GATE_CLEAR = 514`. ## Auswirkung 1. **Kernmechanik fehlt:** Eine der drei Grundaktionen (Rollen) ist für das Überleben unnötig. `ROLL`, `JUMP_ROLL`, `ROLL_JUMP` und `HOOK_TO_ROLL` sind ohne Rollpflicht. 2. **Punkte- und Fokus-Inflation:** Jedes Durchlaufen zählt als Ausweichmanöver (`simStep`, Zeile 2477–2483: `+50` Punkte, `+4 %` Fokus). 3. **Solver prüft gegen ein Geister-Hindernis:** `verifyContent()`/`simStrategy()` nutzen dieselbe `hitTest()`/`testBox()`-Funktion. Damit verifiziert die Lösbarkeitsprüfung Lichtschranken-Muster gegen eine nicht existende Kollision. Die Pflicht aus AGENTS.md „*Kein Haken darf so platziert werden, dass die Figur … unter die Unterkante einer Lichtschranke schlägt*“ ist dadurch nicht durchsetzbar – der Solver kann diesen Konflikt nicht sehen. 4. **Visualisierung kaputt:** Lichtschranke erscheint als 22-px-Markierung am oberen Rand. ## Lösungsvorschlag Entweder Y-Position beim Erzeugen setzen: ```js else if (type === "GATE") { o.h = 22; o.bottom = baseY - K.GATE_CLEAR; o.y = o.bottom - o.h; } else if (type === "MGATE") { o.h = 22; o.bottom = baseY - K.MGATE_CLEAR; o.y = o.bottom - o.amp - o.h; } ``` oder – robuster, weil `o.y` dann nie mehr von Hand gepflegt werden muss – einen `GATE`-Zweig in `obstacleRect()` ergänzen: ```js if (o.type === "GATE") return { x: o.x, y: o.bottom - o.h, w: o.w, h: o.h }; ``` Danach ist auch `drawObstacle()` (Zeile 1669) wieder konsistent, und die Lösbarkeitsprüfung erfasst Lichtschranken automatisch korrekt. Zusätzlich sollte ein Regressionstest die drei Fälle abdecken: stehend = Treffer, rollend = kein Treffer, Sprung über `bottom` = kein Treffer. --- *Dokumentiert in `gitlab-issues/01-gate-lichtschranke-keine-kollision.md` (Commit `238ed66`). Alle Befunde ausgeführt und gemessen; Repro-Schritte im Text.*
G1LL1 commented 2026-08-29 01:45:21 +02:00 (Migrated from gitlab.g1ll1.com)

changed the description

changed the description
G1LL1 commented 2026-08-29 01:48:06 +02:00 (Migrated from gitlab.g1ll1.com)

changed the description

changed the description
G1LL1 commented 2026-08-29 10:48:26 +02:00 (Migrated from gitlab.g1ll1.com)

mentioned in commit 174aeb72a1

mentioned in commit 174aeb72a1761adeda9f2fbb6060ca54ac1a4f82
G1LL1 commented 2026-08-29 10:50:09 +02:00 (Migrated from gitlab.g1ll1.com)

Behoben in 174aeb7 (main).

Umsetzung

  1. obstacleRect() leitet die Tor-Geometrie jetzt aus o.bottom ab; mkObs() pflegt o.y
    nicht mehr von Hand. Die Kollisionsbox ist die komplette Säule von der Decke bis zur
    Balkenunterkante
    – exakt das, was gezeichnet wird (zwei Seile bis y = 0, Balken unten).
    Rollen ist der einzige Weg durch; Springen greift nicht mehr.
  2. MGATE_CLEAR 64 → 48. Mit 64 lag der tiefste Punkt des beweglichen Tors über der
    stehenden Hitbox (Oberkante 500) und konnte damit nie treffen. Der tiefste Punkt ist jetzt
    512, der Solver prüft gegen genau diese Lage (worstCase, osc = 0), in der höchsten Lage
    (bottom − amp ≈ 434…456) passt die stehende Figur durch – die Periode entscheidet also
    wirklich mit.
  3. Lichtschranken-Geometrie ist damit konsistent: Balkenunterkante 514, rollende Hitbox
    bodengleiche 34 px (526…560) → 12 px frei, stehende Hitbox 60 px (500…560) → 14 px Überlappung.

Nebenbefund beim Testen (wichtig)

Sobald das Tor eine echte Hitbox hatte, verschwanden ROLL und MOVING_GATE komplett aus der
Generierung (nur noch SAFE als Fallback). Ursache lag nicht am Tor, sondern am Solver:
autopilot() las seg.x1 als Grubenkante und deutete damit jede Naht zweier Bodensegmente
derselben Höhe
als Abgrund – in der Simulationswelt stehen Mustersegmente unverbunden neben-
eininander. Der Autopilot sprang also an jedem Musteranfang und kollidierte mit dem kurz
dahinter liegenden Tor; alle 54 Strategien fielen durch. Neu: groundEdgeAfter() folgt-
zusammenhängenden Segmenten derselben Höhe und kennt nur noch echte Kanten.

Nach beiden Änderungen erscheinen wieder alle 14 Musterfamilien, und verifyContent akzeptiert
ROLL/MOVING_GATE über alle Geschwindigkeiten (360/450/640 px/s, je 20 Seeds).

Tests

  • node .pi/tests/issue1.mjs – 27 Prüfungen: Geometrie, Treffer stehend / frei rollend,
    Seitentoleranz, Solver-Marge, MGATE-Oszillation (tiefster Moment tödlich, höchster durchlässig),
    alle 200 erzeugten Lichtschranken, Solver-Gegentest (ohne Rollen = hit).
  • node .pi/tests/soak.mjs – 10 000 generierte Muster: keine Lichtschranke direkt nach einer
    Grube, kein Hindernis im Anlauf einer Kante, keine Invariantenverletzung, SAFE-Fallbacks 0 %.
  • Headless Chromium (.pi/tests/browser_smoke.py): Runtime-Hitbox, echter Tod gegen das Tor,
    -rollender Durchgang zählt als Ausweichmanöver, Pixelprobe: heller Querbalken bei
    logisch y ≈ 514, am oberen Rand nur die beiden Seile.
**Behoben in `174aeb7` (`main`).** ## Umsetzung 1. `obstacleRect()` leitet die Tor-Geometrie jetzt aus `o.bottom` ab; `mkObs()` pflegt `o.y` nicht mehr von Hand. Die Kollisionsbox ist die **komplette Säule von der Decke bis zur Balkenunterkante** – exakt das, was gezeichnet wird (zwei Seile bis `y = 0`, Balken unten). Rollen ist der einzige Weg durch; Springen greift nicht mehr. 2. `MGATE_CLEAR` 64 → **48**. Mit 64 lag der tiefste Punkt des beweglichen Tors *über* der stehenden Hitbox (Oberkante 500) und konnte damit nie treffen. Der tiefste Punkt ist jetzt 512, der Solver prüft gegen genau diese Lage (`worstCase`, `osc = 0`), in der höchsten Lage (`bottom − amp` ≈ 434…456) passt die stehende Figur durch – die Periode entscheidet also wirklich mit. 3. Lichtschranken-Geometrie ist damit konsistent: Balkenunterkante 514, rollende Hitbox bodengleiche 34 px (526…560) → 12 px frei, stehende Hitbox 60 px (500…560) → 14 px Überlappung. ## Nebenbefund beim Testen (wichtig) Sobald das Tor eine echte Hitbox hatte, verschwanden `ROLL` und `MOVING_GATE` komplett aus der Generierung (nur noch `SAFE` als Fallback). Ursache lag nicht am Tor, sondern am Solver: `autopilot()` las `seg.x1` als Grubenkante und deutete damit **jede Naht zweier Bodensegmente derselben Höhe** als Abgrund – in der Simulationswelt stehen Mustersegmente unverbunden neben- eininander. Der Autopilot sprang also an jedem Musteranfang und kollidierte mit dem kurz dahinter liegenden Tor; alle 54 Strategien fielen durch. Neu: `groundEdgeAfter()` folgt- zusammenhängenden Segmenten derselben Höhe und kennt nur noch echte Kanten. Nach beiden Änderungen erscheinen wieder alle 14 Musterfamilien, und `verifyContent` akzeptiert `ROLL`/`MOVING_GATE` über alle Geschwindigkeiten (360/450/640 px/s, je 20 Seeds). ## Tests * `node .pi/tests/issue1.mjs` – 27 Prüfungen: Geometrie, Treffer stehend / frei rollend, Seitentoleranz, Solver-Marge, MGATE-Oszillation (tiefster Moment tödlich, höchster durchlässig), alle 200 erzeugten Lichtschranken, Solver-Gegentest (ohne Rollen = `hit`). * `node .pi/tests/soak.mjs` – 10 000 generierte Muster: keine Lichtschranke direkt nach einer Grube, kein Hindernis im Anlauf einer Kante, keine Invariantenverletzung, SAFE-Fallbacks 0 %. * Headless Chromium (`.pi/tests/browser_smoke.py`): Runtime-Hitbox, echter Tod gegen das Tor, -rollender Durchgang zählt als Ausweichmanöver, **Pixelprobe**: heller Querbalken bei logisch y ≈ 514, am oberen Rand nur die beiden Seile.
G1LL1 (Migrated from gitlab.g1ll1.com) closed this issue 2026-08-29 10:53:44 +02:00
G1LL1 commented 2026-08-29 10:53:46 +02:00 (Migrated from gitlab.g1ll1.com)

mentioned in issue #6

mentioned in issue #6
Sign in to join this conversation.
No description provided.