feat(gamecli): implement pure Physics.Step with unit tests

This commit is contained in:
meelstorm
2026-07-17 16:44:15 +00:00
committed by EugeneTes
parent f31f01d321
commit 34fb901fe6
2 changed files with 142 additions and 0 deletions

78
GameCli/Physics.cs Normal file
View 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);
}
}