Завершить инженерный кандидат Stage 5

This commit is contained in:
2026-08-19 02:04:45 +03:00
parent b840684ef3
commit cde886ca96
45 changed files with 3236 additions and 98 deletions
@@ -0,0 +1,40 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
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 AudioSettingsPersistenceTest {
@Test
public void independentAudioVolumesSurviveActivityRecreation() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
activity.onActivity(current -> {
GameView view = current.gameViewForAutomation();
view.resetAudioSettingsForAutomation();
view.selectNextMusicVolumeForAutomation();
view.selectNextEffectsVolumeForAutomation();
view.selectNextVoiceVolumeForAutomation();
});
activity.recreate();
activity.onActivity(current -> {
GameView view = current.gameViewForAutomation();
AudioSettings settings = view.audioSettingsForAutomation();
assertEquals(1f, settings.musicVolume(), 0f);
assertEquals(0f, settings.effectsVolume(), 0f);
assertEquals(0f, settings.voiceVolume(), 0f);
view.openSettingsForAutomation();
assertTrue(view.visibleLocalizedTextForAutomation().contains("Audio"));
assertTrue(view.screenDescriptionForAutomation().contains("Music 100%"));
view.resetAudioSettingsForAutomation();
});
}
}
}
@@ -0,0 +1,87 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.SharedPreferences;
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 GameSessionRestoreTest {
@Test
public void recreationRestoresActivePuzzleBodiesTimersAndPausedSafety() {
GameSessionSnapshot[] before = new GameSessionSnapshot[1];
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
GameView[] view = new GameView[1];
activity.onActivity(current -> {
view[0] = current.gameViewForAutomation();
view[0].enableDeterministicAutomationClock();
view[0].startLevelForAutomation(5);
view[0].setAutomationInput(false, true, false);
for (int decision = 0; decision < 400
&& view[0].automationSnapshot().deaths() == 0; decision++) {
view[0].advanceDeterministicAutomation(16L);
}
assertEquals(1, view[0].automationSnapshot().deaths());
assertEquals(1, view[0].automationSnapshot().corpseCount());
view[0].setAutomationInput(false, false, false);
before[0] = view[0].sessionSnapshotForAutomation();
});
activity.recreate();
activity.onActivity(current -> {
view[0] = current.gameViewForAutomation();
GameSessionSnapshot after = view[0].sessionSnapshotForAutomation();
assertEquals(before[0].levelNumber(), after.levelNumber());
assertEquals(before[0].deaths(), after.deaths());
assertEquals(before[0].elapsedSeconds(), after.elapsedSeconds(), 0.001f);
assertEquals(before[0].player().x(), after.player().x(), 0.001f);
assertEquals(before[0].player().alive(), after.player().alive());
assertEquals(before[0].bodies().size(), after.bodies().size());
assertEquals(before[0].bodies().get(0).deathType(),
after.bodies().get(0).deathType());
assertEquals(before[0].platformMotionTimes(), after.platformMotionTimes());
assertEquals(before[0].timedSignals(), after.timedSignals());
assertTrue(view[0].automationSnapshot().visiblyPaused());
assertFalse(view[0].automationSnapshot().inputActive());
current.onBackPressed();
view[0].enableDeterministicAutomationClock();
view[0].advanceDeterministicAutomation(1_000L);
assertTrue(view[0].automationSnapshot().playerAlive());
});
}
}
@Test
public void legacyProgressMigratesInPlaceWithoutLosingCompletion() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
SharedPreferences legacy = context.getSharedPreferences(
"project_sacrifice_progress_v1", Context.MODE_PRIVATE);
legacy.edit().clear()
.putInt("unlocked", 6)
.putBoolean("level_5_completed", true)
.putInt("level_5_deaths", 3)
.putLong("level_5_time", 42_000L)
.commit();
ProgressStore migrated = new ProgressStore(context);
assertEquals(ProgressStore.CURRENT_SAVE_VERSION, migrated.saveVersion());
assertEquals(6, migrated.unlockedCount());
assertTrue(migrated.isCompleted(5));
assertEquals(3, migrated.bestDeaths(5));
assertEquals(42_000L, migrated.bestTimeMs(5));
legacy.edit().clear().commit();
}
}
@@ -0,0 +1,178 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
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 StageFiveDeterminismTest {
@Test
public void allTenRecordedRoutesReplayThreeSequentialTimesUnderFiveMinutes() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
GameView[] view = new GameView[1];
activity.onActivity(current -> {
view[0] = current.gameViewForAutomation();
view[0].enableDeterministicAutomationClock();
});
DeterministicDriver driver = new DeterministicDriver(
InstrumentationRegistry.getInstrumentation(), view[0]);
LevelPlaytestBot bot = new LevelPlaytestBot();
DeterministicInputTape[] tapes = new DeterministicInputTape[10];
String[] signatures = new String[10];
tapes[0] = record(bot, driver, EarlyLevelScenarios.levelOne(), 20_000L, 0, 0);
signatures[0] = driver.terminalSignature();
tapes[1] = record(bot, driver, EarlyLevelScenarios.levelTwo(), 30_000L, 3, 3);
signatures[1] = driver.terminalSignature();
tapes[2] = record(bot, driver, EarlyLevelScenarios.levelThree(), 25_000L, 1, 1);
signatures[2] = driver.terminalSignature();
tapes[3] = record(bot, driver, EarlyLevelScenarios.levelFour(), 30_000L, 0, 0);
signatures[3] = driver.terminalSignature();
BotReplayRecorder levelFive = new BotReplayRecorder(driver);
LevelPlaytestBot.Result levelFiveResult = bot.proveLevelFive(levelFive, 25_000L);
assertStandardResult(5, levelFiveResult, 1, 1);
tapes[4] = levelFive.tape();
signatures[4] = driver.terminalSignature();
tapes[5] = record(bot, driver, LateLevelScenarios.levelSix(), 30_000L, 1, 1);
signatures[5] = driver.terminalSignature();
tapes[6] = record(bot, driver, LateLevelScenarios.levelSeven(), 25_000L, 1, 1);
signatures[6] = driver.terminalSignature();
BotReplayRecorder levelEight = new BotReplayRecorder(driver);
LevelEightScenario.Result levelEightResult =
new LevelEightScenario().completeIntendedRoute(levelEight, 30_000L);
assertTrue(levelEightResult.diagnostic(), levelEightResult.completed());
assertEquals(0, levelEightResult.deaths());
tapes[7] = levelEight.tape();
signatures[7] = driver.terminalSignature();
tapes[8] = record(bot, driver, LateLevelScenarios.levelNine(), 35_000L, 2, 2);
signatures[8] = driver.terminalSignature();
tapes[9] = record(bot, driver, LateLevelScenarios.levelTen(), 70_000L, 3, 3);
signatures[9] = driver.terminalSignature();
for (int pass = 1; pass <= 3; pass++) {
long passStartedAt = System.currentTimeMillis();
for (int level = 0; level < tapes.length; level++) {
String replayed = tapes[level].play(driver);
assertEquals("pass=" + pass + ", level=" + (level + 1)
+ ", tape=" + tapes[level].id(),
signatures[level], replayed);
assertTrue("pass=" + pass + ", level=" + (level + 1),
driver.snapshot().completed());
}
long elapsed = System.currentTimeMillis() - passStartedAt;
assertTrue("Replay matrix " + pass + " exceeded 5 minutes: " + elapsed,
elapsed < 300_000L);
}
}
}
private static DeterministicInputTape record(
LevelPlaytestBot bot, DeterministicDriver driver,
LevelPlaytestBot.Scenario scenario, long budget,
int expectedDeaths, int expectedBodies) {
BotReplayRecorder recorder = new BotReplayRecorder(driver);
LevelPlaytestBot.Result result = bot.run(recorder, scenario, budget);
DeterministicInputTape tape = recorder.tape();
assertStandardResult(tape.levelNumber(), result, expectedDeaths, expectedBodies);
return tape;
}
private static void assertStandardResult(int level, LevelPlaytestBot.Result result,
int expectedDeaths, int expectedBodies) {
assertTrue("level=" + level + "; " + result.diagnostic(), result.completed());
assertEquals("level=" + level + "; deaths", expectedDeaths, result.deaths());
assertEquals("level=" + level + "; bodies", expectedBodies, result.corpseCount());
}
private static final class DeterministicDriver implements LevelPlaytestBot.Driver,
DeterministicInputTape.Target {
private final Instrumentation instrumentation;
private final GameView gameView;
private long simulationClockMillis;
private boolean pendingLeft;
private boolean pendingRight;
private boolean pendingJump;
private boolean inputDirty;
private GameView.AutomationSnapshot cachedSnapshot;
DeterministicDriver(Instrumentation instrumentation, GameView gameView) {
this.instrumentation = instrumentation;
this.gameView = gameView;
}
@Override
public void startLevel(int oneBasedLevelNumber) {
simulationClockMillis = 0L;
instrumentation.runOnMainSync(() -> {
gameView.enableDeterministicAutomationClock();
gameView.startLevelForAutomation(oneBasedLevelNumber);
cachedSnapshot = gameView.automationSnapshot();
});
pendingLeft = pendingRight = pendingJump = false;
inputDirty = false;
}
@Override
public void setInput(boolean left, boolean right, boolean jump) {
pendingLeft = left;
pendingRight = right;
pendingJump = jump;
inputDirty = true;
}
@Override
public GameView.AutomationSnapshot snapshot() {
if (inputDirty) {
instrumentation.runOnMainSync(() -> {
gameView.setAutomationInput(pendingLeft, pendingRight, pendingJump);
cachedSnapshot = gameView.automationSnapshot();
});
inputDirty = false;
}
return cachedSnapshot;
}
@Override
public void waitForNextDecision(long millis) { advance(millis); }
@Override
public void advance(long milliseconds) {
simulationClockMillis += milliseconds;
instrumentation.runOnMainSync(() -> {
if (inputDirty) {
gameView.setAutomationInput(pendingLeft, pendingRight, pendingJump);
inputDirty = false;
}
gameView.advanceDeterministicAutomation(milliseconds);
cachedSnapshot = gameView.automationSnapshot();
});
}
@Override
public long currentTimeMillis() { return simulationClockMillis; }
@Override
public String terminalSignature() {
String[] signature = new String[1];
instrumentation.runOnMainSync(() -> {
if (inputDirty) {
gameView.setAutomationInput(pendingLeft, pendingRight, pendingJump);
inputDirty = false;
}
cachedSnapshot = gameView.automationSnapshot();
signature[0] = gameView.deterministicStateSignatureForAutomation();
});
return signature[0];
}
}
}
@@ -0,0 +1,41 @@
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());
}
}
}