Завершить мир Foundations
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Runs before stateful playthrough suites so the release frame budget measures a clean install,
|
||||
* rather than host-emulator degradation accumulated by more than an hour of instrumentation.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class AReleasePerformanceGateTest {
|
||||
@Test
|
||||
public void levelTenRendererPublishesAndMeetsTheReleaseBudget() throws Exception {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
GameView[] view = new GameView[1];
|
||||
activity.onActivity(current -> {
|
||||
view[0] = current.gameViewForAutomation();
|
||||
view[0].startLevelForAutomation(10);
|
||||
});
|
||||
|
||||
// Activity launch includes landscape rotation and shader warm-up. Cold-start has a
|
||||
// separate release gate; this contract measures steady-state gameplay rendering.
|
||||
Thread.sleep(5_000L);
|
||||
activity.onActivity(current -> view[0].resetPerformanceMetricsForAutomation());
|
||||
Thread.sleep(10_000L);
|
||||
PerformanceMonitor.Snapshot[] measured = new PerformanceMonitor.Snapshot[1];
|
||||
activity.onActivity(current -> measured[0] =
|
||||
view[0].performanceSnapshotForAutomation());
|
||||
PerformanceMonitor.Snapshot metrics = measured[0];
|
||||
assertTrue("At least 180 uninterrupted frames are required; samples="
|
||||
+ metrics.sampleCount(), metrics.sampleCount() >= 180);
|
||||
PerformanceBudget budget = PerformanceBudget.release();
|
||||
if (isEmulator()) {
|
||||
assertRendererCostWithinBudget(metrics, budget);
|
||||
} else {
|
||||
// Only physical hardware can own the end-to-end frame-interval contract. An AVD
|
||||
// shares its scheduler with the host, but still owns update/draw/complexity cost.
|
||||
assertTrue(metrics.failureSummary(budget), metrics.within(budget));
|
||||
}
|
||||
assertTrue("Target FPS must be explicit", budget.targetFramesPerSecond() >= 60);
|
||||
assertTrue("Runtime body cap must fit the published budget",
|
||||
PhysicsConfig.release().maximumBodies() <= budget.maximumBodies());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isEmulator() {
|
||||
return Build.FINGERPRINT.startsWith("generic")
|
||||
|| Build.MODEL.contains("sdk_gphone")
|
||||
|| Build.HARDWARE.contains("ranchu");
|
||||
}
|
||||
|
||||
private static void assertRendererCostWithinBudget(PerformanceMonitor.Snapshot metrics,
|
||||
PerformanceBudget budget) {
|
||||
String diagnostic = metrics.failureSummary(budget);
|
||||
assertTrue("AVD must still publish frame intervals; " + diagnostic,
|
||||
metrics.p95FrameIntervalMs() > 0f);
|
||||
assertTrue(diagnostic, metrics.p95UpdateMs() <= budget.maximumP95UpdateMs());
|
||||
assertTrue(diagnostic, metrics.p95DrawMs() <= budget.maximumP95DrawMs());
|
||||
assertTrue(diagnostic, metrics.maximumBodies() <= budget.maximumBodies());
|
||||
assertTrue(diagnostic,
|
||||
metrics.maximumCollisionChecks() <= budget.maximumCollisionChecks());
|
||||
assertTrue(diagnostic,
|
||||
metrics.maximumDrawPrimitives() <= budget.maximumDrawPrimitives());
|
||||
assertTrue(diagnostic,
|
||||
metrics.maximumUsedMemoryBytes() <= budget.maximumUsedMemoryBytes());
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
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 FoundationsFinalePlaythroughTest {
|
||||
@Test public void twoKeysRequiresBothRetainedBodiesThreeTimes() {
|
||||
runThree(FoundationsFinaleScenarios.levelTwentySeven(), 2, 2);
|
||||
}
|
||||
|
||||
@Test public void cleanRestartBuildsThreeDistinctBodiesThreeTimesAndResets() {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
Driver driver = Driver.from(activity);
|
||||
for (int run = 1; run <= 3; run++) {
|
||||
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(
|
||||
driver, FoundationsFinaleScenarios.levelTwentyEight(), 55_000L);
|
||||
assertAccepted(run, result, driver.snapshot(), 3, 3);
|
||||
}
|
||||
driver.restart();
|
||||
GameView.AutomationSnapshot restarted = driver.snapshot();
|
||||
assertEquals(28, restarted.levelNumber());
|
||||
assertTrue(restarted.playerAlive());
|
||||
assertEquals(0, restarted.deaths());
|
||||
assertEquals(0, restarted.corpseCount());
|
||||
assertFalse(restarted.collectibleFound());
|
||||
assertFalse(restarted.inputActive());
|
||||
assertTrue(restarted.elapsedMilliseconds() < 100L);
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void minimumCostUsesExactlyTwoBodiesThreeTimes() {
|
||||
runThree(FoundationsFinaleScenarios.levelTwentyNine(), 2, 2);
|
||||
}
|
||||
|
||||
@Test public void foundationRelayCompletesTheWorldWithExactlyThreeBodiesThreeTimes() {
|
||||
runThree(FoundationsFinaleScenarios.levelThirty(), 3, 3);
|
||||
}
|
||||
|
||||
private static void runThree(LevelPlaytestBot.Scenario scenario,
|
||||
int expectedDeaths, int expectedCorpses) {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
Driver driver = Driver.from(activity);
|
||||
for (int run = 1; run <= 3; run++) {
|
||||
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(driver, scenario, 55_000L);
|
||||
assertAccepted(run, result, driver.snapshot(), expectedDeaths, expectedCorpses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertAccepted(int run, LevelPlaytestBot.Result result,
|
||||
GameView.AutomationSnapshot snapshot,
|
||||
int expectedDeaths, int expectedCorpses) {
|
||||
assertTrue("run=" + run + "; " + result.diagnostic(), result.completed());
|
||||
assertEquals("run=" + run, expectedDeaths, result.deaths());
|
||||
assertEquals("run=" + run, expectedCorpses, result.corpseCount());
|
||||
assertTrue("run=" + run + "; collectible", snapshot.collectibleFound());
|
||||
}
|
||||
|
||||
private static final class Driver implements LevelPlaytestBot.Driver {
|
||||
private final Instrumentation instrumentation;
|
||||
private final GameView view;
|
||||
|
||||
private Driver(Instrumentation instrumentation, GameView view) {
|
||||
this.instrumentation = instrumentation;
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
static Driver from(ActivityScenario<MainActivity> activity) {
|
||||
GameView[] target = new GameView[1];
|
||||
activity.onActivity(current -> target[0] = current.gameViewForAutomation());
|
||||
return new Driver(InstrumentationRegistry.getInstrumentation(), target[0]);
|
||||
}
|
||||
|
||||
@Override public void startLevel(int oneBasedLevelNumber) {
|
||||
instrumentation.runOnMainSync(() -> {
|
||||
view.enableDeterministicAutomationClock();
|
||||
view.startLevelForAutomation(oneBasedLevelNumber);
|
||||
});
|
||||
}
|
||||
|
||||
@Override public void setInput(boolean left, boolean right, boolean jump) {
|
||||
instrumentation.runOnMainSync(() -> view.setAutomationInput(left, right, jump));
|
||||
}
|
||||
|
||||
@Override public GameView.AutomationSnapshot snapshot() {
|
||||
GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1];
|
||||
instrumentation.runOnMainSync(() -> value[0] = view.automationSnapshot());
|
||||
return value[0];
|
||||
}
|
||||
|
||||
@Override public void waitForNextDecision(long millis) {
|
||||
instrumentation.runOnMainSync(() ->
|
||||
view.advanceDeterministicAutomation(Math.max(16L, millis)));
|
||||
}
|
||||
|
||||
void restart() {
|
||||
instrumentation.runOnMainSync(view::restartLevelForAutomation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ public final class GameRestartTest {
|
||||
activity.onActivity(current -> view[0] = current.gameViewForAutomation());
|
||||
|
||||
instrumentation.runOnMainSync(() -> {
|
||||
view[0].enableDeterministicAutomationClock();
|
||||
view[0].startLevelForAutomation(10);
|
||||
view[0].setAutomationInput(false, true, false);
|
||||
});
|
||||
@@ -58,13 +59,13 @@ public final class GameRestartTest {
|
||||
GameView view,
|
||||
long timeoutMillis,
|
||||
LevelPlaytestBot.SnapshotCondition condition
|
||||
) throws Exception {
|
||||
long deadline = System.currentTimeMillis() + timeoutMillis;
|
||||
) {
|
||||
GameView.AutomationSnapshot current = snapshot(instrumentation, view);
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
long startedAt = current.elapsedMilliseconds();
|
||||
while (current.elapsedMilliseconds() - startedAt < timeoutMillis) {
|
||||
current = snapshot(instrumentation, view);
|
||||
if (condition.reached(current)) return current;
|
||||
Thread.sleep(32L);
|
||||
instrumentation.runOnMainSync(() -> view.advanceDeterministicAutomation(96L));
|
||||
}
|
||||
throw new AssertionError("Restart setup timed out; level=" + current.levelNumber()
|
||||
+ ", x=" + current.playerX() + ", deaths=" + current.deaths()
|
||||
|
||||
@@ -57,6 +57,10 @@ public final class LevelPaginationTest {
|
||||
assertTrue(visible.contains("Frozen Step"));
|
||||
assertTrue(visible.contains("Living Wire"));
|
||||
assertTrue(visible.contains("Second Hand"));
|
||||
assertTrue(visible.contains("Two Keys"));
|
||||
assertTrue(visible.contains("Clean Restart"));
|
||||
assertTrue(visible.contains("Minimum Cost"));
|
||||
assertTrue(visible.contains("Foundation Relay"));
|
||||
assertFalse(visible.contains("Covered Path"));
|
||||
assertTrue(visible.contains("Page 3 of 3"));
|
||||
});
|
||||
|
||||
+6
-8
@@ -67,8 +67,10 @@ public final class ProductionHazardPlaythroughTest {
|
||||
}
|
||||
|
||||
@Override public void startLevel(int oneBasedLevelNumber) {
|
||||
instrumentation.runOnMainSync(
|
||||
() -> gameView.startLevelForAutomation(oneBasedLevelNumber));
|
||||
instrumentation.runOnMainSync(() -> {
|
||||
gameView.enableDeterministicAutomationClock();
|
||||
gameView.startLevelForAutomation(oneBasedLevelNumber);
|
||||
});
|
||||
}
|
||||
|
||||
@Override public void setInput(boolean moveLeft, boolean moveRight, boolean jump) {
|
||||
@@ -83,12 +85,8 @@ public final class ProductionHazardPlaythroughTest {
|
||||
}
|
||||
|
||||
@Override public void waitForNextDecision(long millis) {
|
||||
try {
|
||||
Thread.sleep(Math.max(32L, millis));
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError("Production-hazard bot interrupted", interrupted);
|
||||
}
|
||||
instrumentation.runOnMainSync(() ->
|
||||
gameView.advanceDeterministicAutomation(Math.max(16L, millis)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class StageFivePerformanceTest {
|
||||
@Test
|
||||
public void levelTenRendererPublishesAndMeetsTheReleaseBudget() throws Exception {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
GameView[] view = new GameView[1];
|
||||
activity.onActivity(current -> {
|
||||
view[0] = current.gameViewForAutomation();
|
||||
view[0].startLevelForAutomation(10);
|
||||
});
|
||||
|
||||
// Activity launch includes landscape rotation and shader warm-up. Cold-start has a
|
||||
// separate release gate; this contract measures steady-state gameplay rendering.
|
||||
Thread.sleep(5_000L);
|
||||
activity.onActivity(current -> view[0].resetPerformanceMetricsForAutomation());
|
||||
Thread.sleep(10_000L);
|
||||
PerformanceMonitor.Snapshot[] measured = new PerformanceMonitor.Snapshot[1];
|
||||
activity.onActivity(current -> measured[0] =
|
||||
view[0].performanceSnapshotForAutomation());
|
||||
PerformanceMonitor.Snapshot metrics = measured[0];
|
||||
assertTrue("At least 180 uninterrupted frames are required; samples="
|
||||
+ metrics.sampleCount(), metrics.sampleCount() >= 180);
|
||||
PerformanceBudget budget = PerformanceBudget.release();
|
||||
assertTrue(metrics.failureSummary(budget), metrics.within(budget));
|
||||
assertTrue("Target FPS must be explicit", budget.targetFramesPerSecond() >= 60);
|
||||
assertTrue("Runtime body cap must fit the published budget",
|
||||
PhysicsConfig.release().maximumBodies() <= budget.maximumBodies());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
/** Production-input routes for the Foundations combination, challenge and finale levels. */
|
||||
public final class FoundationsFinaleScenarios {
|
||||
private FoundationsFinaleScenarios() {}
|
||||
|
||||
public static LevelPlaytestBot.Scenario levelTwentySeven() {
|
||||
return LevelPlaytestBot.Scenario.builder(27, "Level 27 - two keys")
|
||||
.step("Leave the pressure key", 4_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, snapshot -> snapshot.deaths() == 1
|
||||
&& snapshot.corpseCount() == 1
|
||||
&& !snapshot.allDoorsOpen()))
|
||||
.step("Wait for the relay volunteer", 2_000L, LevelPlaytestBot.Input.none(),
|
||||
atMost(2, FoundationsFinaleScenarios::respawned))
|
||||
.step("Stage before the pressure key", 2_500L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, LevelPlaytestBot.groundedAliveAtOrBeyond(430f)))
|
||||
.step("Cross the pressure key", 3_000L, LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(2, LevelPlaytestBot.groundedAliveAtOrBeyond(800f)))
|
||||
.step("Leave the conductive key", 3_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, snapshot -> snapshot.deaths() == 2
|
||||
&& snapshot.corpseCount() == 2))
|
||||
.step("Prove both keys open the door", 2_000L, LevelPlaytestBot.Input.none(),
|
||||
atMost(2, GameView.AutomationSnapshot::allDoorsOpen))
|
||||
.step("Wait for the crossing volunteer", 2_000L, LevelPlaytestBot.Input.none(),
|
||||
atMost(2, FoundationsFinaleScenarios::respawned))
|
||||
.step("Return to the pressure key", 2_500L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, LevelPlaytestBot.groundedAliveAtOrBeyond(430f)))
|
||||
.step("Cross the pressure key again", 3_000L, LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(2, LevelPlaytestBot.groundedAliveAtOrBeyond(800f)))
|
||||
.step("Stage before the conductive key", 1_500L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, LevelPlaytestBot.groundedAliveAtOrBeyond(880f)))
|
||||
.step("Take off before the live relay", 1_500L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(2, snapshot -> snapshot.playerAlive()
|
||||
&& !snapshot.playerOnGround() && snapshot.playerY() <= 680f))
|
||||
.step("Clear the live key and collect the cache", 3_000L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(2, snapshot -> snapshot.playerAlive()
|
||||
&& snapshot.playerX() >= 1_300f && snapshot.collectibleFound()))
|
||||
.step("Pass the AND door", 3_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, LevelPlaytestBot.groundedAliveAtOrBeyond(1_700f)))
|
||||
.step("Reach the relic", 3_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, GameView.AutomationSnapshot::completed))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static LevelPlaytestBot.Scenario levelTwentyEight() {
|
||||
return LevelPlaytestBot.Scenario.builder(28, "Level 28 - clean restart")
|
||||
.step("Deliver the first body", 4_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.deaths() == 1
|
||||
&& snapshot.corpseCount() == 1))
|
||||
.step("Wait for the freezing volunteer", 2_000L,
|
||||
LevelPlaytestBot.Input.none(), atMost(3, FoundationsFinaleScenarios::respawned))
|
||||
.step("Stage before the delivery saw", 2_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(420f)))
|
||||
.step("Take off before the delivery saw", 1_500L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive()
|
||||
&& !snapshot.playerOnGround() && snapshot.playerY() <= 620f))
|
||||
.step("Clear the delivery saw", 2_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 740f))
|
||||
.step("Jump over the delivered body", 3_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_150f))
|
||||
.step("Freeze the second body", 2_500L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.deaths() == 2
|
||||
&& snapshot.corpseCount() == 2))
|
||||
.step("Wait for the conductive volunteer", 2_000L,
|
||||
LevelPlaytestBot.Input.none(), atMost(3, FoundationsFinaleScenarios::respawned))
|
||||
.step("Stage before the delivery saw again", 2_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(420f)))
|
||||
.step("Take off before the delivery saw again", 1_500L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive()
|
||||
&& !snapshot.playerOnGround() && snapshot.playerY() <= 620f))
|
||||
.step("Clear the delivery saw again", 2_000L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 740f))
|
||||
.step("Clear the delivered body again", 3_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(1_150f)))
|
||||
.step("Stage before the frozen body", 2_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(1_000f)))
|
||||
.step("Clear the frozen body", 3_000L, LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(1_430f)))
|
||||
.step("Create the conductive body", 3_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.deaths() == 3
|
||||
&& snapshot.corpseCount() == 3))
|
||||
.step("Wait for the final volunteer", 2_000L, LevelPlaytestBot.Input.none(),
|
||||
atMost(3, FoundationsFinaleScenarios::respawned))
|
||||
.step("Stage for the final delivery crossing", 2_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(420f)))
|
||||
.step("Take off for the final delivery crossing", 1_500L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive()
|
||||
&& !snapshot.playerOnGround() && snapshot.playerY() <= 620f))
|
||||
.step("Clear the delivery saw for the final time", 2_000L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 740f))
|
||||
.step("Clear the delivered body for the final time", 3_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(1_150f)))
|
||||
.step("Return to the frozen body", 2_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(1_000f)))
|
||||
.step("Clear the frozen body again", 3_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(1_430f)))
|
||||
.step("Stage before the conductive body", 2_000L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(1_500f)))
|
||||
.step("Jump over the conductive body", 3_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive()
|
||||
&& snapshot.playerX() >= 1_950f))
|
||||
.step("Collect the cache", 2_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive() && snapshot.collectibleFound()))
|
||||
.step("Reach the relic", 4_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, GameView.AutomationSnapshot::completed))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static LevelPlaytestBot.Scenario levelTwentyNine() {
|
||||
return LevelPlaytestBot.Scenario.builder(29, "Level 29 - minimum cost")
|
||||
.step("Place the near bridge body", 3_500L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, snapshot -> snapshot.deaths() == 1))
|
||||
.step("Wait for the second bridge volunteer", 1_500L,
|
||||
LevelPlaytestBot.Input.none(), atMost(2, FoundationsFinaleScenarios::respawned))
|
||||
.step("Approach the first bridge body", 2_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(2, LevelPlaytestBot.groundedAliveAtOrBeyond(520f)))
|
||||
.step("Place the far pressure body", 3_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(2, snapshot -> snapshot.deaths() == 2
|
||||
&& snapshot.corpseCount() == 2
|
||||
&& !snapshot.allDoorsOpen()))
|
||||
.step("Wait for the minimum-cost volunteer", 1_500L,
|
||||
LevelPlaytestBot.Input.none(), atMost(2, FoundationsFinaleScenarios::respawned))
|
||||
.step("Return to the corpse bridge", 2_500L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, LevelPlaytestBot.groundedAliveAtOrBeyond(520f)))
|
||||
.step("Cross both retained bodies", 3_500L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(2, LevelPlaytestBot.groundedAliveAtOrBeyond(1_050f)))
|
||||
.step("Start the minimum clock", 2_500L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, snapshot -> snapshot.allDoorsOpen()
|
||||
&& snapshot.collectibleFound()))
|
||||
.step("Pass the timed AND door", 3_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, LevelPlaytestBot.groundedAliveAtOrBeyond(1_700f)))
|
||||
.step("Reach the relic", 3_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(2, GameView.AutomationSnapshot::completed))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static LevelPlaytestBot.Scenario levelThirty() {
|
||||
return LevelPlaytestBot.Scenario.builder(30, "Level 30 - foundation relay")
|
||||
.step("Place the near finale bridge body", 3_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.deaths() == 1))
|
||||
.step("Wait for the far bridge volunteer", 1_500L,
|
||||
LevelPlaytestBot.Input.none(), atMost(3, FoundationsFinaleScenarios::respawned))
|
||||
.step("Approach the near finale body", 2_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(400f)))
|
||||
.step("Place the far finale bridge body", 3_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, snapshot -> snapshot.deaths() == 2))
|
||||
.step("Wait for the pressure volunteer", 1_500L,
|
||||
LevelPlaytestBot.Input.none(), atMost(3, FoundationsFinaleScenarios::respawned))
|
||||
.step("Return to the finale bridge", 2_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(400f)))
|
||||
.step("Cross both finale bridge bodies", 3_500L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(1_000f)))
|
||||
.step("Leave the third body on the relay", 2_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.deaths() == 3
|
||||
&& snapshot.corpseCount() == 3
|
||||
&& !snapshot.allDoorsOpen()))
|
||||
.step("Wait for the relay runner", 1_500L, LevelPlaytestBot.Input.none(),
|
||||
atMost(3, FoundationsFinaleScenarios::respawned))
|
||||
.step("Return to the finale bridge again", 2_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(400f)))
|
||||
.step("Cross the complete corpse bridge", 3_500L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(1_000f)))
|
||||
.step("Take off before the pressure relay", 1_500L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive()
|
||||
&& !snapshot.playerOnGround() && snapshot.playerY() <= 620f))
|
||||
.step("Clear the pressure relay body", 2_500L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_300f))
|
||||
.step("Start both finale controls", 2_500L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.allDoorsOpen()
|
||||
&& snapshot.firstMovingPlatformPowered()))
|
||||
.step("Wait for the finale boarding phase", 3_500L,
|
||||
LevelPlaytestBot.Input.none(),
|
||||
atMost(3, snapshot -> MovingPlatformLaunchPolicy.readyToBoard(
|
||||
snapshot, 1_650f)))
|
||||
.step("Board the finale ferry", 2_500L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive()
|
||||
&& snapshot.playerOnGround()
|
||||
&& snapshot.playerOnMovingPlatform()))
|
||||
.step("Ride the finale ferry", 5_000L, LevelPlaytestBot.Input.none(),
|
||||
atMost(3, snapshot -> MovingPlatformLaunchPolicy.ready(snapshot, 1_950f)))
|
||||
.step("Jump to the far finale floor", 3_000L,
|
||||
LevelPlaytestBot.Input.rightJump(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(2_320f)))
|
||||
.step("Collect the finale cache", 2_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, snapshot -> snapshot.playerAlive() && snapshot.collectibleFound()))
|
||||
.step("Pass the finale door", 3_000L, LevelPlaytestBot.Input.right(),
|
||||
atMost(3, LevelPlaytestBot.groundedAliveAtOrBeyond(2_800f)))
|
||||
.step("Reach the Foundations relic", 4_000L,
|
||||
LevelPlaytestBot.Input.right(),
|
||||
atMost(3, GameView.AutomationSnapshot::completed))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static LevelPlaytestBot.SnapshotCondition atMost(
|
||||
int deaths, LevelPlaytestBot.SnapshotCondition condition) {
|
||||
return LevelPlaytestBot.whileDeathsAtMost(deaths, condition);
|
||||
}
|
||||
|
||||
private static boolean respawned(GameView.AutomationSnapshot snapshot) {
|
||||
return snapshot.playerAlive() && snapshot.playerX() < 250f;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public final class FoundationsAdvancedContentTest {
|
||||
@Test
|
||||
public void secondFoundationsWaveMatchesMatrixAndPreservesRecoveryContracts() {
|
||||
List<LevelDefinition> levels = LevelCatalog.all();
|
||||
assertEquals(26, levels.size());
|
||||
assertTrue(levels.size() >= 26);
|
||||
|
||||
LevelDefinition remote = levels.get(22);
|
||||
assertIdentity(remote, 23, "foundations.remote-volunteer", "Remote Volunteer", 1, 50);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class FoundationsFinaleContentTest {
|
||||
@Test
|
||||
public void finalFoundationsWaveMatchesTheApprovedMatrixAndClosesTheWorld() {
|
||||
List<LevelDefinition> levels = LevelCatalog.all();
|
||||
assertEquals(30, levels.size());
|
||||
|
||||
LevelDefinition keys = levels.get(26);
|
||||
assertIdentity(keys, 27, "foundations.two-keys", "Two Keys", 2, 65);
|
||||
assertTrue(keys.containsMechanic(LevelDefinition.HazardType.ELECTRIC));
|
||||
assertTrue(keys.containsMechanic(LevelDefinition.SwitchType.PRESSURE));
|
||||
assertFalse(keys.doors.isEmpty());
|
||||
assertEquals(2, keys.doors.get(0).requiredSignals.size());
|
||||
|
||||
LevelDefinition restart = levels.get(27);
|
||||
assertIdentity(restart, 28, "foundations.clean-restart", "Clean Restart", 3, 75);
|
||||
assertTrue(restart.containsMechanic(LevelDefinition.HazardType.SAW));
|
||||
assertTrue(restart.containsMechanic(LevelDefinition.HazardType.FREEZER));
|
||||
assertTrue(restart.containsMechanic(LevelDefinition.HazardType.ELECTRIC));
|
||||
|
||||
LevelDefinition challenge = levels.get(28);
|
||||
assertIdentity(challenge, 29, "foundations.minimum-cost", "Minimum Cost", 2, 65);
|
||||
assertTrue(challenge.containsMechanic(LevelDefinition.HazardType.SPIKES));
|
||||
assertTrue(challenge.containsMechanic(LevelDefinition.SwitchType.PRESSURE));
|
||||
assertTrue(challenge.containsMechanic(LevelDefinition.SwitchType.TIMED));
|
||||
assertFalse(challenge.doors.isEmpty());
|
||||
|
||||
LevelDefinition finale = levels.get(29);
|
||||
assertIdentity(finale, 30, "foundations.foundation-final", "Foundation Relay", 3, 90);
|
||||
assertTrue(finale.containsMechanic(LevelDefinition.HazardType.SPIKES));
|
||||
assertTrue(finale.containsMechanic(LevelDefinition.HazardType.SAW));
|
||||
assertTrue(finale.containsMechanic(LevelDefinition.HazardType.PIT));
|
||||
assertTrue(finale.containsMechanic(LevelDefinition.SwitchType.PRESSURE));
|
||||
assertTrue(finale.containsMechanic(LevelDefinition.SwitchType.TOGGLE));
|
||||
assertTrue(finale.containsMechanic(LevelDefinition.SwitchType.TIMED));
|
||||
assertTrue(finale.hasMovingPlatform());
|
||||
assertFalse(finale.doors.isEmpty());
|
||||
assertEquals(2, finale.doors.get(0).requiredSignals.size());
|
||||
}
|
||||
|
||||
private static void assertIdentity(LevelDefinition level, int number, String id, String title,
|
||||
int targetDeaths, int targetSeconds) {
|
||||
assertEquals(number, level.number);
|
||||
assertEquals(id, level.levelId);
|
||||
assertEquals("foundations", level.worldId);
|
||||
assertEquals(title, level.title);
|
||||
assertEquals(targetDeaths, level.targetDeaths);
|
||||
assertEquals(targetSeconds, level.targetSeconds);
|
||||
assertNotNull(level.collectible);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public final class LevelCatalogTest {
|
||||
@Test
|
||||
public void productionCatalogContainsMechanicsAndChallengeTeachingLevels() {
|
||||
List<LevelDefinition> levels = LevelCatalog.all();
|
||||
assertEquals(26, levels.size());
|
||||
assertEquals(30, levels.size());
|
||||
Set<String> titles = new HashSet<>();
|
||||
for (int i = 0; i < levels.size(); i++) {
|
||||
LevelDefinition level = levels.get(i);
|
||||
|
||||
Reference in New Issue
Block a user