Добавить production-состояния тел
This commit is contained in:
@@ -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"));
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ public final class CorpseManager {
|
||||
public enum Transformation {
|
||||
PINNED,
|
||||
FROZEN,
|
||||
CONDUCTIVE
|
||||
CONDUCTIVE,
|
||||
BURNED,
|
||||
CRUSHED
|
||||
}
|
||||
|
||||
public static final class Body {
|
||||
@@ -24,6 +26,8 @@ public final class CorpseManager {
|
||||
boolean frozen;
|
||||
boolean conductive;
|
||||
boolean pinned;
|
||||
boolean burned;
|
||||
boolean crushed;
|
||||
boolean grounded;
|
||||
int supportIndex = -1;
|
||||
|
||||
@@ -45,6 +49,9 @@ public final class CorpseManager {
|
||||
public boolean frozen() { return frozen; }
|
||||
public boolean conductive() { return conductive; }
|
||||
public boolean pinned() { return pinned; }
|
||||
public boolean burned() { return burned; }
|
||||
public boolean crushed() { return crushed; }
|
||||
public boolean activatesPressure() { return !burned; }
|
||||
public boolean grounded() { return grounded; }
|
||||
public int supportIndex() { return supportIndex; }
|
||||
|
||||
@@ -148,6 +155,12 @@ public final class CorpseManager {
|
||||
case CONDUCTIVE:
|
||||
body.conductive = true;
|
||||
break;
|
||||
case BURNED:
|
||||
body.burned = true;
|
||||
break;
|
||||
case CRUSHED:
|
||||
body.crushed = true;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public final class GameSessionSnapshot {
|
||||
|
||||
public static final class BodyData {
|
||||
private final float x, y, width, height, velocityX, velocityY;
|
||||
private final boolean frozen, conductive, pinned, grounded;
|
||||
private final boolean frozen, conductive, pinned, grounded, crushed, burned;
|
||||
private final int supportIndex;
|
||||
private final String deathType;
|
||||
|
||||
@@ -51,6 +51,15 @@ public final class GameSessionSnapshot {
|
||||
float velocityX, float velocityY, boolean frozen,
|
||||
boolean conductive, boolean pinned, boolean grounded,
|
||||
int supportIndex, String deathType) {
|
||||
this(x, y, width, height, velocityX, velocityY, frozen, conductive,
|
||||
pinned, grounded, false, false, supportIndex, deathType);
|
||||
}
|
||||
|
||||
public BodyData(float x, float y, float width, float height,
|
||||
float velocityX, float velocityY, boolean frozen,
|
||||
boolean conductive, boolean pinned, boolean grounded,
|
||||
boolean crushed, boolean burned,
|
||||
int supportIndex, String deathType) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
@@ -61,6 +70,8 @@ public final class GameSessionSnapshot {
|
||||
this.conductive = conductive;
|
||||
this.pinned = pinned;
|
||||
this.grounded = grounded;
|
||||
this.crushed = crushed;
|
||||
this.burned = burned;
|
||||
this.supportIndex = supportIndex;
|
||||
this.deathType = deathType == null ? "SPIKE" : deathType;
|
||||
}
|
||||
@@ -75,6 +86,8 @@ public final class GameSessionSnapshot {
|
||||
public boolean conductive() { return conductive; }
|
||||
public boolean pinned() { return pinned; }
|
||||
public boolean grounded() { return grounded; }
|
||||
public boolean crushed() { return crushed; }
|
||||
public boolean burned() { return burned; }
|
||||
public int supportIndex() { return supportIndex; }
|
||||
public String deathType() { return deathType; }
|
||||
}
|
||||
|
||||
@@ -75,6 +75,11 @@ public final class GameView extends View implements Runnable {
|
||||
private final boolean firstHazardActive;
|
||||
private final float firstHazardX;
|
||||
private final float firstHazardY;
|
||||
private final boolean firstCorpseBurned;
|
||||
private final boolean firstCorpseCrushed;
|
||||
private final boolean firstCorpseActivatesPressure;
|
||||
private final float firstCorpseWidth;
|
||||
private final float firstCorpseHeight;
|
||||
|
||||
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
|
||||
boolean visiblyPaused, boolean inputActive, int levelNumber,
|
||||
@@ -163,6 +168,31 @@ public final class GameView extends View implements Runnable {
|
||||
boolean playerOnGround, float playerVelocityX,
|
||||
float playerVelocityY, boolean firstHazardActive,
|
||||
float firstHazardX, float firstHazardY) {
|
||||
this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber,
|
||||
elapsedMilliseconds, playerX, playerY, deaths, corpseCount,
|
||||
firstMovingPlatformX, firstMovingPlatformY, playerOnMovingPlatform,
|
||||
firstMovingPlatformPowered, firstMovingPlatformMovingRight,
|
||||
firstMovingPlatformMovingDown, firstCorpseX, playerOnGround,
|
||||
playerVelocityX, playerVelocityY, firstHazardActive,
|
||||
firstHazardX, firstHazardY, false, false, false,
|
||||
Float.NaN, Float.NaN);
|
||||
}
|
||||
|
||||
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
|
||||
boolean visiblyPaused, boolean inputActive, int levelNumber,
|
||||
long elapsedMilliseconds,
|
||||
float playerX, float playerY, int deaths, int corpseCount,
|
||||
float firstMovingPlatformX, float firstMovingPlatformY,
|
||||
boolean playerOnMovingPlatform,
|
||||
boolean firstMovingPlatformPowered,
|
||||
boolean firstMovingPlatformMovingRight,
|
||||
boolean firstMovingPlatformMovingDown, float firstCorpseX,
|
||||
boolean playerOnGround, float playerVelocityX,
|
||||
float playerVelocityY, boolean firstHazardActive,
|
||||
float firstHazardX, float firstHazardY,
|
||||
boolean firstCorpseBurned, boolean firstCorpseCrushed,
|
||||
boolean firstCorpseActivatesPressure,
|
||||
float firstCorpseWidth, float firstCorpseHeight) {
|
||||
this.completed = completed;
|
||||
this.playerAlive = playerAlive;
|
||||
this.allDoorsOpen = allDoorsOpen;
|
||||
@@ -187,6 +217,11 @@ public final class GameView extends View implements Runnable {
|
||||
this.firstHazardActive = firstHazardActive;
|
||||
this.firstHazardX = firstHazardX;
|
||||
this.firstHazardY = firstHazardY;
|
||||
this.firstCorpseBurned = firstCorpseBurned;
|
||||
this.firstCorpseCrushed = firstCorpseCrushed;
|
||||
this.firstCorpseActivatesPressure = firstCorpseActivatesPressure;
|
||||
this.firstCorpseWidth = firstCorpseWidth;
|
||||
this.firstCorpseHeight = firstCorpseHeight;
|
||||
}
|
||||
|
||||
public boolean completed() { return completed; }
|
||||
@@ -213,6 +248,13 @@ public final class GameView extends View implements Runnable {
|
||||
public boolean firstHazardActive() { return firstHazardActive; }
|
||||
public float firstHazardX() { return firstHazardX; }
|
||||
public float firstHazardY() { return firstHazardY; }
|
||||
public boolean firstCorpseBurned() { return firstCorpseBurned; }
|
||||
public boolean firstCorpseCrushed() { return firstCorpseCrushed; }
|
||||
public boolean firstCorpseActivatesPressure() {
|
||||
return firstCorpseActivatesPressure;
|
||||
}
|
||||
public float firstCorpseWidth() { return firstCorpseWidth; }
|
||||
public float firstCorpseHeight() { return firstCorpseHeight; }
|
||||
}
|
||||
|
||||
private static final class Player {
|
||||
@@ -593,6 +635,7 @@ public final class GameView extends View implements Runnable {
|
||||
int mass = 0;
|
||||
if (player.alive && intersects(player.x, player.y, PLAYER_WIDTH, PLAYER_HEIGHT, item.box)) mass++;
|
||||
for (Body corpse : corpses) {
|
||||
if (!corpse.activatesPressure()) continue;
|
||||
if (intersects(corpse.x, corpse.y, corpse.width, corpse.height, item.box)) mass++;
|
||||
}
|
||||
return mass >= item.requiredMass;
|
||||
@@ -868,6 +911,16 @@ public final class GameView extends View implements Runnable {
|
||||
corpse.vx = corpse.vy = 0f;
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.CONDUCTIVE);
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||
} else if (type == DeathType.FIRE) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.BURNED);
|
||||
} else if (type == DeathType.CRUSH && source != null) {
|
||||
corpse.width = 140f;
|
||||
corpse.height = 24f;
|
||||
corpse.x = source.box.x - 10f;
|
||||
corpse.y = source.box.bottom() - corpse.height;
|
||||
corpse.vx = corpse.vy = 0f;
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.CRUSHED);
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1305,6 +1358,19 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
|
||||
private void drawCorpse(Canvas canvas, Body corpse) {
|
||||
if (corpse.crushed) {
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE : Color.rgb(126, 93, 58));
|
||||
RectF slab = corpseRect(corpse);
|
||||
canvas.drawRoundRect(slab, 8f, 8f, paint);
|
||||
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.BLACK : Color.rgb(226, 177, 82));
|
||||
stroke.setStrokeWidth(5f);
|
||||
canvas.drawLine(slab.left + 10f, slab.centerY(),
|
||||
slab.right - 10f, slab.centerY(), stroke);
|
||||
drawWorldLabel(canvas, "CRUSHED", slab.centerX(), slab.top - 16f);
|
||||
return;
|
||||
}
|
||||
canvas.save();
|
||||
float cx = corpse.x + corpse.width * 0.5f;
|
||||
float cy = corpse.y + corpse.height * 0.5f;
|
||||
@@ -1319,6 +1385,17 @@ public final class GameView extends View implements Runnable {
|
||||
levelElapsed * 1.6f, 1, 0.82f,
|
||||
accessibilitySettings.highContrastEnabled());
|
||||
canvas.restore();
|
||||
if (corpse.burned) {
|
||||
RectF charred = corpseRect(corpse);
|
||||
paint.setColor(Color.argb(145, 28, 24, 29));
|
||||
canvas.drawRoundRect(charred, 12f, 12f, paint);
|
||||
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE : Color.rgb(255, 138, 54));
|
||||
stroke.setStrokeWidth(5f);
|
||||
canvas.drawLine(charred.left + 10f, charred.bottom - 10f,
|
||||
charred.right - 10f, charred.top + 10f, stroke);
|
||||
drawWorldLabel(canvas, "BURNED", charred.centerX(), charred.top - 16f);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawHud(Canvas canvas) {
|
||||
@@ -2494,7 +2571,8 @@ public final class GameView extends View implements Runnable {
|
||||
builder.addBody(new GameSessionSnapshot.BodyData(
|
||||
corpse.x, corpse.y, corpse.width, corpse.height,
|
||||
corpse.vx, corpse.vy, corpse.frozen, corpse.conductive,
|
||||
corpse.pinned, corpse.grounded, corpse.supportIndex,
|
||||
corpse.pinned, corpse.grounded, corpse.crushed, corpse.burned,
|
||||
corpse.supportIndex,
|
||||
corpse.deathType()));
|
||||
}
|
||||
ArrayList<Float> motionTimes = new ArrayList<>(platformStates.size());
|
||||
@@ -2579,6 +2657,12 @@ public final class GameView extends View implements Runnable {
|
||||
if (savedBody.pinned()) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.PINNED);
|
||||
}
|
||||
if (savedBody.crushed()) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.CRUSHED);
|
||||
}
|
||||
if (savedBody.burned()) {
|
||||
corpseManager.transform(corpse, CorpseManager.Transformation.BURNED);
|
||||
}
|
||||
}
|
||||
updateSignals(0f, false);
|
||||
|
||||
@@ -2636,6 +2720,7 @@ public final class GameView extends View implements Runnable {
|
||||
firstHazardX = firstHazardBox.x;
|
||||
firstHazardY = firstHazardBox.y;
|
||||
}
|
||||
Body firstCorpse = corpses.isEmpty() ? null : corpses.get(0);
|
||||
return new AutomationSnapshot(
|
||||
screen == Screen.COMPLETE,
|
||||
player.alive,
|
||||
@@ -2660,7 +2745,12 @@ public final class GameView extends View implements Runnable {
|
||||
player.vy,
|
||||
firstHazardActive,
|
||||
firstHazardX,
|
||||
firstHazardY
|
||||
firstHazardY,
|
||||
firstCorpse != null && firstCorpse.burned(),
|
||||
firstCorpse != null && firstCorpse.crushed(),
|
||||
firstCorpse != null && firstCorpse.activatesPressure(),
|
||||
firstCorpse == null ? Float.NaN : firstCorpse.width(),
|
||||
firstCorpse == null ? Float.NaN : firstCorpse.height()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public final class VersionedSessionCodec {
|
||||
private static final int MAX_BODIES = 16;
|
||||
private static final int MAX_PLATFORMS = 64;
|
||||
private static final int MAX_MAP_ENTRIES = 128;
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
public static final int CURRENT_VERSION = 3;
|
||||
|
||||
public enum Status { RESTORED, MIGRATED, UNSUPPORTED, CORRUPT }
|
||||
|
||||
@@ -41,6 +41,10 @@ public final class VersionedSessionCodec {
|
||||
return encodeWithVersion(snapshot, 1);
|
||||
}
|
||||
|
||||
static byte[] encodeLegacyV2(GameSessionSnapshot snapshot) {
|
||||
return encodeWithVersion(snapshot, 2);
|
||||
}
|
||||
|
||||
static byte[] encodeVersionForTest(int version) {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
@@ -75,7 +79,7 @@ public final class VersionedSessionCodec {
|
||||
.player(readPlayer(input));
|
||||
|
||||
int bodyCount = checkedCount(input.readInt(), MAX_BODIES);
|
||||
for (int i = 0; i < bodyCount; i++) builder.addBody(readBody(input));
|
||||
for (int i = 0; i < bodyCount; i++) builder.addBody(readBody(input, version));
|
||||
|
||||
int platformCount = checkedCount(input.readInt(), MAX_PLATFORMS);
|
||||
ArrayList<Float> motionTimes = new ArrayList<>(platformCount);
|
||||
@@ -108,7 +112,9 @@ public final class VersionedSessionCodec {
|
||||
output.writeFloat(snapshot.respawnSeconds());
|
||||
writePlayer(output, snapshot.player());
|
||||
output.writeInt(snapshot.bodies().size());
|
||||
for (GameSessionSnapshot.BodyData body : snapshot.bodies()) writeBody(output, body);
|
||||
for (GameSessionSnapshot.BodyData body : snapshot.bodies()) {
|
||||
writeBody(output, body, version);
|
||||
}
|
||||
output.writeInt(snapshot.platformMotionTimes().size());
|
||||
for (Float time : snapshot.platformMotionTimes()) output.writeFloat(time);
|
||||
writeBooleanMap(output, snapshot.toggleSignals());
|
||||
@@ -145,7 +151,8 @@ public final class VersionedSessionCodec {
|
||||
input.readInt(), input.readInt());
|
||||
}
|
||||
|
||||
private static void writeBody(DataOutputStream output, GameSessionSnapshot.BodyData body)
|
||||
private static void writeBody(DataOutputStream output, GameSessionSnapshot.BodyData body,
|
||||
int version)
|
||||
throws IOException {
|
||||
output.writeFloat(body.x());
|
||||
output.writeFloat(body.y());
|
||||
@@ -157,11 +164,15 @@ public final class VersionedSessionCodec {
|
||||
output.writeBoolean(body.conductive());
|
||||
output.writeBoolean(body.pinned());
|
||||
output.writeBoolean(body.grounded());
|
||||
if (version >= 3) {
|
||||
output.writeBoolean(body.crushed());
|
||||
output.writeBoolean(body.burned());
|
||||
}
|
||||
output.writeInt(body.supportIndex());
|
||||
output.writeUTF(body.deathType());
|
||||
}
|
||||
|
||||
private static GameSessionSnapshot.BodyData readBody(DataInputStream input)
|
||||
private static GameSessionSnapshot.BodyData readBody(DataInputStream input, int version)
|
||||
throws IOException {
|
||||
float x = finite(input.readFloat());
|
||||
float y = finite(input.readFloat());
|
||||
@@ -173,12 +184,14 @@ public final class VersionedSessionCodec {
|
||||
boolean conductive = input.readBoolean();
|
||||
boolean pinned = input.readBoolean();
|
||||
boolean grounded = input.readBoolean();
|
||||
boolean crushed = version >= 3 && input.readBoolean();
|
||||
boolean burned = version >= 3 && input.readBoolean();
|
||||
int support = input.readInt();
|
||||
String deathType = input.readUTF();
|
||||
if (deathType.length() > 24) throw new IllegalArgumentException("Death type too long");
|
||||
return new GameSessionSnapshot.BodyData(x, y, width, height,
|
||||
velocityX, velocityY, frozen, conductive, pinned, grounded,
|
||||
support, deathType);
|
||||
crushed, burned, support, deathType);
|
||||
}
|
||||
|
||||
private static void writeBooleanMap(DataOutputStream output, Map<String, Boolean> values)
|
||||
|
||||
@@ -68,4 +68,34 @@ public final class CorpseManagerTest {
|
||||
CorpseManager.Body foreign = new CorpseManager(1).register("SAW", 0f, 0f);
|
||||
assertFalse(manager.transform(foreign, CorpseManager.Transformation.FROZEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void burnedBodyRemainsPhysicalButCannotActivatePressureMechanisms() {
|
||||
CorpseManager manager = new CorpseManager(2);
|
||||
CorpseManager.Body body = manager.register("FIRE", 10f, 20f);
|
||||
|
||||
assertTrue(manager.transform(body, CorpseManager.Transformation.BURNED));
|
||||
|
||||
assertTrue(body.burned());
|
||||
assertFalse(body.crushed());
|
||||
assertFalse(body.activatesPressure());
|
||||
assertTrue(manager.bodies().contains(body));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void crushedBodyKeepsOtherTransformationsAndExposesItsLowProfile() {
|
||||
CorpseManager manager = new CorpseManager(2);
|
||||
CorpseManager.Body body = manager.register("CRUSH", 10f, 20f)
|
||||
.size(140f, 24f);
|
||||
|
||||
assertTrue(manager.transform(body, CorpseManager.Transformation.PINNED));
|
||||
assertTrue(manager.transform(body, CorpseManager.Transformation.CRUSHED));
|
||||
|
||||
assertTrue(body.crushed());
|
||||
assertFalse(body.burned());
|
||||
assertTrue(body.pinned());
|
||||
assertTrue(body.activatesPressure());
|
||||
assertEquals(140f, body.width(), 0f);
|
||||
assertEquals(24f, body.height(), 0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import java.util.Set;
|
||||
|
||||
public final class LevelCatalogTest {
|
||||
@Test
|
||||
public void productionCatalogContainsFourHazardTeachingLevelsAfterTheVerticalSlice() {
|
||||
public void productionCatalogContainsHazardsAndTwoCorpseStateTeachingLevels() {
|
||||
List<LevelDefinition> levels = LevelCatalog.all();
|
||||
assertEquals(14, levels.size());
|
||||
assertEquals(16, levels.size());
|
||||
Set<String> titles = new HashSet<>();
|
||||
for (int i = 0; i < levels.size(); i++) {
|
||||
LevelDefinition level = levels.get(i);
|
||||
@@ -26,6 +26,9 @@ public final class LevelCatalogTest {
|
||||
assertTrue(levels.get(11).containsMechanic(LevelDefinition.HazardType.FIRE));
|
||||
assertTrue(levels.get(12).containsMechanic(LevelDefinition.HazardType.PROJECTILE));
|
||||
assertTrue(levels.get(13).containsMechanic(LevelDefinition.HazardType.EXPLOSION));
|
||||
assertTrue(levels.get(14).containsMechanic(LevelDefinition.HazardType.CRUSHER));
|
||||
assertTrue(levels.get(15).containsMechanic(LevelDefinition.HazardType.FIRE));
|
||||
assertFalse(levels.get(15).platforms.get(1).controlId.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -8,8 +8,8 @@ import org.junit.Test;
|
||||
|
||||
public final class LevelPageTest {
|
||||
@Test
|
||||
public void fourteenLevelsUseTwoBoundedTenCardPages() {
|
||||
LevelPage page = new LevelPage(14, 10);
|
||||
public void sixteenLevelsUseTwoBoundedTenCardPages() {
|
||||
LevelPage page = new LevelPage(16, 10);
|
||||
|
||||
assertEquals(2, page.pageCount());
|
||||
assertEquals(0, page.firstIndex());
|
||||
@@ -19,7 +19,7 @@ public final class LevelPageTest {
|
||||
|
||||
page.next();
|
||||
assertEquals(10, page.firstIndex());
|
||||
assertEquals(14, page.endExclusive());
|
||||
assertEquals(16, page.endExclusive());
|
||||
assertTrue(page.hasPrevious());
|
||||
assertFalse(page.hasNext());
|
||||
|
||||
@@ -27,7 +27,7 @@ public final class LevelPageTest {
|
||||
assertEquals(1, page.pageIndex());
|
||||
page.selectIndex(3);
|
||||
assertEquals(0, page.pageIndex());
|
||||
page.selectIndex(13);
|
||||
page.selectIndex(15);
|
||||
assertEquals(1, page.pageIndex());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ public final class VersionedSessionCodecTest {
|
||||
assertEquals(2, restored.bodies().size());
|
||||
assertTrue(restored.bodies().get(0).pinned());
|
||||
assertTrue(restored.bodies().get(1).conductive());
|
||||
assertTrue(restored.bodies().get(0).crushed());
|
||||
assertTrue(restored.bodies().get(1).burned());
|
||||
assertEquals(Arrays.asList(1.25f, 3.5f), restored.platformMotionTimes());
|
||||
assertTrue(restored.toggleSignals().get("gate"));
|
||||
assertEquals(2.25f, restored.timedSignals().get("clock"), 0f);
|
||||
@@ -43,6 +45,19 @@ public final class VersionedSessionCodecTest {
|
||||
decoded.snapshot().saveVersion());
|
||||
assertTrue(decoded.snapshot().previousSwitchContacts().isEmpty());
|
||||
assertEquals(2, decoded.snapshot().bodies().size());
|
||||
assertFalse(decoded.snapshot().bodies().get(0).crushed());
|
||||
assertFalse(decoded.snapshot().bodies().get(1).burned());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyV2MigratesWithSafeDefaultCorpseStates() {
|
||||
VersionedSessionCodec.DecodeResult decoded = VersionedSessionCodec.decode(
|
||||
VersionedSessionCodec.encodeLegacyV2(sampleSnapshot()));
|
||||
|
||||
assertEquals(VersionedSessionCodec.Status.MIGRATED, decoded.status());
|
||||
assertTrue(decoded.snapshot().previousSwitchContacts().get("gate@420.0"));
|
||||
assertFalse(decoded.snapshot().bodies().get(0).crushed());
|
||||
assertFalse(decoded.snapshot().bodies().get(1).burned());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -84,10 +99,12 @@ public final class VersionedSessionCodecTest {
|
||||
0.1f, 0.08f, -1, -1))
|
||||
.addBody(new GameSessionSnapshot.BodyData(
|
||||
500f, 826f, 72f, 54f, 0f, 0f,
|
||||
false, false, true, true, -1, "SPIKE"))
|
||||
false, false, true, true, true, false,
|
||||
-1, "CRUSH"))
|
||||
.addBody(new GameSessionSnapshot.BodyData(
|
||||
720f, 826f, 72f, 54f, 0f, 0f,
|
||||
false, true, true, true, -1, "ELECTRIC"))
|
||||
false, true, true, true, false, true,
|
||||
-1, "FIRE"))
|
||||
.platformMotionTimes(Arrays.asList(1.25f, 3.5f))
|
||||
.toggleSignal("gate", true)
|
||||
.timedSignal("clock", 2.25f)
|
||||
|
||||
Reference in New Issue
Block a user