feat(state): add validated persistent game state
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
final class DefaultConfigurationTest {
|
||||
@Test
|
||||
void bundledConfigurationIsCompleteAndValid() {
|
||||
InputStream stream = getClass().getClassLoader().getResourceAsStream("config.yml");
|
||||
assertNotNull(stream);
|
||||
|
||||
Map<String, ?> values = new Yaml().load(stream);
|
||||
PluginSettings settings = PluginSettings.from(values);
|
||||
assertEquals(50.0, settings.tyrantRangeBlocks());
|
||||
assertEquals(Duration.ofDays(7), settings.optOutDuration());
|
||||
assertEquals("FISHING_ROD", settings.tamerItem().material());
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsNestedSectionsAsBukkitProvidesThem() throws Exception {
|
||||
InputStream stream = getClass().getClassLoader().getResourceAsStream("config.yml");
|
||||
assertNotNull(stream);
|
||||
String yamlText = new String(stream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
yaml.loadFromString(yamlText);
|
||||
|
||||
PluginSettings settings = PluginSettings.from(yaml.getValues(false));
|
||||
|
||||
assertEquals("Assassin Cloak", settings.assassinItem().name());
|
||||
assertEquals("Fixer's Wrench", settings.fixerItem().name());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class PauseAwareTimeTest {
|
||||
@Test
|
||||
void shiftsDeadlineByTimeSpentPaused() {
|
||||
Instant pausedAt = Instant.parse("2026-08-14T12:00:00Z");
|
||||
Instant resumedAt = pausedAt.plus(Duration.ofHours(3));
|
||||
Instant originalDeadline = pausedAt.plus(Duration.ofMinutes(30));
|
||||
|
||||
Instant shifted = PauseAwareTime.shiftDeadline(originalDeadline, pausedAt, resumedAt);
|
||||
|
||||
assertEquals(originalDeadline.plus(Duration.ofHours(3)), shifted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class PluginSettingsTest {
|
||||
@Test
|
||||
void approvedDefaultsCoverCoreGameTimingAndRanges() {
|
||||
PluginSettings settings = PluginSettings.from(Map.of());
|
||||
|
||||
assertEquals(50.0, settings.tyrantRangeBlocks());
|
||||
assertEquals(50.0, settings.followerRangeBlocks());
|
||||
assertEquals(Duration.ofDays(7), settings.optOutDuration());
|
||||
assertEquals(Duration.ofHours(48), settings.roleInactivity());
|
||||
assertEquals(Duration.ofHours(24), settings.candidateActivityWindow());
|
||||
assertEquals(Duration.ofHours(24), settings.pendingSelectionTimeout());
|
||||
assertEquals(Duration.ofHours(1), settings.assassinCooldown());
|
||||
assertEquals(Duration.ofMinutes(10), settings.assassinInvisibilityDuration());
|
||||
assertEquals(Duration.ofHours(1), settings.fixerCooldown());
|
||||
assertEquals(Duration.ofMinutes(10), settings.fixerEffectDuration());
|
||||
assertEquals(Duration.ofHours(24), settings.rosterIntelligenceCooldown());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedDefaultsCoverAbilitiesItemsMobsAndMessages() {
|
||||
PluginSettings settings = PluginSettings.from(Map.of());
|
||||
|
||||
assertEquals(Duration.ofMinutes(1), settings.selectionRetryInterval());
|
||||
assertEquals(Duration.ofSeconds(60), settings.assassinDoubleJumpCooldown());
|
||||
assertEquals(Duration.ofSeconds(15), settings.assassinSpeedDuration());
|
||||
assertEquals(Duration.ofSeconds(20), settings.assassinWeaknessDuration());
|
||||
assertEquals(3, settings.assassinWeaknessLevel());
|
||||
assertEquals(5, settings.followerStrengthCap());
|
||||
assertEquals(4, settings.followerResistanceCap());
|
||||
assertEquals(2, settings.fixerNormalHeartRows());
|
||||
assertEquals(3, settings.fixerNearTyrantHeartRows());
|
||||
assertEquals(Set.of("ENDER_DRAGON", "WITHER"), settings.deniedMobTypes());
|
||||
assertEquals("STICK", settings.assassinItem().material());
|
||||
assertEquals("Assassin Cloak", settings.assassinItem().name());
|
||||
assertEquals("FISHING_ROD", settings.tamerItem().material());
|
||||
assertEquals("/tyrant item", settings.recoveryCommand());
|
||||
assertEquals("Your inventory is full. Make room and use /tyrant item.",
|
||||
settings.inventoryFullMessage());
|
||||
assertEquals(true, settings.freezeTimersWhilePaused());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnsafeRangesAndDurations() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettings.from(Map.of("tyrant-range-blocks", 0))
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettings.from(Map.of("follower-range-blocks", Double.NaN))
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettings.from(Map.of("role-inactivity-seconds", -1))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class PluginSettingsValidatorTest {
|
||||
@Test
|
||||
void acceptsBundledItemMaterialsAndKnownEntityTypes() {
|
||||
PluginSettings settings = PluginSettings.from(Map.of());
|
||||
|
||||
assertSame(settings, PluginSettingsValidator.validate(settings));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidItemMaterialAndEntityType() {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettingsValidator.validate(PluginSettings.from(Map.of(
|
||||
"assassin-item", Map.of("material", "NOT_REAL", "name", "Cloak")
|
||||
)))
|
||||
);
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> PluginSettingsValidator.validate(PluginSettings.from(Map.of(
|
||||
"denied-mob-types", java.util.List.of("NOT_REAL")
|
||||
)))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class TyrantStateManagerTest {
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void persistsPlayerNamesAndGameUpdatesWhenDirty() throws Exception {
|
||||
Path stateFile = temporaryDirectory.resolve("state.yml");
|
||||
YamlTyrantStateRepository repository = new YamlTyrantStateRepository(stateFile);
|
||||
TyrantStateManager manager = new TyrantStateManager(
|
||||
repository,
|
||||
Logger.getLogger("test")
|
||||
);
|
||||
UUID playerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
manager.player(playerId, "FirstName");
|
||||
manager.player(playerId, "LatestName");
|
||||
manager.updateGame(current -> new GameState(
|
||||
GameLifecycle.RUNNING,
|
||||
java.util.Optional.of(playerId),
|
||||
java.util.Optional.empty(),
|
||||
java.util.Optional.empty(),
|
||||
java.util.Optional.empty(),
|
||||
java.util.Optional.empty(),
|
||||
java.time.Duration.ZERO,
|
||||
0,
|
||||
1,
|
||||
java.util.Set.of()
|
||||
));
|
||||
manager.saveIfDirty();
|
||||
|
||||
PersistentState restored = repository.load();
|
||||
assertEquals("LatestName", restored.players().get(playerId).latestName());
|
||||
assertEquals(GameLifecycle.RUNNING, restored.game().lifecycle());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class YamlTyrantStateRepositoryTest {
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void roundTripsLifecycleRolesProgressionPlayersCooldownsItemsAndMobs() throws Exception {
|
||||
UUID tyrant = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID vigilante = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
Instant now = Instant.parse("2026-08-14T12:00:00Z");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.PAUSED,
|
||||
Optional.of(tyrant),
|
||||
Optional.of(vigilante),
|
||||
Optional.empty(),
|
||||
Optional.of(new PendingSelection(vigilante, now.plusSeconds(3600))),
|
||||
Optional.of(now),
|
||||
Duration.ofMinutes(15),
|
||||
3,
|
||||
2,
|
||||
Set.of(TyrantUnlock.ASSASSIN, TyrantUnlock.STRENGTH)
|
||||
);
|
||||
PlayerState player = new PlayerState(
|
||||
tyrant,
|
||||
"TyrantPlayer",
|
||||
Optional.of(now.minusSeconds(10)),
|
||||
Optional.of(now.plusSeconds(600)),
|
||||
TyrantClass.ASSASSIN,
|
||||
Optional.of(vigilante),
|
||||
Map.of(Ability.ASSASSIN_INVISIBILITY, now.plusSeconds(3600)),
|
||||
Set.of(Ability.ASSASSIN_INVISIBILITY),
|
||||
List.of(new CapturedMob("ZOMBIE", Map.of("custom-name", "Bob")))
|
||||
);
|
||||
PersistentState expected = new PersistentState(game, Map.of(tyrant, player));
|
||||
YamlTyrantStateRepository repository = new YamlTyrantStateRepository(
|
||||
temporaryDirectory.resolve("state.yml")
|
||||
);
|
||||
|
||||
repository.save(expected);
|
||||
|
||||
assertEquals(expected, repository.load());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesUnknownFieldsForRetainedState() throws Exception {
|
||||
UUID playerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
Path stateFile = temporaryDirectory.resolve("state.yml");
|
||||
Files.writeString(stateFile, """
|
||||
future-root: retained
|
||||
players:
|
||||
11111111-1111-1111-1111-111111111111:
|
||||
name: Player
|
||||
future-player-field: retained-too
|
||||
""");
|
||||
YamlTyrantStateRepository repository = new YamlTyrantStateRepository(stateFile);
|
||||
|
||||
PersistentState loaded = repository.load();
|
||||
repository.save(loaded);
|
||||
|
||||
String saved = Files.readString(stateFile);
|
||||
assertEquals(true, saved.contains("future-root: retained"));
|
||||
assertEquals(true, saved.contains("future-player-field: retained-too"));
|
||||
assertEquals(playerId, loaded.players().get(playerId).playerId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidRecordsCannotRestoreProgressOrCapturedDragons() throws Exception {
|
||||
Path stateFile = temporaryDirectory.resolve("state.yml");
|
||||
Files.writeString(stateFile, """
|
||||
game:
|
||||
lifecycle: RUNNING
|
||||
tyrant-level: -10
|
||||
players:
|
||||
11111111-1111-1111-1111-111111111111:
|
||||
name: Player
|
||||
captured-mobs:
|
||||
- entity-type: ENDER_DRAGON
|
||||
- entity-type: NOT_REAL
|
||||
""");
|
||||
YamlTyrantStateRepository repository = new YamlTyrantStateRepository(stateFile);
|
||||
|
||||
PersistentState loaded = repository.load();
|
||||
|
||||
assertEquals(GameState.empty(), loaded.game());
|
||||
assertEquals(List.of(), loaded.players().values().iterator().next().capturedMobs());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user