feat(*): Add a distance-to-ball penalty so idling scores negative, boost ball-seeking rewards, and halve the wall-contact penalty

This commit is contained in:
Josh Creek
2026-07-19 18:04:36 +01:00
parent 80e9b676d5
commit 4c227c24b4
+26 -5
View File
@@ -15,15 +15,23 @@ 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.25
@export var velocity_to_ball_weight := 0.002
@export var ball_touch_reward := 1.0
@export var velocity_to_ball_weight := 0.02
@export var ball_velocity_to_goal_weight := 0.004
# Per-tick penalty scaled by distance to the ball (full value at the arena's
# far diagonal, 0 on top of the ball). Run04 lesson: with idling worth a flat
# 0, camping in a corner strictly dominated risking the wall/tilt penalties
# to chase the ball — this makes "do nothing far from the ball" the worst
# option instead of the safest. A penalty, not a proximity bonus, so orbiting
# the ball farms nothing.
@export var ball_distance_penalty := 0.002
# Per-tick penalty while pressed against a side wall, end wall, or the
# ceiling — NOT the floor (run03 lesson: taxing floor contact punishes the
# ship's natural low flight and drowns every other signal). At 60 ticks per
# sim-second this is -0.3/s: 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
# sim-second this is -0.15/s. Halved for run05: the ball lives near walls,
# and the old -0.3/s made the productive region of the pitch aversive
# relative to the (then far weaker) ball-seeking shaping.
@export var wall_contact_penalty := 0.0025
# Per-tick penalty for not being upright, scaled by tilt: 0 when flat, full
# value (-0.12/s) when inverted. A penalty rather than an upright bonus so a
# flat, idle ship farms nothing.
@@ -33,6 +41,14 @@ extends AIController3D
# penalty); below it they read as wall (sideways) or ceiling (downward).
const FLOOR_NORMAL_MIN_Y := 0.7
# Longest possible ship-to-ball separation: the enclosure's interior diagonal.
# Normalizes ball_distance_penalty so its export is the worst-case per-tick cost.
const MAX_BALL_DISTANCE := sqrt(
(2.0 * ArenaBoundary.INNER_HALF_X) ** 2
+ (2.0 * ArenaBoundary.INNER_HALF_Z) ** 2
+ ArenaBoundary.INNER_HEIGHT ** 2
)
var ship: Ship
var rl_controller: RLShipController
var ball: RigidBody3D
@@ -92,6 +108,11 @@ func _physics_process(delta):
var closing_speed := ship.linear_velocity.dot(to_ball.normalized())
reward += velocity_to_ball_weight * closing_speed / ship.max_speed
# Dense penalty: distance to the ball, so idling far away bleeds reward
# instead of scoring a safe zero (see ball_distance_penalty).
if ball_distance_penalty > 0.0:
reward -= ball_distance_penalty * to_ball.length() / MAX_BALL_DISTANCE
# Dense shaping: ball velocity toward the goal we attack
var ball_to_goal := attack_goal_position - ball.global_position
if ball_to_goal.length_squared() > 0.0001: