chore(training): add air_approach_weight and restart stage-5 intercepts

Stage 5 blocked all three attempts on productive_air_touch_fraction
stuck exactly at 0.0 across a continuous 180M-step lineage, while
goal_rate/upright_fraction/forward_motion_fraction kept improving on
the same budget. forward_velocity_to_ball_weight (the term that solved
Stage 4's ground pursuit) is hard-gated below GROUND_HANDLING_HEIGHT
and does nothing in the air, so Stage 5's air_intercept_chance had no
matching aerial incentive to learn from. air_approach_weight adds the
airborne mirror (nose-first 3D closing speed, no uprightness
multiplier) and folds into HANDLING_REWARD_FLAGS so Stage 6 inherits
it too. Deleted the three blocked attempts and reset state to resume
Stage 5 from the Stage-4 checkpoint with the new term.
This commit is contained in:
Josh Creek
2026-08-18 16:03:56 +01:00
parent 0e685edcf8
commit 88591e031f
15 changed files with 93 additions and 254 deletions
+29
View File
@@ -46,6 +46,20 @@ extends AIController3D
# frozen checkpoints keep their original objective; generation 5 handling
# turns it on while reducing the orientation-agnostic term.
@export var forward_velocity_to_ball_weight := 0.0
# Aerial mirror of forward_velocity_to_ball_weight: nose-first closing speed
# on the ball, active above GROUND_HANDLING_HEIGHT instead of below it (the
# two are mutually exclusive by altitude, never both active on the same
# tick). Generation 5's intercepts stage added air_intercept_chance without
# an airborne equivalent of the term that actually solved ground handling;
# above 3m the only remaining approach incentive was the generic, orientation
# -agnostic velocity_to_ball_weight (0.02-0.04), which three consecutive
# 60M-step attempts (180M cumulative, resuming each time) showed produces
# zero learnable gradient toward touching an aerial ball at all —
# productive_air_touch_fraction stayed exactly 0.0 the whole time while every
# other metric kept improving on the same budget. Uses the full 3D nose
# vector rather than the planar-only one, since a real aerial requires
# pitching away from level.
@export var air_approach_weight := 0.0
@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
@@ -357,6 +371,21 @@ func _physics_process(delta):
reward += forward_velocity_to_ball_weight * forward_speed * facing_ball \
* approach_uprightness * handling_ground_factor
# Aerial shaping: nose-first 3D closing speed on the ball (see
# air_approach_weight). Mirrors the ground block above but with the full
# nose vector instead of the planar one, and no uprightness multiplier —
# a genuine aerial approach requires pitching away from level, so paying
# only while upright would oppose the exact behaviour this rewards.
if air_approach_weight > 0.0 and ship.global_position.y >= GROUND_HANDLING_HEIGHT \
and to_ball.length_squared() > 0.0001:
var nose_forward := -ship.global_transform.basis.z
if nose_forward.length_squared() > 0.0001:
nose_forward = nose_forward.normalized()
var to_ball_dir := to_ball.normalized()
var air_facing_ball: float = maxf(nose_forward.dot(to_ball_dir), 0.0)
var air_closing_speed: float = maxf(ship.linear_velocity.dot(to_ball_dir), 0.0) / ship.max_speed
reward += air_approach_weight * air_closing_speed * air_facing_ball
# 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:
+2 -1
View File
@@ -265,7 +265,7 @@ const TRAINING_MODE_OVERRIDES := [
const SHIP_AI_OVERRIDES := [
"ball_touch_reward", "ball_touch_cooldown_ticks", "ball_touch_direction_floor",
"velocity_to_ball_weight", "ball_velocity_to_goal_weight", "ball_distance_penalty",
"forward_velocity_to_ball_weight", "wall_contact_penalty", "tilt_penalty",
"forward_velocity_to_ball_weight", "air_approach_weight", "wall_contact_penalty", "tilt_penalty",
"ground_tilt_penalty", "non_forward_penalty", "grounded_upright_reward",
"speed_reward_weight", "time_penalty", "airborne_penalty",
]
@@ -323,6 +323,7 @@ func _ai_default(name: String) -> Variant:
"ball_touch_direction_floor": return 0.3
"velocity_to_ball_weight": return 0.02
"forward_velocity_to_ball_weight": return 0.0
"air_approach_weight": return 0.0
"ball_velocity_to_goal_weight": return 0.004
"ball_distance_penalty": return 0.002
"wall_contact_penalty": return 0.0025