fix(training): correct non-forward penalty math and add a grounding incentive

Adversarial review of the previous stage-4 retune found two problems:
non_forward_speed used planar_speed - forward_component, which under-charges
diagonal motion relative to true lateral speed (e.g. ~29% penalty at 45
degrees off the nose instead of the correct ~71%); fixed to the Pythagorean
magnitude for forward-facing angles, full speed for backward-facing ones.

Also, ground_tilt_penalty and non_forward_penalty only ever cost reward near
the floor with nothing offsetting them above it, which could teach a policy
that's still bad at ground handling to just avoid the floor rather than get
better at it. Added grounded_upright_reward (ship_ai_controller.gd) plus a
new ShipObservations.is_floor_contact helper for genuine belly-on-floor
contact detection, so grounding well while upright is the locally profitable
choice, not just the least-punished one.
This commit is contained in:
Josh Creek
2026-08-09 13:23:00 +01:00
parent c56f5ed1a3
commit 6f7536f03c
5 changed files with 71 additions and 5 deletions
+18
View File
@@ -143,3 +143,21 @@ static func contact_normal(ship: Ship) -> Vector3:
if normal.y < FLOOR_NORMAL_MIN_Y:
return normal
return Vector3.ZERO
# True belly-on-floor contact — the complement of contact_normal, which
# deliberately excludes floor contact (see its comment). Used by
# ShipAIController.grounded_upright_reward to reward genuinely resting on
# the floor rather than just being below the GROUND_HANDLING_HEIGHT proxy
# altitude, so a ship can't collect ground-handling reward by hovering just
# under the threshold without ever touching down.
static func is_floor_contact(ship: Ship) -> bool:
var state := PhysicsServer3D.body_get_direct_state(ship.get_rid())
if state == null:
return false
for i in state.get_contact_count():
if not state.get_contact_collider_object(i) is ArenaBoundary:
continue
if state.get_contact_local_normal(i).y >= FLOOR_NORMAL_MIN_Y:
return true
return false