Завершить интеграционную устойчивость ботов этапа 3

This commit is contained in:
2026-08-18 18:27:33 +03:00
parent a8d72024f2
commit 9d792bdee4
8 changed files with 156 additions and 44 deletions
@@ -2,7 +2,6 @@ package com.projectsacrifice.game;
import android.app.Instrumentation; import android.app.Instrumentation;
import androidx.lifecycle.Lifecycle;
import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.platform.app.InstrumentationRegistry;
@@ -21,12 +20,16 @@ public final class GameLifecycleTest {
public void returningFromBackgroundKeepsPuzzleVisiblePausedAndClearsHeldInput() { public void returningFromBackgroundKeepsPuzzleVisiblePausedAndClearsHeldInput() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
try (ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class)) { try (ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class)) {
final MainActivity[] activity = new MainActivity[1];
final GameView[] view = new GameView[1]; final GameView[] view = new GameView[1];
scenario.onActivity(activity -> view[0] = activity.gameViewForAutomation()); scenario.onActivity(current -> {
activity[0] = current;
view[0] = current.gameViewForAutomation();
});
createRetainedCorpse(instrumentation, view[0]); createRetainedCorpse(instrumentation, view[0]);
runOnGame(instrumentation, view[0], false, false, true, false); runOnGame(instrumentation, view[0], false, false, true, false);
scenario.moveToState(Lifecycle.State.STARTED); instrumentation.runOnMainSync(activity[0]::onPause);
GameView.AutomationSnapshot backgroundStart = snapshot(instrumentation, view[0]); GameView.AutomationSnapshot backgroundStart = snapshot(instrumentation, view[0]);
sleep(700L); sleep(700L);
GameView.AutomationSnapshot backgroundEnd = snapshot(instrumentation, view[0]); GameView.AutomationSnapshot backgroundEnd = snapshot(instrumentation, view[0]);
@@ -37,7 +40,7 @@ public final class GameLifecycleTest {
assertEquals(backgroundStart.playerX(), backgroundEnd.playerX(), 0.01f); assertEquals(backgroundStart.playerX(), backgroundEnd.playerX(), 0.01f);
assertEquals(backgroundStart.elapsedMilliseconds(), backgroundEnd.elapsedMilliseconds()); assertEquals(backgroundStart.elapsedMilliseconds(), backgroundEnd.elapsedMilliseconds());
scenario.moveToState(Lifecycle.State.RESUMED); instrumentation.runOnMainSync(activity[0]::onResume);
GameView.AutomationSnapshot returned = snapshot(instrumentation, view[0]); GameView.AutomationSnapshot returned = snapshot(instrumentation, view[0]);
assertTrue("Returning to an active puzzle must show the pause screen", returned.visiblyPaused()); assertTrue("Returning to an active puzzle must show the pause screen", returned.visiblyPaused());
assertFalse("Held touch input must not survive backgrounding", returned.inputActive()); assertFalse("Held touch input must not survive backgrounding", returned.inputActive());
@@ -20,10 +20,13 @@ final class LateLevelScenarios {
snapshot -> snapshot.playerX() >= 626f snapshot -> snapshot.playerX() >= 626f
&& snapshot.playerY() >= 668f && snapshot.playerY() >= 668f
&& snapshot.playerY() <= 690f) && snapshot.playerY() <= 690f)
.step("Jump from the block to the first ledge", 2_500L, .step("Land on the first ledge from the frozen block", 3_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerX() >= 750f LevelPlaytestBot.whileDeathsAtMost(1,
&& snapshot.playerY() <= 505f) snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerX() >= 810f
&& snapshot.playerY() <= 500f))
.step("Cross the first ledge", 3_000L, LevelPlaytestBot.Input.right(), .step("Cross the first ledge", 3_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 1_230f) snapshot -> snapshot.playerX() >= 1_230f)
.step("Jump the gap to the second ledge", 2_500L, .step("Jump the gap to the second ledge", 2_500L,
@@ -117,19 +120,24 @@ final class LateLevelScenarios {
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
snapshot -> snapshot.deaths() == 1 snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1) && snapshot.corpseCount() == 1)
.step("Observe the delivered body power the lift", 3_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1
&& snapshot.firstMovingPlatformPowered())
.step("Wait for the freezer volunteer", 3_000L, .step("Wait for the freezer volunteer", 3_000L,
LevelPlaytestBot.Input.none(), LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f) snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
.step("Approach the launch saw", 2_500L, .step("Approach the launch saw", 2_500L,
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1, LevelPlaytestBot.whileDeathsAtMost(1,
LevelPlaytestBot.groundedAliveAtOrBeyond(280f))) LevelPlaytestBot.groundedAliveAtOrBeyond(320f)))
.step("Take off before the launch saw", 1_000L, .step("Take off before the launch saw", 1_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(1, LevelPlaytestBot.whileDeathsAtMost(1,
snapshot -> snapshot.playerAlive() snapshot -> snapshot.playerAlive()
&& !snapshot.playerOnGround() && !snapshot.playerOnGround()
&& snapshot.playerY() <= 680f)) && snapshot.playerY() <= 580f))
.step("Clear the launch saw", 2_000L, .step("Clear the launch saw", 2_000L,
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1, LevelPlaytestBot.whileDeathsAtMost(1,
@@ -150,13 +158,13 @@ final class LateLevelScenarios {
.step("Return to the launch saw", 2_500L, .step("Return to the launch saw", 2_500L,
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(2, LevelPlaytestBot.whileDeathsAtMost(2,
LevelPlaytestBot.groundedAliveAtOrBeyond(280f))) LevelPlaytestBot.groundedAliveAtOrBeyond(320f)))
.step("Take off before the saw again", 1_000L, .step("Take off before the saw again", 1_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(2, LevelPlaytestBot.whileDeathsAtMost(2,
snapshot -> snapshot.playerAlive() snapshot -> snapshot.playerAlive()
&& !snapshot.playerOnGround() && !snapshot.playerOnGround()
&& snapshot.playerY() <= 680f)) && snapshot.playerY() <= 580f))
.step("Clear the saw again", 2_000L, .step("Clear the saw again", 2_000L,
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(2, LevelPlaytestBot.whileDeathsAtMost(2,
@@ -203,13 +211,13 @@ final class LateLevelScenarios {
.step("Approach the saw for the final crossing", 2_500L, .step("Approach the saw for the final crossing", 2_500L,
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(3, LevelPlaytestBot.whileDeathsAtMost(3,
LevelPlaytestBot.groundedAliveAtOrBeyond(280f))) LevelPlaytestBot.groundedAliveAtOrBeyond(320f)))
.step("Take off for the final saw crossing", 1_000L, .step("Take off for the final saw crossing", 1_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(3, LevelPlaytestBot.whileDeathsAtMost(3,
snapshot -> snapshot.playerAlive() snapshot -> snapshot.playerAlive()
&& !snapshot.playerOnGround() && !snapshot.playerOnGround()
&& snapshot.playerY() <= 680f)) && snapshot.playerY() <= 580f))
.step("Clear the saw for the final crossing", 2_000L, .step("Clear the saw for the final crossing", 2_000L,
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(3, LevelPlaytestBot.whileDeathsAtMost(3,
@@ -33,6 +33,14 @@ final class LevelEightScenario {
return failure("Cross the pit before the clock expires", snapshot, budget, driver); return failure("Cross the pit before the clock expires", snapshot, budget, driver);
} }
snapshot = await(driver, budget, "Move clear of the platform sweep",
LevelPlaytestBot.Input.right(),
current -> current.playerX() >= 1_680f
&& current.playerY() >= 779f && current.allDoorsOpen());
if (failed(snapshot, budget, driver)) {
return failure("Move clear of the platform sweep", snapshot, budget, driver);
}
snapshot = await(driver, budget, "Wait for the far-side trap", snapshot = await(driver, budget, "Wait for the far-side trap",
LevelPlaytestBot.Input.none(), LevelPlaytestBot.Input.none(),
current -> current.playerX() >= 1_510f && !current.allDoorsOpen()); current -> current.playerX() >= 1_510f && !current.allDoorsOpen());
@@ -40,21 +48,21 @@ final class LevelEightScenario {
return failure("Wait for the far-side trap", snapshot, budget, driver); return failure("Wait for the far-side trap", snapshot, budget, driver);
} }
snapshot = await(driver, budget, "Return beneath the recovery relay", snapshot = positionBeneathRecoveryRelay(driver, budget);
LevelPlaytestBot.Input.left(),
current -> current.playerX() <= 1_680f && current.playerY() >= 779f);
if (failed(snapshot, budget, driver)) { if (failed(snapshot, budget, driver)) {
return failure("Return beneath the recovery relay", snapshot, budget, driver); return failure("Return beneath the recovery relay", snapshot, budget, driver);
} }
snapshot = pulseLeftJumpThenAwait(driver, budget, "Jump into the recovery relay", snapshot = pulseRightJumpThenAwait(driver, budget, "Jump into the recovery relay",
current -> current.allDoorsOpen()); current -> current.allDoorsOpen());
if (failed(snapshot, budget, driver)) { if (failed(snapshot, budget, driver)) {
return failure("Jump into the recovery relay", snapshot, budget, driver); return failure("Jump into the recovery relay", snapshot, budget, driver);
} }
snapshot = await(driver, budget, "Land after rearming the clock", snapshot = await(driver, budget, "Land after rearming the clock",
LevelPlaytestBot.Input.none(), current -> current.playerY() >= 779f); LevelPlaytestBot.Input.right(),
current -> current.playerX() >= 1_650f
&& current.playerY() >= 779f && current.allDoorsOpen());
if (failed(snapshot, budget, driver)) { if (failed(snapshot, budget, driver)) {
return failure("Land after rearming the clock", snapshot, budget, driver); return failure("Land after rearming the clock", snapshot, budget, driver);
} }
@@ -85,10 +93,10 @@ final class LevelEightScenario {
LevelPlaytestBot.Input.right(), current -> current.playerX() >= 760f); LevelPlaytestBot.Input.right(), current -> current.playerX() >= 760f);
if (failed(snapshot, budget, driver)) return snapshot; if (failed(snapshot, budget, driver)) return snapshot;
snapshot = await(driver, budget, "Wait for the moving platform", snapshot = await(driver, budget, "Wait for the outbound moving platform",
LevelPlaytestBot.Input.none(), LevelPlaytestBot.Input.none(),
current -> current.elapsedMilliseconds() >= timerStartedAt + 2_250L current -> current.elapsedMilliseconds() >= timerStartedAt + 2_250L
&& current.firstMovingPlatformX() <= 1_050f); && MovingPlatformLaunchPolicy.readyToBoard(current, 1_050f));
if (failed(snapshot, budget, driver)) return snapshot; if (failed(snapshot, budget, driver)) return snapshot;
driver.setInput(false, true, true); driver.setInput(false, true, true);
@@ -135,10 +143,8 @@ final class LevelEightScenario {
private Result finishFromFarSide(LevelPlaytestBot.Driver driver, private Result finishFromFarSide(LevelPlaytestBot.Driver driver,
PlaytestSimulationBudget budget, PlaytestSimulationBudget budget,
GameView.AutomationSnapshot snapshot, String step) { GameView.AutomationSnapshot snapshot, String step) {
snapshot = await(driver, budget, "Reach the open-door jump", snapshot = positionForFinalJump(driver, budget);
LevelPlaytestBot.Input.right(), if (failed(snapshot, budget, driver) || !snapshot.allDoorsOpen()) {
current -> current.playerX() >= 1_730f && current.allDoorsOpen());
if (failed(snapshot, budget, driver)) {
return failure("Reach the open-door jump", snapshot, budget, driver); return failure("Reach the open-door jump", snapshot, budget, driver);
} }
@@ -154,14 +160,62 @@ final class LevelEightScenario {
+ "; " + state(snapshot)); + "; " + state(snapshot));
} }
private GameView.AutomationSnapshot pulseLeftJumpThenAwait( private GameView.AutomationSnapshot positionForFinalJump(
LevelPlaytestBot.Driver driver, PlaytestSimulationBudget budget
) {
lastStep = "Reach the open-door jump";
GameView.AutomationSnapshot snapshot = driver.snapshot();
while (!budget.exhausted(snapshot.elapsedMilliseconds(), driver.currentTimeMillis())
&& snapshot.playerAlive() && !snapshot.completed() && snapshot.allDoorsOpen()) {
if (snapshot.playerOnGround()
&& snapshot.playerX() >= 1_690f && snapshot.playerX() <= 1_730f) {
driver.setInput(false, false, false);
return snapshot;
}
if (snapshot.playerX() > 1_730f) {
driver.setInput(true, false, false);
} else {
driver.setInput(false, true, false);
}
driver.waitForNextDecision(DECISION_MILLIS);
snapshot = driver.snapshot();
}
driver.setInput(false, false, false);
return snapshot;
}
private GameView.AutomationSnapshot pulseRightJumpThenAwait(
LevelPlaytestBot.Driver driver, PlaytestSimulationBudget budget, String step, LevelPlaytestBot.Driver driver, PlaytestSimulationBudget budget, String step,
LevelPlaytestBot.SnapshotCondition completion LevelPlaytestBot.SnapshotCondition completion
) { ) {
driver.setInput(true, false, true); driver.setInput(false, true, true);
driver.waitForNextDecision(64L); driver.waitForNextDecision(64L);
driver.setInput(false, true, false);
return await(driver, budget, step, LevelPlaytestBot.Input.right(), completion);
}
private GameView.AutomationSnapshot positionBeneathRecoveryRelay(
LevelPlaytestBot.Driver driver, PlaytestSimulationBudget budget
) {
lastStep = "Return beneath the recovery relay";
GameView.AutomationSnapshot snapshot = driver.snapshot();
while (!budget.exhausted(snapshot.elapsedMilliseconds(), driver.currentTimeMillis())
&& snapshot.playerAlive() && !snapshot.completed()) {
if (snapshot.playerOnGround()
&& snapshot.playerX() >= 1_620f && snapshot.playerX() <= 1_650f) {
driver.setInput(false, false, false);
return snapshot;
}
if (snapshot.playerX() > 1_650f) {
driver.setInput(true, false, false); driver.setInput(true, false, false);
return await(driver, budget, step, LevelPlaytestBot.Input.left(), completion); } else {
driver.setInput(false, true, false);
}
driver.waitForNextDecision(DECISION_MILLIS);
snapshot = driver.snapshot();
}
driver.setInput(false, false, false);
return snapshot;
} }
private GameView.AutomationSnapshot await( private GameView.AutomationSnapshot await(
@@ -224,6 +278,7 @@ final class LevelEightScenario {
+ ", platformY=" + snapshot.firstMovingPlatformY() + ", platformY=" + snapshot.firstMovingPlatformY()
+ ", onMovingPlatform=" + snapshot.playerOnMovingPlatform() + ", onMovingPlatform=" + snapshot.playerOnMovingPlatform()
+ ", onGround=" + snapshot.playerOnGround() + ", onGround=" + snapshot.playerOnGround()
+ ", platformPowered=" + snapshot.firstMovingPlatformPowered(); + ", platformPowered=" + snapshot.firstMovingPlatformPowered()
+ ", platformMovingRight=" + snapshot.firstMovingPlatformMovingRight();
} }
} }
@@ -67,15 +67,16 @@ public final class EarlyLevelScenarios {
.step("Wait for the next volunteer", 2_000L, LevelPlaytestBot.Input.none(), .step("Wait for the next volunteer", 2_000L, LevelPlaytestBot.Input.none(),
EarlyLevelScenarios::respawnedAtStart) EarlyLevelScenarios::respawnedAtStart)
.step("Approach the saw", 3_000L, LevelPlaytestBot.Input.right(), .step("Approach the saw", 3_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() LevelPlaytestBot.whileDeathsAtMost(1, snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround() && snapshot.playerOnGround()
&& snapshot.playerX() >= 280f) && snapshot.playerX() >= 390f))
.step("Take off before the saw", 1_000L, LevelPlaytestBot.Input.rightJump(), .step("Take off before the saw", 1_000L, LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() LevelPlaytestBot.whileDeathsAtMost(1, snapshot -> snapshot.playerAlive()
&& !snapshot.playerOnGround() && !snapshot.playerOnGround()
&& snapshot.playerY() <= 680f) && snapshot.playerY() <= 620f))
.step("Clear the saw and plate body", 3_000L, LevelPlaytestBot.Input.right(), .step("Clear the saw and plate body", 3_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 700f) LevelPlaytestBot.whileDeathsAtMost(1, snapshot ->
snapshot.playerAlive() && snapshot.playerX() >= 700f))
.step("Reach the relic through the open door", 5_000L, LevelPlaytestBot.Input.right(), .step("Reach the relic through the open door", 5_000L, LevelPlaytestBot.Input.right(),
GameView.AutomationSnapshot::completed) GameView.AutomationSnapshot::completed)
.build(); .build();
@@ -59,6 +59,7 @@ public final class GameView extends View implements Runnable {
private final float firstMovingPlatformY; private final float firstMovingPlatformY;
private final boolean playerOnMovingPlatform; private final boolean playerOnMovingPlatform;
private final boolean firstMovingPlatformPowered; private final boolean firstMovingPlatformPowered;
private final boolean firstMovingPlatformMovingRight;
private final float firstCorpseX; private final float firstCorpseX;
private final boolean playerOnGround; private final boolean playerOnGround;
@@ -68,7 +69,7 @@ public final class GameView extends View implements Runnable {
float playerX, float playerY, int deaths, int corpseCount) { float playerX, float playerY, int deaths, int corpseCount) {
this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber, this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber,
elapsedMilliseconds, playerX, playerY, deaths, corpseCount, elapsedMilliseconds, playerX, playerY, deaths, corpseCount,
Float.NaN, Float.NaN, false, false, Float.NaN, false); Float.NaN, Float.NaN, false, false, false, Float.NaN, false);
} }
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen, AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
@@ -79,6 +80,21 @@ public final class GameView extends View implements Runnable {
boolean playerOnMovingPlatform, boolean playerOnMovingPlatform,
boolean firstMovingPlatformPowered, float firstCorpseX, boolean firstMovingPlatformPowered, float firstCorpseX,
boolean playerOnGround) { boolean playerOnGround) {
this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber,
elapsedMilliseconds, playerX, playerY, deaths, corpseCount,
firstMovingPlatformX, firstMovingPlatformY, playerOnMovingPlatform,
firstMovingPlatformPowered, false, firstCorpseX, playerOnGround);
}
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
boolean visiblyPaused, boolean inputActive, int levelNumber,
long elapsedMilliseconds,
float playerX, float playerY, int deaths, int corpseCount,
float firstMovingPlatformX, float firstMovingPlatformY,
boolean playerOnMovingPlatform,
boolean firstMovingPlatformPowered,
boolean firstMovingPlatformMovingRight, float firstCorpseX,
boolean playerOnGround) {
this.completed = completed; this.completed = completed;
this.playerAlive = playerAlive; this.playerAlive = playerAlive;
this.allDoorsOpen = allDoorsOpen; this.allDoorsOpen = allDoorsOpen;
@@ -94,6 +110,7 @@ public final class GameView extends View implements Runnable {
this.firstMovingPlatformY = firstMovingPlatformY; this.firstMovingPlatformY = firstMovingPlatformY;
this.playerOnMovingPlatform = playerOnMovingPlatform; this.playerOnMovingPlatform = playerOnMovingPlatform;
this.firstMovingPlatformPowered = firstMovingPlatformPowered; this.firstMovingPlatformPowered = firstMovingPlatformPowered;
this.firstMovingPlatformMovingRight = firstMovingPlatformMovingRight;
this.firstCorpseX = firstCorpseX; this.firstCorpseX = firstCorpseX;
this.playerOnGround = playerOnGround; this.playerOnGround = playerOnGround;
} }
@@ -113,6 +130,7 @@ public final class GameView extends View implements Runnable {
public float firstMovingPlatformY() { return firstMovingPlatformY; } public float firstMovingPlatformY() { return firstMovingPlatformY; }
public boolean playerOnMovingPlatform() { return playerOnMovingPlatform; } public boolean playerOnMovingPlatform() { return playerOnMovingPlatform; }
public boolean firstMovingPlatformPowered() { return firstMovingPlatformPowered; } public boolean firstMovingPlatformPowered() { return firstMovingPlatformPowered; }
public boolean firstMovingPlatformMovingRight() { return firstMovingPlatformMovingRight; }
public float firstCorpseX() { return firstCorpseX; } public float firstCorpseX() { return firstCorpseX; }
public boolean playerOnGround() { return playerOnGround; } public boolean playerOnGround() { return playerOnGround; }
} }
@@ -1958,11 +1976,13 @@ public final class GameView extends View implements Runnable {
float firstMovingPlatformX = Float.NaN; float firstMovingPlatformX = Float.NaN;
float firstMovingPlatformY = Float.NaN; float firstMovingPlatformY = Float.NaN;
boolean firstMovingPlatformPowered = false; boolean firstMovingPlatformPowered = false;
boolean firstMovingPlatformMovingRight = false;
for (PlatformState state : platformStates) { for (PlatformState state : platformStates) {
if (state.definition.isMoving()) { if (state.definition.isMoving()) {
firstMovingPlatformX = state.x; firstMovingPlatformX = state.x;
firstMovingPlatformY = state.y; firstMovingPlatformY = state.y;
firstMovingPlatformPowered = signalActive(state.definition.controlId); firstMovingPlatformPowered = signalActive(state.definition.controlId);
firstMovingPlatformMovingRight = state.x > state.previousX;
break; break;
} }
} }
@@ -1986,6 +2006,7 @@ public final class GameView extends View implements Runnable {
firstMovingPlatformY, firstMovingPlatformY,
playerOnMovingPlatform, playerOnMovingPlatform,
firstMovingPlatformPowered, firstMovingPlatformPowered,
firstMovingPlatformMovingRight,
corpses.isEmpty() ? Float.NaN : corpses.get(0).x, corpses.isEmpty() ? Float.NaN : corpses.get(0).x,
player.onGround player.onGround
); );
@@ -6,6 +6,7 @@ import java.util.List;
/** Runs black-box acceptance scenarios through the same input and state boundary as touch play. */ /** Runs black-box acceptance scenarios through the same input and state boundary as touch play. */
public final class LevelPlaytestBot { public final class LevelPlaytestBot {
private static final long DECISION_MILLIS = 16L;
private static final long MIN_WALL_WATCHDOG_MILLIS = 120_000L; private static final long MIN_WALL_WATCHDOG_MILLIS = 120_000L;
private static final long WALL_WATCHDOG_MULTIPLIER = 10L; private static final long WALL_WATCHDOG_MULTIPLIER = 10L;
@@ -58,7 +59,11 @@ public final class LevelPlaytestBot {
return snapshot -> { return snapshot -> {
if (snapshot.deaths() > maximumDeaths) { if (snapshot.deaths() > maximumDeaths) {
throw new AssertionError("Route expected at most " + maximumDeaths throw new AssertionError("Route expected at most " + maximumDeaths
+ " deaths but observed " + snapshot.deaths()); + " deaths but observed " + snapshot.deaths()
+ "; x=" + snapshot.playerX()
+ ", y=" + snapshot.playerY()
+ ", alive=" + snapshot.playerAlive()
+ ", grounded=" + snapshot.playerOnGround());
} }
return completion.reached(snapshot); return completion.reached(snapshot);
}; };
@@ -196,7 +201,7 @@ public final class LevelPlaytestBot {
} }
driver.setInput(step.input.moveLeft, step.input.moveRight, step.input.jump); driver.setInput(step.input.moveLeft, step.input.moveRight, step.input.jump);
driver.waitForNextDecision(32L); driver.waitForNextDecision(DECISION_MILLIS);
} }
driver.setInput(false, false, false); driver.setInput(false, false, false);
@@ -4,6 +4,12 @@ package com.projectsacrifice.game;
final class MovingPlatformLaunchPolicy { final class MovingPlatformLaunchPolicy {
private MovingPlatformLaunchPolicy() {} private MovingPlatformLaunchPolicy() {}
static boolean readyToBoard(GameView.AutomationSnapshot snapshot, float maximumPlatformX) {
return snapshot.firstMovingPlatformPowered()
&& snapshot.firstMovingPlatformMovingRight()
&& snapshot.firstMovingPlatformX() <= maximumPlatformX;
}
static boolean ready(GameView.AutomationSnapshot snapshot, float minimumPlatformX) { static boolean ready(GameView.AutomationSnapshot snapshot, float minimumPlatformX) {
return snapshot.playerOnMovingPlatform() return snapshot.playerOnMovingPlatform()
&& snapshot.playerOnGround() && snapshot.playerOnGround()
@@ -6,22 +6,35 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public final class MovingPlatformLaunchPolicyTest { public final class MovingPlatformLaunchPolicyTest {
@Test
public void boardingWaitsForThePoweredOutboundPhaseNearTheLeftFloor() {
assertFalse(MovingPlatformLaunchPolicy.readyToBoard(
snapshot(1_000f, false, true, true, false), 1_050f));
assertFalse(MovingPlatformLaunchPolicy.readyToBoard(
snapshot(1_000f, false, true, false, true), 1_050f));
assertFalse(MovingPlatformLaunchPolicy.readyToBoard(
snapshot(1_100f, false, true, true, true), 1_050f));
assertTrue(MovingPlatformLaunchPolicy.readyToBoard(
snapshot(1_000f, false, true, true, true), 1_050f));
}
@Test @Test
public void launchRequiresStablePoweredSupportInTheLaunchZone() { public void launchRequiresStablePoweredSupportInTheLaunchZone() {
assertFalse(MovingPlatformLaunchPolicy.ready(snapshot(910f, true, true, true), 1_150f)); assertFalse(MovingPlatformLaunchPolicy.ready(snapshot(910f, true, true, true, true), 1_150f));
assertFalse(MovingPlatformLaunchPolicy.ready(snapshot(1_150f, false, true, true), 1_150f)); assertFalse(MovingPlatformLaunchPolicy.ready(snapshot(1_150f, false, true, true, true), 1_150f));
assertFalse(MovingPlatformLaunchPolicy.ready(snapshot(1_150f, true, false, true), 1_150f)); assertFalse(MovingPlatformLaunchPolicy.ready(snapshot(1_150f, true, false, true, true), 1_150f));
assertFalse(MovingPlatformLaunchPolicy.ready(snapshot(1_150f, true, true, false), 1_150f)); assertFalse(MovingPlatformLaunchPolicy.ready(snapshot(1_150f, true, true, false, true), 1_150f));
assertTrue(MovingPlatformLaunchPolicy.ready(snapshot(1_150f, true, true, true), 1_150f)); assertTrue(MovingPlatformLaunchPolicy.ready(snapshot(1_150f, true, true, true, true), 1_150f));
} }
private static GameView.AutomationSnapshot snapshot( private static GameView.AutomationSnapshot snapshot(
float platformX, boolean supported, boolean grounded, boolean powered float platformX, boolean supported, boolean grounded, boolean powered,
boolean movingRight
) { ) {
return new GameView.AutomationSnapshot( return new GameView.AutomationSnapshot(
false, true, true, false, false, 8, false, true, true, false, false, 8,
4_000L, 1_100f, 670f, 0, 0, 4_000L, 1_100f, 670f, 0, 0,
platformX, 770f, supported, powered, Float.NaN, grounded platformX, 770f, supported, powered, movingRight, Float.NaN, grounded
); );
} }
} }