feat: add TicTacToe example project

Implement a complete Tic-Tac-Toe game using OECS to demonstrate
ECS patterns, including:
- Command pattern for placing marks
- System groups for logical updates and rendering
- CSV loading for initial board state
- Reactivity logging for observing entity and component changes
This commit is contained in:
2026-07-18 20:43:13 +08:00
parent dddddbdbd6
commit a19861c080
14 changed files with 467 additions and 0 deletions
@@ -0,0 +1,67 @@
using OECS;
namespace TicTacToe;
/// <summary>
/// Prints the board to the console.
/// </summary>
public class RenderSystem : ISystem
{
public QueryDescriptor Query { get; }
public RenderSystem(World world)
{
Query = world.Query().With<Cell>().Build();
}
public void Run(World world)
{
ref var state = ref world.GetSingleton<GameState>();
// Build a 3×3 grid.
var grid = new char[3, 3];
for (int r = 0; r < 3; r++)
for (int c = 0; c < 3; c++)
grid[r, c] = '.';
// Fill in placed marks.
var markQuery = world.Query().With<Cell>().With<Mark>().Build();
world.ForEach(markQuery, (Entity entity, ref Cell cell, ref Mark mark) =>
{
grid[cell.Row, cell.Col] = mark.Player == Player.X ? 'X' : 'O';
});
Console.WriteLine();
Console.WriteLine(" Tic-Tac-Toe");
Console.WriteLine(" ═══════════");
Console.WriteLine();
Console.WriteLine(" 0 1 2");
Console.WriteLine(" ┌───┬───┬───┐");
for (int r = 0; r < 3; r++)
{
Console.Write($"{r} │");
for (int c = 0; c < 3; c++)
{
Console.Write($" {grid[r, c]} ");
if (c < 2) Console.Write("│");
}
Console.WriteLine("│");
if (r < 2) Console.WriteLine(" ├───┼───┼───┤");
}
Console.WriteLine(" └───┴───┴───┘");
Console.WriteLine();
// Status line.
var statusText = state.Status switch
{
GameStatus.Playing => $"{(state.CurrentPlayer == Player.X ? 'X' : 'O')}'s turn (move {state.MoveCount + 1})",
GameStatus.XWon => "X wins!",
GameStatus.OWon => "O wins!",
GameStatus.Draw => "It's a draw!",
_ => ""
};
Console.WriteLine($" {statusText}");
Console.WriteLine();
Console.Write(" Enter row col (e.g. \"1 2\"): ");
}
}
@@ -0,0 +1,89 @@
using OECS;
namespace TicTacToe;
/// <summary>
/// After each move, checks whether the game has been won or drawn.
/// Updates the GameState singleton accordingly.
/// </summary>
public class WinCheckSystem : ISystem
{
public QueryDescriptor Query { get; }
public WinCheckSystem(World world)
{
Query = world.Query().With<Cell>().With<Mark>().Build();
}
public void Run(World world)
{
ref var state = ref world.GetSingleton<GameState>();
if (state.Status != GameStatus.Playing)
return;
// Build a 3×3 grid of marks.
var grid = new Player[3, 3];
world.ForEach(Query, (Entity entity, ref Cell cell, ref Mark mark) =>
{
grid[cell.Row, cell.Col] = mark.Player;
});
// Check rows.
for (int r = 0; r < 3; r++)
{
if (TryGetWinner(grid[r, 0], grid[r, 1], grid[r, 2], out var winner))
{
SetWinner(world, ref state, winner);
return;
}
}
// Check columns.
for (int c = 0; c < 3; c++)
{
if (TryGetWinner(grid[0, c], grid[1, c], grid[2, c], out var winner))
{
SetWinner(world, ref state, winner);
return;
}
}
// Check diagonals.
if (TryGetWinner(grid[0, 0], grid[1, 1], grid[2, 2], out var diag1))
{
SetWinner(world, ref state, diag1);
return;
}
if (TryGetWinner(grid[0, 2], grid[1, 1], grid[2, 0], out var diag2))
{
SetWinner(world, ref state, diag2);
return;
}
// Check draw.
if (state.MoveCount >= 9)
{
state.Status = GameStatus.Draw;
world.MarkModified<GameState>(World.SingletonEntity);
}
}
private static bool TryGetWinner(Player a, Player b, Player c, out Player winner)
{
if (a != Player.None && a == b && b == c)
{
winner = a;
return true;
}
winner = Player.None;
return false;
}
private static void SetWinner(World world, ref GameState state, Player winner)
{
state.Status = winner == Player.X ? GameStatus.XWon : GameStatus.OWon;
world.MarkModified<GameState>(World.SingletonEntity);
}
}