diff --git a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java index f399d65..9890643 100644 --- a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java +++ b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java @@ -107,19 +107,56 @@ public final class LateLevelPlaythroughTest { } } + @Test + public void masteryProcessionSurvivesThrottledFinalRelayDecisions() { + try (ActivityScenario 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 final Instrumentation instrumentation; 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.gameView = gameView; + this.minimumDecisionMillis = minimumDecisionMillis; + this.throttleFinalRelayOnly = throttleFinalRelayOnly; } static GameDriver from(ActivityScenario activity) { + return create(activity, 32L, false); + } + + static GameDriver withFinalRelayDelay(ActivityScenario activity, + long minimumDecisionMillis) { + return create(activity, minimumDecisionMillis, true); + } + + private static GameDriver create(ActivityScenario activity, + long minimumDecisionMillis, + boolean throttleFinalRelayOnly) { GameView[] view = new GameView[1]; activity.onActivity(current -> view[0] = current.gameViewForAutomation()); - return new GameDriver(InstrumentationRegistry.getInstrumentation(), view[0]); + return new GameDriver(InstrumentationRegistry.getInstrumentation(), view[0], + minimumDecisionMillis, throttleFinalRelayOnly); } @Override @@ -138,13 +175,22 @@ public final class LateLevelPlaythroughTest { public GameView.AutomationSnapshot snapshot() { GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1]; instrumentation.runOnMainSync(() -> value[0] = gameView.automationSnapshot()); - return value[0]; + lastSnapshot = value[0]; + return lastSnapshot; } @Override public void waitForNextDecision(long millis) { 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) { 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 6ead3fb..a13e53e 100644 --- a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java +++ b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java @@ -155,10 +155,10 @@ final class LateLevelScenarios { snapshot -> snapshot.playerX() >= 730f) .step("Clear the lift body again", 2_000L, LevelPlaytestBot.Input.rightJump(), - snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 900f) + snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 860f) .step("Prepare the frozen-block jump", 2_000L, LevelPlaytestBot.Input.none(), - snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 740f) + snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 720f) .step("Jump over the frozen block", 2_500L, LevelPlaytestBot.Input.rightJump(), snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f) @@ -185,25 +185,42 @@ final class LateLevelScenarios { snapshot -> snapshot.playerX() >= 730f) .step("Clear the retained lift body", 2_000L, LevelPlaytestBot.Input.rightJump(), - snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 900f) + snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 860f) .step("Prepare the final frozen-block jump", 2_000L, LevelPlaytestBot.Input.none(), - snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 740f) + snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 720f) .step("Clear the frozen block", 2_500L, LevelPlaytestBot.Input.rightJump(), snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f) .step("Approach the powered relay", 2_500L, LevelPlaytestBot.Input.right(), - snapshot -> snapshot.playerX() >= 1_390f) - .step("Clear the powered relay", 2_500L, + snapshot -> snapshot.playerX() >= 1_320f) + .step("Take off before the final relay", 1_000L, 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) - .step("Approach the lift", 2_000L, LevelPlaytestBot.Input.right(), - snapshot -> snapshot.playerX() >= 1_730f) - .step("Board the moving lift", 3_000L, + .step("Wait for the lift at floor height", 6_000L, + LevelPlaytestBot.Input.none(), + 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(), - snapshot -> snapshot.playerX() >= 1_790f - && snapshot.playerY() <= 670f) + snapshot -> snapshot.playerAlive() && 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, LevelPlaytestBot.Input.none(), snapshot -> snapshot.playerY() <= 310f) diff --git a/app/src/main/java/com/projectsacrifice/game/GameView.java b/app/src/main/java/com/projectsacrifice/game/GameView.java index a258a74..faa3df4 100644 --- a/app/src/main/java/com/projectsacrifice/game/GameView.java +++ b/app/src/main/java/com/projectsacrifice/game/GameView.java @@ -53,11 +53,23 @@ public final class GameView extends View implements Runnable { private final float playerY; private final int deaths; private final int corpseCount; + private final float firstMovingPlatformY; + private final boolean playerOnMovingPlatform; AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen, boolean visiblyPaused, boolean inputActive, int levelNumber, long elapsedMilliseconds, 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.playerAlive = playerAlive; this.allDoorsOpen = allDoorsOpen; @@ -69,6 +81,8 @@ public final class GameView extends View implements Runnable { this.playerY = playerY; this.deaths = deaths; this.corpseCount = corpseCount; + this.firstMovingPlatformY = firstMovingPlatformY; + this.playerOnMovingPlatform = playerOnMovingPlatform; } public boolean completed() { return completed; } @@ -82,6 +96,8 @@ public final class GameView extends View implements Runnable { public float playerY() { return playerY; } public int deaths() { return deaths; } public int corpseCount() { return corpseCount; } + public float firstMovingPlatformY() { return firstMovingPlatformY; } + public boolean playerOnMovingPlatform() { return playerOnMovingPlatform; } } private static final class Player { @@ -1128,6 +1144,17 @@ public final class GameView extends View implements Runnable { if (level != null) { 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( screen == Screen.COMPLETE, player.alive, @@ -1139,7 +1166,9 @@ public final class GameView extends View implements Runnable { player.x, player.y, deaths, - corpses.size() + corpses.size(), + firstMovingPlatformY, + playerOnMovingPlatform ); } } diff --git a/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java b/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java index 3c19fdf..887cb15 100644 --- a/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java +++ b/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java @@ -80,6 +80,16 @@ public final class LevelPlaytestBotTest { 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 int startedLevel; private long nowMillis;