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"); } [Fact] public void AngleWrap_FullRevolutionsProduceSameReward() { // Physically identical poses (angle differs by 2π) must produce the same reward. // Otherwise a spinning ship accumulates unbounded angle penalty. var upright = new ShipState(0.5f, 0.5f, 0f, 0f, 0f, 0f); var oneSpin = new ShipState(0.5f, 0.5f, 0f, 0f, 2f * MathF.PI, 0f); var fiveSpins = new ShipState(0.5f, 0.5f, 0f, 0f, 10f * MathF.PI, 0f); float r0 = Reward.Compute(upright, target: (0.5f, 0.5f), action: 0); float r1 = Reward.Compute(oneSpin, target: (0.5f, 0.5f), action: 0); float r5 = Reward.Compute(fiveSpins, target: (0.5f, 0.5f), action: 0); Assert.Equal(r0, r1, precision: 4); Assert.Equal(r0, r5, precision: 4); } }