diff --git a/Game.TicTacToe.Tests/Game.TicTacToe.Tests.csproj b/Game.TicTacToe.Tests/Game.TicTacToe.Tests.csproj
index 61b1908..afd9a9e 100644
--- a/Game.TicTacToe.Tests/Game.TicTacToe.Tests.csproj
+++ b/Game.TicTacToe.Tests/Game.TicTacToe.Tests.csproj
@@ -19,6 +19,7 @@
+
diff --git a/Game.TicTacToe.Tests/GameFlowTests.cs b/Game.TicTacToe.Tests/GameFlowTests.cs
index 7ef208b..ed15434 100644
--- a/Game.TicTacToe.Tests/GameFlowTests.cs
+++ b/Game.TicTacToe.Tests/GameFlowTests.cs
@@ -10,7 +10,7 @@ public class GameFlowTests
[Fact]
public void NewGame_HasEmptyBoard()
{
- var (world, _) = SetupGame();
+ var (world, _) = TestHelpers.SetupGame();
var emptyCount = 0;
foreach (var _ in world.Select(new Query().Without()))
@@ -22,7 +22,7 @@ public class GameFlowTests
[Fact]
public void PlaceMark_ClaimsCell()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 });
group.RunLogical();
@@ -42,7 +42,7 @@ public class GameFlowTests
[Fact]
public void PlaceMark_TogglesPlayer()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // X
group.RunLogical();
@@ -56,14 +56,13 @@ public class GameFlowTests
[Fact]
public void PlaceMark_CannotOverwriteClaimedCell()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.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.Val1.Row, it.Val1.Col, it.Val2.Player));
@@ -75,10 +74,9 @@ public class GameFlowTests
[Fact]
public void XWins_Row()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.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));
+ TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
world.ReadSingleton().Status.Should().Be(GameStatus.XWon);
}
@@ -86,10 +84,9 @@ public class GameFlowTests
[Fact]
public void OWins_Column()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.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));
+ TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (2, 2), (1, 2));
world.ReadSingleton().Status.Should().Be(GameStatus.OWon);
}
@@ -97,10 +94,9 @@ public class GameFlowTests
[Fact]
public void XWins_Diagonal()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.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));
+ TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (1, 1), (1, 2), (2, 2));
world.ReadSingleton().Status.Should().Be(GameStatus.XWon);
}
@@ -108,12 +104,9 @@ public class GameFlowTests
[Fact]
public void Draw_AllCellsFilled()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.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,
+ TestHelpers.PlayMoves(world, group,
(0, 0), (0, 1), (0, 2),
(1, 1), (1, 0), (1, 2),
(2, 1), (2, 0), (2, 2));
@@ -124,14 +117,12 @@ public class GameFlowTests
[Fact]
public void MovesAfterGameOver_AreIgnored()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
- // X wins.
- PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
+ TestHelpers.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();
@@ -142,24 +133,17 @@ public class GameFlowTests
[Fact]
public void SaveLoad_RoundTrips()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.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();
+ TestHelpers.PlayMoves(world, group, (0, 0), (1, 0));
- // 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);
@@ -170,37 +154,4 @@ public class GameFlowTests
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();
- }
- }
-}
+}
\ No newline at end of file
diff --git a/Game.TicTacToe.Tests/PlayTests.cs b/Game.TicTacToe.Tests/PlayTests.cs
index b3792bd..0dbfc81 100644
--- a/Game.TicTacToe.Tests/PlayTests.cs
+++ b/Game.TicTacToe.Tests/PlayTests.cs
@@ -1,7 +1,7 @@
using FluentAssertions;
using Game.TicTacToe;
using OECS;
-using R3;
+using OECS.PlayTest;
using Xunit;
namespace Game.TicTacToe.Tests;
@@ -11,133 +11,53 @@ public class PlayTests
[Fact]
public void Play_GreedyVsRandom()
{
- var log = new PlayLog();
+ var (world, group) = TestHelpers.SetupGame();
+ var agentX = new GreedyTicTacToeAgent(GreedyTicTacToeAgent.Strategy.WinOrBlock);
+ var agentO = new RandomTicTacToeAgent();
- var (world, group) = SetupGame();
- var agentX = new GreedyAgent(GreedyAgent.Strategy.WinOrBlock);
- var agentO = new RandomAgent();
+ var log = RunGame(world, group, agentX, agentO, "TicTacToe: Greedy X vs Random O");
- // Subscribe reactivity.
- world.ObserveComponentChanges().Subscribe(change =>
- {
- var mark = world.ReadComponent(change.Entity);
- log.Reactivity.Add($"{change.Kind} Mark = {mark} on {change.Entity}");
- });
-
- log.Header = "TicTacToe: Greedy X vs Random O";
-
- RunGame(world, group, agentX, agentO, log);
-
- log.FinalSnapshot = SnapshotWorld(world);
-
- var path = SavePlayLog("tic_tac_toe_greedy_vs_random.playlog", log);
+ var path = log.SaveTo("tic_tac_toe_greedy_vs_random.playlog");
ReadAndVerify(path);
}
[Fact]
public void Play_RandomVsRandom()
{
- var log = new PlayLog();
+ var (world, group) = TestHelpers.SetupGame();
+ var agentX = new RandomTicTacToeAgent();
+ var agentO = new RandomTicTacToeAgent();
- var (world, group) = SetupGame();
- var agentX = new RandomAgent();
- var agentO = new RandomAgent();
+ var log = RunGame(world, group, agentX, agentO, "TicTacToe: Random X vs Random O");
- world.ObserveComponentChanges().Subscribe(change =>
- {
- var mark = world.ReadComponent(change.Entity);
- log.Reactivity.Add($"{change.Kind} Mark = {mark} on {change.Entity}");
- });
-
- log.Header = "TicTacToe: Random X vs Random O";
-
- RunGame(world, group, agentX, agentO, log);
-
- log.FinalSnapshot = SnapshotWorld(world);
-
- var path = SavePlayLog("tic_tac_toe_random_vs_random.playlog", log);
+ var path = log.SaveTo("tic_tac_toe_random_vs_random.playlog");
ReadAndVerify(path);
}
[Fact]
public void Play_GreedyVsGreedy()
{
- var log = new PlayLog();
+ var (world, group) = TestHelpers.SetupGame();
+ var agentX = new GreedyTicTacToeAgent(GreedyTicTacToeAgent.Strategy.WinOrBlock);
+ var agentO = new GreedyTicTacToeAgent(GreedyTicTacToeAgent.Strategy.WinOrBlock);
- var (world, group) = SetupGame();
- var agentX = new GreedyAgent(GreedyAgent.Strategy.WinOrBlock);
- var agentO = new GreedyAgent(GreedyAgent.Strategy.WinOrBlock);
+ var log = RunGame(world, group, agentX, agentO, "TicTacToe: Greedy X vs Greedy O");
- world.ObserveComponentChanges().Subscribe(change =>
- {
- var mark = world.ReadComponent(change.Entity);
- log.Reactivity.Add($"{change.Kind} Mark = {mark} on {change.Entity}");
- });
-
- log.Header = "TicTacToe: Greedy X vs Greedy O";
-
- RunGame(world, group, agentX, agentO, log);
-
- log.FinalSnapshot = SnapshotWorld(world);
-
- var path = SavePlayLog("tic_tac_toe_greedy_vs_greedy.playlog", log);
+ var path = log.SaveTo("tic_tac_toe_greedy_vs_greedy.playlog");
ReadAndVerify(path);
}
- // ── Play Log ──────────────────────────────────────────────────────
-
- private sealed class PlayLog
- {
- public string Header { get; set; } = "";
- public readonly List Moves = new();
- public readonly List Reactivity = new();
- public string FinalSnapshot { get; set; } = "";
-
- public string Build()
- {
- var sb = new System.Text.StringBuilder();
- sb.AppendLine(Header);
- sb.AppendLine(new string('=', Header.Length));
- sb.AppendLine();
- sb.AppendLine("--- Moves ---");
- for (int i = 0; i < Moves.Count; i++)
- sb.AppendLine($" {i + 1}. {Moves[i]}");
- sb.AppendLine();
- sb.AppendLine("--- Reactivity ---");
- foreach (var entry in Reactivity)
- sb.AppendLine($" {entry}");
- sb.AppendLine();
- sb.AppendLine("--- Final Board ---");
- sb.AppendLine(FinalSnapshot);
- return sb.ToString();
- }
- }
-
// ── Game Runner ───────────────────────────────────────────────────
- private static (World, SystemGroup) SetupGame()
+ private static PlayLog RunGame(World world, SystemGroup group,
+ IAgent agentX, IAgent agentO, string header)
{
- var world = new World();
- var group = new SystemGroup(world);
- group.Add(new WinCheckSystem());
+ var log = new PlayLog { Header = header };
- for (int r = 0; r < 3; r++)
- for (int c = 0; c < 3; c++)
- world.AddComponent(world.CreateEntity(), new Cell { Row = r, Col = c });
+ using var capture = new ObservableCapture(world);
+ capture.FormatWith(m => m.Player.ToString());
- world.SetSingleton(new GameState
- {
- CurrentPlayer = Player.X,
- Status = GameStatus.Playing,
- MoveCount = 0
- });
- world.PostChanges();
-
- return (world, group);
- }
-
- private static void RunGame(World world, SystemGroup group, IAgent agentX, IAgent agentO, PlayLog log)
- {
+ var moves = new List();
while (world.ReadSingleton().Status == GameStatus.Playing)
{
var state = world.ReadSingleton();
@@ -147,132 +67,76 @@ public class PlayTests
world.Commands.Enqueue(cmd);
group.RunLogical();
- log.Moves.Add($"{state.CurrentPlayer} → ({cmd.Row},{cmd.Col})");
+ moves.Add($"{state.CurrentPlayer} → ({cmd.Row},{cmd.Col})");
}
var final = world.ReadSingleton();
log.Header += $" — Result: {final.Status}";
- }
- // ── Snapshot ──────────────────────────────────────────────────────
+ log.AddSection("Moves", moves.Select((m, i) => $"{i + 1}. {m}"));
+ log.AddSection("Reactivity", capture.GetLogLines());
+ log.FinalSnapshot = TestHelpers.SnapshotWorld(world);
- private static string SnapshotWorld(World world)
- {
- var sb = new System.Text.StringBuilder();
- 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';
-
- for (int r = 0; r < 3; r++)
- {
- sb.Append(" ");
- for (int c = 0; c < 3; c++)
- sb.Append(grid[r, c]?.ToString() ?? ".");
- sb.AppendLine();
- }
- return sb.ToString().TrimEnd();
+ return log;
}
// ── File I/O ──────────────────────────────────────────────────────
- private static string SavePlayLog(string filename, PlayLog log)
- {
- var dir = Path.Combine(AppContext.BaseDirectory, "playlogs");
- Directory.CreateDirectory(dir);
- var path = Path.Combine(dir, filename);
- File.WriteAllText(path, log.Build());
- return path;
- }
-
private static void ReadAndVerify(string path)
{
var content = File.ReadAllText(path);
- // Verify the play log is well-formed.
content.Should().Contain("--- Moves ---");
content.Should().Contain("--- Reactivity ---");
- content.Should().Contain("--- Final Board ---");
-
- // The game must have finished.
+ content.Should().Contain("--- Final State ---");
content.Should().Contain("Result:");
content.Should().NotContain("Result: Playing");
}
// ── Agents ────────────────────────────────────────────────────────
- private interface IAgent
+ private sealed class RandomTicTacToeAgent : GreedyAgent
{
- PlaceMarkCommand Decide(World world);
- }
-
- private sealed class RandomAgent : IAgent
- {
- private static readonly Random _rng = new();
- public PlaceMarkCommand Decide(World world)
+ protected override List GetLegalActions(World world)
{
- var empty = GetEmptyCells(world);
- var (row, col) = empty[_rng.Next(empty.Count)];
- return new PlaceMarkCommand { Row = row, Col = col };
+ return TestHelpers.GetEmptyCells(world)
+ .Select(c => new PlaceMarkCommand { Row = c.Row, Col = c.Col })
+ .ToList();
}
}
- private sealed class GreedyAgent : IAgent
+ private sealed class GreedyTicTacToeAgent : GreedyAgent
{
public enum Strategy { WinOrBlock }
private readonly Strategy _strategy;
private static readonly Random _rng = new();
- public GreedyAgent(Strategy strategy) => _strategy = strategy;
- public PlaceMarkCommand Decide(World world)
+ 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 empty = GetEmptyCells(world);
-
- foreach (var (r, c) in empty)
- if (WouldWin(world, r, c, me))
- return new PlaceMarkCommand { Row = r, Col = c };
-
var opponent = me == Player.X ? Player.O : Player.X;
- foreach (var (r, c) in empty)
- if (WouldWin(world, r, c, opponent))
- return new PlaceMarkCommand { Row = r, Col = c };
- if (empty.Any(c => c.Row == 1 && c.Col == 1))
- return new PlaceMarkCommand { Row = 1, Col = 1 };
+ 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;
- var (rr, cc) = empty[_rng.Next(empty.Count)];
- return new PlaceMarkCommand { Row = rr, Col = cc };
+ return 0f;
}
}
-
- private static List<(int Row, int Col)> GetEmptyCells(World world)
- {
- var empty = new List<(int, int)>();
- foreach (var it in world.Select(new Query().Without()))
- empty.Add((it.Val1.Row, it.Val1.Col));
- return empty;
- }
-
- private static bool WouldWin(World world, int row, int col, Player player)
- {
- var grid = new Player[3, 3];
- foreach (var it in world.Select())
- grid[it.Val1.Row, it.Val1.Col] = it.Val2.Player;
- grid[row, col] = player;
-
- for (int r = 0; r < 3; r++)
- if (grid[r, 0] == player && grid[r, 1] == player && grid[r, 2] == player)
- return true;
- for (int c = 0; c < 3; c++)
- if (grid[0, c] == player && grid[1, c] == player && grid[2, c] == player)
- return true;
- if (grid[0, 0] == player && grid[1, 1] == player && grid[2, 2] == player)
- return true;
- if (grid[0, 2] == player && grid[1, 1] == player && grid[2, 0] == player)
- return true;
-
- return false;
- }
-}
+}
\ No newline at end of file
diff --git a/Game.TicTacToe.Tests/SnapshotTests.cs b/Game.TicTacToe.Tests/SnapshotTests.cs
index 39acedb..847184e 100644
--- a/Game.TicTacToe.Tests/SnapshotTests.cs
+++ b/Game.TicTacToe.Tests/SnapshotTests.cs
@@ -1,7 +1,7 @@
using FluentAssertions;
using Game.TicTacToe;
using OECS;
-using R3;
+using OECS.PlayTest;
using Xunit;
using Xunit.Abstractions;
@@ -36,10 +36,9 @@ public class SnapshotTests
MoveCount = 0
});
- var snapshot = SnapshotWorld(world);
+ var snapshot = TestHelpers.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");
}
@@ -47,12 +46,11 @@ public class SnapshotTests
[Fact]
public void Snapshot_AfterThreeMoves()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
- // X: (0,0), O: (1,1), X: (2,2)
- PlayMoves(world, group, (0, 0), (1, 1), (2, 2));
+ TestHelpers.PlayMoves(world, group, (0, 0), (1, 1), (2, 2));
- var snapshot = SnapshotWorld(world);
+ var snapshot = TestHelpers.SnapshotWorld(world);
_output.WriteLine(snapshot);
snapshot.Should().Contain("GameState: O's turn, 3 moves");
@@ -65,12 +63,11 @@ public class SnapshotTests
[Fact]
public void Snapshot_XWins()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.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));
+ TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
- var snapshot = SnapshotWorld(world);
+ var snapshot = TestHelpers.SnapshotWorld(world);
_output.WriteLine(snapshot);
snapshot.Should().Contain("GameState: XWon");
@@ -80,14 +77,14 @@ public class SnapshotTests
[Fact]
public void Snapshot_Draw()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
- PlayMoves(world, group,
+ TestHelpers.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);
+ var snapshot = TestHelpers.SnapshotWorld(world);
_output.WriteLine(snapshot);
snapshot.Should().Contain("GameState: Draw");
@@ -97,46 +94,29 @@ public class SnapshotTests
[Fact]
public void Log_ReactivityDuringGame()
{
- var (world, group) = SetupGame();
- var log = new List();
+ var (world, group) = TestHelpers.SetupGame();
- // Subscribe to all entity-level changes.
- world.ObserveEntityChanges().Subscribe(change =>
- {
- log.Add($"[entity] {change}");
- });
+ using var capture = new ObservableCapture(world);
+ capture.FormatWith(m => m.Player.ToString());
+ capture.FormatWith(s => $"{s.Status} ({s.CurrentPlayer}, {s.MoveCount} moves)");
- // Subscribe to component-specific changes.
- world.ObserveComponentChanges().Subscribe(change =>
- {
- log.Add($"[mark] {change}");
- });
+ TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
- 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);
+ var logText = string.Join("\n", capture.GetLogLines());
_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"));
+ capture.GetLogLines().Should().Contain(l => l.Contains("ComponentAdded") && l.Contains("Mark"));
+ capture.GetLogLines().Should().Contain(l => l.Contains("ComponentModified") && l.Contains("GameState"));
}
[Fact]
public void Serialization_RoundTrip_PreservesSnapshot()
{
- var (world, group) = SetupGame();
+ var (world, group) = TestHelpers.SetupGame();
- // Play a few moves.
- PlayMoves(world, group, (0, 0), (1, 1), (2, 2));
+ TestHelpers.PlayMoves(world, group, (0, 0), (1, 1), (2, 2));
- var before = SnapshotWorld(world);
+ var before = TestHelpers.SnapshotWorld(world);
using var stream = new MemoryStream();
WorldSerializer.Save(world, stream);
@@ -145,7 +125,7 @@ public class SnapshotTests
var world2 = new World();
WorldSerializer.Load(world2, stream);
- var after = SnapshotWorld(world2);
+ var after = TestHelpers.SnapshotWorld(world2);
_output.WriteLine("=== Before ===");
_output.WriteLine(before);
@@ -154,86 +134,4 @@ public class SnapshotTests
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();
- }
-}
+}
\ No newline at end of file
diff --git a/Game.TicTacToe.Tests/TestHelpers.cs b/Game.TicTacToe.Tests/TestHelpers.cs
new file mode 100644
index 0000000..78288ff
--- /dev/null
+++ b/Game.TicTacToe.Tests/TestHelpers.cs
@@ -0,0 +1,126 @@
+using Game.TicTacToe;
+using OECS;
+using System.Text;
+
+namespace Game.TicTacToe.Tests;
+
+internal static class TestHelpers
+{
+ ///
+ /// Creates a fresh TicTacToe world with 9 empty cells, GameState singleton, and WinCheckSystem.
+ ///
+ public static (World World, SystemGroup Group) 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);
+ }
+
+ ///
+ /// Enqueues and runs a sequence of moves.
+ ///
+ public 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();
+ }
+ }
+
+ ///
+ /// Returns a human-readable textual snapshot of the world state.
+ ///
+ public static string SnapshotWorld(World world)
+ {
+ var sb = new StringBuilder();
+ 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}");
+
+ 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();
+ }
+
+ ///
+ /// Returns the (Row, Col) of all empty cells.
+ ///
+ public static List<(int Row, int Col)> GetEmptyCells(World world)
+ {
+ var empty = new List<(int, int)>();
+ foreach (var it in world.Select(new Query().Without()))
+ empty.Add((it.Val1.Row, it.Val1.Col));
+ return empty;
+ }
+
+ ///
+ /// Checks whether placing a mark at (row, col) for the given player would win the game.
+ ///
+ public static bool WouldWin(World world, int row, int col, Player player)
+ {
+ var grid = new Player[3, 3];
+ foreach (var it in world.Select| ())
+ grid[it.Val1.Row, it.Val1.Col] = it.Val2.Player;
+ grid[row, col] = player;
+
+ for (int r = 0; r < 3; r++)
+ if (grid[r, 0] == player && grid[r, 1] == player && grid[r, 2] == player)
+ return true;
+ for (int c = 0; c < 3; c++)
+ if (grid[0, c] == player && grid[1, c] == player && grid[2, c] == player)
+ return true;
+ if (grid[0, 0] == player && grid[1, 1] == player && grid[2, 2] == player)
+ return true;
+ if (grid[0, 2] == player && grid[1, 1] == player && grid[2, 0] == player)
+ return true;
+
+ return false;
+ }
+}
\ No newline at end of file
| | | | | | | | |