Доказать прохождение поздних уровней ботами

This commit is contained in:
2026-08-18 02:06:58 +03:00
parent 1ebad500f3
commit a8f5ddc0a9
5 changed files with 396 additions and 2 deletions
@@ -0,0 +1,154 @@
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;
/** Production-input acceptance coverage for levels 6, 7, 9 and 10. */
@RunWith(AndroidJUnit4.class)
public final class LateLevelPlaythroughTest {
private static final int CONSECUTIVE_RUNS = 3;
@Test
public void frozenVolunteerIsUsedToCompleteLevelSixThreeTimes() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
GameDriver driver = GameDriver.from(activity);
for (int run = 1; run <= CONSECUTIVE_RUNS; run++) {
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(
driver,
LateLevelScenarios.levelSix(),
30_000L
);
assertTrue("run=" + run + "; " + result.diagnostic(), result.completed());
assertEquals("run=" + run + "; intended route uses one frozen volunteer",
1, result.deaths());
assertEquals("run=" + run + "; frozen block must remain in the puzzle",
1, result.corpseCount());
}
}
}
@Test
public void conductiveVolunteerKeepsLevelSevenDoorOpenThreeTimes() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
GameDriver driver = GameDriver.from(activity);
for (int run = 1; run <= CONSECUTIVE_RUNS; run++) {
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(
driver,
LateLevelScenarios.levelSeven(),
25_000L
);
assertTrue("run=" + run + "; " + result.diagnostic(), result.completed());
assertEquals("run=" + run + "; intended route uses one conductive volunteer",
1, result.deaths());
assertEquals("run=" + run + "; conductive body must remain in the relay",
1, result.corpseCount());
assertTrue("run=" + run + "; conductive relay must open the door",
result.doorOpened());
}
}
}
@Test
public void weightAndPowerOpenLevelNineDoorThreeTimes() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
GameDriver driver = GameDriver.from(activity);
for (int run = 1; run <= CONSECUTIVE_RUNS; run++) {
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(
driver,
LateLevelScenarios.levelNine(),
35_000L
);
assertTrue("run=" + run + "; " + result.diagnostic(), result.completed());
assertEquals("run=" + run + "; weight and power require two volunteers",
2, result.deaths());
assertEquals("run=" + run + "; both signal bodies must remain",
2, result.corpseCount());
assertTrue("run=" + run + "; both signals must open the AND door",
result.doorOpened());
}
}
}
@Test
public void masteryProcessionCompletesLevelTenThreeTimes() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
GameDriver driver = GameDriver.from(activity);
for (int run = 1; run <= CONSECUTIVE_RUNS; run++) {
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(
driver,
LateLevelScenarios.levelTen(),
55_000L
);
assertTrue("run=" + run + "; " + result.diagnostic(), result.completed());
assertEquals("run=" + run + "; mastery route uses three volunteers",
3, result.deaths());
assertEquals("run=" + run + "; launch, freeze and relay bodies must remain",
3, result.corpseCount());
assertTrue("run=" + run + "; conductive relay must open the final door",
result.doorOpened());
}
}
}
private static final class GameDriver implements LevelPlaytestBot.Driver {
private final Instrumentation instrumentation;
private final GameView gameView;
private GameDriver(Instrumentation instrumentation, GameView gameView) {
this.instrumentation = instrumentation;
this.gameView = gameView;
}
static GameDriver from(ActivityScenario<MainActivity> activity) {
GameView[] view = new GameView[1];
activity.onActivity(current -> view[0] = current.gameViewForAutomation());
return new GameDriver(InstrumentationRegistry.getInstrumentation(), view[0]);
}
@Override
public void startLevel(int oneBasedLevelNumber) {
instrumentation.runOnMainSync(
() -> gameView.startLevelForAutomation(oneBasedLevelNumber));
}
@Override
public void setInput(boolean moveLeft, boolean moveRight, boolean jump) {
instrumentation.runOnMainSync(
() -> gameView.setAutomationInput(moveLeft, moveRight, jump));
}
@Override
public GameView.AutomationSnapshot snapshot() {
GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1];
instrumentation.runOnMainSync(() -> value[0] = gameView.automationSnapshot());
return value[0];
}
@Override
public void waitForNextDecision(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new AssertionError("Late-level bot interrupted", interrupted);
}
}
}
}
@@ -0,0 +1,222 @@
package com.projectsacrifice.game;
/** Declarative intended-route scenarios for the late vertical-slice levels. */
final class LateLevelScenarios {
private LateLevelScenarios() {}
static LevelPlaytestBot.Scenario levelSix() {
return LevelPlaytestBot.Scenario.builder(6, "Level 6 - frozen corpse step")
.step("Freeze one volunteer", 4_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1)
.step("Wait for respawn", 3_000L, LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
.step("Approach the frozen block", 3_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 390f)
.step("Jump toward the frozen block", 1_500L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerX() >= 640f)
.step("Stand on the frozen block", 2_500L, LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerX() >= 626f
&& snapshot.playerY() >= 668f
&& snapshot.playerY() <= 690f)
.step("Jump from the block to the first ledge", 2_500L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerX() >= 750f
&& snapshot.playerY() <= 505f)
.step("Cross the first ledge", 3_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 1_230f)
.step("Jump the gap to the second ledge", 2_500L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerX() >= 1_400f
&& snapshot.playerY() <= 520f)
.step("Reach the relic", 5_000L, LevelPlaytestBot.Input.right(),
GameView.AutomationSnapshot::completed)
.build();
}
static LevelPlaytestBot.Scenario levelSeven() {
return LevelPlaytestBot.Scenario.builder(7, "Level 7 - conductive relay")
.step("Power the relay with one volunteer", 4_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1)
.step("Observe the powered door", 1_500L, LevelPlaytestBot.Input.none(),
GameView.AutomationSnapshot::allDoorsOpen)
.step("Wait for respawn", 3_000L, LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
.step("Approach the live relay", 3_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 460f)
.step("Take off before the conductive relay", 1_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 700f)
.step("Clear the conductive relay", 2_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 780f)
.step("Pass the powered door", 3_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 1_230f)
.step("Reach the relic", 4_000L, LevelPlaytestBot.Input.right(),
GameView.AutomationSnapshot::completed)
.build();
}
static LevelPlaytestBot.Scenario levelNine() {
return LevelPlaytestBot.Scenario.builder(9, "Level 9 - weight AND power")
.step("Leave one volunteer on the weight plate", 4_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1
&& !snapshot.allDoorsOpen())
.step("Wait for the second volunteer", 3_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
.step("Approach the weighted spikes", 2_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 390f)
.step("Jump over the weighted spikes", 2_500L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 680f)
.step("Approach the unpowered relay", 2_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 820f)
.step("Power the relay with the second volunteer", 2_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.deaths() == 2
&& snapshot.corpseCount() == 2)
.step("Observe weight AND power opening the door", 1_500L,
LevelPlaytestBot.Input.none(),
GameView.AutomationSnapshot::allDoorsOpen)
.step("Wait for the final volunteer", 3_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
.step("Return to the weighted spikes", 2_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 390f)
.step("Clear the weighted spikes", 2_500L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 680f)
.step("Approach the powered relay", 2_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 720f)
.step("Take off before the powered relay", 1_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 700f)
.step("Clear the powered relay", 2_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_040f)
.step("Pass the AND door", 3_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 1_610f)
.step("Reach the relic", 4_000L, LevelPlaytestBot.Input.right(),
GameView.AutomationSnapshot::completed)
.build();
}
static LevelPlaytestBot.Scenario levelTen() {
return LevelPlaytestBot.Scenario.builder(10, "Level 10 - mastery procession")
.step("Launch one volunteer toward the lift plate", 4_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1)
.step("Wait for the freezer volunteer", 3_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
.step("Approach the launch saw", 2_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 350f)
.step("Take off before the launch saw", 1_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 680f)
.step("Clear the launch saw", 2_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 680f)
.step("Approach the delivered lift body", 1_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 730f)
.step("Jump over the delivered lift body", 2_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 950f)
.step("Enter the freezer", 3_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.deaths() == 2
&& snapshot.corpseCount() == 2)
.step("Wait for the relay volunteer", 3_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
.step("Return to the launch saw", 2_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 350f)
.step("Take off before the saw again", 1_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 680f)
.step("Clear the saw again", 2_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 680f)
.step("Approach the lift body again", 1_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 730f)
.step("Clear the lift body again", 2_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 900f)
.step("Prepare the frozen-block jump", 2_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 740f)
.step("Jump over the frozen block", 2_500L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f)
.step("Power the final relay", 3_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.deaths() == 3
&& snapshot.corpseCount() == 3)
.step("Observe the final door open", 1_500L,
LevelPlaytestBot.Input.none(),
GameView.AutomationSnapshot::allDoorsOpen)
.step("Wait for the final volunteer", 3_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
.step("Approach the saw for the final crossing", 2_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 350f)
.step("Take off for the final saw crossing", 1_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 680f)
.step("Clear the saw for the final crossing", 2_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 680f)
.step("Approach the retained lift body", 1_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 730f)
.step("Clear the retained lift body", 2_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 900f)
.step("Prepare the final frozen-block jump", 2_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 740f)
.step("Clear the frozen block", 2_500L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f)
.step("Approach the powered relay", 2_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 1_390f)
.step("Clear the powered relay", 2_500L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_650f)
.step("Approach the lift", 2_000L, LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 1_730f)
.step("Board the moving lift", 3_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerX() >= 1_790f
&& snapshot.playerY() <= 670f)
.step("Ride the lift to the upper door", 5_000L,
LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerY() <= 310f)
.step("Move to the raised lift edge", 2_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 2_000f
&& snapshot.playerY() <= 340f)
.step("Jump through the door to the upper gallery", 3_000L,
LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerX() >= 2_400f
&& snapshot.playerY() <= 360f)
.step("Reach the final relic", 4_000L, LevelPlaytestBot.Input.right(),
GameView.AutomationSnapshot::completed)
.build();
}
}
@@ -90,7 +90,7 @@ public final class LevelCatalog {
6, "Cold Comfort", "The frost makes a solid block. Use the frozen volunteer as a step.",
2150, 130, 760, goal(1880, 500), 1, 65,
platforms(floor(0, 1120), ledge(810, 590, 520), ledge(1430, 610, 570)),
hazards(hazard(FREEZER, 690, 750, 100, 130)), switches(), doors()
hazards(hazard(FREEZER, 690, 776, 100, 104)), switches(), doors()
);
}
@@ -82,6 +82,24 @@ public final class LevelCatalogTest {
assertFalse(finale.doors.isEmpty());
}
@Test
public void levelSixFreezerKillsOnTheFloorButNotOnTopOfItsFrozenBlock() {
LevelDefinition level = LevelCatalog.get(5);
LevelDefinition.Hazard freezer = null;
for (LevelDefinition.Hazard hazard : level.hazards) {
if (hazard.type == LevelDefinition.HazardType.FREEZER) freezer = hazard;
}
assertTrue("Level 6 must contain its freezer", freezer != null);
float frozenBlockTop = 880f - 104f;
assertTrue("A floor-running volunteer must still hit the freezer",
GameMath.intersects(688f, 780f, 62f, 100f,
freezer.box.x, freezer.box.y, freezer.box.width, freezer.box.height));
assertFalse("A volunteer standing on the frozen block must clear the freezer",
GameMath.intersects(688f, frozenBlockTop - 100f, 62f, 100f,
freezer.box.x, freezer.box.y, freezer.box.width, freezer.box.height));
}
@Test
public void everyDoorAndControlledPlatformHasADeclaredSignalSource() {
for (LevelDefinition level : LevelCatalog.all()) {