feat(gamecli): implement pure Reward.Compute with unit tests

This commit is contained in:
meelstorm
2026-07-17 16:45:53 +00:00
committed by EugeneTes
parent 34fb901fe6
commit 3bca85bcc4
2 changed files with 85 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
using GameCli;
using Xunit;
namespace GameCli.Tests;
public class RewardTests
{
[Fact]
public void AtTarget_ZeroVel_ZeroAngle_Noop_HasZeroReward()
{
var ship = new ShipState(0.5f, 0.5f, 0f, 0f, 0f, 0f);
float r = Reward.Compute(ship, target: (0.5f, 0.5f), action: 0);
Assert.Equal(0f, r, precision: 5);
}
[Fact]
public void FarFromTarget_HasLargerNegativeThanNear()
{
var near = new ShipState(0.5f, 0.5f, 0f, 0f, 0f, 0f);
var far = new ShipState(0.9f, 0.9f, 0f, 0f, 0f, 0f);
float rNear = Reward.Compute(near, target: (0.5f, 0.5f), action: 0);
float rFar = Reward.Compute(far, target: (0.5f, 0.5f), action: 0);
Assert.True(rFar < rNear, $"far reward {rFar} should be < near reward {rNear}");
}
[Fact]
public void HighVelocity_PenalizesReward()
{
var stopped = new ShipState(0.5f, 0.5f, 0f, 0f, 0f, 0f);
var moving = new ShipState(0.5f, 0.5f, 1f, 1f, 0f, 0f);
float rStop = Reward.Compute(stopped, target: (0.5f, 0.5f), action: 0);
float rMove = Reward.Compute(moving, target: (0.5f, 0.5f), action: 0);
Assert.True(rMove < rStop, $"moving reward {rMove} should be < stopped reward {rStop}");
}
[Fact]
public void Tilted_PenalizesReward()
{
var upright = new ShipState(0.5f, 0.5f, 0f, 0f, 0f, 0f);
var tilted = new ShipState(0.5f, 0.5f, 0f, 0f, 0.8f, 0f);
float rUp = Reward.Compute(upright, target: (0.5f, 0.5f), action: 0);
float rTi = Reward.Compute(tilted, target: (0.5f, 0.5f), action: 0);
Assert.True(rTi < rUp, $"tilted reward {rTi} should be < upright reward {rUp}");
}
[Fact]
public void FiringEngine_PenalizesReward()
{
var ship = new ShipState(0.5f, 0.5f, 0f, 0f, 0f, 0f);
float rNoop = Reward.Compute(ship, target: (0.5f, 0.5f), action: 0);
float rMain = Reward.Compute(ship, target: (0.5f, 0.5f), action: 2);
float rLeft = Reward.Compute(ship, target: (0.5f, 0.5f), action: 1);
Assert.True(rMain < rNoop, "firing main should be worse than noop");
Assert.True(rLeft < rNoop, "firing left should be worse than noop");
}
}

29
GameCli/Reward.cs Normal file
View File

@@ -0,0 +1,29 @@
namespace GameCli;
/// <summary>
/// Reward = -distance_to_target
/// - λ_v · speed
/// - λ_θ · |angle|
/// - λ_fuel · engine_on
/// Coefficients are public so training scripts can log/inspect them.
/// </summary>
public static class Reward
{
public const float LambdaVelocity = 0.10f;
public const float LambdaAngle = 0.10f;
public const float LambdaFuel = 0.03f;
public static float Compute(ShipState s, (float X, float Y) target, int action)
{
float dx = target.X - s.X;
float dy = target.Y - s.Y;
float distance = MathF.Sqrt(dx * dx + dy * dy);
float speed = MathF.Sqrt(s.VX * s.VX + s.VY * s.VY);
float engineOn = action == Physics.ActionNoop ? 0f : 1f;
return -distance
- LambdaVelocity * speed
- LambdaAngle * MathF.Abs(s.Angle)
- LambdaFuel * engineOn;
}
}