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; } }