From a87e6cd62b8d959d5ec7cc0510be0fd54d17a19e Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Tue, 18 Aug 2026 04:18:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=82=D0=B0=D0=B1=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D0=B0=D0=B4=D0=BA=D1=83=20=D0=BD=D0=B0=20=D0=BB=D0=B8?= =?UTF-8?q?=D1=84=D1=82=20=D1=83=D1=80=D0=BE=D0=B2=D0=BD=D1=8F=2010?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/LateLevelPlaythroughTest.java | 27 +++++++++-------- .../game/LateLevelScenarios.java | 2 +- .../projectsacrifice/game/LevelCatalog.java | 4 +-- .../game/LevelCatalogTest.java | 29 +++++++++++++++++++ docs/LEVEL_SOLUTION_GRAPHS.md | 10 +++---- 5 files changed, 50 insertions(+), 22 deletions(-) diff --git a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java index 9890643..8cd85a2 100644 --- a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java +++ b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java @@ -108,9 +108,9 @@ public final class LateLevelPlaythroughTest { } @Test - public void masteryProcessionSurvivesThrottledFinalRelayDecisions() { + public void masteryProcessionSurvivesThrottledFinalTraversalDecisions() { try (ActivityScenario activity = ActivityScenario.launch(MainActivity.class)) { - GameDriver driver = GameDriver.withFinalRelayDelay(activity, 96L); + GameDriver driver = GameDriver.withFinalTraversalDelay(activity, 96L); LevelPlaytestBot.Result result = new LevelPlaytestBot().run( driver, @@ -130,33 +130,33 @@ public final class LateLevelPlaythroughTest { private final Instrumentation instrumentation; private final GameView gameView; private final long minimumDecisionMillis; - private final boolean throttleFinalRelayOnly; + private final boolean throttleFinalTraversalOnly; private GameView.AutomationSnapshot lastSnapshot; private GameDriver(Instrumentation instrumentation, GameView gameView, - long minimumDecisionMillis, boolean throttleFinalRelayOnly) { + long minimumDecisionMillis, boolean throttleFinalTraversalOnly) { this.instrumentation = instrumentation; this.gameView = gameView; this.minimumDecisionMillis = minimumDecisionMillis; - this.throttleFinalRelayOnly = throttleFinalRelayOnly; + this.throttleFinalTraversalOnly = throttleFinalTraversalOnly; } static GameDriver from(ActivityScenario activity) { return create(activity, 32L, false); } - static GameDriver withFinalRelayDelay(ActivityScenario activity, - long minimumDecisionMillis) { + static GameDriver withFinalTraversalDelay(ActivityScenario activity, + long minimumDecisionMillis) { return create(activity, minimumDecisionMillis, true); } private static GameDriver create(ActivityScenario activity, long minimumDecisionMillis, - boolean throttleFinalRelayOnly) { + boolean throttleFinalTraversalOnly) { GameView[] view = new GameView[1]; activity.onActivity(current -> view[0] = current.gameViewForAutomation()); return new GameDriver(InstrumentationRegistry.getInstrumentation(), view[0], - minimumDecisionMillis, throttleFinalRelayOnly); + minimumDecisionMillis, throttleFinalTraversalOnly); } @Override @@ -182,15 +182,14 @@ public final class LateLevelPlaythroughTest { @Override public void waitForNextDecision(long millis) { try { - boolean inFinalRelayWindow = throttleFinalRelayOnly + boolean inFinalTraversalWindow = throttleFinalTraversalOnly && lastSnapshot != null && lastSnapshot.levelNumber() == 10 && lastSnapshot.deaths() == 3 && lastSnapshot.playerX() >= 1_200f - && lastSnapshot.playerX() <= 1_700f; - Thread.sleep(inFinalRelayWindow - ? Math.max(millis, minimumDecisionMillis) - : millis); + && lastSnapshot.playerX() <= 2_200f; + Thread.sleep(inFinalTraversalWindow + ? Math.max(millis, minimumDecisionMillis) : millis); } catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new AssertionError("Late-level bot interrupted", interrupted); diff --git a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java index a13e53e..5882f6c 100644 --- a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java +++ b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java @@ -206,7 +206,7 @@ final class LateLevelScenarios { snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 770f && !Float.isNaN(snapshot.firstMovingPlatformY()) - && snapshot.firstMovingPlatformY() >= 740f) + && snapshot.firstMovingPlatformY() >= 780f) .step("Take off toward the moving lift", 1_000L, LevelPlaytestBot.Input.rightJump(), snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 670f) diff --git a/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java b/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java index d2836c2..11fb0d3 100644 --- a/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java +++ b/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java @@ -140,14 +140,14 @@ public final class LevelCatalog { 10, "The Last Procession", "Launch, freeze, conduct, ascend. Every lesson opens the last relic.", 3100, 130, 760, goal(2880, 330), 3, 120, platforms( - floor(0, 1880), floor(2140, 960), ledge(2420, 440, 680), + floor(0, 1940), floor(2140, 960), ledge(2420, 440, 680), moving(1840, 790, 250, 34, 0, -410, 5.0f, "lift") ), hazards( hazard(SAW, 520, 760, 96, 96, null, 700, -240), hazard(FREEZER, 1110, 750, 100, 130), hazard(ELECTRIC, 1510, 780, 110, 100, "power", 0, 0), - hazard(PIT, 1880, 880, 260, 200) + hazard(PIT, 1940, 880, 200, 200) ), switches(sw(PRESSURE, 860, 860, 160, 20, "lift", 0, 1)), doors(door(2310, 310, 82, 570, "power")) diff --git a/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java b/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java index ca3856b..7ad39f1 100644 --- a/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java +++ b/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java @@ -100,6 +100,35 @@ public final class LevelCatalogTest { freezer.box.x, freezer.box.y, freezer.box.width, freezer.box.height)); } + @Test + public void masteryLiftHasLaunchOverlapAndStillSpansARealPit() { + LevelDefinition level = LevelCatalog.get(9); + LevelDefinition.Platform launchFloor = level.platforms.get(0); + LevelDefinition.Platform landingFloor = level.platforms.get(1); + LevelDefinition.Platform lift = null; + LevelDefinition.Hazard pit = null; + for (LevelDefinition.Platform platform : level.platforms) { + if (platform.isMoving()) lift = platform; + } + for (LevelDefinition.Hazard hazard : level.hazards) { + if (hazard.type == LevelDefinition.HazardType.PIT) pit = hazard; + } + assertTrue("Level 10 must contain its moving lift", lift != null); + assertTrue("Level 10 must retain its final pit", pit != null); + + float launchOverlap = Math.min(launchFloor.box.right(), lift.box.right()) + - Math.max(launchFloor.box.x, lift.box.x); + assertTrue("The floor must overlap the lift by at least 100px for a stable launch", + launchOverlap >= 100f); + assertEquals("The pit must begin exactly where the launch floor ends", + launchFloor.box.right(), pit.box.x, 0f); + assertEquals("The pit must continue to the landing floor", + landingFloor.box.x, pit.box.right(), 0f); + assertTrue("The final pit must remain at least 200px wide", pit.box.width >= 200f); + assertTrue("The upper gallery must still require the vertical lift", + lift.travelY <= -400f && level.goal.y < launchFloor.box.y - 300f); + } + @Test public void everyDoorAndControlledPlatformHasADeclaredSignalSource() { for (LevelDefinition level : LevelCatalog.all()) { diff --git a/docs/LEVEL_SOLUTION_GRAPHS.md b/docs/LEVEL_SOLUTION_GRAPHS.md index 308f77a..2f7a6f1 100644 --- a/docs/LEVEL_SOLUTION_GRAPHS.md +++ b/docs/LEVEL_SOLUTION_GRAPHS.md @@ -257,9 +257,9 @@ Final Door использует AND из "weight" и "power" ## Уровень 10 — `The Last Procession` -**Статус:** mastery-маршрут доказан ботом, 3/3 последовательных прохождения с тремя телами. -**Ожидаемые смерти:** `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"`; `PIT=(1880,880,260,200)`; дверь `(2310,310,82,570)` требует `"power"`; верхний уступ `(2420,440,680)`; `goal=(2880,330,86,110)`. +**Статус:** 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 @@ -281,9 +281,9 @@ Goal расположен на верхнем уступе за PIT и закр **Механика и сигналы:** 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 `supportIndex`, в верхней фазе `playerY≤280`; после прыжка `playerX≥2420`, `playerY≤340`; финально `completed=true`. Snapshot должен различать типы тел, оба сигнала, platform position/support. +**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/неопределённость:** доставка saw-тела, три сохранённых тела, подъём и lift→ledge прыжок подтверждены тремя запусками. Обязательность frozen corpse всё ещё не доказана отрицательным тестом: возможный двухсмертный sequence break остаётся follow-up. +**Softlock/неопределённость:** интегрированный прогон воспроизвёл падение при посадке с исходным 40 px перекрытием floor/lift. Перекрытие увеличено до 100 px, при этом сохранены 200 px `PIT` и обязательный вертикальный подъём. Доставка saw-тела, три сохранённых тела, посадка, подъём и lift→ledge прыжок подтверждены тремя запусками и стресс-сценарием. Обязательность frozen corpse всё ещё не доказана отрицательным тестом: возможный двухсмертный sequence break остаётся follow-up. ## Проверки, следующие из графов