Завершить мир Foundations

This commit is contained in:
2026-08-19 15:26:51 +03:00
parent 4d8aa28f0a
commit f53e068cb1
23 changed files with 768 additions and 86 deletions
@@ -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());
}
}
@@ -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"));
});
@@ -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());
}
}
}