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 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 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 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 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()); } } } @Test public void masteryProcessionSurvivesThrottledFinalTraversalDecisions() { try (ActivityScenario activity = ActivityScenario.launch(MainActivity.class)) { GameDriver driver = GameDriver.withFinalTraversalDelay(activity, 96L); LevelPlaytestBot.Result result = new LevelPlaytestBot().run( driver, LateLevelScenarios.levelTen(), 65_000L ); assertTrue(result.diagnostic(), result.completed()); assertEquals("throttling must not add accidental volunteers", 3, result.deaths()); assertEquals("all three intended bodies must remain", 3, result.corpseCount()); assertTrue("the conductive relay must still open the final door", result.doorOpened()); } } @Test public void sawDeliveredBodyKeepsLevelTenLiftPoweredAfterSettling() { try (ActivityScenario activity = ActivityScenario.launch(MainActivity.class)) { GameDriver driver = GameDriver.from(activity); driver.startLevel(10); await(driver, 4_000L, false, true, false, snapshot -> snapshot.deaths() == 1 && snapshot.corpseCount() == 1); GameView.AutomationSnapshot engaged = await(driver, 5_000L, false, false, false, GameView.AutomationSnapshot::firstMovingPlatformPowered); driver.setInput(false, false, false); driver.waitForNextDecision(750L); GameView.AutomationSnapshot settled = driver.snapshot(); assertEquals("The body must settle when it engages the lift plate instead of sliding", engaged.firstCorpseX(), settled.firstCorpseX(), 2f); driver.waitForNextDecision(6_000L); settled = driver.snapshot(); assertTrue("The saw-delivered body slid off the lift plate after settling; " + "platformY=" + settled.firstMovingPlatformY() + ", deaths=" + settled.deaths() + ", corpses=" + settled.corpseCount(), settled.firstMovingPlatformPowered()); assertEquals("The stability wait must not consume another volunteer", 1, settled.deaths()); assertEquals("The delivered body must remain in the level", 1, settled.corpseCount()); } } private static GameView.AutomationSnapshot await( GameDriver driver, long timeoutMillis, boolean moveLeft, boolean moveRight, boolean jump, LevelPlaytestBot.SnapshotCondition condition ) { long deadline = System.currentTimeMillis() + timeoutMillis; GameView.AutomationSnapshot snapshot = driver.snapshot(); while (System.currentTimeMillis() < deadline) { snapshot = driver.snapshot(); if (condition.reached(snapshot)) { driver.setInput(false, false, false); return snapshot; } driver.setInput(moveLeft, moveRight, jump); driver.waitForNextDecision(32L); } throw new AssertionError("Timed out waiting for level 10 lift body; x=" + snapshot.playerX() + ", y=" + snapshot.playerY() + ", deaths=" + snapshot.deaths() + ", corpses=" + snapshot.corpseCount() + ", platformY=" + snapshot.firstMovingPlatformY()); } private static final class GameDriver implements LevelPlaytestBot.Driver { private final Instrumentation instrumentation; private final GameView gameView; private final long minimumDecisionMillis; private final boolean throttleFinalTraversalOnly; private GameView.AutomationSnapshot lastSnapshot; private GameDriver(Instrumentation instrumentation, GameView gameView, long minimumDecisionMillis, boolean throttleFinalTraversalOnly) { this.instrumentation = instrumentation; this.gameView = gameView; this.minimumDecisionMillis = minimumDecisionMillis; this.throttleFinalTraversalOnly = throttleFinalTraversalOnly; } static GameDriver from(ActivityScenario activity) { return create(activity, 32L, false); } static GameDriver withFinalTraversalDelay(ActivityScenario activity, long minimumDecisionMillis) { return create(activity, minimumDecisionMillis, true); } private static GameDriver create(ActivityScenario activity, long minimumDecisionMillis, boolean throttleFinalTraversalOnly) { GameView[] view = new GameView[1]; activity.onActivity(current -> view[0] = current.gameViewForAutomation()); return new GameDriver(InstrumentationRegistry.getInstrumentation(), view[0], minimumDecisionMillis, throttleFinalTraversalOnly); } @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()); lastSnapshot = value[0]; return lastSnapshot; } @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); } } } }