Завершить инженерную проверку этапа 2
This commit is contained in:
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user