using System.Collections.Immutable; using SideScrollerGame.Sim.Input; using SideScrollerGame.Sim.Replay; using SideScrollerGame.Sim.Serialization; namespace SideScrollerGame.Sim.Tests; public sealed class SimulationReplayTests { [Fact] public void SameSeedAndActions_ProduceSameHashes() { var definition = SimulationTestFactory.CreateGameDefinition(); var config = SimulationTestFactory.CreateConfig(); TickActionBatch[] batches = [ SimulationTestFactory.CreateTick(1, new MoveAxisChanged(new(1), 1, 0)), SimulationTestFactory.CreateTick(2, new AimAxisChanged(new(1), 3, 4)), SimulationTestFactory.CreateTick(3, new ButtonChanged(new(1), InputButton.Dash, true)) ]; Simulation left = new(definition, config, 42); Simulation right = new(definition, config, 42); var leftHashes = ImmutableArray.CreateBuilder(); var rightHashes = ImmutableArray.CreateBuilder(); foreach (var batch in batches) { leftHashes.Add(left.Step(batch).StateHash); rightHashes.Add(right.Step(batch).StateHash); } Assert.Equal(leftHashes.ToImmutable(), rightHashes.ToImmutable()); } [Fact] public void DifferentSeeds_ProduceDifferentHashes() { var definition = SimulationTestFactory.CreateGameDefinition(); var config = SimulationTestFactory.CreateConfig(); var batch = TickActionBatch.Empty(1); Simulation left = new(definition, config, 1); Simulation right = new(definition, config, 2); Assert.NotEqual(left.Step(batch).StateHash, right.Step(batch).StateHash); } [Fact] public void ReplayRecord_RoundTripsAndReplaysExpectedHashes() { var definition = SimulationTestFactory.CreateGameDefinition(); var config = SimulationTestFactory.CreateConfig(); Simulation simulation = new(definition, config, 9); ReplayRecorder recorder = new(); TickActionBatch[] batches = [ SimulationTestFactory.CreateTick(1, new MoveAxisChanged(new(1), 2, 0), new AimAxisChanged(new(1), 5, 6)), SimulationTestFactory.CreateTick(2, new ButtonChanged(new(1), InputButton.FireSecondary, true)), SimulationTestFactory.CreateTick(3, new WeaponSlotSelected(new(1), 4)) ]; foreach (var batch in batches) { var result = simulation.Step(batch); recorder.Append(batch, result); } var replay = recorder.Build(definition, config, 9); var payload = ReplayRecordSerializer.Serialize(replay); var loadedReplay = ReplayRecordSerializer.Deserialize(payload); var replayedHashes = ReplayPlayer.Play(loadedReplay, definition, config); Assert.Equal(loadedReplay.Ticks.Select(static tick => tick.ExpectedStateHash).ToImmutableArray(), replayedHashes); } [Fact] public void ReplayPlayer_RejectsMismatchedDefinition() { var config = SimulationTestFactory.CreateConfig(); var definition = SimulationTestFactory.CreateGameDefinition(); ReplayRecord replay = new(123, 9, config.TicksPerSecond, ImmutableArray.Empty); var exception = Assert.Throws(() => ReplayPlayer.Play(replay, definition, config)); Assert.Contains("content hash", exception.Message.ToLowerInvariant()); } [Fact] public void ReplayPlayer_RejectsMismatchedTickRate() { var definition = SimulationTestFactory.CreateGameDefinition(); var replay = new ReplayRecorder().Build(definition, SimulationTestFactory.CreateConfig(), 9) with { TicksPerSecond = 30 }; var exception = Assert.Throws(() => ReplayPlayer.Play(replay, definition, SimulationTestFactory.CreateConfig())); Assert.Contains("tick rate", exception.Message.ToLowerInvariant()); } [Fact] public void ReplayPlayer_RejectsDivergentHashes() { var definition = SimulationTestFactory.CreateGameDefinition(); var config = SimulationTestFactory.CreateConfig(); Simulation simulation = new(definition, config, 55); ReplayRecorder recorder = new(); var batch = SimulationTestFactory.CreateTick(1, new MoveAxisChanged(new(1), 1, 0)); var result = simulation.Step(batch); recorder.Append(batch, result); var replay = recorder.Build(definition, config, 55); var divergentReplay = replay with { Ticks = ImmutableArray.Create(replay.Ticks[0] with { ExpectedStateHash = replay.Ticks[0].ExpectedStateHash + 1 }) }; var exception = Assert.Throws(() => ReplayPlayer.Play(divergentReplay, definition, config)); Assert.Contains("diverged", exception.Message.ToLowerInvariant()); } }