diff --git a/GameCli.Tests/RewardTests.cs b/GameCli.Tests/RewardTests.cs new file mode 100644 index 0000000..8c7f639 --- /dev/null +++ b/GameCli.Tests/RewardTests.cs @@ -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"); + } +} diff --git a/GameCli/Reward.cs b/GameCli/Reward.cs new file mode 100644 index 0000000..5c36bdf --- /dev/null +++ b/GameCli/Reward.cs @@ -0,0 +1,29 @@ +namespace GameCli; + +/// +/// Reward = -distance_to_target +/// - λ_v · speed +/// - λ_θ · |angle| +/// - λ_fuel · engine_on +/// Coefficients are public so training scripts can log/inspect them. +/// +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; + } +}