Выделить event-driven PuzzleSystem

This commit is contained in:
2026-08-19 04:07:29 +03:00
parent cc3b5902cc
commit cc7f487cf7
9 changed files with 763 additions and 68 deletions
@@ -0,0 +1,153 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.Collections;
import java.util.Arrays;
import org.junit.Test;
public final class PuzzleSystemTest {
@Test
public void toggleChannelChangesOnlyOnRisingEdgesAndPublishesEvents() {
PuzzleSystem system = new PuzzleSystem();
PuzzleSystem.Input held = PuzzleSystem.Input.toggle("lever-1", "gate", true);
system.update(0.016f, Collections.singletonList(held));
assertTrue(system.signalActive("gate"));
assertEquals(PuzzleSystem.Event.Kind.SIGNAL_ACTIVATED,
system.events().get(0).kind());
assertEquals("gate", system.events().get(0).channelId());
system.update(0.016f, Collections.singletonList(held));
assertTrue(system.signalActive("gate"));
assertTrue(system.events().isEmpty());
system.update(0.016f, Collections.singletonList(
PuzzleSystem.Input.toggle("lever-1", "gate", false)));
system.update(0.016f, Collections.singletonList(held));
assertFalse(system.signalActive("gate"));
assertEquals(PuzzleSystem.Event.Kind.SIGNAL_DEACTIVATED,
system.events().get(0).kind());
}
@Test
public void pressureChannelAggregatesEveryActiveSourceWithoutFlicker() {
PuzzleSystem system = new PuzzleSystem();
system.update(0.016f, Arrays.asList(
PuzzleSystem.Input.pressure("plate-left", "weight", true),
PuzzleSystem.Input.pressure("plate-right", "weight", false)));
assertTrue(system.signalActive("weight"));
assertEquals(1, system.events().size());
system.update(0.016f, Arrays.asList(
PuzzleSystem.Input.pressure("plate-left", "weight", false),
PuzzleSystem.Input.pressure("plate-right", "weight", true)));
assertTrue(system.signalActive("weight"));
assertTrue(system.events().isEmpty());
system.update(0.016f, Arrays.asList(
PuzzleSystem.Input.pressure("plate-left", "weight", false),
PuzzleSystem.Input.pressure("plate-right", "weight", false)));
assertFalse(system.signalActive("weight"));
assertEquals(PuzzleSystem.Event.Kind.SIGNAL_DEACTIVATED,
system.events().get(0).kind());
}
@Test
public void timedChannelStartsOnRisingEdgeExpiresAndCanBeRearmed() {
PuzzleSystem system = new PuzzleSystem();
system.update(0f, Collections.singletonList(
PuzzleSystem.Input.timed("clock-switch", "clock", true, 0.5f)));
assertTrue(system.signalActive("clock"));
assertEquals(0.5f, system.remainingSeconds("clock"), 0f);
system.update(0.2f, Collections.singletonList(
PuzzleSystem.Input.timed("clock-switch", "clock", true, 0.5f)));
assertEquals(0.3f, system.remainingSeconds("clock"), 0.0001f);
assertTrue(system.events().isEmpty());
system.update(0.31f, Collections.singletonList(
PuzzleSystem.Input.timed("clock-switch", "clock", false, 0.5f)));
assertFalse(system.signalActive("clock"));
assertEquals(PuzzleSystem.Event.Kind.SIGNAL_DEACTIVATED,
system.events().get(0).kind());
system.update(0f, Collections.singletonList(
PuzzleSystem.Input.timed("clock-switch", "clock", true, 0.5f)));
assertTrue(system.signalActive("clock"));
assertEquals(0.5f, system.remainingSeconds("clock"), 0f);
}
@Test
public void andDoorPublishesExplicitOpenAndCloseTransitions() {
DoorController exit = new DoorController("exit",
Arrays.asList("weight", "power"));
PuzzleSystem system = new PuzzleSystem(Collections.singletonList(exit));
system.update(0.016f, Arrays.asList(
PuzzleSystem.Input.pressure("plate", "weight", true),
PuzzleSystem.Input.relay("conductor", "power", false)));
assertEquals(DoorController.State.CLOSED, exit.state());
system.update(0.016f, Arrays.asList(
PuzzleSystem.Input.pressure("plate", "weight", true),
PuzzleSystem.Input.relay("conductor", "power", true)));
assertEquals(DoorController.State.OPEN, exit.state());
assertTrue(hasEvent(system, PuzzleSystem.Event.Kind.DOOR_OPENED, "exit"));
system.update(0.016f, Arrays.asList(
PuzzleSystem.Input.pressure("plate", "weight", false),
PuzzleSystem.Input.relay("conductor", "power", true)));
assertEquals(DoorController.State.CLOSED, exit.state());
assertTrue(hasEvent(system, PuzzleSystem.Event.Kind.DOOR_CLOSED, "exit"));
}
@Test
public void resetAndRestoreOwnAnImmutablePuzzleStateSnapshot() {
DoorController exit = new DoorController("exit", Arrays.asList("gate", "clock"));
PuzzleSystem source = new PuzzleSystem(Collections.singletonList(exit));
source.update(0f, Arrays.asList(
PuzzleSystem.Input.toggle("lever", "gate", true),
PuzzleSystem.Input.timed("timer", "clock", true, 2f)));
PuzzleSystem.State saved = source.snapshot();
source.reset();
assertFalse(source.signalActive("gate"));
assertFalse(source.signalActive("clock"));
assertEquals(DoorController.State.CLOSED, exit.state());
DoorController restoredDoor = new DoorController("exit",
Arrays.asList("gate", "clock"));
PuzzleSystem restored = new PuzzleSystem(Collections.singletonList(restoredDoor));
restored.restore(saved, Collections.singletonMap("lever", true));
assertTrue(restored.signalActive("gate"));
assertEquals(2f, restored.remainingSeconds("clock"), 0f);
assertEquals(DoorController.State.OPEN, restoredDoor.state());
restored.update(0f, Collections.singletonList(
PuzzleSystem.Input.toggle("lever", "gate", true)));
assertTrue("Held restored contact must not toggle again",
restored.signalActive("gate"));
assertEquals(Boolean.TRUE, saved.toggleSignals().get("gate"));
assertEquals(2f, saved.timedSignals().get("clock"), 0f);
try {
saved.toggleSignals().put("other", true);
throw new AssertionError("Puzzle state maps must be immutable");
} catch (UnsupportedOperationException expected) {
// Contract proven.
}
}
private static boolean hasEvent(PuzzleSystem system, PuzzleSystem.Event.Kind kind,
String targetId) {
for (PuzzleSystem.Event event : system.events()) {
if (event.kind() == kind && targetId.equals(event.targetId())) return true;
}
return false;
}
}