Добавить 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"));
});
@@ -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)