diff --git a/GameCli.Tests/PhysicsTests.cs b/GameCli.Tests/PhysicsTests.cs
new file mode 100644
index 0000000..9e2ec9a
--- /dev/null
+++ b/GameCli.Tests/PhysicsTests.cs
@@ -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);
+ }
+}
diff --git a/GameCli/Physics.cs b/GameCli/Physics.cs
new file mode 100644
index 0000000..d1f0477
--- /dev/null
+++ b/GameCli/Physics.cs
@@ -0,0 +1,78 @@
+namespace GameCli;
+
+///
+/// Pure physics stepper. All constants are public so training/tuning can inspect them.
+/// Explicit Euler integration at fixed dt.
+///
+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;
+
+ ///
+ /// Advance the ship state by one dt given a discrete action.
+ /// Gravity is always applied. Thrusters add to acceleration/torque.
+ ///
+ 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);
+ }
+}