Стабилизировать посадку на лифт уровня 10
This commit is contained in:
@@ -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()) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
## Проверки, следующие из графов
|
||||
|
||||
|
||||
Reference in New Issue
Block a user