diff --git a/app/src/androidTest/java/com/projectsacrifice/game/LevelEightPlaythroughTest.java b/app/src/androidTest/java/com/projectsacrifice/game/LevelEightPlaythroughTest.java index cc40fbc..3ab6831 100644 --- a/app/src/androidTest/java/com/projectsacrifice/game/LevelEightPlaythroughTest.java +++ b/app/src/androidTest/java/com/projectsacrifice/game/LevelEightPlaythroughTest.java @@ -43,19 +43,39 @@ public final class LevelEightPlaythroughTest { } } + @Test + public void intendedRouteSurvivesStressedDecisionScheduling() { + try (ActivityScenario activity = ActivityScenario.launch(MainActivity.class)) { + LevelEightScenario.Result result = new LevelEightScenario() + .completeIntendedRoute(driver(activity, 96L), 20_000L); + + assertTrue("Stressed scheduling: " + result.diagnostic(), result.completed()); + assertEquals("The stressed intended route is deathless", 0, result.deaths()); + } + } + private static AndroidDriver driver(ActivityScenario activity) { + return driver(activity, 0L); + } + + private static AndroidDriver driver(ActivityScenario activity, + long extraDecisionDelayMillis) { final GameView[] view = new GameView[1]; activity.onActivity(mainActivity -> view[0] = mainActivity.gameViewForAutomation()); - return new AndroidDriver(InstrumentationRegistry.getInstrumentation(), view[0]); + return new AndroidDriver(InstrumentationRegistry.getInstrumentation(), view[0], + extraDecisionDelayMillis); } private static final class AndroidDriver implements LevelPlaytestBot.Driver { private final Instrumentation instrumentation; private final GameView gameView; + private final long extraDecisionDelayMillis; - AndroidDriver(Instrumentation instrumentation, GameView gameView) { + AndroidDriver(Instrumentation instrumentation, GameView gameView, + long extraDecisionDelayMillis) { this.instrumentation = instrumentation; this.gameView = gameView; + this.extraDecisionDelayMillis = extraDecisionDelayMillis; } @Override @@ -80,7 +100,7 @@ public final class LevelEightPlaythroughTest { @Override public void waitForNextDecision(long millis) { try { - Thread.sleep(millis); + Thread.sleep(millis + extraDecisionDelayMillis); } catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new AssertionError("Level-8 bot interrupted", interrupted); diff --git a/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java b/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java index 31781bb..d2836c2 100644 --- a/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java +++ b/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java @@ -109,10 +109,10 @@ public final class LevelCatalog { 8, "Borrowed Time", "Jump for the far blue relay if the timed gate closes.", 2450, 130, 760, goal(2220, 600), 0, 38, platforms( - floor(0, 900), floor(1510, 940), ledge(1980, 710, 470), + floor(0, 970), floor(1510, 940), ledge(1980, 710, 470), moving(990, 770, 240, 34, 380, 0, 4.0f, "clock") ), - hazards(hazard(PIT, 900, 880, 610, 200)), + hazards(hazard(PIT, 970, 880, 540, 200)), switches( sw(TIMED, 560, 810, 90, 70, "clock", 6.0f, 1), sw(TIMED, 1600, 650, 90, 70, "clock", 3.5f, 1) diff --git a/docs/LEVEL_SOLUTION_GRAPHS.md b/docs/LEVEL_SOLUTION_GRAPHS.md index 383954e..308f77a 100644 --- a/docs/LEVEL_SOLUTION_GRAPHS.md +++ b/docs/LEVEL_SOLUTION_GRAPHS.md @@ -199,7 +199,7 @@ Door закрывает путь к goal **Статус:** 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=(900,880,610,200)`; дверь `(1770,560,82,320)` требует `"clock"`; `goal=(2220,600,86,110)`. +**Источник:** `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 @@ -207,7 +207,7 @@ Door закрывает путь к goal PIT разрывает floor, а Door закрывает дальнейший путь → войти в TIMED и запустить signal "clock" на 6.0 s → "clock" одновременно запускает horizontal moving platform и открывает Door -→ без задержки добежать до края floor x=900 +→ без задержки добежать до края floor x=970 → прыгнуть на moving platform и пересечь PIT → сойти на правый floor x≥1510 → пройти Door x=1770..1852 до истечения "clock" @@ -222,9 +222,9 @@ PIT разрывает floor, а Door закрывает дальнейший п → пройти 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` дают запас на проход двери, не меняя crossing или recovery. +**Механика и сигналы:** фронт контакта основного `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. +**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.