using FluentAssertions; using Game.TicTacToe; using OECS; using R3; using Xunit; using Xunit.Abstractions; namespace Game.TicTacToe.Tests; /// /// Baseline snapshot and log tests for TicTacToe. /// Captures the world state as text and R3 change logs so they can be /// reviewed manually and compared across changes. /// public class SnapshotTests { private readonly ITestOutputHelper _output; public SnapshotTests(ITestOutputHelper output) { _output = output; } [Fact] public void Snapshot_InitialBoard() { var world = new World(); for (int r = 0; r < 3; r++) for (int c = 0; c < 3; c++) world.AddComponent(world.CreateEntity(), new Cell { Row = r, Col = c }); world.SetSingleton(new GameState { CurrentPlayer = Player.X, Status = GameStatus.Playing, MoveCount = 0 }); var snapshot = SnapshotWorld(world); _output.WriteLine(snapshot); // Verify baseline properties. snapshot.Should().Contain("GameState: X's turn, 0 moves"); snapshot.Should().Contain("Cells: 9 total, 0 marked"); } [Fact] public void Snapshot_AfterThreeMoves() { var (world, group) = SetupGame(); // X: (0,0), O: (1,1), X: (2,2) PlayMoves(world, group, (0, 0), (1, 1), (2, 2)); var snapshot = SnapshotWorld(world); _output.WriteLine(snapshot); snapshot.Should().Contain("GameState: O's turn, 3 moves"); snapshot.Should().Contain("Cells: 9 total, 3 marked"); snapshot.Should().Contain("(0,0): X"); snapshot.Should().Contain("(1,1): O"); snapshot.Should().Contain("(2,2): X"); } [Fact] public void Snapshot_XWins() { var (world, group) = SetupGame(); // X: (0,0), O: (1,0), X: (0,1), O: (1,1), X: (0,2) → X wins row 0 PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2)); var snapshot = SnapshotWorld(world); _output.WriteLine(snapshot); snapshot.Should().Contain("GameState: XWon"); snapshot.Should().Contain("Cells: 9 total, 5 marked"); } [Fact] public void Snapshot_Draw() { var (world, group) = SetupGame(); PlayMoves(world, group, (0, 0), (0, 1), (0, 2), (1, 1), (1, 0), (1, 2), (2, 1), (2, 0), (2, 2)); var snapshot = SnapshotWorld(world); _output.WriteLine(snapshot); snapshot.Should().Contain("GameState: Draw"); snapshot.Should().Contain("Cells: 9 total, 9 marked"); } [Fact] public void Log_ReactivityDuringGame() { var (world, group) = SetupGame(); var log = new List(); // Subscribe to all entity-level changes. world.ObserveEntityChanges().Subscribe(change => { log.Add($"[entity] {change}"); }); // Subscribe to component-specific changes. world.ObserveComponentChanges().Subscribe(change => { log.Add($"[mark] {change}"); }); world.ObserveComponentChanges().Subscribe(change => { log.Add($"[gamestate] {change}"); }); // Play a full game: X wins on row 0. PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2)); var logText = string.Join("\n", log); _output.WriteLine(logText); // Verify key events appear in the log. log.Should().Contain(l => l.Contains("ComponentAdded") && l.Contains("Mark")); log.Should().Contain(l => l.Contains("ComponentModified") && l.Contains("GameState")); } [Fact] public void Serialization_RoundTrip_PreservesSnapshot() { var (world, group) = SetupGame(); // Play a few moves. PlayMoves(world, group, (0, 0), (1, 1), (2, 2)); var before = SnapshotWorld(world); using var stream = new MemoryStream(); WorldSerializer.Save(world, stream); stream.Position = 0; var world2 = new World(); WorldSerializer.Load(world2, stream); var after = SnapshotWorld(world2); _output.WriteLine("=== Before ==="); _output.WriteLine(before); _output.WriteLine("=== After ==="); _output.WriteLine(after); after.Should().Be(before); } // ── Helpers ─────────────────────────────────────────────────────── private static (World, SystemGroup) SetupGame() { var world = new World(); var group = new SystemGroup(world); group.Add(new WinCheckSystem()); for (int r = 0; r < 3; r++) for (int c = 0; c < 3; c++) world.AddComponent(world.CreateEntity(), new Cell { Row = r, Col = c }); world.SetSingleton(new GameState { CurrentPlayer = Player.X, Status = GameStatus.Playing, MoveCount = 0 }); world.PostChanges(); return (world, group); } private static void PlayMoves(World world, SystemGroup group, params (int Row, int Col)[] moves) { foreach (var (row, col) in moves) { world.Commands.Enqueue(new PlaceMarkCommand { Row = row, Col = col }); group.RunLogical(); } } /// /// Produces a human-readable textual snapshot of the world state. /// private static string SnapshotWorld(World world) { var sb = new System.Text.StringBuilder(); // Singleton: GameState. var state = world.ReadSingleton(); var statusText = state.Status switch { GameStatus.Playing => $"{(state.CurrentPlayer == Player.X ? 'X' : 'O')}'s turn, {state.MoveCount} moves", GameStatus.XWon => "XWon", GameStatus.OWon => "OWon", GameStatus.Draw => "Draw", _ => "Unknown" }; sb.AppendLine($"GameState: {statusText}"); // Board: build a 3x3 grid. var grid = new char?[3, 3]; foreach (var it in world.Select()) grid[it.Val1.Row, it.Val1.Col] = it.Val2.Player == Player.X ? 'X' : 'O'; int totalCells = 0; int markedCells = 0; for (int r = 0; r < 3; r++) { for (int c = 0; c < 3; c++) { totalCells++; var mark = grid[r, c]; if (mark.HasValue) { markedCells++; sb.AppendLine($" ({r},{c}): {mark.Value}"); } else { sb.AppendLine($" ({r},{c}): ."); } } } sb.AppendLine($"Cells: {totalCells} total, {markedCells} marked"); return sb.ToString().TrimEnd(); } }