207 lines
6.1 KiB
C#
207 lines
6.1 KiB
C#
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<Cell>().Without<Mark>()))
|
|
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<Cell, Mark>())
|
|
{
|
|
markedCount++;
|
|
it.Val1.Row.Should().Be(0);
|
|
it.Val1.Col.Should().Be(0);
|
|
it.Val2.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<GameState>().CurrentPlayer.Should().Be(Player.O);
|
|
|
|
world.Commands.Enqueue(new PlaceMarkCommand { Row = 1, Col = 0 }); // O
|
|
group.RunLogical();
|
|
world.ReadSingleton<GameState>().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<Cell, Mark>())
|
|
marks.Add((it.Val1.Row, it.Val1.Col, it.Val2.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<GameState>().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<GameState>().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<GameState>().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<GameState>().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<GameState>().Status.Should().Be(GameStatus.XWon);
|
|
|
|
// Try to place another mark — should be ignored.
|
|
var moveCountBefore = world.ReadSingleton<GameState>().MoveCount;
|
|
world.Commands.Enqueue(new PlaceMarkCommand { Row = 2, Col = 2 });
|
|
group.RunLogical();
|
|
|
|
world.ReadSingleton<GameState>().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<GameState>();
|
|
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<Cell, Mark>())
|
|
marks.Add((it.Val1.Row, it.Val1.Col, it.Val2.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();
|
|
}
|
|
}
|
|
}
|