feat(gamecli): add JSON protocol DTOs with source-gen context
This commit is contained in:
81
GameCli.Tests/ProtocolTests.cs
Normal file
81
GameCli.Tests/ProtocolTests.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using GameCli;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace GameCli.Tests;
|
||||||
|
|
||||||
|
public class ProtocolTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void StepInput_ParsesActionAndTarget()
|
||||||
|
{
|
||||||
|
var json = """{"action":2,"target":[0.35,0.60]}""";
|
||||||
|
var input = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.StepInput);
|
||||||
|
Assert.NotNull(input);
|
||||||
|
Assert.Equal(2, input!.Action);
|
||||||
|
Assert.NotNull(input.Target);
|
||||||
|
Assert.Equal(0.35f, input.Target![0]);
|
||||||
|
Assert.Equal(0.60f, input.Target![1]);
|
||||||
|
Assert.Null(input.Cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StepInput_ParsesResetCommand()
|
||||||
|
{
|
||||||
|
var json = """{"cmd":"reset","seed":12345}""";
|
||||||
|
var input = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.StepInput);
|
||||||
|
Assert.NotNull(input);
|
||||||
|
Assert.Equal("reset", input!.Cmd);
|
||||||
|
Assert.Equal(12345, input.Seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StepInput_ParsesResetCommandWithoutSeed()
|
||||||
|
{
|
||||||
|
var json = """{"cmd":"reset"}""";
|
||||||
|
var input = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.StepInput);
|
||||||
|
Assert.NotNull(input);
|
||||||
|
Assert.Equal("reset", input!.Cmd);
|
||||||
|
Assert.Null(input.Seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StepOutput_SerializesAllFields()
|
||||||
|
{
|
||||||
|
var output = new StepOutput
|
||||||
|
{
|
||||||
|
Obs = new[] { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f },
|
||||||
|
State = new ShipStateDto { X = 0.5f, Y = 0.6f, Angle = 0.1f, Engine = 2 },
|
||||||
|
Reward = -0.42f,
|
||||||
|
Done = false,
|
||||||
|
Step = 137,
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(output, ProtocolJsonContext.Default.StepOutput);
|
||||||
|
Assert.Contains("\"obs\":", json);
|
||||||
|
Assert.Contains("\"state\":", json);
|
||||||
|
Assert.Contains("\"reward\":", json);
|
||||||
|
Assert.Contains("\"done\":false", json);
|
||||||
|
Assert.Contains("\"step\":137", json);
|
||||||
|
Assert.Contains("\"engine\":2", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InitMessage_SerializesExpectedShape()
|
||||||
|
{
|
||||||
|
var init = new InitMessage
|
||||||
|
{
|
||||||
|
Init = new InitPayload
|
||||||
|
{
|
||||||
|
World = new[] { 1.0f, 1.0f },
|
||||||
|
Dt = 0.02f,
|
||||||
|
ObsDim = 7,
|
||||||
|
NActions = 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(init, ProtocolJsonContext.Default.InitMessage);
|
||||||
|
Assert.Contains("\"world\":[1,1]", json);
|
||||||
|
Assert.Contains("\"dt\":0.02", json);
|
||||||
|
Assert.Contains("\"obs_dim\":7", json);
|
||||||
|
Assert.Contains("\"n_actions\":4", json);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,8 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="GameCli.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
56
GameCli/Protocol.cs
Normal file
56
GameCli/Protocol.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace GameCli;
|
||||||
|
|
||||||
|
/// <summary>Input line from the client (per step OR a reset command).</summary>
|
||||||
|
public sealed class StepInput
|
||||||
|
{
|
||||||
|
[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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Output line from the CLI after each step or reset.</summary>
|
||||||
|
public sealed class StepOutput
|
||||||
|
{
|
||||||
|
[JsonPropertyName("obs")] public float[] Obs { get; set; } = System.Array.Empty<float>();
|
||||||
|
[JsonPropertyName("state")] public ShipStateDto 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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Renderable ship pose (subset of ShipState that the UI actually draws).</summary>
|
||||||
|
public sealed class ShipStateDto
|
||||||
|
{
|
||||||
|
[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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Startup handshake emitted as the very first stdout line.</summary>
|
||||||
|
public sealed class InitMessage
|
||||||
|
{
|
||||||
|
[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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Source-generated JSON contracts for AOT-friendly, allocation-lean (de)serialization.</summary>
|
||||||
|
[JsonSourceGenerationOptions(WriteIndented = false)]
|
||||||
|
[JsonSerializable(typeof(StepInput))]
|
||||||
|
[JsonSerializable(typeof(StepOutput))]
|
||||||
|
[JsonSerializable(typeof(ShipStateDto))]
|
||||||
|
[JsonSerializable(typeof(InitMessage))]
|
||||||
|
[JsonSerializable(typeof(InitPayload))]
|
||||||
|
internal partial class ProtocolJsonContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user