using FluentAssertions; using Game.TicTacToe; using OECS; using OECS.PlayTest; using Xunit; namespace Game.TicTacToe.Tests; public class PlayTests { [Fact] public void Play_GreedyVsRandom() { var (world, group) = TestHelpers.SetupGame(); var agentX = new GreedyTicTacToeAgent(GreedyTicTacToeAgent.Strategy.WinOrBlock); var agentO = new RandomTicTacToeAgent(); var log = RunGame(world, group, agentX, agentO, "TicTacToe: Greedy X vs Random O"); var path = log.SaveTo("tic_tac_toe_greedy_vs_random.playlog"); ReadAndVerify(path); } [Fact] public void Play_RandomVsRandom() { var (world, group) = TestHelpers.SetupGame(); var agentX = new RandomTicTacToeAgent(); var agentO = new RandomTicTacToeAgent(); var log = RunGame(world, group, agentX, agentO, "TicTacToe: Random X vs Random O"); var path = log.SaveTo("tic_tac_toe_random_vs_random.playlog"); ReadAndVerify(path); } [Fact] public void Play_GreedyVsGreedy() { var (world, group) = TestHelpers.SetupGame(); var agentX = new GreedyTicTacToeAgent(GreedyTicTacToeAgent.Strategy.WinOrBlock); var agentO = new GreedyTicTacToeAgent(GreedyTicTacToeAgent.Strategy.WinOrBlock); var log = RunGame(world, group, agentX, agentO, "TicTacToe: Greedy X vs Greedy O"); var path = log.SaveTo("tic_tac_toe_greedy_vs_greedy.playlog"); ReadAndVerify(path); } // ── Game Runner ─────────────────────────────────────────────────── private static PlayLog RunGame(World world, SystemGroup group, IAgent agentX, IAgent agentO, string header) { var log = new PlayLog { Header = header }; using var capture = new ObservableCapture(world); capture.FormatWith(m => m.Player.ToString()); var moves = new List(); while (world.ReadSingleton().Status == GameStatus.Playing) { var state = world.ReadSingleton(); var agent = state.CurrentPlayer == Player.X ? agentX : agentO; var cmd = agent.Decide(world); world.Commands.Enqueue(cmd); group.RunLogical(); moves.Add($"{state.CurrentPlayer} → ({cmd.Row},{cmd.Col})"); } var final = world.ReadSingleton(); log.Header += $" — Result: {final.Status}"; log.AddSection("Moves", moves.Select((m, i) => $"{i + 1}. {m}")); log.AddSection("Reactivity", capture.GetLogLines()); log.FinalSnapshot = TestHelpers.SnapshotWorld(world); return log; } // ── File I/O ────────────────────────────────────────────────────── private static void ReadAndVerify(string path) { var content = File.ReadAllText(path); content.Should().Contain("--- Moves ---"); content.Should().Contain("--- Reactivity ---"); content.Should().Contain("--- Final State ---"); content.Should().Contain("Result:"); content.Should().NotContain("Result: Playing"); } // ── Agents ──────────────────────────────────────────────────────── private sealed class RandomTicTacToeAgent : GreedyAgent { protected override List GetLegalActions(World world) { return TestHelpers.GetEmptyCells(world) .Select(c => new PlaceMarkCommand { Row = c.Row, Col = c.Col }) .ToList(); } } private sealed class GreedyTicTacToeAgent : GreedyAgent { public enum Strategy { WinOrBlock } private readonly Strategy _strategy; private static readonly Random _rng = new(); public GreedyTicTacToeAgent(Strategy strategy) => _strategy = strategy; protected override List GetLegalActions(World world) { return TestHelpers.GetEmptyCells(world) .Select(c => new PlaceMarkCommand { Row = c.Row, Col = c.Col }) .ToList(); } protected override float ScoreAction(World world, PlaceMarkCommand action) { if (_strategy != Strategy.WinOrBlock) return 0f; var state = world.ReadSingleton(); var me = state.CurrentPlayer; var opponent = me == Player.X ? Player.O : Player.X; if (TestHelpers.WouldWin(world, action.Row, action.Col, me)) return 100f; if (TestHelpers.WouldWin(world, action.Row, action.Col, opponent)) return 50f; if (action.Row == 1 && action.Col == 1) return 10f; return 0f; } } }