Стабилизировать бота уровня 10 при задержках

This commit is contained in:
2026-08-18 03:33:53 +03:00
parent 6790a84d86
commit 9e418df184
4 changed files with 118 additions and 16 deletions
@@ -107,19 +107,56 @@ public final class LateLevelPlaythroughTest {
} }
} }
@Test
public void masteryProcessionSurvivesThrottledFinalRelayDecisions() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
GameDriver driver = GameDriver.withFinalRelayDelay(activity, 96L);
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(
driver,
LateLevelScenarios.levelTen(),
65_000L
);
assertTrue(result.diagnostic(), result.completed());
assertEquals("throttling must not add accidental volunteers", 3, result.deaths());
assertEquals("all three intended bodies must remain", 3, result.corpseCount());
assertTrue("the conductive relay must still open the final door",
result.doorOpened());
}
}
private static final class GameDriver implements LevelPlaytestBot.Driver { private static final class GameDriver implements LevelPlaytestBot.Driver {
private final Instrumentation instrumentation; private final Instrumentation instrumentation;
private final GameView gameView; private final GameView gameView;
private final long minimumDecisionMillis;
private final boolean throttleFinalRelayOnly;
private GameView.AutomationSnapshot lastSnapshot;
private GameDriver(Instrumentation instrumentation, GameView gameView) { private GameDriver(Instrumentation instrumentation, GameView gameView,
long minimumDecisionMillis, boolean throttleFinalRelayOnly) {
this.instrumentation = instrumentation; this.instrumentation = instrumentation;
this.gameView = gameView; this.gameView = gameView;
this.minimumDecisionMillis = minimumDecisionMillis;
this.throttleFinalRelayOnly = throttleFinalRelayOnly;
} }
static GameDriver from(ActivityScenario<MainActivity> activity) { static GameDriver from(ActivityScenario<MainActivity> activity) {
return create(activity, 32L, false);
}
static GameDriver withFinalRelayDelay(ActivityScenario<MainActivity> activity,
long minimumDecisionMillis) {
return create(activity, minimumDecisionMillis, true);
}
private static GameDriver create(ActivityScenario<MainActivity> activity,
long minimumDecisionMillis,
boolean throttleFinalRelayOnly) {
GameView[] view = new GameView[1]; GameView[] view = new GameView[1];
activity.onActivity(current -> view[0] = current.gameViewForAutomation()); activity.onActivity(current -> view[0] = current.gameViewForAutomation());
return new GameDriver(InstrumentationRegistry.getInstrumentation(), view[0]); return new GameDriver(InstrumentationRegistry.getInstrumentation(), view[0],
minimumDecisionMillis, throttleFinalRelayOnly);
} }
@Override @Override
@@ -138,13 +175,22 @@ public final class LateLevelPlaythroughTest {
public GameView.AutomationSnapshot snapshot() { public GameView.AutomationSnapshot snapshot() {
GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1]; GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1];
instrumentation.runOnMainSync(() -> value[0] = gameView.automationSnapshot()); instrumentation.runOnMainSync(() -> value[0] = gameView.automationSnapshot());
return value[0]; lastSnapshot = value[0];
return lastSnapshot;
} }
@Override @Override
public void waitForNextDecision(long millis) { public void waitForNextDecision(long millis) {
try { try {
Thread.sleep(millis); boolean inFinalRelayWindow = throttleFinalRelayOnly
&& lastSnapshot != null
&& lastSnapshot.levelNumber() == 10
&& lastSnapshot.deaths() == 3
&& lastSnapshot.playerX() >= 1_200f
&& lastSnapshot.playerX() <= 1_700f;
Thread.sleep(inFinalRelayWindow
? Math.max(millis, minimumDecisionMillis)
: millis);
} catch (InterruptedException interrupted) { } catch (InterruptedException interrupted) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new AssertionError("Late-level bot interrupted", interrupted); throw new AssertionError("Late-level bot interrupted", interrupted);
@@ -155,10 +155,10 @@ final class LateLevelScenarios {
snapshot -> snapshot.playerX() >= 730f) snapshot -> snapshot.playerX() >= 730f)
.step("Clear the lift body again", 2_000L, .step("Clear the lift body again", 2_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 900f) snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 860f)
.step("Prepare the frozen-block jump", 2_000L, .step("Prepare the frozen-block jump", 2_000L,
LevelPlaytestBot.Input.none(), LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 740f) snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 720f)
.step("Jump over the frozen block", 2_500L, .step("Jump over the frozen block", 2_500L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f) snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f)
@@ -185,25 +185,42 @@ final class LateLevelScenarios {
snapshot -> snapshot.playerX() >= 730f) snapshot -> snapshot.playerX() >= 730f)
.step("Clear the retained lift body", 2_000L, .step("Clear the retained lift body", 2_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 900f) snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 860f)
.step("Prepare the final frozen-block jump", 2_000L, .step("Prepare the final frozen-block jump", 2_000L,
LevelPlaytestBot.Input.none(), LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 740f) snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 720f)
.step("Clear the frozen block", 2_500L, .step("Clear the frozen block", 2_500L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f) snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f)
.step("Approach the powered relay", 2_500L, .step("Approach the powered relay", 2_500L,
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 1_390f) snapshot -> snapshot.playerX() >= 1_320f)
.step("Clear the powered relay", 2_500L, .step("Take off before the final relay", 1_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 700f)
.step("Clear the powered relay", 2_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_650f) snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_650f)
.step("Approach the lift", 2_000L, LevelPlaytestBot.Input.right(), .step("Wait for the lift at floor height", 6_000L,
snapshot -> snapshot.playerX() >= 1_730f) LevelPlaytestBot.Input.none(),
.step("Board the moving lift", 3_000L, snapshot -> snapshot.playerAlive()
&& snapshot.playerY() >= 770f
&& !Float.isNaN(snapshot.firstMovingPlatformY())
&& snapshot.firstMovingPlatformY() >= 740f)
.step("Take off toward the moving lift", 1_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerX() >= 1_790f snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 670f)
&& snapshot.playerY() <= 670f) .step("Board the moving lift", 2_000L,
LevelPlaytestBot.Input.right(),
GameView.AutomationSnapshot::playerOnMovingPlatform)
.step("Brake toward the lift center", 1_000L,
LevelPlaytestBot.Input.left(),
snapshot -> snapshot.playerOnMovingPlatform()
&& snapshot.playerX() <= 1_900f)
.step("Center on the moving lift", 1_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerOnMovingPlatform()
&& snapshot.playerX() >= 1_920f)
.step("Ride the lift to the upper door", 5_000L, .step("Ride the lift to the upper door", 5_000L,
LevelPlaytestBot.Input.none(), LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerY() <= 310f) snapshot -> snapshot.playerY() <= 310f)
@@ -53,11 +53,23 @@ public final class GameView extends View implements Runnable {
private final float playerY; private final float playerY;
private final int deaths; private final int deaths;
private final int corpseCount; private final int corpseCount;
private final float firstMovingPlatformY;
private final boolean playerOnMovingPlatform;
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen, AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
boolean visiblyPaused, boolean inputActive, int levelNumber, boolean visiblyPaused, boolean inputActive, int levelNumber,
long elapsedMilliseconds, long elapsedMilliseconds,
float playerX, float playerY, int deaths, int corpseCount) { float playerX, float playerY, int deaths, int corpseCount) {
this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber,
elapsedMilliseconds, playerX, playerY, deaths, corpseCount,
Float.NaN, false);
}
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
boolean visiblyPaused, boolean inputActive, int levelNumber,
long elapsedMilliseconds,
float playerX, float playerY, int deaths, int corpseCount,
float firstMovingPlatformY, boolean playerOnMovingPlatform) {
this.completed = completed; this.completed = completed;
this.playerAlive = playerAlive; this.playerAlive = playerAlive;
this.allDoorsOpen = allDoorsOpen; this.allDoorsOpen = allDoorsOpen;
@@ -69,6 +81,8 @@ public final class GameView extends View implements Runnable {
this.playerY = playerY; this.playerY = playerY;
this.deaths = deaths; this.deaths = deaths;
this.corpseCount = corpseCount; this.corpseCount = corpseCount;
this.firstMovingPlatformY = firstMovingPlatformY;
this.playerOnMovingPlatform = playerOnMovingPlatform;
} }
public boolean completed() { return completed; } public boolean completed() { return completed; }
@@ -82,6 +96,8 @@ public final class GameView extends View implements Runnable {
public float playerY() { return playerY; } public float playerY() { return playerY; }
public int deaths() { return deaths; } public int deaths() { return deaths; }
public int corpseCount() { return corpseCount; } public int corpseCount() { return corpseCount; }
public float firstMovingPlatformY() { return firstMovingPlatformY; }
public boolean playerOnMovingPlatform() { return playerOnMovingPlatform; }
} }
private static final class Player { private static final class Player {
@@ -1128,6 +1144,17 @@ public final class GameView extends View implements Runnable {
if (level != null) { if (level != null) {
for (LevelDefinition.Door door : level.doors) allOpen &= doorOpen(door); for (LevelDefinition.Door door : level.doors) allOpen &= doorOpen(door);
} }
float firstMovingPlatformY = Float.NaN;
for (PlatformState state : platformStates) {
if (state.definition.isMoving()) {
firstMovingPlatformY = state.y;
break;
}
}
boolean playerOnMovingPlatform = player.alive
&& player.supportIndex >= 0
&& player.supportIndex < platformStates.size()
&& platformStates.get(player.supportIndex).definition.isMoving();
return new AutomationSnapshot( return new AutomationSnapshot(
screen == Screen.COMPLETE, screen == Screen.COMPLETE,
player.alive, player.alive,
@@ -1139,7 +1166,9 @@ public final class GameView extends View implements Runnable {
player.x, player.x,
player.y, player.y,
deaths, deaths,
corpses.size() corpses.size(),
firstMovingPlatformY,
playerOnMovingPlatform
); );
} }
} }
@@ -80,6 +80,16 @@ public final class LevelPlaytestBotTest {
assertTrue(result.doorOpened()); assertTrue(result.doorOpened());
} }
@Test
public void automationSnapshotDefaultsToNoMovingPlatform() {
GameView.AutomationSnapshot snapshot = new GameView.AutomationSnapshot(
false, true, false, false, false,
1, 0L, 130f, 760f, 0, 0);
assertTrue(Float.isNaN(snapshot.firstMovingPlatformY()));
assertFalse(snapshot.playerOnMovingPlatform());
}
private static final class ScriptedDriver implements LevelPlaytestBot.Driver { private static final class ScriptedDriver implements LevelPlaytestBot.Driver {
private int startedLevel; private int startedLevel;
private long nowMillis; private long nowMillis;