Стабилизировать ботов при медленном рендеринге

This commit is contained in:
2026-08-18 17:20:04 +03:00
parent 88a1ba9b34
commit 650bfb3533
3 changed files with 138 additions and 11 deletions
@@ -6,6 +6,9 @@ import java.util.List;
/** Runs black-box acceptance scenarios through the same input and state boundary as touch play. */
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 {
void startLevel(int oneBasedLevelNumber);
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) {
driver.startLevel(scenario.levelNumber);
long scenarioStartedAt = driver.currentTimeMillis();
long stepStartedAt = scenarioStartedAt;
long scenarioStartedAtWall = driver.currentTimeMillis();
long wallWatchdogMillis = wallWatchdogMillis(timeoutMillis);
int stepIndex = 0;
String lastStep = scenario.steps.isEmpty() ? null : scenario.steps.get(0).name;
boolean doorOpened = false;
GameView.AutomationSnapshot last = driver.snapshot();
long scenarioStartedAtSimulation = last.elapsedMilliseconds();
long stepStartedAtSimulation = scenarioStartedAtSimulation;
while (driver.currentTimeMillis() - scenarioStartedAt < timeoutMillis) {
while (true) {
last = driver.snapshot();
long wallElapsed = elapsed(driver.currentTimeMillis(), scenarioStartedAtWall);
long simulationElapsed = elapsed(
last.elapsedMilliseconds(), scenarioStartedAtSimulation);
doorOpened |= last.allDoorsOpen();
if (last.completed()) {
driver.setInput(false, false, false);
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);
}
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;
Step step = scenario.steps.get(stepIndex);
@@ -146,13 +165,16 @@ public final class LevelPlaytestBot {
if (step.completion.reached(last)) {
driver.setInput(false, false, false);
stepIndex++;
stepStartedAt = driver.currentTimeMillis();
stepStartedAtSimulation = last.elapsedMilliseconds();
continue;
}
if (driver.currentTimeMillis() - stepStartedAt >= step.timeoutMillis) {
if (elapsed(last.elapsedMilliseconds(), stepStartedAtSimulation)
>= step.timeoutMillis) {
driver.setInput(false, false, false);
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);
}
@@ -161,11 +183,32 @@ public final class LevelPlaytestBot {
}
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(),
scenario.name + " did not complete; step=" + lastStep + "; " + snapshotState(last),
scenario.name + " exhausted its simulation budget; step=" + lastStep
+ "; " + timingState(simulationElapsed, wallElapsed) + "; "
+ snapshotState(last),
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) {
return "x=" + snapshot.playerX()
+ ", y=" + snapshot.playerY()