using OECS; using R3; namespace TicTacToe; public static class Program { private static readonly string SavePath = Path.Combine( AppContext.BaseDirectory, "save.bin"); public static void Main() { var world = new World(); var reactivityLog = new List(); SetupReactivityLogging(world, reactivityLog); // ── Try to load a saved game ────────────────────────────────── bool loaded = false; if (File.Exists(SavePath)) { 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(); } } // ── Load fresh board from CSV (if not loaded) ───────────────── if (!loaded) { var csvPath = Path.Combine(AppContext.BaseDirectory, "Data", "board.csv"); CsvLoader.LoadCells(world, csvPath); world.PostChanges(); world.SetSingleton(new GameState { CurrentPlayer = Player.X, Status = GameStatus.Playing, MoveCount = 0 }); world.PostChanges(); } // ── 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(); if (state.Status != GameStatus.Playing) break; var input = Console.ReadLine(); if (string.IsNullOrWhiteSpace(input)) continue; // "save" command. if (input.Trim().ToLower() == "save") { SaveGame(world); Console.WriteLine(" Game saved."); continue; } 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)) { Console.WriteLine(" Invalid input. Try \"1 2\" or \"save\"."); 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); // Auto-save after every move. SaveGame(world); } // ── Final state ─────────────────────────────────────────────── Console.WriteLine(); Console.WriteLine("=== Game over ==="); PrintReactivityLog(reactivityLog); // 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 log) { world.ObserveEntityChanges().Subscribe(change => { if (change.ComponentType != null) return; log.Add($"[react] {change}"); }); world.ObserveComponentChanges().Subscribe(change => { log.Add($"[react] {change}"); }); world.ObserveComponentChanges().Subscribe(change => { log.Add($"[react] {change}"); }); var markedQuery = world.Query().With().With().Build(); world.ObserveQuery(markedQuery).Subscribe(change => { log.Add($"[react:query] {change}"); }); } private static void PrintReactivityLog(List log) { if (log.Count == 0) return; Console.WriteLine("── reactivity log ──"); foreach (var entry in log) Console.WriteLine(entry); Console.WriteLine(); log.Clear(); } }