Добавить испытания и прогрессию по LevelID

This commit is contained in:
2026-08-19 09:25:32 +03:00
parent 259fcd316b
commit 3de8d45039
37 changed files with 968 additions and 63 deletions
@@ -0,0 +1,53 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertFalse;
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 ChallengeCollectibleIntegrationTest {
@Test
public void collectedCacheSurvivesRecreationAndRestartResetsAttemptState() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
activity.onActivity(current -> {
GameView view = current.gameViewForAutomation();
view.enableDeterministicAutomationClock();
view.startLevelForAutomation(18);
assertTrue(view.automationSnapshot().collectibleVisible());
view.setAutomationInput(false, true, false);
for (int decision = 0; decision < 400
&& view.automationSnapshot().playerX() < 650f; decision++) {
view.advanceDeterministicAutomation(16L);
}
assertTrue("approach x=" + view.automationSnapshot().playerX(),
view.automationSnapshot().playerX() >= 650f);
view.setAutomationInput(false, true, true);
for (int decision = 0; decision < 180
&& !view.automationSnapshot().collectibleFound(); decision++) {
view.advanceDeterministicAutomation(16L);
}
view.setAutomationInput(false, false, false);
assertTrue(view.automationSnapshot().collectibleFound());
assertFalse(view.automationSnapshot().collectibleVisible());
assertTrue(view.sessionSnapshotForAutomation().collectibleFound());
});
activity.recreate();
activity.onActivity(current -> {
GameView view = current.gameViewForAutomation();
assertTrue(view.automationSnapshot().visiblyPaused());
assertTrue(view.automationSnapshot().collectibleFound());
assertFalse(view.automationSnapshot().collectibleVisible());
view.startLevelForAutomation(18);
assertFalse(view.automationSnapshot().collectibleFound());
assertTrue(view.automationSnapshot().collectibleVisible());
});
}
}
}
@@ -0,0 +1,95 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.app.Instrumentation;
import android.content.Context;
import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public final class ChallengePlaythroughTest {
private Context context;
@Before
public void resetProgress() {
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
context.getSharedPreferences("project_sacrifice_progress_v1", Context.MODE_PRIVATE)
.edit().clear().commit();
}
@After
public void cleanProgress() {
context.getSharedPreferences("project_sacrifice_progress_v1", Context.MODE_PRIVATE)
.edit().clear().commit();
}
@Test
public void intendedRouteEarnsAndPersistsAllThreeStarsThreeTimes() {
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, ChallengeScenarios.levelEighteen(), 20_000L);
assertTrue("run=" + run + "; " + result.diagnostic(), result.completed());
assertEquals(0, result.deaths());
assertTrue(driver.snapshot().collectibleFound());
assertEquals(3, driver.result().challengeResult().stars());
assertEquals(3, driver.result().bestStars());
}
assertEquals(3, new ProgressStore(context).earnedStars(LevelCatalog.get(17)));
}
}
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];
}
CompletionResult result() {
CompletionResult[] value = new CompletionResult[1];
instrumentation.runOnMainSync(() -> value[0] = view.completionResultForAutomation());
return value[0];
}
@Override public void waitForNextDecision(long millis) {
instrumentation.runOnMainSync(() ->
view.advanceDeterministicAutomation(Math.max(16L, millis)));
}
}
}
@@ -0,0 +1,77 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public final class ChallengeProgressPersistenceTest {
private Context context;
private SharedPreferences raw;
@Before
public void clearProgress() {
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
raw = context.getSharedPreferences("project_sacrifice_progress_v1", Context.MODE_PRIVATE);
raw.edit().clear().commit();
}
@After
public void cleanProgress() {
raw.edit().clear().commit();
}
@Test
public void numericV2ProgressMigratesToStableLevelIdWithoutLoss() {
LevelDefinition level = LevelCatalog.get(0);
raw.edit().putInt("save_version", 2)
.putBoolean("level_1_completed", true)
.putInt("level_1_deaths", 2)
.putLong("level_1_time", 41_000L)
.commit();
ProgressStore progress = new ProgressStore(context);
assertEquals(3, progress.saveVersion());
assertTrue(progress.isCompleted(level.levelId));
assertEquals(2, progress.bestDeaths(level.levelId));
assertEquals(41_000L, progress.bestTimeMs(level.levelId));
assertTrue(raw.contains("level_id_" + level.levelId + "_completed"));
}
@Test
public void independentRunsAccumulateBestThreeStarsByLevelId() {
LevelDefinition base = LevelCatalog.get(0);
LevelDefinition level = new LevelDefinition(
base.formatVersion, base.physicsVersion, "foundations.star-lab", "foundations",
base.number, base.title, base.hint, base.titleRu, base.hintRu,
base.worldWidth, base.worldHeight, base.spawnX, base.spawnY, base.goal,
base.targetDeaths, base.targetSeconds,
new LevelDefinition.Box(500f, 700f, 50f, 50f), base.objects,
base.platforms, base.hazards, base.switches, base.doors);
ProgressStore progress = new ProgressStore(context);
progress.recordCompletion(level, base.targetDeaths,
base.targetSeconds * 1_000L + 5_000L, false);
assertEquals(1, progress.earnedStars(level));
progress.recordCompletion(level, base.targetDeaths + 2,
base.targetSeconds * 1_000L - 1_000L, true);
assertEquals(3, progress.earnedStars(level));
assertTrue(progress.collectibleFound(level.levelId));
assertEquals(base.targetDeaths, progress.bestDeaths(level.levelId));
assertEquals(base.targetSeconds * 1_000L - 1_000L,
progress.bestTimeMs(level.levelId));
}
}
@@ -0,0 +1,28 @@
package com.projectsacrifice.game;
/** Production-input route for the first three-goal challenge level. */
final class ChallengeScenarios {
private ChallengeScenarios() {}
static LevelPlaytestBot.Scenario levelEighteen() {
return LevelPlaytestBot.Scenario.builder(18, "Level 18 - threefold proof")
.step("Approach the optional cache", 3_000L,
LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(0,
LevelPlaytestBot.groundedAliveAtOrBeyond(650f)))
.step("Jump through the optional cache", 2_000L,
LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(0,
snapshot -> snapshot.playerAlive()
&& snapshot.collectibleFound()))
.step("Land beyond the cache", 2_500L,
LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(0,
LevelPlaytestBot.groundedAliveAtOrBeyond(1_050f)))
.step("Reach the relic within both efficiency targets", 5_000L,
LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(0,
GameView.AutomationSnapshot::completed))
.build();
}
}
@@ -218,8 +218,10 @@ public final class LateLevelPlaythroughTest {
@Override
public void startLevel(int oneBasedLevelNumber) {
instrumentation.runOnMainSync(
() -> gameView.startLevelForAutomation(oneBasedLevelNumber));
instrumentation.runOnMainSync(() -> {
gameView.enableDeterministicAutomationClock();
gameView.startLevelForAutomation(oneBasedLevelNumber);
});
}
@Override
@@ -238,19 +240,16 @@ public final class LateLevelPlaythroughTest {
@Override
public void waitForNextDecision(long millis) {
try {
boolean inFinalTraversalWindow = throttleFinalTraversalOnly
&& lastSnapshot != null
&& lastSnapshot.levelNumber() == 10
&& lastSnapshot.deaths() == 3
&& lastSnapshot.playerX() >= 1_200f
&& lastSnapshot.playerX() <= 2_200f;
Thread.sleep(inFinalTraversalWindow
? Math.max(millis, minimumDecisionMillis) : millis);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new AssertionError("Late-level bot interrupted", interrupted);
}
boolean inFinalTraversalWindow = throttleFinalTraversalOnly
&& lastSnapshot != null
&& lastSnapshot.levelNumber() == 10
&& lastSnapshot.deaths() == 3
&& lastSnapshot.playerX() >= 1_200f
&& lastSnapshot.playerX() <= 2_200f;
long decisionMillis = inFinalTraversalWindow
? Math.max(millis, minimumDecisionMillis) : millis;
instrumentation.runOnMainSync(
() -> gameView.advanceDeterministicAutomation(decisionMillis));
}
}
}
@@ -38,6 +38,7 @@ public final class LevelPaginationTest {
assertTrue(visible.contains("Steam Charge"));
assertTrue(visible.contains("Ashen Counterweight"));
assertTrue(visible.contains("The Whole Weight"));
assertTrue(visible.contains("Threefold Proof"));
assertFalse(visible.contains("The Volunteer"));
assertTrue(visible.contains("Page 2 of 2"));
});