feat(gamecli): implement pure Physics.Step with unit tests
This commit is contained in:
64
GameCli.Tests/PhysicsTests.cs
Normal file
64
GameCli.Tests/PhysicsTests.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using GameCli;
|
||||
using Xunit;
|
||||
|
||||
namespace GameCli.Tests;
|
||||
|
||||
public class PhysicsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Noop_UnderGravity_ShipFallsDownward()
|
||||
{
|
||||
var start = new ShipState(0.5f, 0.5f, 0f, 0f, 0f, 0f);
|
||||
var next = Physics.Step(start, action: 0);
|
||||
Assert.True(next.VY > 0, $"expected VY > 0 (gravity down), got {next.VY}");
|
||||
Assert.True(next.Y > start.Y, $"expected Y to increase (fall), got {next.Y}");
|
||||
Assert.Equal(start.X, next.X, precision: 5);
|
||||
Assert.Equal(0f, next.Angle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MainEngine_WhenUpright_CancelsThenReversesGravity()
|
||||
{
|
||||
// Main engine thrust > gravity, so acceleration is net upward (VY becomes negative).
|
||||
var start = new ShipState(0.5f, 0.5f, 0f, 0f, 0f, 0f);
|
||||
var next = Physics.Step(start, action: 2);
|
||||
Assert.True(next.VY < 0, $"expected VY < 0 (net upward), got {next.VY}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeftThruster_AppliesNegativeTorque()
|
||||
{
|
||||
var start = new ShipState(0.5f, 0.5f, 0f, 0f, 0f, 0f);
|
||||
var next = Physics.Step(start, action: 1);
|
||||
Assert.True(next.AngularVelocity < 0,
|
||||
$"expected angular velocity < 0, got {next.AngularVelocity}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RightThruster_AppliesPositiveTorque()
|
||||
{
|
||||
var start = new ShipState(0.5f, 0.5f, 0f, 0f, 0f, 0f);
|
||||
var next = Physics.Step(start, action: 3);
|
||||
Assert.True(next.AngularVelocity > 0,
|
||||
$"expected angular velocity > 0, got {next.AngularVelocity}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MainEngine_WhenRotated90Right_ThrustsRight()
|
||||
{
|
||||
// Angle = +pi/2 means ship points to the +x direction (right).
|
||||
// Main engine pushes along body-up, which is now world +x.
|
||||
var start = new ShipState(0.5f, 0.5f, 0f, 0f, MathF.PI / 2f, 0f);
|
||||
var next = Physics.Step(start, action: 2);
|
||||
Assert.True(next.VX > 0, $"expected VX > 0 (thrust right), got {next.VX}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Step_IsDeterministic()
|
||||
{
|
||||
var start = new ShipState(0.3f, 0.4f, 0.1f, -0.05f, 0.2f, 0.3f);
|
||||
var a = Physics.Step(start, action: 2);
|
||||
var b = Physics.Step(start, action: 2);
|
||||
Assert.Equal(a, b);
|
||||
}
|
||||
}
|
||||
78
GameCli/Physics.cs
Normal file
78
GameCli/Physics.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
namespace GameCli;
|
||||
|
||||
/// <summary>
|
||||
/// Pure physics stepper. All constants are public so training/tuning can inspect them.
|
||||
/// Explicit Euler integration at fixed dt.
|
||||
/// </summary>
|
||||
public static class Physics
|
||||
{
|
||||
public const float Dt = 0.02f; // 50 Hz
|
||||
public const float Gravity = 0.5f; // world units / s^2, +Y (downward)
|
||||
public const float MainThrust = 1.2f; // world units / s^2 along body-up
|
||||
public const float SideTorque = 4.0f; // rad / s^2
|
||||
public const float SideLateralImpulse = 0.15f; // world units / s^2 along body-x
|
||||
public const float LinearDrag = 0.10f; // per-second
|
||||
public const float AngularDrag = 0.50f; // per-second
|
||||
|
||||
public const int ActionNoop = 0;
|
||||
public const int ActionLeft = 1;
|
||||
public const int ActionMain = 2;
|
||||
public const int ActionRight = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Advance the ship state by one dt given a discrete action.
|
||||
/// Gravity is always applied. Thrusters add to acceleration/torque.
|
||||
/// </summary>
|
||||
public static ShipState Step(ShipState s, int action)
|
||||
{
|
||||
// Start with gravity.
|
||||
float ax = 0f;
|
||||
float ay = Gravity;
|
||||
float torque = 0f;
|
||||
|
||||
// Body-up direction in world coords, given angle:
|
||||
// angle=0 → (0, -1) "up" on screen (Y is down)
|
||||
// angle=+π/2 → (+1, 0) right
|
||||
// angle=+π → (0, +1) down
|
||||
float sinA = MathF.Sin(s.Angle);
|
||||
float cosA = MathF.Cos(s.Angle);
|
||||
float bodyUpX = sinA;
|
||||
float bodyUpY = -cosA;
|
||||
// Body-right direction (perpendicular, rotated +90°):
|
||||
float bodyRightX = cosA;
|
||||
float bodyRightY = sinA;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case ActionMain:
|
||||
ax += bodyUpX * MainThrust;
|
||||
ay += bodyUpY * MainThrust;
|
||||
break;
|
||||
case ActionLeft:
|
||||
torque -= SideTorque;
|
||||
ax += bodyRightX * SideLateralImpulse;
|
||||
ay += bodyRightY * SideLateralImpulse;
|
||||
break;
|
||||
case ActionRight:
|
||||
torque += SideTorque;
|
||||
ax -= bodyRightX * SideLateralImpulse;
|
||||
ay -= bodyRightY * SideLateralImpulse;
|
||||
break;
|
||||
case ActionNoop:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Integrate velocity, apply linear drag, integrate position.
|
||||
float vx = (s.VX + ax * Dt) * (1f - LinearDrag * Dt);
|
||||
float vy = (s.VY + ay * Dt) * (1f - LinearDrag * Dt);
|
||||
float x = s.X + vx * Dt;
|
||||
float y = s.Y + vy * Dt;
|
||||
|
||||
// Angular: same pattern.
|
||||
float w = (s.AngularVelocity + torque * Dt) * (1f - AngularDrag * Dt);
|
||||
float angle = s.Angle + w * Dt;
|
||||
|
||||
return new ShipState(x, y, vx, vy, angle, w);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user