Стабилизировать посадку на лифт уровня 10

This commit is contained in:
2026-08-18 04:20:31 +03:00
parent de0f8b891f
commit a87e6cd62b
5 changed files with 50 additions and 22 deletions
@@ -108,9 +108,9 @@ public final class LateLevelPlaythroughTest {
}
@Test
public void masteryProcessionSurvivesThrottledFinalRelayDecisions() {
public void masteryProcessionSurvivesThrottledFinalTraversalDecisions() {
try (ActivityScenario<MainActivity> 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<MainActivity> activity) {
return create(activity, 32L, false);
}
static GameDriver withFinalRelayDelay(ActivityScenario<MainActivity> activity,
long minimumDecisionMillis) {
static GameDriver withFinalTraversalDelay(ActivityScenario<MainActivity> activity,
long minimumDecisionMillis) {
return create(activity, minimumDecisionMillis, true);
}
private static GameDriver create(ActivityScenario<MainActivity> 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);
@@ -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)
@@ -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"))
@@ -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()) {
+5 -5
View File
@@ -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.
## Проверки, следующие из графов