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
Labels
No labels
accessibility
bug
cleanup
consistency
crash
critical
game-feel
gameplay
high
input
level-generation
low
maintainability
medium
pause
physics
responsive
security
spec-gap
ui
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gilli/pulse-sprint#24
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Komponente: CORE (
buildPatternZeilen 1101, 1112, 1119, 1127, 1140 — erste Hindernisse;minReaction/runup/landing1084–1086;normalizeContent1258–1274;simStrategy1368–1380;verifyContent1553–1601), GAME (simStep→CORE.hitTest, Zeile 3338)Betroffene Version:
main@86a481d,index.html(3.684 Zeilen, MD5777d18c3edbb834aa41fccc7d10a7d18)Beschreibung
Jedes Muster wird einzeln verifiziert, und zwar ab einem kanonischen Start:
simStrategysetzt den Körper auf Boden bei
content.x0 − clamp(0,45·v, 150, 235)(Zeilen 1370–1373). DieGenerierung sichert außerdem den Nachlauf eines Musters zu —
normalizeContentverlängertden 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
content.x0JUMPDOUBLE_JUMPJUMP_ROLL/ROLL_JUMPROLLMOVING_GATEPIT,PIT_HOOK,PIT_PLATFORM,PIT_DOUBLE,HOOK_*)pad = runup(397 px) + rng(0, 80/90)✓runup(Zeile 1085, 397 px bei 640) undminReaction(Zeile 1084, 608 px bei 640 px/s undK.SOLVER_MIN_REACTION0,95) existieren —runupwird nur vor Grubenkanten benutzt,minReactiongenau einmal: als Innenabstand zwischen den zwei Hindernissen vonJUMP_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
minReactionan 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 echtengenerateAhead-Pfad,onCommitnotiertmin(o.x − x0)über die Hindernisse undmin(p.x0 − x0)über die Gruben. Beim Wechsel desaktuellen Musters wird der Körperzustand notiert, bei der nächsten Bodenberührung die freie
Strecke bis zum nächsten Hindernis.
1. Übergänge
x0nichtgrounded)minReaction(608 px)−291heiß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_JUMPdas zweite Hindernis.Beispiele (alle Seed 4242, v = 640 px/s,
minReaction= 608 px):x0MOVING_GATEJUMPROLL_JUMPROLLDOUBLE_JUMP2. Konkreter Todesfall (Seed 555, t = 134,69 s, 8 192 m, Sektor 19)
ROLL_JUMP81 298 … 82 483(Tor bei 81 350, Block bei 82 074,w38h43)JUMP82 483 … 83 092, ein Block bei 82 539 (w41h44) — 56 px hinter der Grenze{grab:"none", jumpLead:86, hold:0,18, rollLead:52},verifyContentdiesesMusters:
ok: true,takeoffWindow13,simStrategybestanden bei 1/110 und bei 1/120Live-Trace (1/120 s):
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…517gegen Block82 539…82 580 × 516…560→ Überschneidung 3 × 1 px an derUnteren 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
x0 + rng.range(50, 90)bzw.(60, 110)/(70, 120).runup(1085) undminReaction(1084) werden dort nicht angefasst;
normalizeContent(1258–1274) reguliert nur denNachlauf (
needTail) und die Mindestlängec.x1 ≥ c.x0 + 240.simStrategy(1370–1373)beginnt auf Boden; dass der Körper eine Muster-Grenze im Flug überquert (gemessen 10 %), ist
demSolver unbekannt.
verifyContentwiederholt das nur mit Tempo-Varianten(
factors/leadJitters, Zeilen 1556–1558), nie mit anderen Anfangszuständen.needTail371 px lang, ein gehaltener Sprung 321 px, gemessene Flugstrecke des konkretenFalls 385 px — die Landung fällt per Konstruktion in den Kopf des Folgemusters.
Folgeverhalten
JUMP-/DOUBLE_JUMP-/ROLL-/MOVING_GATE-Muster, das als verifiziertin 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).
mit
K.PLAYER_SCREEN_X), und 60 px sind 94 ms.liveStrategy()neu(#15/#17) und überlebt dadurch — der Befund ist demo-seitig unsichtbar.
Nicht verletzt (bewusst abgegrenzt)
Bahn,
K.MIN_TAKEOFF_FRAMESauf allen Mustern.pad = runup + rng(0, 80/90)vor der Kante.verifyContent,takeoffWindow,verifyAllInputsundsimStrategy(beide Schrittweiten) bestehen alle.Lösungsvorschlag
buildPatternfür alle Musterfamily:const head = Math.max(runup, minReaction · K.HEAD_OF_REACTION)stattrng.range(50, 90)—die Zahl gehört in
CORE.K, gemessen an der Strecke, die ein über die Grenze tragenderAnflug übrig lässt (Vorschlag: so, dass nach einer Landung bei
x0 + 385 − needTailnochrunupfrei bleibt).verifyContent(oderverifySequence(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 diebestehende
fallbackChain, oder der Kopf wird verlängert.tests/soak.mjs(nach Fix von #25) ist der natürlicheOrt: mehrere Samen × 5 Minuten mit korrekter Tod-Erkennung; ein Tod in einem verifizierten
Muster ist rot.
Erwarteter Test
tests/issue24.mjs— extrahiertesCORE, echte Generierung:(a) jedes committed Muster lässt ab jedem plausiblen Übergangszustand (Boden plus
Anflugbahnen der Vorgänger-Strategie) ≥
K.SOLVER_MIN_REACTION · vfreie Strecke bis zumersten Hindernis, (b) Reihenfolge-Lauf über N Samen ohne Tod. Gegen
git show HEAD:index.htmlrot: 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→JUMPwie 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(Commit86a481d).Behoben in
37ee45f(Kopf) undb10e09f(Dichte-Füllung), Doku inf1fa9d5.Was geändert wurde
CORE.headClearance(v) = K.HEAD_OF_MAX_ARC · maxJumpDistance(v)(+K.HEAD_JITTERals Würfel darüber) gilt für alle Familien: Hindernismuster überx0 + head, Gruben überpad = head. Der Einzelwertrunupist 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.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.mjswurde rot: 17 von 163 Demo-Grenzen fielen zurück). UndprobeSegmentsinsimProbe– 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.b10e09f):K.DENSITY_FILLlässtJUMPzwei Blöcke,ROLLzwei Tore undMOVING_GATEeinen Block dahinter tragen. Der Abstand der Füllung ist keine neue Zahl:innerGap()inbuildPatternist derselbe Abstand, der zwei Muster trennt (minReaction, darunterrollDuration · vfür die Rolle bzw.1,15 · minJumpDistancefür den Sprung, plus Würfel) – die Ausdrücke standen schon doppelt inJUMP_ROLL/ROLL_JUMPund 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ßverifyContentSAFE-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 echtemgenerateAhead, echtem Autopilot auf der ins Muster geschriebenen Strategie und Schrittweite des Laufs.Messung nach dem Fix
minReaction, tiefster −287 pxDie Dichte ist der bewusste Kompromiss:
maxJumpDistance + minReaction(2,07 ×) ließtests/issue16.mjsauf 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 inCORE.K, Quelle statt Hausnummern (keine Familie würfelt einen eigenen Kopf, neun Kopfstellen an einer Zahl,runupweg), 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 gegenK.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.mjsverliert 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.pymachte 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_smokelas seine Pixel in einem anderenevaluateals es renderte – unter Last lag zwischen beidem ein clear und die Zeilen waren leer.tests/soak.mjsprüft das seit #21 gestrichenebody.deadund kann einen Tod nicht melden. Deshalb zählen die Sequenzläufe intests/issue24.mjsden Tod selbst überCORE.hitTest/y > K.FALL_DEATH_Y.