Завершить контентную архитектуру этапа 4
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class LevelCatalogLoaderTest {
|
||||
@Test
|
||||
public void versionedIndexLoadsSequentialLevelsThroughResourceBoundary() {
|
||||
Map<String, String> resources = new HashMap<>();
|
||||
resources.put("levels/index.json", "{\"formatVersion\":1,\"physicsVersion\":1,"
|
||||
+ "\"levels\":[\"level-01.json\"]}");
|
||||
resources.put("levels/level-01.json", LevelJsonLoaderTest.validLevelJson(1));
|
||||
|
||||
List<LevelDefinition> levels = LevelCatalogLoader.load(path -> {
|
||||
String value = resources.get(path);
|
||||
return value == null ? null : new StringReader(value);
|
||||
}, "levels/index.json");
|
||||
|
||||
assertEquals(1, levels.size());
|
||||
assertEquals(1, levels.get(0).number);
|
||||
assertEquals("The Test", levels.get(0).title);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,19 @@ public final class LevelCatalogTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bundledCatalogCarriesLocalizedCopyAndVersionedContent() {
|
||||
LevelDefinition first = LevelCatalog.get(0);
|
||||
LevelDefinition last = LevelCatalog.get(9);
|
||||
|
||||
assertEquals(LevelFormat.CURRENT_FORMAT_VERSION, first.formatVersion);
|
||||
assertEquals(LevelFormat.CURRENT_PHYSICS_VERSION, first.physicsVersion);
|
||||
assertEquals("Рыцарь-доброволец", first.titleRu);
|
||||
assertEquals("Последнее шествие", last.titleRu);
|
||||
assertFalse(first.hintRu.trim().isEmpty());
|
||||
assertFalse(last.hintRu.trim().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyLevelHasValidSpawnGoalAndGeometry() {
|
||||
for (LevelDefinition level : LevelCatalog.all()) {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.StringReader;
|
||||
|
||||
public final class LevelJsonLoaderTest {
|
||||
@Test
|
||||
public void versionedJsonLoadsLocalizedLevelThroughPublicContract() {
|
||||
String json = validLevelJson(1);
|
||||
|
||||
LevelDefinition level = LevelJsonLoader.load(new StringReader(json), "test-level.json");
|
||||
|
||||
assertEquals(1, level.formatVersion);
|
||||
assertEquals(1, level.physicsVersion);
|
||||
assertEquals(1, level.number);
|
||||
assertEquals("The Test", level.title);
|
||||
assertEquals("Reach the goal.", level.hint);
|
||||
assertEquals("Испытание", level.titleRu);
|
||||
assertEquals("Доберитесь до цели.", level.hintRu);
|
||||
assertEquals(1920f, level.worldWidth, 0f);
|
||||
assertEquals(1080f, level.worldHeight, 0f);
|
||||
assertEquals(1, level.platforms.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownFormatVersionIsRejectedWithSourcePath() {
|
||||
try {
|
||||
LevelJsonLoader.load(new StringReader(validLevelJson(2)), "future-level.json");
|
||||
fail("Unknown format versions must fail closed");
|
||||
} catch (IllegalArgumentException error) {
|
||||
assertTrue(error.getMessage(), error.getMessage().contains("future-level.json.formatVersion"));
|
||||
assertTrue(error.getMessage(), error.getMessage().contains("expected 1"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doorWithUnknownSignalIsRejectedBeforeEnteringCatalog() {
|
||||
String json = validLevelJson(1).replace(
|
||||
"\"hazards\":[],\"switches\":[],\"doors\":[]",
|
||||
"\"hazards\":[],"
|
||||
+ "\"switches\":[{\"type\":\"PRESSURE\","
|
||||
+ "\"box\":{\"x\":540,\"y\":860,\"width\":150,\"height\":20},"
|
||||
+ "\"signalId\":\"gate\",\"durationSeconds\":0,\"requiredMass\":1}],"
|
||||
+ "\"doors\":[{\"box\":{\"x\":1110,\"y\":570,\"width\":78,\"height\":310},"
|
||||
+ "\"requiredSignals\":[\"missing\"]}]");
|
||||
|
||||
try {
|
||||
LevelJsonLoader.load(new StringReader(json), "broken-signal.json");
|
||||
fail("Unknown signal references must be rejected");
|
||||
} catch (IllegalArgumentException error) {
|
||||
assertTrue(error.getMessage(), error.getMessage().contains(
|
||||
"broken-signal.json.doors[0].requiredSignals[0]"));
|
||||
assertTrue(error.getMessage(), error.getMessage().contains("missing signal 'missing'"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonPositiveGeometryIsRejectedWithObjectPath() {
|
||||
String json = validLevelJson(1).replace(
|
||||
"\"width\":1920,\"height\":200",
|
||||
"\"width\":0,\"height\":200");
|
||||
|
||||
try {
|
||||
LevelJsonLoader.load(new StringReader(json), "flat-platform.json");
|
||||
fail("Non-positive platform geometry must be rejected");
|
||||
} catch (IllegalArgumentException error) {
|
||||
assertTrue(error.getMessage(), error.getMessage().contains(
|
||||
"flat-platform.json.platforms[0].box.width"));
|
||||
assertTrue(error.getMessage(), error.getMessage().contains("must be positive"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void currentPuzzleObjectsLoadWithoutJavaGeometryFallback() {
|
||||
String json = validLevelJson(1)
|
||||
.replace(
|
||||
"{\"box\":{\"x\":0,\"y\":880,\"width\":1920,\"height\":200}}",
|
||||
"{\"box\":{\"x\":0,\"y\":880,\"width\":1920,\"height\":200},"
|
||||
+ "\"travelX\":0,\"travelY\":-300,\"periodSeconds\":5.2,"
|
||||
+ "\"controlId\":\"lift\"}")
|
||||
.replace(
|
||||
"\"hazards\":[],\"switches\":[],\"doors\":[]",
|
||||
"\"hazards\":[{\"type\":\"SAW\","
|
||||
+ "\"box\":{\"x\":565,\"y\":760,\"width\":92,\"height\":92},"
|
||||
+ "\"impulseX\":500,\"impulseY\":-700}],"
|
||||
+ "\"switches\":[{\"type\":\"PRESSURE\","
|
||||
+ "\"box\":{\"x\":540,\"y\":860,\"width\":150,\"height\":20},"
|
||||
+ "\"signalId\":\"lift\",\"durationSeconds\":0,\"requiredMass\":1}],"
|
||||
+ "\"doors\":[{\"box\":{\"x\":1110,\"y\":570,\"width\":78,\"height\":310},"
|
||||
+ "\"requiredSignals\":[\"lift\"]}]");
|
||||
|
||||
LevelDefinition level = LevelJsonLoader.load(new StringReader(json), "mechanics.json");
|
||||
|
||||
assertEquals(1, level.platforms.size());
|
||||
assertEquals(-300f, level.platforms.get(0).travelY, 0f);
|
||||
assertEquals(5.2f, level.platforms.get(0).periodSeconds, 0f);
|
||||
assertEquals("lift", level.platforms.get(0).controlId);
|
||||
assertEquals(LevelDefinition.HazardType.SAW, level.hazards.get(0).type);
|
||||
assertEquals(500f, level.hazards.get(0).impulseX, 0f);
|
||||
assertEquals(1, level.switches.size());
|
||||
assertEquals(1, level.doors.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timedSwitchRequiresPositiveDuration() {
|
||||
String json = validLevelJson(1).replace(
|
||||
"\"hazards\":[],\"switches\":[],\"doors\":[]",
|
||||
"\"hazards\":[],"
|
||||
+ "\"switches\":[{\"type\":\"TIMED\","
|
||||
+ "\"box\":{\"x\":560,\"y\":810,\"width\":90,\"height\":70},"
|
||||
+ "\"signalId\":\"clock\",\"durationSeconds\":0,\"requiredMass\":1}],"
|
||||
+ "\"doors\":[]");
|
||||
|
||||
try {
|
||||
LevelJsonLoader.load(new StringReader(json), "zero-timer.json");
|
||||
fail("A timed switch with no active window must be rejected");
|
||||
} catch (IllegalArgumentException error) {
|
||||
assertTrue(error.getMessage(), error.getMessage().contains(
|
||||
"zero-timer.json.switches[0].durationSeconds"));
|
||||
assertTrue(error.getMessage(), error.getMessage().contains("must be positive"));
|
||||
}
|
||||
}
|
||||
|
||||
static String validLevelJson(int formatVersion) {
|
||||
return "{"
|
||||
+ "\"formatVersion\":" + formatVersion + ","
|
||||
+ "\"physicsVersion\":1,"
|
||||
+ "\"number\":1,"
|
||||
+ "\"copy\":{"
|
||||
+ "\"en\":{\"title\":\"The Test\",\"hint\":\"Reach the goal.\"},"
|
||||
+ "\"ru\":{\"title\":\"Испытание\",\"hint\":\"Доберитесь до цели.\"}},"
|
||||
+ "\"world\":{\"width\":1920,\"height\":1080},"
|
||||
+ "\"spawn\":{\"x\":130,\"y\":760},"
|
||||
+ "\"goal\":{\"x\":1690,\"y\":770,\"width\":86,\"height\":110},"
|
||||
+ "\"targets\":{\"deaths\":0,\"seconds\":30},"
|
||||
+ "\"platforms\":[{\"box\":{\"x\":0,\"y\":880,\"width\":1920,\"height\":200}}],"
|
||||
+ "\"hazards\":[],\"switches\":[],\"doors\":[]"
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,51 @@ public final class LevelPlaytestBotTest {
|
||||
driver.inputReleases >= 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resultExposesDeterministicVersionedCommandTrace() {
|
||||
ScriptedDriver driver = new ScriptedDriver();
|
||||
LevelPlaytestBot.Scenario scenario = LevelPlaytestBot.Scenario
|
||||
.builder(7, "Trace scenario")
|
||||
.step("Create a corpse", 1_000L, LevelPlaytestBot.Input.right(),
|
||||
snapshot -> snapshot.deaths() >= 1)
|
||||
.step("Reach the goal", 1_000L, LevelPlaytestBot.Input.left(),
|
||||
GameView.AutomationSnapshot::completed)
|
||||
.build();
|
||||
|
||||
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(driver, scenario, 5_000L);
|
||||
String trace = result.traceJson();
|
||||
|
||||
assertEquals(trace, result.traceJson());
|
||||
assertTrue(trace, trace.contains("\"traceVersion\":1"));
|
||||
assertTrue(trace, trace.contains("\"level\":7"));
|
||||
assertTrue(trace, trace.contains("\"levelFormatVersion\":1"));
|
||||
assertTrue(trace, trace.contains("\"physicsVersion\":1"));
|
||||
assertTrue(trace, trace.contains("\"scenario\":\"Trace scenario\""));
|
||||
assertTrue(trace, trace.contains("\"kind\":\"INPUT\""));
|
||||
assertTrue(trace, trace.contains("\"simulationMs\":"));
|
||||
assertFalse(trace, trace.contains("wallElapsed"));
|
||||
assertFalse(trace, trace.contains("device"));
|
||||
assertTrue(result.diagnostic(), result.diagnostic().contains("traceId=" + result.traceId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tracingDriverCoversBespokeProductionInputScenarios() {
|
||||
BotTraceDriver driver = new BotTraceDriver(new ScriptedDriver(), "Bespoke route");
|
||||
driver.startLevel(8);
|
||||
driver.step("Board platform");
|
||||
driver.setInput(false, true, true);
|
||||
driver.waitForNextDecision(16L);
|
||||
GameView.AutomationSnapshot snapshot = driver.snapshot();
|
||||
|
||||
BotCommandTrace trace = driver.finish("COMPLETED", "Board platform", snapshot);
|
||||
|
||||
assertTrue(trace.json(), trace.json().contains("\"scenario\":\"Bespoke route\""));
|
||||
assertTrue(trace.json(), trace.json().contains("\"step\":\"Board platform\""));
|
||||
assertTrue(trace.json(), trace.json().contains(
|
||||
"\"input\":{\"left\":false,\"right\":true,\"jump\":true}"));
|
||||
assertEquals(64, trace.id().length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameDriverCanRestartTheCompleteScenarioThreeTimes() {
|
||||
ScriptedDriver driver = new ScriptedDriver();
|
||||
|
||||
Reference in New Issue
Block a user