Добавить production-состояния тел

This commit is contained in:
2026-08-19 06:34:40 +03:00
parent dc2ebcbf8c
commit 19bb64edfb
21 changed files with 580 additions and 31 deletions
@@ -0,0 +1,40 @@
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 CorpseStateIntegrationTest {
@Test
public void crusherAndFireDeathsPublishDistinctPuzzleBodyStates() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
activity.onActivity(current -> {
GameView view = current.gameViewForAutomation();
view.enableDeterministicAutomationClock();
view.startLevelForAutomation(11);
view.setAutomationInput(false, true, false);
view.advanceDeterministicAutomation(2_000L);
GameView.AutomationSnapshot crushed = view.automationSnapshot();
assertTrue(crushed.firstCorpseCrushed());
assertFalse(crushed.firstCorpseBurned());
assertTrue(crushed.firstCorpseActivatesPressure());
assertTrue(crushed.firstCorpseWidth() > crushed.firstCorpseHeight());
view.startLevelForAutomation(12);
view.setAutomationInput(false, true, false);
view.advanceDeterministicAutomation(2_000L);
GameView.AutomationSnapshot burned = view.automationSnapshot();
assertTrue(burned.firstCorpseBurned());
assertFalse(burned.firstCorpseCrushed());
assertFalse(burned.firstCorpseActivatesPressure());
});
}
}
}
@@ -0,0 +1,81 @@
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 CorpseStatePlaythroughTest {
private static final int CONSECUTIVE_RUNS = 3;
@Test public void crushedBodyIsARequiredLowStepThreeTimes() {
prove(CorpseStateScenarios.levelFifteen(), 45_000L);
}
@Test public void burnedBodyLeavesThePressureBridgeDormantThreeTimes() {
prove(CorpseStateScenarios.levelSixteen(), 45_000L);
}
private static void prove(LevelPlaytestBot.Scenario scenario, long budget) {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
AndroidDriver driver = AndroidDriver.from(activity);
for (int run = 1; run <= CONSECUTIVE_RUNS; run++) {
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(driver, scenario, budget);
assertTrue("run=" + run + "; " + result.diagnostic(), result.completed());
assertEquals("run=" + run + "; route uses exactly one transformed body",
1, result.deaths());
assertEquals("run=" + run + "; transformed body must remain",
1, result.corpseCount());
}
}
}
private static final class AndroidDriver implements LevelPlaytestBot.Driver {
private final Instrumentation instrumentation;
private final GameView gameView;
private AndroidDriver(Instrumentation instrumentation, GameView gameView) {
this.instrumentation = instrumentation;
this.gameView = gameView;
}
static AndroidDriver from(ActivityScenario<MainActivity> activity) {
GameView[] view = new GameView[1];
activity.onActivity(current -> view[0] = current.gameViewForAutomation());
return new AndroidDriver(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[] snapshot = new GameView.AutomationSnapshot[1];
instrumentation.runOnMainSync(() -> snapshot[0] = gameView.automationSnapshot());
return snapshot[0];
}
@Override public void waitForNextDecision(long millis) {
try {
Thread.sleep(Math.max(32L, millis));
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new AssertionError("Corpse-state bot interrupted", interrupted);
}
}
}
}
@@ -0,0 +1,93 @@
package com.projectsacrifice.game;
/** Intended routes whose solution depends on the transformed corpse state. */
final class CorpseStateScenarios {
private CorpseStateScenarios() {}
static LevelPlaytestBot.Scenario levelFifteen() {
return LevelPlaytestBot.Scenario.builder(15, "Level 15 - crushed low step")
.step("Create one crushed body", 4_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1
&& snapshot.firstCorpseCrushed()
&& snapshot.firstCorpseWidth() > snapshot.firstCorpseHeight())
.step("Wait for respawn", 3_000L, LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
.step("Approach the press step", 2_000L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1,
LevelPlaytestBot.groundedAliveAtOrBeyond(520f)))
.step("Observe a fresh press cycle", 4_000L,
LevelPlaytestBot.Input.none(),
GameView.AutomationSnapshot::firstHazardActive)
.step("Wait for the full rest window", 4_000L,
LevelPlaytestBot.Input.none(),
snapshot -> !snapshot.firstHazardActive())
.step("Land on the crushed low step", 2_000L,
LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(1,
snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerX() >= 690f
&& snapshot.playerY() <= 757f))
.step("Jump from the step to the ledge", 2_500L,
LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(1,
snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerX() >= 850f
&& snapshot.playerY() <= 621f))
.step("Reach the relic", 6_000L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1,
GameView.AutomationSnapshot::completed))
.build();
}
static LevelPlaytestBot.Scenario levelSixteen() {
return LevelPlaytestBot.Scenario.builder(16, "Level 16 - burned pressure immunity")
.step("Create one burned body on the plate", 4_000L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1
&& snapshot.firstCorpseBurned()
&& !snapshot.firstCorpseActivatesPressure())
.step("Prove the burned body leaves the bridge dormant", 2_000L,
LevelPlaytestBot.Input.none(),
snapshot -> !snapshot.firstMovingPlatformPowered()
&& snapshot.firstMovingPlatformY() >= 800f)
.step("Wait for respawn", 3_000L, LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() < 250f)
.step("Approach the flame before touching its plate", 2_000L,
LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1,
LevelPlaytestBot.groundedAliveAtOrBeyond(535f)))
.step("Take off over flame and plate", 1_000L,
LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(1,
snapshot -> snapshot.playerAlive()
&& !snapshot.playerOnGround()
&& snapshot.playerY() <= 700f))
.step("Clear the burned plate", 2_000L,
LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1,
snapshot -> snapshot.playerAlive()
&& snapshot.playerX() >= 850f))
.step("Approach the dormant bridge", 1_500L,
LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 900f
&& !snapshot.firstMovingPlatformPowered())
.step("Board the dormant bridge", 2_000L,
LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(1,
GameView.AutomationSnapshot::playerOnMovingPlatform))
.step("Cross to the far floor", 3_000L,
LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1,
snapshot -> snapshot.playerAlive()
&& snapshot.playerX() >= 1_450f))
.step("Reach the relic", 5_000L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1,
GameView.AutomationSnapshot::completed))
.build();
}
}
@@ -36,6 +36,7 @@ public final class LevelPaginationTest {
List<String> visible = view.visibleLocalizedTextForAutomation();
assertEquals(1, view.levelPageIndexForAutomation());
assertTrue(visible.contains("Steam Charge"));
assertTrue(visible.contains("Ashen Counterweight"));
assertFalse(visible.contains("The Volunteer"));
assertTrue(visible.contains("Page 2 of 2"));
});