Устранить softlock восьмого уровня

This commit is contained in:
2026-08-18 02:41:45 +03:00
parent 3ed6d9ebab
commit 70106e29d8
4 changed files with 309 additions and 8 deletions
@@ -0,0 +1,90 @@
package com.projectsacrifice.game;
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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/** Device acceptance tests for the level-8 timed-platform puzzle and recovery route. */
@RunWith(AndroidJUnit4.class)
public final class LevelEightPlaythroughTest {
private static final int REQUIRED_CONSECUTIVE_RUNS = 3;
@Test
public void expiredClockCanBeRearmedFromTheFarSideWithoutRestart() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
AndroidDriver driver = driver(activity);
for (int run = 1; run <= REQUIRED_CONSECUTIVE_RUNS; run++) {
LevelEightScenario.Result result = new LevelEightScenario()
.recoverAfterClockExpires(driver, 20_000L);
assertTrue("Recovery run " + run + ": " + result.diagnostic(), result.completed());
assertEquals("Recovery must not hide a reset/death shortcut", 0, result.deaths());
}
}
}
@Test
public void intendedTimedPlatformRouteCompletesThreeConsecutiveTimes() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
AndroidDriver driver = driver(activity);
for (int run = 1; run <= REQUIRED_CONSECUTIVE_RUNS; run++) {
LevelEightScenario.Result result = new LevelEightScenario()
.completeIntendedRoute(driver, 15_000L);
assertTrue("Intended run " + run + ": " + result.diagnostic(), result.completed());
assertEquals("The intended route is deathless", 0, result.deaths());
}
}
}
private static AndroidDriver driver(ActivityScenario<MainActivity> activity) {
final GameView[] view = new GameView[1];
activity.onActivity(mainActivity -> view[0] = mainActivity.gameViewForAutomation());
return new AndroidDriver(InstrumentationRegistry.getInstrumentation(), view[0]);
}
private static final class AndroidDriver implements LevelPlaytestBot.Driver {
private final Instrumentation instrumentation;
private final GameView gameView;
AndroidDriver(Instrumentation instrumentation, GameView gameView) {
this.instrumentation = instrumentation;
this.gameView = gameView;
}
@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() {
final 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("Level-8 bot interrupted", interrupted);
}
}
}
}
@@ -0,0 +1,201 @@
package com.projectsacrifice.game;
/** Black-box level-8 scenarios driven only through the production input boundary. */
final class LevelEightScenario {
private static final long DECISION_MILLIS = 32L;
static final class Result {
private final boolean completed;
private final int deaths;
private final String diagnostic;
Result(boolean completed, int deaths, String diagnostic) {
this.completed = completed;
this.deaths = deaths;
this.diagnostic = diagnostic;
}
boolean completed() { return completed; }
int deaths() { return deaths; }
String diagnostic() { return diagnostic; }
}
Result recoverAfterClockExpires(LevelPlaytestBot.Driver driver, long timeoutMillis) {
long deadline = driver.currentTimeMillis() + timeoutMillis;
driver.startLevel(8);
GameView.AutomationSnapshot snapshot = crossPit(driver, deadline);
if (failed(snapshot, deadline, driver)) {
return failure("Cross the pit before the clock expires", snapshot);
}
snapshot = await(driver, deadline, "Wait for the far-side trap",
LevelPlaytestBot.Input.none(),
current -> current.playerX() >= 1_510f && !current.allDoorsOpen());
if (failed(snapshot, deadline, driver)) {
return failure("Wait for the far-side trap", snapshot);
}
snapshot = await(driver, deadline, "Return beneath the recovery relay",
LevelPlaytestBot.Input.left(),
current -> current.playerX() <= 1_680f && current.playerY() >= 779f);
if (failed(snapshot, deadline, driver)) {
return failure("Return beneath the recovery relay", snapshot);
}
snapshot = pulseLeftJumpThenAwait(driver, deadline, "Jump into the recovery relay",
current -> current.allDoorsOpen());
if (failed(snapshot, deadline, driver)) {
return failure("Jump into the recovery relay", snapshot);
}
snapshot = await(driver, deadline, "Land after rearming the clock",
LevelPlaytestBot.Input.none(), current -> current.playerY() >= 779f);
if (failed(snapshot, deadline, driver)) {
return failure("Land after rearming the clock", snapshot);
}
return finishFromFarSide(driver, deadline, snapshot, "Finish after recovery");
}
Result completeIntendedRoute(LevelPlaytestBot.Driver driver, long timeoutMillis) {
long deadline = driver.currentTimeMillis() + timeoutMillis;
driver.startLevel(8);
GameView.AutomationSnapshot snapshot = crossPit(driver, deadline);
if (failed(snapshot, deadline, driver)) {
return failure("Cross the pit before the clock expires", snapshot);
}
return finishFromFarSide(driver, deadline, snapshot, "Finish on the original timer");
}
private GameView.AutomationSnapshot crossPit(LevelPlaytestBot.Driver driver, long deadline) {
GameView.AutomationSnapshot snapshot = await(driver, deadline, "Trigger the left timer",
LevelPlaytestBot.Input.right(), GameView.AutomationSnapshot::allDoorsOpen);
if (failed(snapshot, deadline, driver)) return snapshot;
long timerStartedAt = snapshot.elapsedMilliseconds();
snapshot = await(driver, deadline, "Approach the pit",
LevelPlaytestBot.Input.right(), current -> current.playerX() >= 760f);
if (failed(snapshot, deadline, driver)) return snapshot;
snapshot = await(driver, deadline, "Wait for the moving platform",
LevelPlaytestBot.Input.none(),
current -> current.elapsedMilliseconds() >= timerStartedAt + 2_250L);
if (failed(snapshot, deadline, driver)) return snapshot;
driver.setInput(false, true, true);
driver.waitForNextDecision(64L);
driver.setInput(false, true, false);
snapshot = await(driver, deadline, "Land on the moving platform",
LevelPlaytestBot.Input.right(),
current -> current.elapsedMilliseconds() >= timerStartedAt + 3_200L);
if (failed(snapshot, deadline, driver)) return snapshot;
driver.setInput(false, true, true);
driver.waitForNextDecision(64L);
driver.setInput(false, true, false);
snapshot = await(driver, deadline, "Jump from the moving platform to the far floor",
LevelPlaytestBot.Input.right(), current -> current.playerX() >= 1_510f);
if (failed(snapshot, deadline, driver)) return snapshot;
snapshot = awaitFarFloorLandingOnOriginalTimer(driver, deadline);
if (failed(snapshot, deadline, driver)) return snapshot;
return await(driver, deadline, "Move under the relay after landing",
LevelPlaytestBot.Input.right(),
current -> current.playerX() >= 1_600f
&& current.playerY() >= 779f && current.allDoorsOpen());
}
private GameView.AutomationSnapshot awaitFarFloorLandingOnOriginalTimer(
LevelPlaytestBot.Driver driver, long deadline
) {
boolean moveLeft = true;
GameView.AutomationSnapshot snapshot = driver.snapshot();
while (driver.currentTimeMillis() < deadline && snapshot.playerAlive()
&& !(snapshot.playerX() >= 1_510f && snapshot.playerX() < 1_580f
&& snapshot.playerY() >= 779f && snapshot.allDoorsOpen())) {
driver.setInput(moveLeft, !moveLeft, false);
moveLeft = !moveLeft;
driver.waitForNextDecision(DECISION_MILLIS);
snapshot = driver.snapshot();
}
driver.setInput(false, false, false);
return snapshot;
}
private Result finishFromFarSide(LevelPlaytestBot.Driver driver, long deadline,
GameView.AutomationSnapshot snapshot, String step) {
snapshot = await(driver, deadline, "Reach the open-door jump",
LevelPlaytestBot.Input.right(),
current -> current.playerX() >= 1_730f && current.allDoorsOpen());
if (failed(snapshot, deadline, driver)) {
return failure("Reach the open-door jump", snapshot);
}
driver.setInput(false, true, true);
driver.waitForNextDecision(64L);
driver.setInput(false, true, false);
snapshot = await(driver, deadline, step, LevelPlaytestBot.Input.right(),
GameView.AutomationSnapshot::completed);
driver.setInput(false, false, false);
if (!snapshot.completed()) return failure(step, snapshot);
return new Result(true, snapshot.deaths(), step + " completed; " + state(snapshot));
}
private GameView.AutomationSnapshot pulseLeftJumpThenAwait(
LevelPlaytestBot.Driver driver, long deadline, String step,
LevelPlaytestBot.SnapshotCondition completion
) {
driver.setInput(true, false, true);
driver.waitForNextDecision(64L);
driver.setInput(true, false, false);
return await(driver, deadline, step, LevelPlaytestBot.Input.left(), completion);
}
private GameView.AutomationSnapshot await(
LevelPlaytestBot.Driver driver, long deadline, String step,
LevelPlaytestBot.Input input, LevelPlaytestBot.SnapshotCondition completion
) {
GameView.AutomationSnapshot snapshot = driver.snapshot();
while (driver.currentTimeMillis() < deadline && snapshot.playerAlive()
&& !snapshot.completed() && !completion.reached(snapshot)) {
setInput(driver, input);
driver.waitForNextDecision(DECISION_MILLIS);
snapshot = driver.snapshot();
}
if (driver.currentTimeMillis() >= deadline || !snapshot.playerAlive()) {
driver.setInput(false, false, false);
}
return snapshot;
}
private static void setInput(LevelPlaytestBot.Driver driver, LevelPlaytestBot.Input input) {
if (input == LevelPlaytestBot.Input.right()) {
driver.setInput(false, true, false);
} else if (input == LevelPlaytestBot.Input.left()) {
driver.setInput(true, false, false);
} else if (input == LevelPlaytestBot.Input.rightJump()) {
driver.setInput(false, true, true);
} else {
driver.setInput(false, false, false);
}
}
private static boolean failed(GameView.AutomationSnapshot snapshot, long deadline,
LevelPlaytestBot.Driver driver) {
return !snapshot.completed()
&& (!snapshot.playerAlive() || driver.currentTimeMillis() >= deadline);
}
private static Result failure(String step, GameView.AutomationSnapshot snapshot) {
return new Result(false, snapshot.deaths(), step + " failed; " + state(snapshot));
}
private static String state(GameView.AutomationSnapshot snapshot) {
return "x=" + snapshot.playerX()
+ ", y=" + snapshot.playerY()
+ ", alive=" + snapshot.playerAlive()
+ ", doorOpen=" + snapshot.allDoorsOpen()
+ ", completed=" + snapshot.completed()
+ ", elapsedMs=" + snapshot.elapsedMilliseconds()
+ ", deaths=" + snapshot.deaths();
}
}