Завершить контентную архитектуру этапа 4

This commit is contained in:
2026-08-18 22:03:38 +03:00
parent 2ddac2775f
commit 5051f316fe
39 changed files with 1954 additions and 259 deletions
@@ -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();