Завершить инженерную проверку этапа 2

This commit is contained in:
2026-08-18 13:54:48 +03:00
parent 3ad959dde0
commit 80822b74b1
17 changed files with 384 additions and 69 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ android {
applicationId 'com.projectsacrifice.game'
minSdk 23
targetSdk 34
versionCode 4
versionName '0.3.0-stage2-playability'
versionCode 5
versionName '0.4.0-stage2-complete'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
@@ -0,0 +1,82 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.app.Instrumentation;
import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public final class GameRestartTest {
@Test
public void restartClearsBodiesSignalsTimerAndHeldInput() throws Exception {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
GameView[] view = new GameView[1];
activity.onActivity(current -> view[0] = current.gameViewForAutomation());
instrumentation.runOnMainSync(() -> {
view[0].startLevelForAutomation(10);
view[0].setAutomationInput(false, true, false);
});
await(instrumentation, view[0], 4_000L,
snapshot -> snapshot.deaths() == 1 && snapshot.corpseCount() == 1);
instrumentation.runOnMainSync(
() -> view[0].setAutomationInput(false, false, false));
await(instrumentation, view[0], 5_000L,
GameView.AutomationSnapshot::firstMovingPlatformPowered);
instrumentation.runOnMainSync(() -> {
view[0].setAutomationInput(false, true, true);
view[0].restartLevelForAutomation();
});
GameView.AutomationSnapshot restarted = snapshot(instrumentation, view[0]);
assertEquals(10, restarted.levelNumber());
assertTrue(restarted.playerAlive());
assertEquals(0, restarted.deaths());
assertEquals(0, restarted.corpseCount());
assertFalse(restarted.firstMovingPlatformPowered());
assertFalse(restarted.inputActive());
assertTrue("Restarted timer must begin near zero: "
+ restarted.elapsedMilliseconds(), restarted.elapsedMilliseconds() < 200L);
assertTrue("Restarted player must be at the level spawn: x=" + restarted.playerX(),
restarted.playerX() >= 125f && restarted.playerX() <= 135f);
assertEquals(790f, restarted.firstMovingPlatformY(), 0.5f);
}
}
private static GameView.AutomationSnapshot await(
Instrumentation instrumentation,
GameView view,
long timeoutMillis,
LevelPlaytestBot.SnapshotCondition condition
) throws Exception {
long deadline = System.currentTimeMillis() + timeoutMillis;
GameView.AutomationSnapshot current = snapshot(instrumentation, view);
while (System.currentTimeMillis() < deadline) {
current = snapshot(instrumentation, view);
if (condition.reached(current)) return current;
Thread.sleep(32L);
}
throw new AssertionError("Restart setup timed out; level=" + current.levelNumber()
+ ", x=" + current.playerX() + ", deaths=" + current.deaths()
+ ", corpses=" + current.corpseCount());
}
private static GameView.AutomationSnapshot snapshot(
Instrumentation instrumentation,
GameView view
) {
GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1];
instrumentation.runOnMainSync(() -> value[0] = view.automationSnapshot());
return value[0];
}
}
@@ -126,6 +126,63 @@ public final class LateLevelPlaythroughTest {
}
}
@Test
public void sawDeliveredBodyKeepsLevelTenLiftPoweredAfterSettling() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
GameDriver driver = GameDriver.from(activity);
driver.startLevel(10);
await(driver, 4_000L, false, true, false,
snapshot -> snapshot.deaths() == 1 && snapshot.corpseCount() == 1);
GameView.AutomationSnapshot engaged = await(driver, 5_000L, false, false, false,
GameView.AutomationSnapshot::firstMovingPlatformPowered);
driver.setInput(false, false, false);
driver.waitForNextDecision(750L);
GameView.AutomationSnapshot settled = driver.snapshot();
assertEquals("The body must settle when it engages the lift plate instead of sliding",
engaged.firstCorpseX(), settled.firstCorpseX(), 2f);
driver.waitForNextDecision(6_000L);
settled = driver.snapshot();
assertTrue("The saw-delivered body slid off the lift plate after settling; "
+ "platformY=" + settled.firstMovingPlatformY()
+ ", deaths=" + settled.deaths()
+ ", corpses=" + settled.corpseCount(),
settled.firstMovingPlatformPowered());
assertEquals("The stability wait must not consume another volunteer",
1, settled.deaths());
assertEquals("The delivered body must remain in the level",
1, settled.corpseCount());
}
}
private static GameView.AutomationSnapshot await(
GameDriver driver,
long timeoutMillis,
boolean moveLeft,
boolean moveRight,
boolean jump,
LevelPlaytestBot.SnapshotCondition condition
) {
long deadline = System.currentTimeMillis() + timeoutMillis;
GameView.AutomationSnapshot snapshot = driver.snapshot();
while (System.currentTimeMillis() < deadline) {
snapshot = driver.snapshot();
if (condition.reached(snapshot)) {
driver.setInput(false, false, false);
return snapshot;
}
driver.setInput(moveLeft, moveRight, jump);
driver.waitForNextDecision(32L);
}
throw new AssertionError("Timed out waiting for level 10 lift body; x="
+ snapshot.playerX() + ", y=" + snapshot.playerY()
+ ", deaths=" + snapshot.deaths()
+ ", corpses=" + snapshot.corpseCount()
+ ", platformY=" + snapshot.firstMovingPlatformY());
}
private static final class GameDriver implements LevelPlaytestBot.Driver {
private final Instrumentation instrumentation;
private final GameView gameView;
@@ -156,10 +156,27 @@ final class LateLevelScenarios {
.step("Clear the lift body again", 2_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 860f)
.step("Prepare the frozen-block jump", 2_000L,
.step("Release after the lift-body jump", 500L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 720f)
.step("Jump over the frozen block", 2_500L,
snapshot -> !snapshot.inputActive())
.step("Land after the lift-body jump", 1_500L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerOnGround())
.step("Back away from the freezer", 1_000L,
LevelPlaytestBot.Input.left(),
snapshot -> snapshot.playerAlive()
&& snapshot.playerX() <= 960f)
.step("Jump onto the frozen block", 2_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive()
&& snapshot.playerX() >= 1_040f
&& snapshot.playerY() <= 700f)
.step("Settle on the frozen block", 1_500L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerY() <= 690f)
.step("Jump from the frozen block", 2_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f)
.step("Power the final relay", 3_000L, LevelPlaytestBot.Input.right(),
@@ -186,12 +203,34 @@ final class LateLevelScenarios {
.step("Clear the retained lift body", 2_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 860f)
.step("Prepare the final frozen-block jump", 2_000L,
.step("Release after the retained-body jump", 500L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 720f)
.step("Clear the frozen block", 2_500L,
snapshot -> !snapshot.inputActive())
.step("Land after the retained-body jump", 1_500L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerOnGround())
.step("Back away from the retained freezer", 1_000L,
LevelPlaytestBot.Input.left(),
snapshot -> snapshot.playerAlive()
&& snapshot.playerX() <= 960f)
.step("Jump onto the retained frozen block", 2_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive()
&& snapshot.playerX() >= 1_040f
&& snapshot.playerY() <= 700f)
.step("Settle on the retained frozen block", 1_500L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerY() <= 690f)
.step("Jump from the retained frozen block", 2_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f)
.step("Land before the powered relay", 1_500L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerY() >= 779f)
.step("Approach the powered relay", 2_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 1_320f)
@@ -201,15 +240,23 @@ final class LateLevelScenarios {
.step("Clear the powered relay", 2_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_650f)
.step("Wait for the lift at floor height", 6_000L,
.step("Observe the lift above the launch floor", 6_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive()
&& snapshot.playerY() >= 770f
&& !Float.isNaN(snapshot.firstMovingPlatformY())
&& snapshot.firstMovingPlatformY() >= 780f)
&& snapshot.firstMovingPlatformY() <= 650f)
.step("Wait for the lift at floor height", 9_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive()
&& (snapshot.playerOnMovingPlatform()
|| (snapshot.playerY() >= 770f
&& !Float.isNaN(snapshot.firstMovingPlatformY())
&& snapshot.firstMovingPlatformY() >= 780f)))
.step("Take off toward the moving lift", 1_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 670f)
snapshot -> snapshot.playerAlive()
&& (snapshot.playerOnMovingPlatform()
|| snapshot.playerY() <= 670f))
.step("Board the moving lift", 2_000L,
LevelPlaytestBot.Input.right(),
GameView.AutomationSnapshot::playerOnMovingPlatform)
@@ -221,7 +268,7 @@ final class LateLevelScenarios {
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", 7_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerY() <= 310f)
.step("Move to the raised lift edge", 2_000L,
@@ -236,4 +283,5 @@ final class LateLevelScenarios {
GameView.AutomationSnapshot::completed)
.build();
}
}
@@ -79,7 +79,8 @@ final class LevelEightScenario {
snapshot = await(driver, deadline, "Wait for the moving platform",
LevelPlaytestBot.Input.none(),
current -> current.elapsedMilliseconds() >= timerStartedAt + 2_250L);
current -> current.elapsedMilliseconds() >= timerStartedAt + 2_250L
&& current.firstMovingPlatformX() <= 1_050f);
if (failed(snapshot, deadline, driver)) return snapshot;
driver.setInput(false, true, true);
@@ -87,7 +88,7 @@ final class LevelEightScenario {
driver.setInput(false, true, false);
snapshot = await(driver, deadline, "Land on the moving platform",
LevelPlaytestBot.Input.right(),
current -> current.elapsedMilliseconds() >= timerStartedAt + 3_200L);
GameView.AutomationSnapshot::playerOnMovingPlatform);
if (failed(snapshot, deadline, driver)) return snapshot;
driver.setInput(false, true, true);
@@ -107,18 +108,15 @@ final class LevelEightScenario {
private GameView.AutomationSnapshot awaitFarFloorLandingOnOriginalTimer(
LevelPlaytestBot.Driver driver, long deadline
) {
boolean moveLeft = true;
GameView.AutomationSnapshot snapshot = driver.snapshot();
while (driver.currentTimeMillis() < deadline && snapshot.playerAlive()
&& !(snapshot.playerX() >= 1_510f && snapshot.playerX() < 1_580f
&& snapshot.playerY() >= 779f && snapshot.allDoorsOpen())) {
driver.setInput(moveLeft, !moveLeft, false);
moveLeft = !moveLeft;
driver.waitForNextDecision(DECISION_MILLIS);
snapshot = driver.snapshot();
}
driver.setInput(false, false, false);
return snapshot;
GameView.AutomationSnapshot snapshot = await(driver, deadline,
"Land on the far floor", LevelPlaytestBot.Input.right(),
current -> current.playerX() >= 1_510f
&& current.playerY() >= 779f && current.allDoorsOpen());
if (failed(snapshot, deadline, driver)) return snapshot;
return await(driver, deadline, "Stabilize on the far floor",
LevelPlaytestBot.Input.left(),
current -> current.playerX() < 1_580f
&& current.playerY() >= 779f && current.allDoorsOpen());
}
private Result finishFromFarSide(LevelPlaytestBot.Driver driver, long deadline,
@@ -53,8 +53,12 @@ public final class GameView extends View implements Runnable {
private final float playerY;
private final int deaths;
private final int corpseCount;
private final float firstMovingPlatformX;
private final float firstMovingPlatformY;
private final boolean playerOnMovingPlatform;
private final boolean firstMovingPlatformPowered;
private final float firstCorpseX;
private final boolean playerOnGround;
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
boolean visiblyPaused, boolean inputActive, int levelNumber,
@@ -62,14 +66,17 @@ public final class GameView extends View implements Runnable {
float playerX, float playerY, int deaths, int corpseCount) {
this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber,
elapsedMilliseconds, playerX, playerY, deaths, corpseCount,
Float.NaN, false);
Float.NaN, Float.NaN, false, false, 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) {
float firstMovingPlatformX, float firstMovingPlatformY,
boolean playerOnMovingPlatform,
boolean firstMovingPlatformPowered, float firstCorpseX,
boolean playerOnGround) {
this.completed = completed;
this.playerAlive = playerAlive;
this.allDoorsOpen = allDoorsOpen;
@@ -81,8 +88,12 @@ public final class GameView extends View implements Runnable {
this.playerY = playerY;
this.deaths = deaths;
this.corpseCount = corpseCount;
this.firstMovingPlatformX = firstMovingPlatformX;
this.firstMovingPlatformY = firstMovingPlatformY;
this.playerOnMovingPlatform = playerOnMovingPlatform;
this.firstMovingPlatformPowered = firstMovingPlatformPowered;
this.firstCorpseX = firstCorpseX;
this.playerOnGround = playerOnGround;
}
public boolean completed() { return completed; }
@@ -96,8 +107,12 @@ 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 firstMovingPlatformX() { return firstMovingPlatformX; }
public float firstMovingPlatformY() { return firstMovingPlatformY; }
public boolean playerOnMovingPlatform() { return playerOnMovingPlatform; }
public boolean firstMovingPlatformPowered() { return firstMovingPlatformPowered; }
public float firstCorpseX() { return firstCorpseX; }
public boolean playerOnGround() { return playerOnGround; }
}
private static final class Player {
@@ -235,6 +250,8 @@ public final class GameView extends View implements Runnable {
respawnTimer = 0f;
cameraX = 0f;
userPaused = false;
pointers.clear();
deriveControls();
corpses.clear();
platformStates.clear();
toggleSignals.clear();
@@ -465,11 +482,29 @@ public final class GameView extends View implements Runnable {
resolveCorpseHorizontal(corpse);
corpse.y += corpse.vy * dt;
resolveCorpseVertical(corpse);
if (corpse.grounded && settleCorpseOnPressurePlate(corpse)) continue;
if (corpse.grounded) corpse.vx *= (float) Math.pow(0.05, dt);
if (Math.abs(corpse.vx) < 4f) corpse.vx = 0f;
}
}
private boolean settleCorpseOnPressurePlate(Corpse corpse) {
for (LevelDefinition.Switch item : level.switches) {
if (item.type != LevelDefinition.SwitchType.PRESSURE) continue;
if (!intersects(corpse.x, corpse.y, corpse.width, corpse.height, item.box)) continue;
if (item.box.width >= corpse.width) {
corpse.x = GameMath.clamp(corpse.x, item.box.x, item.box.right() - corpse.width);
} else {
corpse.x = item.box.x + (item.box.width - corpse.width) * 0.5f;
}
corpse.vx = 0f;
corpse.vy = 0f;
corpse.pinned = true;
return true;
}
return false;
}
private void resolveCorpseHorizontal(Corpse corpse) {
RectF current = corpse.rect();
for (RectF solid : solidRects(false)) {
@@ -1131,6 +1166,10 @@ public final class GameView extends View implements Runnable {
startLevel(oneBasedLevelNumber - 1);
}
public void restartLevelForAutomation() {
restartLevel();
}
public void setAutomationInput(boolean moveLeft, boolean moveRight, boolean jump) {
boolean jumpWasPressed = jumpPressed;
leftPressed = moveLeft;
@@ -1144,10 +1183,14 @@ public final class GameView extends View implements Runnable {
if (level != null) {
for (LevelDefinition.Door door : level.doors) allOpen &= doorOpen(door);
}
float firstMovingPlatformX = Float.NaN;
float firstMovingPlatformY = Float.NaN;
boolean firstMovingPlatformPowered = false;
for (PlatformState state : platformStates) {
if (state.definition.isMoving()) {
firstMovingPlatformX = state.x;
firstMovingPlatformY = state.y;
firstMovingPlatformPowered = signalActive(state.definition.controlId);
break;
}
}
@@ -1167,8 +1210,12 @@ public final class GameView extends View implements Runnable {
player.y,
deaths,
corpses.size(),
firstMovingPlatformX,
firstMovingPlatformY,
playerOnMovingPlatform
playerOnMovingPlatform,
firstMovingPlatformPowered,
corpses.isEmpty() ? Float.NaN : corpses.get(0).x,
player.onGround
);
}
}
@@ -109,12 +109,12 @@ public final class LevelCatalog {
8, "Borrowed Time", "Jump for the far blue relay if the timed gate closes.",
2450, 130, 760, goal(2220, 600), 0, 38,
platforms(
floor(0, 970), floor(1510, 940), ledge(1980, 710, 470),
moving(990, 770, 240, 34, 380, 0, 4.0f, "clock")
floor(0, 1040), floor(1510, 940), ledge(1980, 710, 470),
moving(910, 770, 320, 34, 380, 0, 4.0f, "clock")
),
hazards(hazard(PIT, 970, 880, 540, 200)),
hazards(hazard(PIT, 1040, 880, 470, 200)),
switches(
sw(TIMED, 560, 810, 90, 70, "clock", 6.0f, 1),
sw(TIMED, 560, 810, 90, 70, "clock", 8.0f, 1),
sw(TIMED, 1600, 650, 90, 70, "clock", 3.5f, 1)
),
doors(door(1770, 560, 82, 320, "clock"))
@@ -141,11 +141,11 @@ public final class LevelCatalog {
3100, 130, 760, goal(2880, 330), 3, 120,
platforms(
floor(0, 1940), floor(2140, 960), ledge(2420, 440, 680),
moving(1840, 790, 250, 34, 0, -410, 5.0f, "lift")
moving(1800, 790, 290, 34, 0, -410, 10.0f, "lift")
),
hazards(
hazard(SAW, 520, 760, 96, 96, null, 700, -240),
hazard(FREEZER, 1110, 750, 100, 130),
hazard(FREEZER, 1110, 776, 100, 104),
hazard(ELECTRIC, 1510, 780, 110, 100, "power", 0, 0),
hazard(PIT, 1940, 880, 200, 200)
),
@@ -144,6 +144,7 @@ public final class LevelPlaytestBot {
Step step = scenario.steps.get(stepIndex);
lastStep = step.name;
if (step.completion.reached(last)) {
driver.setInput(false, false, false);
stepIndex++;
stepStartedAt = driver.currentTimeMillis();
continue;
@@ -100,6 +100,24 @@ public final class LevelCatalogTest {
freezer.box.x, freezer.box.y, freezer.box.width, freezer.box.height));
}
@Test
public void masteryFreezerKillsOnTheFloorButNotOnTopOfItsFrozenBlock() {
LevelDefinition level = LevelCatalog.get(9);
LevelDefinition.Hazard freezer = null;
for (LevelDefinition.Hazard hazard : level.hazards) {
if (hazard.type == LevelDefinition.HazardType.FREEZER) freezer = hazard;
}
assertTrue("Level 10 must contain its freezer", freezer != null);
float frozenBlockTop = 880f - 104f;
assertTrue("A floor-running volunteer must still hit the mastery freezer",
GameMath.intersects(1_108f, 780f, 62f, 100f,
freezer.box.x, freezer.box.y, freezer.box.width, freezer.box.height));
assertFalse("A volunteer standing on the mastery frozen block must clear the freezer",
GameMath.intersects(1_108f, frozenBlockTop - 100f, 62f, 100f,
freezer.box.x, freezer.box.y, freezer.box.width, freezer.box.height));
}
@Test
public void masteryLiftHasLaunchOverlapAndStillSpansARealPit() {
LevelDefinition level = LevelCatalog.get(9);
@@ -118,17 +136,40 @@ public final class LevelCatalogTest {
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);
assertTrue("The floor must overlap the lift by at least 140px for a delayed launch",
launchOverlap >= 140f);
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 mastery lift cycle must leave a readable delayed-input boarding window",
lift.periodSeconds >= 10f);
assertTrue("The upper gallery must still require the vertical lift",
lift.travelY <= -400f && level.goal.y < launchFloor.box.y - 300f);
}
@Test
public void timedPlatformHasLaunchOverlapAndStillSpansARealPit() {
LevelDefinition level = LevelCatalog.get(7);
LevelDefinition.Platform launchFloor = level.platforms.get(0);
LevelDefinition.Platform landingFloor = level.platforms.get(1);
LevelDefinition.Platform movingPlatform = level.platforms.get(3);
LevelDefinition.Hazard pit = level.hazards.get(0);
float launchOverlap = Math.min(launchFloor.box.right(), movingPlatform.box.right())
- Math.max(launchFloor.box.x, movingPlatform.box.x);
assertTrue("The timed platform must overlap the launch floor by at least 50px",
launchOverlap >= 50f);
assertTrue("The timed platform needs a 320px left-side catch window",
movingPlatform.box.width >= 320f);
assertEquals("Widening must preserve the original right edge for recovery safety",
1_230f, movingPlatform.box.right(), 0f);
assertEquals(launchFloor.box.right(), pit.box.x, 0f);
assertEquals(landingFloor.box.x, pit.box.right(), 0f);
assertTrue("Borrowed Time must retain a real platforming pit", pit.box.width >= 450f);
}
@Test
public void everyDoorAndControlledPlatformHasADeclaredSignalSource() {
for (LevelDefinition level : LevelCatalog.all()) {
@@ -23,6 +23,8 @@ public final class LevelPlaytestBotTest {
assertTrue(result.diagnostic(), result.completed());
assertEquals(7, driver.startedLevel);
assertEquals("Reach the goal", result.lastStep());
assertTrue("Completed steps must release held controls before the next action",
driver.inputReleases >= 2);
}
@Test
@@ -98,6 +100,7 @@ public final class LevelPlaytestBotTest {
private int deaths;
private boolean completed;
private final boolean progresses;
private int inputReleases;
ScriptedDriver() {
this(true);
@@ -119,6 +122,7 @@ public final class LevelPlaytestBotTest {
@Override
public void setInput(boolean moveLeft, boolean moveRight, boolean jump) {
if (!moveLeft && !moveRight && !jump) inputReleases++;
this.moveLeft = moveLeft;
this.moveRight = moveRight;
}