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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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