295 lines
28 KiB
Markdown
295 lines
28 KiB
Markdown
# Графы решений уровней 1–10
|
||
|
||
Документ фиксирует **авторский маршрут** каждого уровня текущего vertical slice. Он получен из `LevelCatalog`, моделей `LevelDefinition` и фактической логики `GameView`, затем проверен device-level ботами через production-команды движения и прыжка. `targetDeaths` ниже — целевое число жертв для результата уровня, но оно не доказывает минимальность решения.
|
||
|
||
На 18 августа 2026 года intended route всех уровней 1–10 автоматически выполнен по три раза подряд без retry masking. `EarlyLevelPlaythroughTest`, `LevelFivePlaythroughTest`, `LateLevelPlaythroughTest` и `LevelEightPlaythroughTest` управляют production `GameView` через те же флаги, что и touch-контролы. Для уровня 8 отдельно проверено восстановление после истечения таймера на дальней стороне `PIT`.
|
||
|
||
## Семантика, общая для графов
|
||
|
||
- `PRESSURE` активен, пока с его `box` пересекается достаточная масса живого игрока и/или тел (`requiredMass`). Сигнал не защёлкивается.
|
||
- `TOGGLE` меняет значение сигнала только на фронте контакта; повторный вход переключает его обратно.
|
||
- `TIMED` запускает таймер только на фронте контакта; удержание на переключателе не перезапускает истёкший таймер.
|
||
- `Door.requiredSignals` использует AND-семантику: дверь открыта, только когда активны все указанные сигналы.
|
||
- Управляемая движущаяся платформа накапливает `motionTime` только при активном `controlId`; при потере сигнала она замирает в текущей фазе `GameMath.pingPong`.
|
||
- `SPIKES` оставляет закреплённое тело над шипами; `SAW` передаёт телу `impulseX`/`impulseY`; `FREEZER` создаёт неподвижный блок `104×104`; `ELECTRIC` оставляет закреплённое проводящее тело внутри опасности; `PIT` тела не оставляет.
|
||
- Тела являются твёрдыми опорами для игрока. Смерть сохраняет тела и состояния уровня, respawn возвращает нового игрока в `spawnX`/`spawnY`; restart очищает состояние.
|
||
- Пересечение живого игрока с `goal` переводит сцену в `COMPLETE`.
|
||
|
||
## Уровень 1 — `The Volunteer`
|
||
|
||
**Статус:** доказан ботом, 3/3 последовательных прохождения без смертей.
|
||
**Ожидаемые смерти:** `0` (`targetDeaths=0`).
|
||
**Источник:** `LevelCatalog.level1()`; уступы `(640,760,230)` и `(1060,690,250)`; `goal=(1690,770,86,110)`.
|
||
|
||
### Solution graph 1
|
||
|
||
```text
|
||
Goal расположен справа от двух перекрывающих путь уступов
|
||
→ разогнаться вправо и запрыгнуть на уступ x=640..870
|
||
→ сохранить движение и перепрыгнуть на более высокий уступ x=1060..1310
|
||
→ спуститься на сплошной floor(0,1920)
|
||
→ пересечь goal x=1690..1776
|
||
→ COMPLETE без смерти
|
||
```
|
||
|
||
**Механика и сигналы:** только движение, прыжок и статические `Platform`; сигналов и дверей нет.
|
||
|
||
**Bot checkpoints:** `playerX>640` при `playerY≤660` → `playerX>1060` при `playerY≤590` → `playerX>1310` → `completed=true`, `deaths=0`.
|
||
|
||
**Softlock/неопределённость:** intended route и окна двух прыжков воспроизводимо подтверждены. Альтернативный проход под/мимо уступов отдельно не проверялся.
|
||
|
||
## Уровень 2 — `A Useful End`
|
||
|
||
**Статус:** доказан ботом, 3/3 последовательных прохождения с тремя телами.
|
||
**Ожидаемые смерти:** `3` (`targetDeaths=3`).
|
||
**Источник:** `LevelCatalog.level2()`; `SPIKES=(610,850,260,30)`; статичный уступ `(1180,740,240)`; `goal=(1830,770,86,110)`.
|
||
|
||
### Solution graph 2
|
||
|
||
```text
|
||
Spike lane x=610..870 перекрывает наземный маршрут
|
||
→ первый доброволец погибает на ближнем краю SPIKES
|
||
→ закреплённое тело становится первой твёрдой опорой
|
||
→ следующие добровольцы продвигаются прыжками и оставляют ещё два тела
|
||
→ цепочка из трёх тел образует предусмотренный путь через lane
|
||
→ следующий доброволец пересекает тела, возвращается на floor
|
||
→ проходит уступ x=1180..1420 и достигает goal
|
||
→ COMPLETE с целевыми тремя смертями
|
||
```
|
||
|
||
**Механика и сигналы:** `SPIKES` закрепляет каждое тело на `hazard.box.y-corpse.height`; двери и сигналы отсутствуют.
|
||
|
||
**Bot checkpoints:** после каждой предусмотренной жертвы `deaths/corpseCount=1/1`, `2/2`, `3/3`; затем живой игрок `playerX>870`; финально `completed=true`, `deaths=3`.
|
||
|
||
**Softlock/неопределённость:** intended route с тремя телами воспроизводимо подтверждён. Отдельная попытка прямого zero-death прохода не подтвердила sequence break, но не является исчерпывающим negative-тестом; минимальность трёх смертей остаётся открытым follow-up.
|
||
|
||
## Уровень 3 — `Dead Weight`
|
||
|
||
**Статус:** доказан ботом, 3/3 последовательных прохождения с одной жертвой.
|
||
**Ожидаемые смерти:** `1` (`targetDeaths=1`).
|
||
**Источник:** `LevelCatalog.level3()`; `SAW=(565,760,92,92)`, impulse `(0,-150)`; `PRESSURE=(540,860,150,20)`, `signalId="gate"`; дверь `(1110,570,78,310)` требует `"gate"`.
|
||
|
||
### Solution graph 3
|
||
|
||
```text
|
||
Door закрывает путь к goal
|
||
→ для Door требуется активный signal "gate"
|
||
→ PRESSURE удерживает "gate" только при постоянной массе
|
||
→ первый доброволец входит в SAW над зоной плиты
|
||
→ тело с нулевым horizontal impulse оседает на PRESSURE
|
||
→ "gate" становится active и Door открывается
|
||
→ respawn следующего добровольца
|
||
→ перепрыгнуть SAW/тело, пройти открытую Door
|
||
→ пересечь goal и получить COMPLETE
|
||
```
|
||
|
||
**Механика и сигналы:** checkpoint цепочки — `corpseCount=1` → `switchContact(PRESSURE)=true` → `signalActive("gate")=true` → `doorOpen=true`.
|
||
|
||
**Bot checkpoints:** `deaths=1`, `corpseCount=1`; после respawn `playerAlive=true`; требуется расширить snapshot состоянием `"gate"` либо использовать `allDoorsOpen=true`; затем `playerX>1188` и `completed=true`.
|
||
|
||
**Softlock/неопределённость:** доставка тела на плиту, удержание двери и последующий проход подтверждены в трёх последовательных запусках. Альтернативные точки контакта с `SAW` отдельно не перебирались.
|
||
|
||
## Уровень 4 — `The Long Lift`
|
||
|
||
**Статус:** доказан ботом, 3/3 последовательных прохождения без смертей.
|
||
**Ожидаемые смерти:** `0` (`targetDeaths=0`).
|
||
**Источник:** `LevelCatalog.level4()`; `TOGGLE=(520,820,72,60)`, `signalId="lift"`; moving platform `(1040,790,260,34)`, travel `(0,-390)`, period `5.2`, `controlId="lift"`; `PIT=(1500,880,320,200)`; верхний уступ `(1660,500,540)`; `goal=(1960,390,86,110)`.
|
||
|
||
### Solution graph 4
|
||
|
||
```text
|
||
Goal находится на верхней галерее за PIT
|
||
→ обычный прыжок с floor не даёт требуемой высоты
|
||
→ один раз войти в TOGGLE и включить signal "lift"
|
||
→ активный "lift" запускает вертикальный цикл moving platform
|
||
→ запрыгнуть на platform x=1040..1300 и подняться
|
||
→ в верхней фазе прыгнуть вправо через край PIT на ledge x=1660..2200
|
||
→ пройти по галерее до goal
|
||
→ COMPLETE без смерти
|
||
```
|
||
|
||
**Механика и сигналы:** фронт контакта `TOGGLE` → `toggleSignals["lift"]=true` → растёт `motionTime` платформы → `travelY*pingPong` поднимает её максимум на `390`.
|
||
|
||
**Bot checkpoints:** `signal "lift"=true`; координата платформы уменьшается от `y=790`; игрок получает `supportIndex` движущейся платформы; затем `playerX≥1660`, `playerY≤400`; финально `completed=true`, `deaths=0`. Для этого snapshot нужно расширить сигналами, `supportIndex` и состоянием платформы.
|
||
|
||
**Softlock/неопределённость:** lift→gallery прыжок подтверждён в трёх последовательных запусках. Повторный контакт с `TOGGLE` по-прежнему выключает сигнал и может заморозить платформу; это предусмотренное состояние, доступное для restart.
|
||
|
||
## Уровень 5 — `Express Delivery`
|
||
|
||
**Статус:** доказан `LevelFivePlaythroughTest`, 3/3 последовательных прохождения.
|
||
**Ожидаемые смерти:** `1` (`targetDeaths=1`); acceptance-тест требует ровно одну.
|
||
**Источник:** `LevelCatalog.level5()`; `SAW=(570,760,96,96)`, impulse `(500,-700)`; уступ `(850,800,330)`; `PRESSURE=(930,780,160,20)`, `signalId="gate"`; дверь `(1390,560,82,320)` требует `"gate"`; `goal=(2080,770,86,110)`.
|
||
|
||
### Solution graph 5
|
||
|
||
```text
|
||
Door закрывает путь к goal
|
||
→ для Door требуется signal "gate"
|
||
→ удалённую PRESSURE нужно удерживать постоянной массой
|
||
→ первый доброволец идёт вправо в SAW
|
||
→ SAW убивает и запускает тело impulse (500,-700)
|
||
→ тело приземляется на уступе/плите x=930..1090
|
||
→ PRESSURE активирует "gate", Door открывается
|
||
→ следующий доброволец respawn-ится
|
||
→ прыжками пересекает SAW и зону доставленного тела
|
||
→ проходит открытую Door и достигает goal
|
||
→ COMPLETE
|
||
```
|
||
|
||
**Механика и сигналы:** `SAW` создаёт динамическое тело → контакт тела с `PRESSURE` → `signalActive("gate")=true` → `allDoorsOpen=true`.
|
||
|
||
**Bot checkpoints:** реализованы в `LevelPlaytestBot.proveLevelFive`: первая смерть; `corpseCount≥1`; `allDoorsOpen=true`; прыжок после `playerX≥390`, дополнительный прыжок после `playerX≥750`; затем `completed=true`. Тест проверяет `completed`, смерть, сохранённое тело и факт открытия двери.
|
||
|
||
**Softlock/неопределённость:** основной маршрут воспроизводимо подтверждён тремя последовательными запусками. Альтернативные решения и физическая стабильность на нескольких frame rates этим тестом не покрыты.
|
||
|
||
## Уровень 6 — `Cold Comfort`
|
||
|
||
**Статус:** доказан ботом, 3/3 последовательных прохождения с frozen stepping stone.
|
||
**Ожидаемые смерти:** `1` (`targetDeaths=1`).
|
||
**Источник:** `LevelCatalog.level6()`; `FREEZER=(690,776,100,104)`; floor заканчивается на `x=1120`; уступы `(810,590,520)` и `(1430,610,570)`; `goal=(1880,500,86,110)`.
|
||
|
||
### Solution graph 6
|
||
|
||
```text
|
||
Высокий уступ x=810..1330 недоступен с уровня floor обычным прыжком
|
||
→ первый доброволец входит в FREEZER
|
||
→ смерть создаёт неподвижный frozen corpse block 104×104 у x=688
|
||
→ следующий доброволец respawn-ится
|
||
→ запрыгивает на frozen block как на промежуточную ступень
|
||
→ с блока прыгает на высокий уступ x=810..1330
|
||
→ пересекает разрыв до уступа x=1430..2000
|
||
→ достигает goal на втором уступе
|
||
→ COMPLETE с одной смертью
|
||
```
|
||
|
||
**Механика и сигналы:** `FREEZER` принудительно ставит `frozen=true`, размеры `104×104`, позицию `(source.x-2, 880-height)`; тело не обновляется физикой и остаётся твёрдой ступенью. Сигналов нет.
|
||
|
||
**Bot checkpoints:** `deaths=1`, `corpseCount=1`; после respawn игрок стоит на блоке около `playerY=676`; затем на первом уступе `playerY=490`, `playerX≥810`; на втором `playerY=510`, `playerX≥1430`; финально `completed=true`.
|
||
|
||
**Softlock/неопределённость:** первоначальная зона `FREEZER` перекрывала игрока, стоящего на блоке, на 26 px и делала intended route невозможным. Высота зоны минимально исправлена до `104`; floor runner всё ещё погибает, а оба прыжка подтверждены тремя запусками.
|
||
|
||
## Уровень 7 — `Live Wire`
|
||
|
||
**Статус:** доказан ботом, 3/3 последовательных прохождения с conductive corpse.
|
||
**Ожидаемые смерти:** `1` (`targetDeaths=1`).
|
||
**Источник:** `LevelCatalog.level7()`; `ELECTRIC=(650,780,110,100)`, `relayId="power"`; дверь `(1130,560,84,320)` требует `"power"`; `goal=(1930,770,86,110)`.
|
||
|
||
### Solution graph 7
|
||
|
||
```text
|
||
Door закрывает путь к goal
|
||
→ Door требует signal "power"
|
||
→ "power" активен только от conductive corpse внутри ELECTRIC relay
|
||
→ первый доброволец входит в ELECTRIC
|
||
→ смерть оставляет закреплённое проводящее тело внутри hazard.box
|
||
→ relay "power" становится active и Door открывается
|
||
→ следующий доброволец respawn-ится
|
||
→ перепрыгивает ELECTRIC/тело и проходит открытую Door
|
||
→ достигает goal
|
||
→ COMPLETE с одной смертью
|
||
```
|
||
|
||
**Механика и сигналы:** `ELECTRIC` выставляет `conductive=true`, центрирует тело внутри hazard и закрепляет его; `signalActive("power")` требует пересечения этого тела с relay hazard.
|
||
|
||
**Bot checkpoints:** `deaths=1`, `corpseCount=1`; `allDoorsOpen=true` после появления conductive corpse; после respawn `playerX>1214`; финально `completed=true`, `deaths=1`. Для прямой проверки механики snapshot должен отдельно сообщать conductive state или активный `"power"`.
|
||
|
||
**Softlock/неопределённость:** relay-сигнал и прыжок через совмещённые hazard/body подтверждены тремя запусками ровно с одной смертью.
|
||
|
||
## Уровень 8 — `Borrowed Time`
|
||
|
||
**Статус:** intended route и восстановление после истечения таймера доказаны production-input ботом, по три раза подряд.
|
||
**Ожидаемые смерти:** `0` (`targetDeaths=0`).
|
||
**Источник:** `LevelCatalog.level8()`; основной `TIMED=(560,810,90,70)`, `signalId="clock"`, duration `6.0`; recovery relay `TIMED=(1600,650,90,70)`, duration `3.5`; moving platform `(990,770,240,34)`, travel `(380,0)`, period `4.0`, `controlId="clock"`; `PIT=(970,880,540,200)`; дверь `(1770,560,82,320)` требует `"clock"`; `goal=(2220,600,86,110)`.
|
||
|
||
### Solution graph 8
|
||
|
||
```text
|
||
PIT разрывает floor, а Door закрывает дальнейший путь
|
||
→ войти в TIMED и запустить signal "clock" на 6.0 s
|
||
→ "clock" одновременно запускает horizontal moving platform и открывает Door
|
||
→ без задержки добежать до края floor x=970
|
||
→ прыгнуть на moving platform и пересечь PIT
|
||
→ сойти на правый floor x≥1510
|
||
→ пройти Door x=1770..1852 до истечения "clock"
|
||
→ запрыгнуть на ledge x=1980..2450
|
||
→ пересечь goal и получить COMPLETE без смерти
|
||
|
||
Ветка восстановления после пересечения PIT:
|
||
→ дождаться истечения "clock" на правом floor x≥1510
|
||
→ Door закрывается и подтверждает прежнюю trap-state
|
||
→ намеренно прыгнуть в высокий recovery relay x=1600, y=650
|
||
→ повторно запустить "clock" на 3.5 s
|
||
→ пройти Door и достичь goal без смерти/restart
|
||
```
|
||
|
||
**Механика и сигналы:** фронт контакта основного `TIMED` → `timedSignals["clock"]=6.0` → движение платформы по `travelX=380` и открытие Door; высокий relay выставляет тот же сигнал на `3.5 s`. Обычный бег проходит под relay, поэтому original route не активирует восстановление автоматически. Строгий grounded-checkpoint выявил нестабильное закрытие Door на `5.5 s`; добавленные `0.5 s` дают запас на проход двери. Android 16 при задержках между решениями бота выявил отдельное падение с узкого стартового края: floor продлён на `70` px, а начало PIT сдвинуто с `900` на `970` без изменения его правого края `1510`, платформы, таймеров или recovery relay.
|
||
|
||
**Bot checkpoints:** основной timer активен; игрок двумя прыжками пересекает PIT через moving platform; перед движением к relay явно подтверждены far floor `playerX≥1510`, `playerY≥779` и всё ещё открытая Door. Intended route проходит Door на первом таймере. Recovery route ждёт `allDoorsOpen=false` справа от PIT, прыжком получает `allDoorsOpen=true`, проходит Door и завершает уровень. Оба маршрута финально дают `completed=true`, `deaths=0`; каждый выполнен три раза подряд на Android 16. Дополнительный intended run с `96 ms` задержки на каждое решение также завершён без смерти. Попытка расширить платформу была отклонена тестом: она могла унести ожидающего игрока с дальнего floor обратно над PIT после закрытия двери.
|
||
|
||
**Softlock/неопределённость:** прежний blocker устранён: после истечения `"clock"` справа от PIT высокий relay гарантирует restart-free повторное открытие двери. Relay не помогает слева от PIT и не отменяет timed-platform crossing. Оба пути проверены без прямой мутации состояния, teleport, auto-complete или restart.
|
||
|
||
## Уровень 9 — `Two Good Reasons`
|
||
|
||
**Статус:** доказан ботом, 3/3 последовательных прохождения с AND из двух тел.
|
||
**Ожидаемые смерти:** `2` (`targetDeaths=2`).
|
||
**Источник:** `LevelCatalog.level9()`; `SPIKES=(520,850,130,30)`; `PRESSURE=(510,842,160,38)`, `signalId="weight"`; `ELECTRIC=(910,780,110,100)`, `relayId="power"`; дверь `(1510,540,86,340)` требует `"weight"` и `"power"`; `goal=(2310,770,86,110)`.
|
||
|
||
### Solution graph 9
|
||
|
||
```text
|
||
Final Door использует AND из "weight" и "power"
|
||
→ первый доброволец погибает на SPIKES внутри зоны PRESSURE
|
||
→ закреплённое spike-тело удерживает signal "weight"
|
||
→ следующий доброволец пересекает spike-тело и погибает в ELECTRIC
|
||
→ закреплённое conductive body удерживает relay signal "power"
|
||
→ "weight" AND "power" одновременно active
|
||
→ Door открывается
|
||
→ третий доброволец respawn-ится
|
||
→ перепрыгивает обе опасности, проходит открытую Door
|
||
→ достигает goal и получает COMPLETE
|
||
```
|
||
|
||
**Механика и сигналы:** spike corpse → `switchContact(PRESSURE)` → `"weight"`; conductive corpse в relay → `"power"`; `doorOpen` проверяет оба `requiredSignals`.
|
||
|
||
**Bot checkpoints:** после первой смерти `deaths/corpses=1/1`, `"weight"=true`, Door ещё closed; после второй `2/2`, `"power"=true`, `allDoorsOpen=true`; после respawn `playerX>1596`; финально `completed=true`, `deaths=2`. Snapshot нужно расширить поимёнными сигналами, чтобы доказать AND, а не только открытие двери.
|
||
|
||
**Softlock/неопределённость:** первая смерть стабильно удерживает `"weight"`, но дверь остаётся закрытой; вторая активирует `"power"` и открывает AND-door. Цепочка подтверждена тремя запусками ровно с двумя телами.
|
||
|
||
## Уровень 10 — `The Last Procession`
|
||
|
||
**Статус:** mastery-маршрут доказан production-input ботом: 3/3 последовательных прохождения и ограниченный стресс финального перехода `96 ms` без ретраев.
|
||
**Ожидаемые и доказанные смерти:** `3` (`targetDeaths=3`).
|
||
**Источник:** `LevelCatalog.level10()`; `SAW=(520,760,96,96)`, impulse `(700,-240)`; `PRESSURE=(860,860,160,20)`, `signalId="lift"`; `FREEZER=(1110,750,100,130)`; `ELECTRIC=(1510,780,110,100)`, `relayId="power"`; moving platform `(1840,790,250,34)`, travel `(0,-410)`, period `5.0`, `controlId="lift"`; стартовый floor заканчивается на `x=1940`; `PIT=(1940,880,200,200)`; дверь `(2310,310,82,570)` требует `"power"`; верхний уступ `(2420,440,680)`; `goal=(2880,330,86,110)`.
|
||
|
||
### Solution graph 10
|
||
|
||
```text
|
||
Goal расположен на верхнем уступе за PIT и закрытой высокой Door
|
||
→ первый доброволец входит в SAW
|
||
→ impulse (700,-240) доставляет тело на PRESSURE x=860..1020
|
||
→ тело удерживает signal "lift" и запускает вертикальную moving platform
|
||
→ следующий доброволец погибает в FREEZER
|
||
→ frozen corpse становится стабильной ступенью для повторного прохода этой зоны
|
||
→ следующий доброволец погибает в ELECTRIC
|
||
→ conductive corpse удерживает relay signal "power" и открывает Door
|
||
→ четвёртый доброволец respawn-ится и пересекает сохранённые тела/опасности
|
||
→ садится на активную platform у x=1840..2090
|
||
→ поднимается над PIT и в верхней фазе прыгает вправо через открытую Door
|
||
→ приземляется на ledge x=2420..3100
|
||
→ достигает goal и получает COMPLETE с тремя смертями
|
||
```
|
||
|
||
**Механика и сигналы:** launched saw corpse → `"lift"` → vertical platform; frozen corpse → твёрдая ступень; conductive corpse → `"power"` → Door open; platform и Door используют независимые постоянные сигналы от сохранённых тел.
|
||
|
||
**Bot checkpoints:** после SAW `deaths=1`, `"lift"=true`, platform `y<790`; после FREEZER `deaths=2` и наличие frozen corpse; после ELECTRIC `deaths=3`, `"power"=true`, `allDoorsOpen=true`; перед посадкой первая moving platform должна вернуться к `y≥780`, затем snapshot подтверждает `playerOnMovingPlatform=true`; в верхней фазе `playerY≤280`; после прыжка `playerX≥2420`, `playerY≤340`; финально `completed=true`.
|
||
|
||
**Softlock/неопределённость:** интегрированный прогон воспроизвёл падение при посадке с исходным 40 px перекрытием floor/lift. Перекрытие увеличено до 100 px, при этом сохранены 200 px `PIT` и обязательный вертикальный подъём. Доставка saw-тела, три сохранённых тела, посадка, подъём и lift→ledge прыжок подтверждены тремя запусками и стресс-сценарием. Обязательность frozen corpse всё ещё не доказана отрицательным тестом: возможный двухсмертный sequence break остаётся follow-up.
|
||
|
||
## Проверки, следующие из графов
|
||
|
||
1. Сохранять полный integrated device-regression: intended route уровней 1–10 по три раза и отдельный recovery route уровня 8.
|
||
2. Отдельно проверить предполагаемый sequence break уровня 2 и необязательность `FREEZER` на уровне 10.
|
||
3. Добавить параметризованные проверки restart из ключевых промежуточных состояний.
|
||
4. Повторить bot matrix на дополнительных frame rates и поддерживаемых версиях Android.
|
||
5. Провести ручные сессии этапа 2 и скорректировать подсказки/допуски без ослабления механик.
|