2026-07-18 20:43:13 +08:00
|
|
|
|
using OECS;
|
|
|
|
|
|
using R3;
|
|
|
|
|
|
|
|
|
|
|
|
namespace TicTacToe;
|
|
|
|
|
|
|
|
|
|
|
|
public static class Program
|
|
|
|
|
|
{
|
2026-07-18 21:06:57 +08:00
|
|
|
|
private static readonly string SavePath = Path.Combine(
|
|
|
|
|
|
AppContext.BaseDirectory, "save.bin");
|
|
|
|
|
|
|
2026-07-18 20:43:13 +08:00
|
|
|
|
public static void Main()
|
|
|
|
|
|
{
|
|
|
|
|
|
var world = new World();
|
|
|
|
|
|
var reactivityLog = new List<string>();
|
2026-07-18 21:06:57 +08:00
|
|
|
|
SetupReactivityLogging(world, reactivityLog);
|
2026-07-18 20:43:13 +08:00
|
|
|
|
|
2026-07-18 21:06:57 +08:00
|
|
|
|
// ── Try to load a saved game ──────────────────────────────────
|
2026-07-18 20:43:13 +08:00
|
|
|
|
|
2026-07-18 21:06:57 +08:00
|
|
|
|
bool loaded = false;
|
|
|
|
|
|
if (File.Exists(SavePath))
|
2026-07-18 20:43:13 +08:00
|
|
|
|
{
|
2026-07-18 21:06:57 +08:00
|
|
|
|
Console.Write("Saved game found. Load it? (y/n): ");
|
|
|
|
|
|
if (Console.ReadLine()?.Trim().ToLower() == "y")
|
|
|
|
|
|
{
|
|
|
|
|
|
using var stream = File.OpenRead(SavePath);
|
|
|
|
|
|
WorldSerializer.Load(world, stream);
|
|
|
|
|
|
loaded = true;
|
|
|
|
|
|
Console.WriteLine("Game loaded!");
|
|
|
|
|
|
Console.WriteLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-18 20:43:13 +08:00
|
|
|
|
|
2026-07-18 21:06:57 +08:00
|
|
|
|
// ── Load fresh board from CSV (if not loaded) ─────────────────
|
2026-07-18 20:43:13 +08:00
|
|
|
|
|
2026-07-18 21:06:57 +08:00
|
|
|
|
if (!loaded)
|
2026-07-18 20:43:13 +08:00
|
|
|
|
{
|
2026-07-18 21:06:57 +08:00
|
|
|
|
var csvPath = Path.Combine(AppContext.BaseDirectory, "Data", "board.csv");
|
|
|
|
|
|
CsvLoader.LoadCells(world, csvPath);
|
|
|
|
|
|
world.PostChanges();
|
2026-07-18 20:43:13 +08:00
|
|
|
|
|
2026-07-18 21:06:57 +08:00
|
|
|
|
world.SetSingleton(new GameState
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentPlayer = Player.X,
|
|
|
|
|
|
Status = GameStatus.Playing,
|
|
|
|
|
|
MoveCount = 0
|
|
|
|
|
|
});
|
|
|
|
|
|
world.PostChanges();
|
|
|
|
|
|
}
|
2026-07-18 20:43:13 +08:00
|
|
|
|
|
|
|
|
|
|
// ── Wire up systems ───────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
var group = new SystemGroup(world);
|
|
|
|
|
|
group.Add(new WinCheckSystem(world));
|
|
|
|
|
|
group.Add(new RenderSystem(world));
|
|
|
|
|
|
|
|
|
|
|
|
// ── Game loop ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
group.RunLogical();
|
|
|
|
|
|
PrintReactivityLog(reactivityLog);
|
|
|
|
|
|
|
|
|
|
|
|
while (true)
|
|
|
|
|
|
{
|
|
|
|
|
|
ref var state = ref world.GetSingleton<GameState>();
|
|
|
|
|
|
if (state.Status != GameStatus.Playing)
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
var input = Console.ReadLine();
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(input))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
2026-07-18 21:06:57 +08:00
|
|
|
|
// "save" command.
|
|
|
|
|
|
if (input.Trim().ToLower() == "save")
|
|
|
|
|
|
{
|
|
|
|
|
|
SaveGame(world);
|
|
|
|
|
|
Console.WriteLine(" Game saved.");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-18 20:43:13 +08:00
|
|
|
|
var parts = input.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
if (parts.Length != 2 ||
|
|
|
|
|
|
!int.TryParse(parts[0], out var row) ||
|
|
|
|
|
|
!int.TryParse(parts[1], out var col))
|
|
|
|
|
|
{
|
2026-07-18 21:06:57 +08:00
|
|
|
|
Console.WriteLine(" Invalid input. Try \"1 2\" or \"save\".");
|
2026-07-18 20:43:13 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (row < 0 || row > 2 || col < 0 || col > 2)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine(" Row and col must be 0–2.");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
world.Commands.Enqueue(new PlaceMarkCommand { Row = row, Col = col });
|
|
|
|
|
|
group.RunLogical();
|
|
|
|
|
|
PrintReactivityLog(reactivityLog);
|
2026-07-18 21:06:57 +08:00
|
|
|
|
|
|
|
|
|
|
// Auto-save after every move.
|
|
|
|
|
|
SaveGame(world);
|
2026-07-18 20:43:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Final state ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
Console.WriteLine();
|
|
|
|
|
|
Console.WriteLine("=== Game over ===");
|
|
|
|
|
|
PrintReactivityLog(reactivityLog);
|
2026-07-18 21:06:57 +08:00
|
|
|
|
|
|
|
|
|
|
// Clean up save file on game over.
|
|
|
|
|
|
if (File.Exists(SavePath))
|
|
|
|
|
|
File.Delete(SavePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void SaveGame(World world)
|
|
|
|
|
|
{
|
|
|
|
|
|
using var stream = File.Create(SavePath);
|
|
|
|
|
|
WorldSerializer.Save(world, stream);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void SetupReactivityLogging(World world, List<string> log)
|
|
|
|
|
|
{
|
|
|
|
|
|
world.ObserveEntityChanges().Subscribe(change =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (change.ComponentType != null) return;
|
|
|
|
|
|
log.Add($"[react] {change}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
world.ObserveComponentChanges<Mark>().Subscribe(change =>
|
|
|
|
|
|
{
|
|
|
|
|
|
log.Add($"[react] {change}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
world.ObserveComponentChanges<GameState>().Subscribe(change =>
|
|
|
|
|
|
{
|
|
|
|
|
|
log.Add($"[react] {change}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var markedQuery = world.Query().With<Cell>().With<Mark>().Build();
|
|
|
|
|
|
world.ObserveQuery(markedQuery).Subscribe(change =>
|
|
|
|
|
|
{
|
|
|
|
|
|
log.Add($"[react:query] {change}");
|
|
|
|
|
|
});
|
2026-07-18 20:43:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void PrintReactivityLog(List<string> log)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (log.Count == 0) return;
|
|
|
|
|
|
Console.WriteLine("── reactivity log ──");
|
|
|
|
|
|
foreach (var entry in log)
|
|
|
|
|
|
Console.WriteLine(entry);
|
|
|
|
|
|
Console.WriteLine();
|
|
|
|
|
|
log.Clear();
|
|
|
|
|
|
}
|
2026-07-18 21:06:57 +08:00
|
|
|
|
}
|