240 lines
6.9 KiB
C#
240 lines
6.9 KiB
C#
using FluentAssertions;
|
|
using Game.TicTacToe;
|
|
using OECS;
|
|
using R3;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace Game.TicTacToe.Tests;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<string>();
|
|
|
|
// Subscribe to all entity-level changes.
|
|
world.ObserveEntityChanges().Subscribe(change =>
|
|
{
|
|
log.Add($"[entity] {change}");
|
|
});
|
|
|
|
// Subscribe to component-specific changes.
|
|
world.ObserveComponentChanges<Mark>().Subscribe(change =>
|
|
{
|
|
log.Add($"[mark] {change}");
|
|
});
|
|
|
|
world.ObserveComponentChanges<GameState>().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();
|
|
}
|
|
}
|
|
|
|
/// <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.Item1.Row, it.Item1.Col] =
|
|
it.Item2.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();
|
|
}
|
|
}
|