diff --git a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java index 437fffc..2ca71ce 100644 --- a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java +++ b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java @@ -122,13 +122,19 @@ final class LateLevelScenarios { snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f) .step("Approach the launch saw", 2_500L, LevelPlaytestBot.Input.right(), - snapshot -> snapshot.playerX() >= 350f) + LevelPlaytestBot.whileDeathsAtMost(1, + LevelPlaytestBot.groundedAliveAtOrBeyond(280f))) .step("Take off before the launch saw", 1_000L, LevelPlaytestBot.Input.rightJump(), - snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 680f) + LevelPlaytestBot.whileDeathsAtMost(1, + snapshot -> snapshot.playerAlive() + && !snapshot.playerOnGround() + && snapshot.playerY() <= 680f)) .step("Clear the launch saw", 2_000L, LevelPlaytestBot.Input.right(), - snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 680f) + LevelPlaytestBot.whileDeathsAtMost(1, + snapshot -> snapshot.playerAlive() + && snapshot.playerX() >= 680f)) .step("Approach the delivered lift body", 1_500L, LevelPlaytestBot.Input.right(), snapshot -> snapshot.playerX() >= 730f) @@ -143,13 +149,19 @@ final class LateLevelScenarios { snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f) .step("Return to the launch saw", 2_500L, LevelPlaytestBot.Input.right(), - snapshot -> snapshot.playerX() >= 350f) + LevelPlaytestBot.whileDeathsAtMost(2, + LevelPlaytestBot.groundedAliveAtOrBeyond(280f))) .step("Take off before the saw again", 1_000L, LevelPlaytestBot.Input.rightJump(), - snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 680f) + LevelPlaytestBot.whileDeathsAtMost(2, + snapshot -> snapshot.playerAlive() + && !snapshot.playerOnGround() + && snapshot.playerY() <= 680f)) .step("Clear the saw again", 2_000L, LevelPlaytestBot.Input.right(), - snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 680f) + LevelPlaytestBot.whileDeathsAtMost(2, + snapshot -> snapshot.playerAlive() + && snapshot.playerX() >= 680f)) .step("Approach the lift body again", 1_500L, LevelPlaytestBot.Input.right(), snapshot -> snapshot.playerX() >= 730f) @@ -190,13 +202,19 @@ final class LateLevelScenarios { snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f) .step("Approach the saw for the final crossing", 2_500L, LevelPlaytestBot.Input.right(), - snapshot -> snapshot.playerX() >= 350f) + LevelPlaytestBot.whileDeathsAtMost(3, + LevelPlaytestBot.groundedAliveAtOrBeyond(280f))) .step("Take off for the final saw crossing", 1_000L, LevelPlaytestBot.Input.rightJump(), - snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 680f) + LevelPlaytestBot.whileDeathsAtMost(3, + snapshot -> snapshot.playerAlive() + && !snapshot.playerOnGround() + && snapshot.playerY() <= 680f)) .step("Clear the saw for the final crossing", 2_000L, LevelPlaytestBot.Input.right(), - snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 680f) + LevelPlaytestBot.whileDeathsAtMost(3, + snapshot -> snapshot.playerAlive() + && snapshot.playerX() >= 680f)) .step("Approach the retained lift body", 1_500L, LevelPlaytestBot.Input.right(), snapshot -> snapshot.playerX() >= 730f) diff --git a/app/src/main/java/com/projectsacrifice/game/LevelPlaytestBot.java b/app/src/main/java/com/projectsacrifice/game/LevelPlaytestBot.java index 343fbd0..e0c6c85 100644 --- a/app/src/main/java/com/projectsacrifice/game/LevelPlaytestBot.java +++ b/app/src/main/java/com/projectsacrifice/game/LevelPlaytestBot.java @@ -47,6 +47,23 @@ public final class LevelPlaytestBot { boolean reached(GameView.AutomationSnapshot snapshot); } + public static SnapshotCondition groundedAliveAtOrBeyond(float minimumX) { + return snapshot -> snapshot.playerAlive() + && snapshot.playerOnGround() + && snapshot.playerX() >= minimumX; + } + + public static SnapshotCondition whileDeathsAtMost( + int maximumDeaths, SnapshotCondition completion) { + return snapshot -> { + if (snapshot.deaths() > maximumDeaths) { + throw new AssertionError("Route expected at most " + maximumDeaths + + " deaths but observed " + snapshot.deaths()); + } + return completion.reached(snapshot); + }; + } + public static final class Scenario { private final int levelNumber; private final String name; diff --git a/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java b/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java index b7fa858..98bcc9a 100644 --- a/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java +++ b/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java @@ -3,6 +3,7 @@ package com.projectsacrifice.game; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import org.junit.Test; @@ -127,6 +128,40 @@ public final class LevelPlaytestBotTest { assertFalse(snapshot.playerOnMovingPlatform()); } + @Test + public void groundedApproachWaitsForAliveGroundedPlayerAtCheckpoint() { + LevelPlaytestBot.SnapshotCondition checkpoint = + LevelPlaytestBot.groundedAliveAtOrBeyond(280f); + + assertFalse(checkpoint.reached(snapshot(true, true, 279f, 1))); + assertFalse(checkpoint.reached(snapshot(false, true, 280f, 1))); + assertFalse(checkpoint.reached(snapshot(true, false, 280f, 1))); + assertTrue(checkpoint.reached(snapshot(true, true, 280f, 1))); + } + + @Test + public void expectedDeathGuardFailsBeforeAnAccidentalRetry() { + LevelPlaytestBot.SnapshotCondition guarded = LevelPlaytestBot.whileDeathsAtMost( + 1, snapshot -> snapshot.playerX() >= 280f); + + assertTrue(guarded.reached(snapshot(true, false, 320f, 1))); + try { + guarded.reached(snapshot(false, false, 470f, 2)); + fail("An accidental second death must fail the route immediately"); + } catch (AssertionError expected) { + assertTrue(expected.getMessage(), expected.getMessage().contains("expected at most 1")); + assertTrue(expected.getMessage(), expected.getMessage().contains("observed 2")); + } + } + + private static GameView.AutomationSnapshot snapshot( + boolean alive, boolean onGround, float x, int deaths) { + return new GameView.AutomationSnapshot( + false, alive, false, false, false, + 10, 0L, x, 760f, deaths, deaths, + Float.NaN, Float.NaN, false, false, Float.NaN, onGround); + } + private static final class ScriptedDriver implements LevelPlaytestBot.Driver { private int startedLevel; private long nowMillis;