feat(*): Fix exported-policy action order to gymnasium's sorted-key layout, add wall-contact penalty and stronger ball-touch reward, and wire Spectate to run01 vs run02

This commit is contained in:
Josh Creek
2026-07-19 13:21:14 +01:00
parent 6f1a840249
commit 772f98b7fe
6 changed files with 34 additions and 8 deletions
+5 -4
View File
@@ -49,14 +49,15 @@ func get_action() -> ShipAction:
func _decide() -> void:
var obs := ShipObservations.build(_ship, _opponent, _ball, _attack_goal_position)
var out := _policy.forward(obs)
# Output layout matches the flattened training action space (Box(7)):
# thrust xyz, rotation xyz, turbo (> 0 means on).
_action.thrust = Vector3(
# Output layout is the trainer's flattened action space (Box(7)), which
# gymnasium orders by SORTED key name — rotation xyz, thrust xyz, turbo
# (> 0 means on) — NOT ShipAction's thrust-first declaration order.
_action.rotation = Vector3(
_axis(out[0]),
_axis(out[1]),
_axis(out[2])
)
_action.rotation = Vector3(
_action.thrust = Vector3(
_axis(out[3]),
_axis(out[4]),
_axis(out[5])
+15 -1
View File
@@ -15,9 +15,14 @@ extends AIController3D
# Reward shaping weights. Dense terms accrue per physics tick (60 sim-ticks
# per sim-second); event terms fire once. Exported so tuning needs no code
# edits. Goal rewards are added by TrainingMode, which owns goal events.
@export var ball_touch_reward := 0.1
@export var ball_touch_reward := 0.25
@export var velocity_to_ball_weight := 0.001
@export var ball_velocity_to_goal_weight := 0.004
# Per-tick penalty while touching the arena enclosure (walls/floor/ceiling).
# At 60 ticks/sim-second this is -0.3/s: a ship parked on a wall for a full
# 30 s episode loses ~9 — comparable to conceding — while a brief graze
# costs almost nothing.
@export var wall_contact_penalty := 0.005
var ship: Ship
var rl_controller: RLShipController
@@ -84,6 +89,15 @@ func _physics_process(delta):
var ball_progress := ball.linear_velocity.dot(ball_to_goal.normalized())
reward += ball_velocity_to_goal_weight * ball_progress / ShipObservations.BALL_SPEED_SCALE
# Dense penalty: every tick spent in contact with the arena enclosure
# (contact monitoring is already on for the ball-touch reward). Ships
# bumping each other or the ball is fine — only the boundary counts.
if wall_contact_penalty > 0.0:
for body in ship.get_colliding_bodies():
if body is ArenaBoundary:
reward -= wall_contact_penalty
break
func _on_ship_body_entered(body: Node) -> void:
if body.is_in_group("ball"):