Стабилизировать ботов при медленном рендеринге
This commit is contained in:
@@ -45,7 +45,9 @@ public final class EarlyLevelScenarios {
|
|||||||
EarlyLevelScenarios::respawnedAtStart)
|
EarlyLevelScenarios::respawnedAtStart)
|
||||||
.step("Approach the completed bridge", 3_000L, LevelPlaytestBot.Input.right(),
|
.step("Approach the completed bridge", 3_000L, LevelPlaytestBot.Input.right(),
|
||||||
snapshot -> snapshot.playerX() >= 470f)
|
snapshot -> snapshot.playerX() >= 470f)
|
||||||
.step("Cross the corpse bridge", 3_000L, LevelPlaytestBot.Input.rightJump(),
|
.step("Land on the far bridge body", 3_000L, LevelPlaytestBot.Input.rightJump(),
|
||||||
|
snapshot -> snapshot.playerOnGround() && snapshot.playerX() >= 800f)
|
||||||
|
.step("Jump from the bridge to safe floor", 3_000L, LevelPlaytestBot.Input.rightJump(),
|
||||||
snapshot -> snapshot.playerX() >= 900f)
|
snapshot -> snapshot.playerX() >= 900f)
|
||||||
.step("Release jump before the final ledge", 2_000L, LevelPlaytestBot.Input.right(),
|
.step("Release jump before the final ledge", 2_000L, LevelPlaytestBot.Input.right(),
|
||||||
snapshot -> snapshot.playerX() >= 1_030f)
|
snapshot -> snapshot.playerX() >= 1_030f)
|
||||||
@@ -65,7 +67,7 @@ public final class EarlyLevelScenarios {
|
|||||||
.step("Wait for the next volunteer", 2_000L, LevelPlaytestBot.Input.none(),
|
.step("Wait for the next volunteer", 2_000L, LevelPlaytestBot.Input.none(),
|
||||||
EarlyLevelScenarios::respawnedAtStart)
|
EarlyLevelScenarios::respawnedAtStart)
|
||||||
.step("Approach the saw", 3_000L, LevelPlaytestBot.Input.right(),
|
.step("Approach the saw", 3_000L, LevelPlaytestBot.Input.right(),
|
||||||
snapshot -> snapshot.playerX() >= 390f)
|
snapshot -> snapshot.playerX() >= 300f)
|
||||||
.step("Jump over the saw and plate body", 3_000L, LevelPlaytestBot.Input.rightJump(),
|
.step("Jump over the saw and plate body", 3_000L, LevelPlaytestBot.Input.rightJump(),
|
||||||
snapshot -> snapshot.playerX() >= 700f)
|
snapshot -> snapshot.playerX() >= 700f)
|
||||||
.step("Reach the relic through the open door", 5_000L, LevelPlaytestBot.Input.right(),
|
.step("Reach the relic through the open door", 5_000L, LevelPlaytestBot.Input.right(),
|
||||||
@@ -76,7 +78,10 @@ public final class EarlyLevelScenarios {
|
|||||||
public static LevelPlaytestBot.Scenario levelFour() {
|
public static LevelPlaytestBot.Scenario levelFour() {
|
||||||
return LevelPlaytestBot.Scenario.builder(4, "Level 4: toggle lift to gallery")
|
return LevelPlaytestBot.Scenario.builder(4, "Level 4: toggle lift to gallery")
|
||||||
.step("Cross the toggle once and activate the lift", 3_000L, LevelPlaytestBot.Input.right(),
|
.step("Cross the toggle once and activate the lift", 3_000L, LevelPlaytestBot.Input.right(),
|
||||||
snapshot -> snapshot.playerX() >= 800f)
|
GameView.AutomationSnapshot::firstMovingPlatformPowered)
|
||||||
|
.step("Approach the powered moving lift", 3_000L, LevelPlaytestBot.Input.right(),
|
||||||
|
snapshot -> snapshot.firstMovingPlatformPowered()
|
||||||
|
&& snapshot.playerX() >= 800f)
|
||||||
.step("Jump onto the moving lift", 3_000L, LevelPlaytestBot.Input.rightJump(),
|
.step("Jump onto the moving lift", 3_000L, LevelPlaytestBot.Input.rightJump(),
|
||||||
snapshot -> snapshot.playerX() >= 1_050f && snapshot.playerY() < 690f)
|
snapshot -> snapshot.playerX() >= 1_050f && snapshot.playerY() < 690f)
|
||||||
.step("Ride the lift to its upper phase", 7_000L, LevelPlaytestBot.Input.none(),
|
.step("Ride the lift to its upper phase", 7_000L, LevelPlaytestBot.Input.none(),
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import java.util.List;
|
|||||||
|
|
||||||
/** Runs black-box acceptance scenarios through the same input and state boundary as touch play. */
|
/** Runs black-box acceptance scenarios through the same input and state boundary as touch play. */
|
||||||
public final class LevelPlaytestBot {
|
public final class LevelPlaytestBot {
|
||||||
|
private static final long MIN_WALL_WATCHDOG_MILLIS = 120_000L;
|
||||||
|
private static final long WALL_WATCHDOG_MULTIPLIER = 10L;
|
||||||
|
|
||||||
public interface Driver {
|
public interface Driver {
|
||||||
void startLevel(int oneBasedLevelNumber);
|
void startLevel(int oneBasedLevelNumber);
|
||||||
void setInput(boolean moveLeft, boolean moveRight, boolean jump);
|
void setInput(boolean moveLeft, boolean moveRight, boolean jump);
|
||||||
@@ -123,22 +126,38 @@ public final class LevelPlaytestBot {
|
|||||||
|
|
||||||
public Result run(Driver driver, Scenario scenario, long timeoutMillis) {
|
public Result run(Driver driver, Scenario scenario, long timeoutMillis) {
|
||||||
driver.startLevel(scenario.levelNumber);
|
driver.startLevel(scenario.levelNumber);
|
||||||
long scenarioStartedAt = driver.currentTimeMillis();
|
long scenarioStartedAtWall = driver.currentTimeMillis();
|
||||||
long stepStartedAt = scenarioStartedAt;
|
long wallWatchdogMillis = wallWatchdogMillis(timeoutMillis);
|
||||||
int stepIndex = 0;
|
int stepIndex = 0;
|
||||||
String lastStep = scenario.steps.isEmpty() ? null : scenario.steps.get(0).name;
|
String lastStep = scenario.steps.isEmpty() ? null : scenario.steps.get(0).name;
|
||||||
boolean doorOpened = false;
|
boolean doorOpened = false;
|
||||||
GameView.AutomationSnapshot last = driver.snapshot();
|
GameView.AutomationSnapshot last = driver.snapshot();
|
||||||
|
long scenarioStartedAtSimulation = last.elapsedMilliseconds();
|
||||||
|
long stepStartedAtSimulation = scenarioStartedAtSimulation;
|
||||||
|
|
||||||
while (driver.currentTimeMillis() - scenarioStartedAt < timeoutMillis) {
|
while (true) {
|
||||||
last = driver.snapshot();
|
last = driver.snapshot();
|
||||||
|
long wallElapsed = elapsed(driver.currentTimeMillis(), scenarioStartedAtWall);
|
||||||
|
long simulationElapsed = elapsed(
|
||||||
|
last.elapsedMilliseconds(), scenarioStartedAtSimulation);
|
||||||
doorOpened |= last.allDoorsOpen();
|
doorOpened |= last.allDoorsOpen();
|
||||||
if (last.completed()) {
|
if (last.completed()) {
|
||||||
driver.setInput(false, false, false);
|
driver.setInput(false, false, false);
|
||||||
return new Result(true, doorOpened, last.deaths(), last.corpseCount(),
|
return new Result(true, doorOpened, last.deaths(), last.corpseCount(),
|
||||||
scenario.name + " completed; step=" + lastStep + "; " + snapshotState(last),
|
scenario.name + " completed; step=" + lastStep + "; "
|
||||||
|
+ timingState(simulationElapsed, wallElapsed) + "; "
|
||||||
|
+ snapshotState(last),
|
||||||
lastStep);
|
lastStep);
|
||||||
}
|
}
|
||||||
|
if (wallElapsed >= wallWatchdogMillis) {
|
||||||
|
driver.setInput(false, false, false);
|
||||||
|
return new Result(false, doorOpened, last.deaths(), last.corpseCount(),
|
||||||
|
scenario.name + " stopped by wall-clock watchdog; step=" + lastStep
|
||||||
|
+ "; " + timingState(simulationElapsed, wallElapsed) + "; "
|
||||||
|
+ snapshotState(last),
|
||||||
|
lastStep);
|
||||||
|
}
|
||||||
|
if (simulationElapsed >= timeoutMillis) break;
|
||||||
if (stepIndex >= scenario.steps.size()) break;
|
if (stepIndex >= scenario.steps.size()) break;
|
||||||
|
|
||||||
Step step = scenario.steps.get(stepIndex);
|
Step step = scenario.steps.get(stepIndex);
|
||||||
@@ -146,13 +165,16 @@ public final class LevelPlaytestBot {
|
|||||||
if (step.completion.reached(last)) {
|
if (step.completion.reached(last)) {
|
||||||
driver.setInput(false, false, false);
|
driver.setInput(false, false, false);
|
||||||
stepIndex++;
|
stepIndex++;
|
||||||
stepStartedAt = driver.currentTimeMillis();
|
stepStartedAtSimulation = last.elapsedMilliseconds();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (driver.currentTimeMillis() - stepStartedAt >= step.timeoutMillis) {
|
if (elapsed(last.elapsedMilliseconds(), stepStartedAtSimulation)
|
||||||
|
>= step.timeoutMillis) {
|
||||||
driver.setInput(false, false, false);
|
driver.setInput(false, false, false);
|
||||||
return new Result(false, doorOpened, last.deaths(), last.corpseCount(),
|
return new Result(false, doorOpened, last.deaths(), last.corpseCount(),
|
||||||
scenario.name + " timed out; step=" + step.name + "; " + snapshotState(last),
|
scenario.name + " timed out in simulation; step=" + step.name
|
||||||
|
+ "; " + timingState(simulationElapsed, wallElapsed) + "; "
|
||||||
|
+ snapshotState(last),
|
||||||
step.name);
|
step.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,11 +183,32 @@ public final class LevelPlaytestBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
driver.setInput(false, false, false);
|
driver.setInput(false, false, false);
|
||||||
|
long wallElapsed = elapsed(driver.currentTimeMillis(), scenarioStartedAtWall);
|
||||||
|
long simulationElapsed = elapsed(
|
||||||
|
last.elapsedMilliseconds(), scenarioStartedAtSimulation);
|
||||||
return new Result(false, doorOpened, last.deaths(), last.corpseCount(),
|
return new Result(false, doorOpened, last.deaths(), last.corpseCount(),
|
||||||
scenario.name + " did not complete; step=" + lastStep + "; " + snapshotState(last),
|
scenario.name + " exhausted its simulation budget; step=" + lastStep
|
||||||
|
+ "; " + timingState(simulationElapsed, wallElapsed) + "; "
|
||||||
|
+ snapshotState(last),
|
||||||
lastStep);
|
lastStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long wallWatchdogMillis(long simulationBudgetMillis) {
|
||||||
|
if (simulationBudgetMillis > Long.MAX_VALUE / WALL_WATCHDOG_MULTIPLIER) {
|
||||||
|
return Long.MAX_VALUE;
|
||||||
|
}
|
||||||
|
return Math.max(MIN_WALL_WATCHDOG_MILLIS,
|
||||||
|
simulationBudgetMillis * WALL_WATCHDOG_MULTIPLIER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long elapsed(long current, long startedAt) {
|
||||||
|
return Math.max(0L, current - startedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String timingState(long simulationElapsed, long wallElapsed) {
|
||||||
|
return "simulationElapsed=" + simulationElapsed + ", wallElapsed=" + wallElapsed;
|
||||||
|
}
|
||||||
|
|
||||||
private static String snapshotState(GameView.AutomationSnapshot snapshot) {
|
private static String snapshotState(GameView.AutomationSnapshot snapshot) {
|
||||||
return "x=" + snapshot.playerX()
|
return "x=" + snapshot.playerX()
|
||||||
+ ", y=" + snapshot.playerY()
|
+ ", y=" + snapshot.playerY()
|
||||||
|
|||||||
@@ -68,6 +68,41 @@ public final class LevelPlaytestBotTest {
|
|||||||
assertTrue(result.diagnostic(), result.diagnostic().contains("allDoorsOpen=false"));
|
assertTrue(result.diagnostic(), result.diagnostic().contains("allDoorsOpen=false"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void slowRenderingUsesSimulationElapsedBudgetsInsteadOfHostTime() {
|
||||||
|
SlowRenderingDriver driver = new SlowRenderingDriver(true);
|
||||||
|
LevelPlaytestBot.Scenario scenario = LevelPlaytestBot.Scenario
|
||||||
|
.builder(1, "Slow rendering")
|
||||||
|
.step("Reach simulated checkpoint", 200L, LevelPlaytestBot.Input.right(),
|
||||||
|
GameView.AutomationSnapshot::completed)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(driver, scenario, 500L);
|
||||||
|
|
||||||
|
assertTrue(result.diagnostic(), result.completed());
|
||||||
|
assertTrue("The host clock must be allowed to outrun simulated gameplay",
|
||||||
|
driver.wallMillis > 500L);
|
||||||
|
assertEquals(96L, driver.simulationMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void frozenSimulationStopsAtWallClockWatchdogWithDiagnostic() {
|
||||||
|
SlowRenderingDriver driver = new SlowRenderingDriver(false);
|
||||||
|
LevelPlaytestBot.Scenario scenario = LevelPlaytestBot.Scenario
|
||||||
|
.builder(1, "Frozen rendering")
|
||||||
|
.step("Wait for simulation", 1_000L, LevelPlaytestBot.Input.right(),
|
||||||
|
GameView.AutomationSnapshot::completed)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(driver, scenario, 5_000L);
|
||||||
|
|
||||||
|
assertFalse(result.completed());
|
||||||
|
assertEquals("Wait for simulation", result.lastStep());
|
||||||
|
assertTrue(result.diagnostic(), result.diagnostic().contains("wall-clock watchdog"));
|
||||||
|
assertTrue(result.diagnostic(), result.diagnostic().contains("simulationElapsed=0"));
|
||||||
|
assertTrue(result.diagnostic(), result.diagnostic().contains("wallElapsed=120000"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void levelFiveUsesTheGenericNamedScenario() {
|
public void levelFiveUsesTheGenericNamedScenario() {
|
||||||
LevelFiveDriver driver = new LevelFiveDriver();
|
LevelFiveDriver driver = new LevelFiveDriver();
|
||||||
@@ -160,6 +195,50 @@ public final class LevelPlaytestBotTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class SlowRenderingDriver implements LevelPlaytestBot.Driver {
|
||||||
|
private final boolean advancesSimulation;
|
||||||
|
private long wallMillis;
|
||||||
|
private long simulationMillis;
|
||||||
|
private boolean movingRight;
|
||||||
|
private boolean completed;
|
||||||
|
|
||||||
|
SlowRenderingDriver(boolean advancesSimulation) {
|
||||||
|
this.advancesSimulation = advancesSimulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startLevel(int oneBasedLevelNumber) {
|
||||||
|
wallMillis = 0L;
|
||||||
|
simulationMillis = 0L;
|
||||||
|
movingRight = false;
|
||||||
|
completed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setInput(boolean moveLeft, boolean moveRight, boolean jump) {
|
||||||
|
movingRight = moveRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GameView.AutomationSnapshot snapshot() {
|
||||||
|
return new GameView.AutomationSnapshot(
|
||||||
|
completed, true, false, false, movingRight,
|
||||||
|
1, simulationMillis, 130f, 760f, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void waitForNextDecision(long millis) {
|
||||||
|
wallMillis += advancesSimulation ? 1_000L : 60_000L;
|
||||||
|
if (advancesSimulation) simulationMillis += millis;
|
||||||
|
if (movingRight && simulationMillis >= 96L) completed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long currentTimeMillis() {
|
||||||
|
return wallMillis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class LevelFiveDriver implements LevelPlaytestBot.Driver {
|
private static final class LevelFiveDriver implements LevelPlaytestBot.Driver {
|
||||||
private int startedLevel;
|
private int startedLevel;
|
||||||
private long nowMillis;
|
private long nowMillis;
|
||||||
|
|||||||
Reference in New Issue
Block a user