167 lines
5.6 KiB
C#
167 lines
5.6 KiB
C#
using System.Diagnostics;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace Backend;
|
|
|
|
/// <summary>
|
|
/// One instance == one <c>GameCli</c> subprocess. Not thread-safe.
|
|
/// Callers must sequence <see cref="StepAsync"/> and <see cref="ResetAsync"/>.
|
|
/// </summary>
|
|
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<StepResult> 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<StepResult> 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>(T value, System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> 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<StepResult> 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<float>();
|
|
[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
|
|
{
|
|
}
|