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,50 @@
using MessagePack;
using OECS;
namespace TicTacToe;
/// <summary>
/// Places the current player's mark on the cell at (Row, Col).
/// Validates that the cell is empty and the game is still playing.
/// </summary>
[MessagePackObject]
public struct PlaceMarkCommand : ICommand
{
[Key(0)] public int Row;
[Key(1)] public int Col;
public void Execute(World world)
{
ref var state = ref world.GetSingleton<GameState>();
if (state.Status != GameStatus.Playing)
return;
// Copy to locals so the lambda in ForEach can capture them
// (this is a struct, so `this` cannot be captured directly).
int row = Row;
int col = Col;
// Find the cell entity at (Row, Col) that has no Mark.
var query = world.Query().With<Cell>().Without<Mark>().Build();
Entity? target = null;
world.ForEach(query, (Entity entity, ref Cell cell) =>
{
if (cell.Row == row && cell.Col == col)
target = entity;
});
if (target == null)
return; // Cell already occupied or invalid position.
// Place the mark.
world.AddComponent(target.Value, new Mark { Player = state.CurrentPlayer });
world.MarkModified<Mark>(target.Value);
// Advance turn.
state.MoveCount++;
state.CurrentPlayer = state.CurrentPlayer == Player.X ? Player.O : Player.X;
world.MarkModified<GameState>(World.SingletonEntity);
}
}