using FluentAssertions; using Game.TicTacToe; using OECS; using Xunit; namespace Game.TicTacToe.Tests; public class GameFlowTests { [Fact] public void NewGame_HasEmptyBoard() { var (world, _) = SetupGame(); var emptyCount = 0; foreach (var _ in world.Select(new Query().Without())) emptyCount++; emptyCount.Should().Be(9); } [Fact] public void PlaceMark_ClaimsCell() { var (world, group) = SetupGame(); world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); group.RunLogical(); var markedCount = 0; foreach (var it in world.Select()) { markedCount++; it.Item1.Row.Should().Be(0); it.Item1.Col.Should().Be(0); it.Item2.Player.Should().Be(Player.X); } markedCount.Should().Be(1); } [Fact] public void PlaceMark_TogglesPlayer() { var (world, group) = SetupGame(); world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // X group.RunLogical(); world.ReadSingleton().CurrentPlayer.Should().Be(Player.O); world.Commands.Enqueue(new PlaceMarkCommand { Row = 1, Col = 0 }); // O group.RunLogical(); world.ReadSingleton().CurrentPlayer.Should().Be(Player.X); } [Fact] public void PlaceMark_CannotOverwriteClaimedCell() { var (world, group) = SetupGame(); world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // X group.RunLogical(); world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // O tries same cell group.RunLogical(); // Only one mark should exist at (0,0), and it should still be X. var marks = new List<(int Row, int Col, Player Player)>(); foreach (var it in world.Select()) marks.Add((it.Item1.Row, it.Item1.Col, it.Item2.Player)); marks.Should().ContainSingle() .Which.Should().Be((0, 0, Player.X)); } [Fact] public void XWins_Row() { 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)); world.ReadSingleton().Status.Should().Be(GameStatus.XWon); } [Fact] public void OWins_Column() { var (world, group) = SetupGame(); // X: (0,0), O: (1,0), X: (0,1), O: (1,1), X: (2,2), O: (1,2) → O wins col 0 PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (2, 2), (1, 2)); world.ReadSingleton().Status.Should().Be(GameStatus.OWon); } [Fact] public void XWins_Diagonal() { var (world, group) = SetupGame(); // X: (0,0), O: (1,0), X: (1,1), O: (1,2), X: (2,2) → X wins diagonal PlayMoves(world, group, (0, 0), (1, 0), (1, 1), (1, 2), (2, 2)); world.ReadSingleton().Status.Should().Be(GameStatus.XWon); } [Fact] public void Draw_AllCellsFilled() { var (world, group) = SetupGame(); // Fill all 9 cells without a winner. // X: (0,0) (0,2) (1,0) (2,1) (2,2) // O: (0,1) (1,1) (1,2) (2,0) PlayMoves(world, group, (0, 0), (0, 1), (0, 2), (1, 1), (1, 0), (1, 2), (2, 1), (2, 0), (2, 2)); world.ReadSingleton().Status.Should().Be(GameStatus.Draw); } [Fact] public void MovesAfterGameOver_AreIgnored() { var (world, group) = SetupGame(); // X wins. PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2)); world.ReadSingleton().Status.Should().Be(GameStatus.XWon); // Try to place another mark — should be ignored. var moveCountBefore = world.ReadSingleton().MoveCount; world.Commands.Enqueue(new PlaceMarkCommand { Row = 2, Col = 2 }); group.RunLogical(); world.ReadSingleton().MoveCount.Should().Be(moveCountBefore); } [Fact] public void SaveLoad_RoundTrips() { var (world, group) = SetupGame(); // Play a few moves. world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); group.RunLogical(); world.Commands.Enqueue(new PlaceMarkCommand { Row = 1, Col = 0 }); group.RunLogical(); // Save. using var stream = new MemoryStream(); WorldSerializer.Save(world, stream); stream.Position = 0; // Load into a new world. var world2 = new World(); WorldSerializer.Load(world2, stream); // Verify state. var state = world2.ReadSingleton(); state.CurrentPlayer.Should().Be(Player.X); state.MoveCount.Should().Be(2); var marks = new List<(int Row, int Col, Player Player)>(); foreach (var it in world2.Select()) marks.Add((it.Item1.Row, it.Item1.Col, it.Item2.Player)); marks.Should().BeEquivalentTo([(0, 0, Player.X), (1, 0, Player.O)]); } // ── Helpers ─────────────────────────────────────────────────────── private static (World, SystemGroup) SetupGame() { var world = new World(); var group = new SystemGroup(world); group.Add(new WinCheckSystem()); // Create the 9 cells. 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(); } } }