Добавить испытания и прогрессию по LevelID

This commit is contained in:
2026-08-19 09:25:32 +03:00
parent 259fcd316b
commit 3de8d45039
37 changed files with 968 additions and 63 deletions
@@ -0,0 +1,44 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public final class ChallengeResultTest {
@Test
public void targetBoundariesEarnAllThreeIndependentStars() {
LevelDefinition base = LevelCatalog.get(0);
LevelDefinition level = new LevelDefinition(
base.formatVersion, base.physicsVersion, base.levelId, base.worldId,
base.number, base.title, base.hint, base.titleRu, base.hintRu,
base.worldWidth, base.worldHeight, base.spawnX, base.spawnY, base.goal,
base.targetDeaths, base.targetSeconds,
new LevelDefinition.Box(500f, 700f, 50f, 50f), base.objects,
base.platforms, base.hazards, base.switches, base.doors);
ChallengeResult result = ChallengeResult.evaluate(level,
level.targetDeaths, level.targetSeconds * 1_000L, true);
assertTrue(result.casualtyEarned());
assertTrue(result.timeEarned());
assertTrue(result.collectibleEarned());
assertTrue(result.collectibleAvailable());
assertEquals(3, result.stars());
}
@Test
public void missedGoalsRemainIndependentAndLegacyLevelHasNoImpossibleStar() {
LevelDefinition level = LevelCatalog.get(0);
ChallengeResult slowAndCostly = ChallengeResult.evaluate(level,
level.targetDeaths + 1, level.targetSeconds * 1_000L + 1L, false);
assertFalse(slowAndCostly.casualtyEarned());
assertFalse(slowAndCostly.timeEarned());
assertFalse(slowAndCostly.collectibleAvailable());
assertTrue(slowAndCostly.collectibleEarned());
assertEquals(1, slowAndCostly.stars());
}
}
@@ -42,7 +42,35 @@ public final class CompletionResultTest {
LevelDefinition last = LevelCatalog.get(LevelCatalog.all().size() - 1);
CompletionResult lastResult = CompletionResult.create(last,
last.targetDeaths, last.targetSeconds * 900L,
last.targetDeaths, last.targetSeconds * 900L, false);
last.targetDeaths, last.targetSeconds * 900L,
true, true, false);
assertEquals(CompletionResult.NextGoal.PERSONAL_BEST, lastResult.nextGoal());
}
@Test
public void collectibleIsThirdExplicitGoalAndStarsExposeCurrentAndBest() {
LevelDefinition base = LevelCatalog.get(0);
LevelDefinition level = new LevelDefinition(
base.formatVersion, base.physicsVersion, "foundations.result-lab", "foundations",
base.number, base.title, base.hint, base.titleRu, base.hintRu,
base.worldWidth, base.worldHeight, base.spawnX, base.spawnY, base.goal,
base.targetDeaths, base.targetSeconds,
new LevelDefinition.Box(500f, 700f, 50f, 50f), base.objects,
base.platforms, base.hazards, base.switches, base.doors);
CompletionResult result = CompletionResult.create(level, base.targetDeaths,
base.targetSeconds * 1_000L, base.targetDeaths,
base.targetSeconds * 900L, false, false, true);
assertEquals(2, result.challengeResult().stars());
assertEquals(2, result.bestStars());
assertEquals(CompletionResult.NextGoal.TARGET_COLLECTIBLE, result.nextGoal());
CompletionResult mastered = CompletionResult.create(level, base.targetDeaths,
base.targetSeconds * 1_000L, base.targetDeaths,
base.targetSeconds * 900L, true, true, true);
assertEquals(3, mastered.challengeResult().stars());
assertEquals(3, mastered.bestStars());
assertEquals(CompletionResult.NextGoal.NEXT_LEVEL, mastered.nextGoal());
}
}
@@ -33,6 +33,10 @@ public final class GameStringsTest {
assertEquals("Добровольцы 2 Время 00:07", russian.hudStats(2, 7_000L));
assertEquals("✓ 2 добровольца · 00:07", russian.levelBestStats(2, 7_000L));
assertEquals("★ 2 / 3 добровольца", russian.completionDeaths(true, 2, 3));
assertEquals("★★☆", english.starRow(2));
assertEquals("★ Тайник найден", russian.completionCollectible(true, true));
assertEquals("☆ Тайник не найден", russian.completionCollectible(false, true));
assertEquals("Следующая цель · найти тайник", russian.nextGoalCollectible());
assertEquals("Смерть — это инструмент", russian.tutorialWelcomeTitle());
assertFalse(russian.tutorialSacrificeBody().trim().isEmpty());
@@ -1,6 +1,8 @@
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;
@@ -26,4 +28,26 @@ public final class LevelCatalogLoaderTest {
assertEquals(1, levels.get(0).number);
assertEquals("The Test", levels.get(0).title);
}
@Test
public void duplicateStableLevelIdsFailBeforeProgressCanCollide() {
Map<String, String> resources = new HashMap<>();
resources.put("levels/index.json", "{\"formatVersion\":1,\"physicsVersion\":1,"
+ "\"levels\":[\"level-01.json\",\"level-02.json\"]}");
resources.put("levels/level-01.json", LevelJsonLoaderTest.validLevelJson(1)
.replace("\"number\":1,", "\"number\":1,\"id\":\"shared.level\","));
resources.put("levels/level-02.json", LevelJsonLoaderTest.validLevelJson(1)
.replace("\"number\":1,", "\"number\":2,\"id\":\"shared.level\","));
try {
LevelCatalogLoader.load(path -> {
String value = resources.get(path);
return value == null ? null : new StringReader(value);
}, "levels/index.json");
fail("Duplicate stable IDs must fail before they share one progress record");
} catch (IllegalArgumentException error) {
assertTrue(error.getMessage(), error.getMessage().contains(
"levels/index.json.levels[1] duplicates level id 'shared.level'"));
}
}
}
@@ -13,9 +13,9 @@ import java.util.Set;
public final class LevelCatalogTest {
@Test
public void productionCatalogContainsHazardsAndTwoCorpseStateTeachingLevels() {
public void productionCatalogContainsMechanicsAndChallengeTeachingLevels() {
List<LevelDefinition> levels = LevelCatalog.all();
assertEquals(17, levels.size());
assertEquals(18, levels.size());
Set<String> titles = new HashSet<>();
for (int i = 0; i < levels.size(); i++) {
LevelDefinition level = levels.get(i);
@@ -40,6 +40,10 @@ public final class LevelCatalogTest {
objectTypes);
assertEquals(9, weighted.switches.get(0).requiredMass);
assertFalse(weighted.doors.isEmpty());
LevelDefinition challenge = levels.get(17);
assertEquals("foundations.threefold-proof", challenge.levelId);
assertEquals("foundations", challenge.worldId);
assertTrue(challenge.collectible != null);
}
@Test
@@ -0,0 +1,25 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
public final class LevelIdentityTest {
@Test
public void everyCatalogEntryHasStableUniqueLevelAndWorldIds() {
Set<String> ids = new HashSet<>();
for (LevelDefinition level : LevelCatalog.all()) {
assertNotNull(level.levelId);
assertNotNull(level.worldId);
assertTrue(level.levelId.matches("[a-z0-9][a-z0-9._-]{2,63}"));
assertTrue(level.worldId.matches("[a-z0-9][a-z0-9._-]{2,31}"));
assertTrue("duplicate levelId " + level.levelId, ids.add(level.levelId));
}
assertEquals(LevelCatalog.all().size(), ids.size());
}
}
@@ -251,6 +251,36 @@ public final class LevelJsonLoaderTest {
}
}
@Test
public void explicitLevelIdentityAndCollectibleLoadFromData() {
String json = validLevelJson(1)
.replace("\"number\":1,", "\"number\":1,"
+ "\"id\":\"foundations.hidden-cache\","
+ "\"worldId\":\"foundations\",")
.replace("\"targets\":", "\"collectible\":{\"x\":840,\"y\":700,"
+ "\"width\":52,\"height\":52},\"targets\":");
LevelDefinition level = LevelJsonLoader.load(new StringReader(json), "challenge.json");
assertEquals("foundations.hidden-cache", level.levelId);
assertEquals("foundations", level.worldId);
assertEquals(840f, level.collectible.x, 0f);
assertEquals(52f, level.collectible.width, 0f);
}
@Test
public void invalidExplicitLevelIdFailsWithSourcePath() {
String json = validLevelJson(1).replace(
"\"number\":1,", "\"number\":1,\"id\":\"Not Stable!\",");
try {
LevelJsonLoader.load(new StringReader(json), "invalid-id.json");
fail("Unstable level IDs must fail closed");
} catch (IllegalArgumentException error) {
assertTrue(error.getMessage(), error.getMessage().contains("invalid-id.json.id"));
}
}
static String validLevelJson(int formatVersion) {
return "{"
+ "\"formatVersion\":" + formatVersion + ","
@@ -8,8 +8,8 @@ import org.junit.Test;
public final class LevelPageTest {
@Test
public void seventeenLevelsUseTwoBoundedTenCardPages() {
LevelPage page = new LevelPage(17, 10);
public void eighteenLevelsUseTwoBoundedTenCardPages() {
LevelPage page = new LevelPage(18, 10);
assertEquals(2, page.pageCount());
assertEquals(0, page.firstIndex());
@@ -19,7 +19,7 @@ public final class LevelPageTest {
page.next();
assertEquals(10, page.firstIndex());
assertEquals(17, page.endExclusive());
assertEquals(18, page.endExclusive());
assertTrue(page.hasPrevious());
assertFalse(page.hasNext());
@@ -31,5 +31,7 @@ public final class LevelPageTest {
assertEquals(1, page.pageIndex());
page.selectIndex(16);
assertEquals(1, page.pageIndex());
page.selectIndex(17);
assertEquals(1, page.pageIndex());
}
}
@@ -33,6 +33,7 @@ public final class VersionedSessionCodecTest {
assertEquals(LevelDefinition.PhysicalObjectType.BOX,
restored.objects().get(0).type());
assertEquals(3, restored.objects().get(1).mass());
assertTrue(restored.collectibleFound());
assertEquals(Arrays.asList(1.25f, 3.5f), restored.platformMotionTimes());
assertTrue(restored.toggleSignals().get("gate"));
assertEquals(2.25f, restored.timedSignals().get("clock"), 0f);
@@ -76,6 +77,16 @@ public final class VersionedSessionCodecTest {
assertTrue(decoded.snapshot().objects().isEmpty());
}
@Test
public void legacyV4MigratesWithCollectibleNotInvented() {
VersionedSessionCodec.DecodeResult decoded = VersionedSessionCodec.decode(
VersionedSessionCodec.encodeLegacyV4(sampleSnapshot()));
assertEquals(VersionedSessionCodec.Status.MIGRATED, decoded.status());
assertEquals(2, decoded.snapshot().objects().size());
assertFalse(decoded.snapshot().collectibleFound());
}
@Test
public void corruptAndFutureSnapshotsFailClosed() {
VersionedSessionCodec.DecodeResult corrupt =
@@ -109,6 +120,7 @@ public final class VersionedSessionCodecTest {
return GameSessionSnapshot.builder(5)
.elapsedSeconds(12.75f)
.deaths(2)
.collectibleFound(true)
.respawnSeconds(0.4f)
.player(new GameSessionSnapshot.PlayerData(
640f, 780f, 120f, -40f, false, false,