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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user