using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; namespace Backend; /// /// One instance == one GameCli subprocess. Not thread-safe. /// Callers must sequence and . /// public sealed class GameProcess : IAsyncDisposable { private readonly string _binaryPath; private Process? _proc; private StreamReader? _stdout; private StreamWriter? _stdin; public float Dt { get; private set; } public int ObsDim { get; private set; } public int NActions { get; private set; } public GameProcess(string binaryPath) { _binaryPath = binaryPath; } public async Task StartAsync() { if (_proc is not null) throw new InvalidOperationException("already started"); if (!File.Exists(_binaryPath)) throw new FileNotFoundException($"GameCli binary not found at {_binaryPath}"); var psi = new ProcessStartInfo { FileName = _binaryPath, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }; _proc = Process.Start(psi) ?? throw new InvalidOperationException("failed to start GameCli"); _stdout = _proc.StandardOutput; _stdin = _proc.StandardInput; var line = await _stdout.ReadLineAsync() ?? throw new IOException("GameCli died before init handshake"); var init = JsonSerializer.Deserialize(line, ProcessJsonContext.Default.InitEnvelope) ?? throw new IOException($"malformed init handshake: {line}"); Dt = init.Init.Dt; ObsDim = init.Init.ObsDim; NActions = init.Init.NActions; } public async Task StepAsync(int action, (float X, float Y) target) { var input = new StepIn { Action = action, Target = new[] { target.X, target.Y }, }; await WriteAsync(input, ProcessJsonContext.Default.StepIn); return await ReadStepResultAsync(); } public async Task ResetAsync(int? seed = null) { var input = new StepIn { Cmd = "reset", Seed = seed }; await WriteAsync(input, ProcessJsonContext.Default.StepIn); return await ReadStepResultAsync(); } private async Task WriteAsync(T value, System.Text.Json.Serialization.Metadata.JsonTypeInfo ctx) { if (_stdin is null) throw new InvalidOperationException("not started"); var json = JsonSerializer.Serialize(value, ctx); await _stdin.WriteLineAsync(json); await _stdin.FlushAsync(); } private async Task ReadStepResultAsync() { if (_stdout is null) throw new InvalidOperationException("not started"); var line = await _stdout.ReadLineAsync() ?? throw new IOException("GameCli closed stdout unexpectedly"); var msg = JsonSerializer.Deserialize(line, ProcessJsonContext.Default.StepOut) ?? throw new IOException($"malformed step output: {line}"); return new StepResult(msg.Obs, msg.State, msg.Reward, msg.Done, msg.Step); } public async ValueTask DisposeAsync() { try { _stdin?.Close(); } catch { } if (_proc is not null) { try { var exited = _proc.WaitForExit(2000); if (!exited) _proc.Kill(entireProcessTree: true); } catch { } _proc.Dispose(); _proc = null; } await ValueTask.CompletedTask; } } // -------- Result -------- public readonly record struct StepResult( float[] Observation, ShipStatePayload State, float Reward, bool Done, int Step); // -------- Wire DTOs (local mirror of GameCli's Protocol.cs) -------- public sealed class StepIn { [JsonPropertyName("action")] public int? Action { get; set; } [JsonPropertyName("target")] public float[]? Target { get; set; } [JsonPropertyName("cmd")] public string? Cmd { get; set; } [JsonPropertyName("seed")] public int? Seed { get; set; } } public sealed class StepOut { [JsonPropertyName("obs")] public float[] Obs { get; set; } = Array.Empty(); [JsonPropertyName("state")] public ShipStatePayload State { get; set; } = new(); [JsonPropertyName("reward")] public float Reward { get; set; } [JsonPropertyName("done")] public bool Done { get; set; } [JsonPropertyName("step")] public int Step { get; set; } } public sealed class ShipStatePayload { [JsonPropertyName("x")] public float X { get; set; } [JsonPropertyName("y")] public float Y { get; set; } [JsonPropertyName("angle")] public float Angle { get; set; } [JsonPropertyName("engine")] public int Engine { get; set; } } public sealed class InitEnvelope { [JsonPropertyName("init")] public InitPayload Init { get; set; } = new(); } public sealed class InitPayload { [JsonPropertyName("world")] public float[] World { get; set; } = new[] { 1f, 1f }; [JsonPropertyName("dt")] public float Dt { get; set; } [JsonPropertyName("obs_dim")] public int ObsDim { get; set; } [JsonPropertyName("n_actions")] public int NActions { get; set; } } [JsonSourceGenerationOptions(WriteIndented = false)] [JsonSerializable(typeof(StepIn))] [JsonSerializable(typeof(StepOut))] [JsonSerializable(typeof(ShipStatePayload))] [JsonSerializable(typeof(InitEnvelope))] [JsonSerializable(typeof(InitPayload))] internal partial class ProcessJsonContext : JsonSerializerContext { }