refactor(tests): extract test helpers and clean up play tests
Refactor the TicTacToe test suite by moving shared logic, such as game setup, snapshotting, and win checking, into a new `TestHelpers` class. This simplifies `PlayTests.cs` and `GameFlowTests.cs` by removing redundant private methods and local agent implementations, replacing them with standardized utility calls from `OECS.PlayTest`.
This commit is contained in:
parent
4871f68fb8
commit
9ef387f010
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Game.TicTacToe\TicTacToe.csproj" />
|
<ProjectReference Include="..\Game.TicTacToe\TicTacToe.csproj" />
|
||||||
|
<ProjectReference Include="..\OECS.PlayTest\OECS.PlayTest.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NewGame_HasEmptyBoard()
|
public void NewGame_HasEmptyBoard()
|
||||||
{
|
{
|
||||||
var (world, _) = SetupGame();
|
var (world, _) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
var emptyCount = 0;
|
var emptyCount = 0;
|
||||||
foreach (var _ in world.Select(new Query<Cell>().Without<Mark>()))
|
foreach (var _ in world.Select(new Query<Cell>().Without<Mark>()))
|
||||||
|
|
@ -22,7 +22,7 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PlaceMark_ClaimsCell()
|
public void PlaceMark_ClaimsCell()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 });
|
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
@ -42,7 +42,7 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PlaceMark_TogglesPlayer()
|
public void PlaceMark_TogglesPlayer()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // X
|
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // X
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
@ -56,14 +56,13 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PlaceMark_CannotOverwriteClaimedCell()
|
public void PlaceMark_CannotOverwriteClaimedCell()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // X
|
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // X
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // O tries same cell
|
world.Commands.Enqueue(new PlaceMarkCommand { Row = 0, Col = 0 }); // O tries same cell
|
||||||
group.RunLogical();
|
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)>();
|
var marks = new List<(int Row, int Col, Player Player)>();
|
||||||
foreach (var it in world.Select<Cell, Mark>())
|
foreach (var it in world.Select<Cell, Mark>())
|
||||||
marks.Add((it.Val1.Row, it.Val1.Col, it.Val2.Player));
|
marks.Add((it.Val1.Row, it.Val1.Col, it.Val2.Player));
|
||||||
|
|
@ -75,10 +74,9 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void XWins_Row()
|
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
|
TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
|
||||||
PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
|
|
||||||
|
|
||||||
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.XWon);
|
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.XWon);
|
||||||
}
|
}
|
||||||
|
|
@ -86,10 +84,9 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OWins_Column()
|
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
|
TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (2, 2), (1, 2));
|
||||||
PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (2, 2), (1, 2));
|
|
||||||
|
|
||||||
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.OWon);
|
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.OWon);
|
||||||
}
|
}
|
||||||
|
|
@ -97,10 +94,9 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void XWins_Diagonal()
|
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
|
TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (1, 1), (1, 2), (2, 2));
|
||||||
PlayMoves(world, group, (0, 0), (1, 0), (1, 1), (1, 2), (2, 2));
|
|
||||||
|
|
||||||
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.XWon);
|
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.XWon);
|
||||||
}
|
}
|
||||||
|
|
@ -108,12 +104,9 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Draw_AllCellsFilled()
|
public void Draw_AllCellsFilled()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
// Fill all 9 cells without a winner.
|
TestHelpers.PlayMoves(world, group,
|
||||||
// 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),
|
(0, 0), (0, 1), (0, 2),
|
||||||
(1, 1), (1, 0), (1, 2),
|
(1, 1), (1, 0), (1, 2),
|
||||||
(2, 1), (2, 0), (2, 2));
|
(2, 1), (2, 0), (2, 2));
|
||||||
|
|
@ -124,14 +117,12 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MovesAfterGameOver_AreIgnored()
|
public void MovesAfterGameOver_AreIgnored()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
// X wins.
|
TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
|
||||||
PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
|
|
||||||
|
|
||||||
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.XWon);
|
world.ReadSingleton<GameState>().Status.Should().Be(GameStatus.XWon);
|
||||||
|
|
||||||
// Try to place another mark — should be ignored.
|
|
||||||
var moveCountBefore = world.ReadSingleton<GameState>().MoveCount;
|
var moveCountBefore = world.ReadSingleton<GameState>().MoveCount;
|
||||||
world.Commands.Enqueue(new PlaceMarkCommand { Row = 2, Col = 2 });
|
world.Commands.Enqueue(new PlaceMarkCommand { Row = 2, Col = 2 });
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
@ -142,24 +133,17 @@ public class GameFlowTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SaveLoad_RoundTrips()
|
public void SaveLoad_RoundTrips()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
// Play a few moves.
|
TestHelpers.PlayMoves(world, group, (0, 0), (1, 0));
|
||||||
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();
|
using var stream = new MemoryStream();
|
||||||
WorldSerializer.Save(world, stream);
|
WorldSerializer.Save(world, stream);
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
|
|
||||||
// Load into a new world.
|
|
||||||
var world2 = new World();
|
var world2 = new World();
|
||||||
WorldSerializer.Load(world2, stream);
|
WorldSerializer.Load(world2, stream);
|
||||||
|
|
||||||
// Verify state.
|
|
||||||
var state = world2.ReadSingleton<GameState>();
|
var state = world2.ReadSingleton<GameState>();
|
||||||
state.CurrentPlayer.Should().Be(Player.X);
|
state.CurrentPlayer.Should().Be(Player.X);
|
||||||
state.MoveCount.Should().Be(2);
|
state.MoveCount.Should().Be(2);
|
||||||
|
|
@ -170,37 +154,4 @@ public class GameFlowTests
|
||||||
|
|
||||||
marks.Should().BeEquivalentTo([(0, 0, Player.X), (1, 0, Player.O)]);
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Game.TicTacToe;
|
using Game.TicTacToe;
|
||||||
using OECS;
|
using OECS;
|
||||||
using R3;
|
using OECS.PlayTest;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Game.TicTacToe.Tests;
|
namespace Game.TicTacToe.Tests;
|
||||||
|
|
@ -11,133 +11,53 @@ public class PlayTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Play_GreedyVsRandom()
|
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 log = RunGame(world, group, agentX, agentO, "TicTacToe: Greedy X vs Random O");
|
||||||
var agentX = new GreedyAgent(GreedyAgent.Strategy.WinOrBlock);
|
|
||||||
var agentO = new RandomAgent();
|
|
||||||
|
|
||||||
// Subscribe reactivity.
|
var path = log.SaveTo("tic_tac_toe_greedy_vs_random.playlog");
|
||||||
world.ObserveComponentChanges<Mark>().Subscribe(change =>
|
|
||||||
{
|
|
||||||
var mark = world.ReadComponent<Mark>(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);
|
|
||||||
ReadAndVerify(path);
|
ReadAndVerify(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Play_RandomVsRandom()
|
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 log = RunGame(world, group, agentX, agentO, "TicTacToe: Random X vs Random O");
|
||||||
var agentX = new RandomAgent();
|
|
||||||
var agentO = new RandomAgent();
|
|
||||||
|
|
||||||
world.ObserveComponentChanges<Mark>().Subscribe(change =>
|
var path = log.SaveTo("tic_tac_toe_random_vs_random.playlog");
|
||||||
{
|
|
||||||
var mark = world.ReadComponent<Mark>(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);
|
|
||||||
ReadAndVerify(path);
|
ReadAndVerify(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Play_GreedyVsGreedy()
|
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 log = RunGame(world, group, agentX, agentO, "TicTacToe: Greedy X vs Greedy O");
|
||||||
var agentX = new GreedyAgent(GreedyAgent.Strategy.WinOrBlock);
|
|
||||||
var agentO = new GreedyAgent(GreedyAgent.Strategy.WinOrBlock);
|
|
||||||
|
|
||||||
world.ObserveComponentChanges<Mark>().Subscribe(change =>
|
var path = log.SaveTo("tic_tac_toe_greedy_vs_greedy.playlog");
|
||||||
{
|
|
||||||
var mark = world.ReadComponent<Mark>(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);
|
|
||||||
ReadAndVerify(path);
|
ReadAndVerify(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Play Log ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private sealed class PlayLog
|
|
||||||
{
|
|
||||||
public string Header { get; set; } = "";
|
|
||||||
public readonly List<string> Moves = new();
|
|
||||||
public readonly List<string> 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 ───────────────────────────────────────────────────
|
// ── Game Runner ───────────────────────────────────────────────────
|
||||||
|
|
||||||
private static (World, SystemGroup) SetupGame()
|
private static PlayLog RunGame(World world, SystemGroup group,
|
||||||
|
IAgent<PlaceMarkCommand> agentX, IAgent<PlaceMarkCommand> agentO, string header)
|
||||||
{
|
{
|
||||||
var world = new World();
|
var log = new PlayLog { Header = header };
|
||||||
var group = new SystemGroup(world);
|
|
||||||
group.Add(new WinCheckSystem());
|
|
||||||
|
|
||||||
for (int r = 0; r < 3; r++)
|
using var capture = new ObservableCapture(world);
|
||||||
for (int c = 0; c < 3; c++)
|
capture.FormatWith<Mark>(m => m.Player.ToString());
|
||||||
world.AddComponent(world.CreateEntity(), new Cell { Row = r, Col = c });
|
|
||||||
|
|
||||||
world.SetSingleton(new GameState
|
var moves = new List<string>();
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
while (world.ReadSingleton<GameState>().Status == GameStatus.Playing)
|
while (world.ReadSingleton<GameState>().Status == GameStatus.Playing)
|
||||||
{
|
{
|
||||||
var state = world.ReadSingleton<GameState>();
|
var state = world.ReadSingleton<GameState>();
|
||||||
|
|
@ -147,132 +67,76 @@ public class PlayTests
|
||||||
world.Commands.Enqueue(cmd);
|
world.Commands.Enqueue(cmd);
|
||||||
group.RunLogical();
|
group.RunLogical();
|
||||||
|
|
||||||
log.Moves.Add($"{state.CurrentPlayer} → ({cmd.Row},{cmd.Col})");
|
moves.Add($"{state.CurrentPlayer} → ({cmd.Row},{cmd.Col})");
|
||||||
}
|
}
|
||||||
|
|
||||||
var final = world.ReadSingleton<GameState>();
|
var final = world.ReadSingleton<GameState>();
|
||||||
log.Header += $" — Result: {final.Status}";
|
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)
|
return log;
|
||||||
{
|
|
||||||
var sb = new System.Text.StringBuilder();
|
|
||||||
var grid = new char?[3, 3];
|
|
||||||
foreach (var it in world.Select<Cell, Mark>())
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── File I/O ──────────────────────────────────────────────────────
|
// ── 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)
|
private static void ReadAndVerify(string path)
|
||||||
{
|
{
|
||||||
var content = File.ReadAllText(path);
|
var content = File.ReadAllText(path);
|
||||||
|
|
||||||
// Verify the play log is well-formed.
|
|
||||||
content.Should().Contain("--- Moves ---");
|
content.Should().Contain("--- Moves ---");
|
||||||
content.Should().Contain("--- Reactivity ---");
|
content.Should().Contain("--- Reactivity ---");
|
||||||
content.Should().Contain("--- Final Board ---");
|
content.Should().Contain("--- Final State ---");
|
||||||
|
|
||||||
// The game must have finished.
|
|
||||||
content.Should().Contain("Result:");
|
content.Should().Contain("Result:");
|
||||||
content.Should().NotContain("Result: Playing");
|
content.Should().NotContain("Result: Playing");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Agents ────────────────────────────────────────────────────────
|
// ── Agents ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private interface IAgent
|
private sealed class RandomTicTacToeAgent : GreedyAgent<PlaceMarkCommand>
|
||||||
{
|
{
|
||||||
PlaceMarkCommand Decide(World world);
|
protected override List<PlaceMarkCommand> GetLegalActions(World world)
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class RandomAgent : IAgent
|
|
||||||
{
|
{
|
||||||
private static readonly Random _rng = new();
|
return TestHelpers.GetEmptyCells(world)
|
||||||
public PlaceMarkCommand Decide(World world)
|
.Select(c => new PlaceMarkCommand { Row = c.Row, Col = c.Col })
|
||||||
{
|
.ToList();
|
||||||
var empty = GetEmptyCells(world);
|
|
||||||
var (row, col) = empty[_rng.Next(empty.Count)];
|
|
||||||
return new PlaceMarkCommand { Row = row, Col = col };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class GreedyAgent : IAgent
|
private sealed class GreedyTicTacToeAgent : GreedyAgent<PlaceMarkCommand>
|
||||||
{
|
{
|
||||||
public enum Strategy { WinOrBlock }
|
public enum Strategy { WinOrBlock }
|
||||||
private readonly Strategy _strategy;
|
private readonly Strategy _strategy;
|
||||||
private static readonly Random _rng = new();
|
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<PlaceMarkCommand> 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<GameState>();
|
var state = world.ReadSingleton<GameState>();
|
||||||
var me = state.CurrentPlayer;
|
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;
|
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))
|
if (TestHelpers.WouldWin(world, action.Row, action.Col, me))
|
||||||
return new PlaceMarkCommand { Row = 1, Col = 1 };
|
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 0f;
|
||||||
return new PlaceMarkCommand { Row = rr, Col = cc };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<(int Row, int Col)> GetEmptyCells(World world)
|
|
||||||
{
|
|
||||||
var empty = new List<(int, int)>();
|
|
||||||
foreach (var it in world.Select(new Query<Cell>().Without<Mark>()))
|
|
||||||
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<Cell, Mark>())
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Game.TicTacToe;
|
using Game.TicTacToe;
|
||||||
using OECS;
|
using OECS;
|
||||||
using R3;
|
using OECS.PlayTest;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
|
@ -36,10 +36,9 @@ public class SnapshotTests
|
||||||
MoveCount = 0
|
MoveCount = 0
|
||||||
});
|
});
|
||||||
|
|
||||||
var snapshot = SnapshotWorld(world);
|
var snapshot = TestHelpers.SnapshotWorld(world);
|
||||||
_output.WriteLine(snapshot);
|
_output.WriteLine(snapshot);
|
||||||
|
|
||||||
// Verify baseline properties.
|
|
||||||
snapshot.Should().Contain("GameState: X's turn, 0 moves");
|
snapshot.Should().Contain("GameState: X's turn, 0 moves");
|
||||||
snapshot.Should().Contain("Cells: 9 total, 0 marked");
|
snapshot.Should().Contain("Cells: 9 total, 0 marked");
|
||||||
}
|
}
|
||||||
|
|
@ -47,12 +46,11 @@ public class SnapshotTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Snapshot_AfterThreeMoves()
|
public void Snapshot_AfterThreeMoves()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
// X: (0,0), O: (1,1), X: (2,2)
|
TestHelpers.PlayMoves(world, group, (0, 0), (1, 1), (2, 2));
|
||||||
PlayMoves(world, group, (0, 0), (1, 1), (2, 2));
|
|
||||||
|
|
||||||
var snapshot = SnapshotWorld(world);
|
var snapshot = TestHelpers.SnapshotWorld(world);
|
||||||
_output.WriteLine(snapshot);
|
_output.WriteLine(snapshot);
|
||||||
|
|
||||||
snapshot.Should().Contain("GameState: O's turn, 3 moves");
|
snapshot.Should().Contain("GameState: O's turn, 3 moves");
|
||||||
|
|
@ -65,12 +63,11 @@ public class SnapshotTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Snapshot_XWins()
|
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
|
TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
|
||||||
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);
|
_output.WriteLine(snapshot);
|
||||||
|
|
||||||
snapshot.Should().Contain("GameState: XWon");
|
snapshot.Should().Contain("GameState: XWon");
|
||||||
|
|
@ -80,14 +77,14 @@ public class SnapshotTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Snapshot_Draw()
|
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),
|
(0, 0), (0, 1), (0, 2),
|
||||||
(1, 1), (1, 0), (1, 2),
|
(1, 1), (1, 0), (1, 2),
|
||||||
(2, 1), (2, 0), (2, 2));
|
(2, 1), (2, 0), (2, 2));
|
||||||
|
|
||||||
var snapshot = SnapshotWorld(world);
|
var snapshot = TestHelpers.SnapshotWorld(world);
|
||||||
_output.WriteLine(snapshot);
|
_output.WriteLine(snapshot);
|
||||||
|
|
||||||
snapshot.Should().Contain("GameState: Draw");
|
snapshot.Should().Contain("GameState: Draw");
|
||||||
|
|
@ -97,46 +94,29 @@ public class SnapshotTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Log_ReactivityDuringGame()
|
public void Log_ReactivityDuringGame()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
var log = new List<string>();
|
|
||||||
|
|
||||||
// Subscribe to all entity-level changes.
|
using var capture = new ObservableCapture(world);
|
||||||
world.ObserveEntityChanges().Subscribe(change =>
|
capture.FormatWith<Mark>(m => m.Player.ToString());
|
||||||
{
|
capture.FormatWith<GameState>(s => $"{s.Status} ({s.CurrentPlayer}, {s.MoveCount} moves)");
|
||||||
log.Add($"[entity] {change}");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to component-specific changes.
|
TestHelpers.PlayMoves(world, group, (0, 0), (1, 0), (0, 1), (1, 1), (0, 2));
|
||||||
world.ObserveComponentChanges<Mark>().Subscribe(change =>
|
|
||||||
{
|
|
||||||
log.Add($"[mark] {change}");
|
|
||||||
});
|
|
||||||
|
|
||||||
world.ObserveComponentChanges<GameState>().Subscribe(change =>
|
var logText = string.Join("\n", capture.GetLogLines());
|
||||||
{
|
|
||||||
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);
|
_output.WriteLine(logText);
|
||||||
|
|
||||||
// Verify key events appear in the log.
|
capture.GetLogLines().Should().Contain(l => l.Contains("ComponentAdded") && l.Contains("Mark"));
|
||||||
log.Should().Contain(l => l.Contains("ComponentAdded") && l.Contains("Mark"));
|
capture.GetLogLines().Should().Contain(l => l.Contains("ComponentModified") && l.Contains("GameState"));
|
||||||
log.Should().Contain(l => l.Contains("ComponentModified") && l.Contains("GameState"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Serialization_RoundTrip_PreservesSnapshot()
|
public void Serialization_RoundTrip_PreservesSnapshot()
|
||||||
{
|
{
|
||||||
var (world, group) = SetupGame();
|
var (world, group) = TestHelpers.SetupGame();
|
||||||
|
|
||||||
// Play a few moves.
|
TestHelpers.PlayMoves(world, group, (0, 0), (1, 1), (2, 2));
|
||||||
PlayMoves(world, group, (0, 0), (1, 1), (2, 2));
|
|
||||||
|
|
||||||
var before = SnapshotWorld(world);
|
var before = TestHelpers.SnapshotWorld(world);
|
||||||
|
|
||||||
using var stream = new MemoryStream();
|
using var stream = new MemoryStream();
|
||||||
WorldSerializer.Save(world, stream);
|
WorldSerializer.Save(world, stream);
|
||||||
|
|
@ -145,7 +125,7 @@ public class SnapshotTests
|
||||||
var world2 = new World();
|
var world2 = new World();
|
||||||
WorldSerializer.Load(world2, stream);
|
WorldSerializer.Load(world2, stream);
|
||||||
|
|
||||||
var after = SnapshotWorld(world2);
|
var after = TestHelpers.SnapshotWorld(world2);
|
||||||
|
|
||||||
_output.WriteLine("=== Before ===");
|
_output.WriteLine("=== Before ===");
|
||||||
_output.WriteLine(before);
|
_output.WriteLine(before);
|
||||||
|
|
@ -154,86 +134,4 @@ public class SnapshotTests
|
||||||
|
|
||||||
after.Should().Be(before);
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Produces a human-readable textual snapshot of the world state.
|
|
||||||
/// </summary>
|
|
||||||
private static string SnapshotWorld(World world)
|
|
||||||
{
|
|
||||||
var sb = new System.Text.StringBuilder();
|
|
||||||
|
|
||||||
// Singleton: GameState.
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
|
||||||
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<Cell, Mark>())
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
using Game.TicTacToe;
|
||||||
|
using OECS;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Game.TicTacToe.Tests;
|
||||||
|
|
||||||
|
internal static class TestHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a fresh TicTacToe world with 9 empty cells, GameState singleton, and WinCheckSystem.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enqueues and runs a sequence of moves.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a human-readable textual snapshot of the world state.
|
||||||
|
/// </summary>
|
||||||
|
public static string SnapshotWorld(World world)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var state = world.ReadSingleton<GameState>();
|
||||||
|
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<Cell, Mark>())
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the (Row, Col) of all empty cells.
|
||||||
|
/// </summary>
|
||||||
|
public static List<(int Row, int Col)> GetEmptyCells(World world)
|
||||||
|
{
|
||||||
|
var empty = new List<(int, int)>();
|
||||||
|
foreach (var it in world.Select(new Query<Cell>().Without<Mark>()))
|
||||||
|
empty.Add((it.Val1.Row, it.Val1.Col));
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether placing a mark at (row, col) for the given player would win the game.
|
||||||
|
/// </summary>
|
||||||
|
public static bool WouldWin(World world, int row, int col, Player player)
|
||||||
|
{
|
||||||
|
var grid = new Player[3, 3];
|
||||||
|
foreach (var it in world.Select<Cell, Mark>())
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue