diff --git a/Game/scripts/ship_ai_controller.gd b/Game/scripts/ship_ai_controller.gd index af4bd042..6e74fd38 100644 --- a/Game/scripts/ship_ai_controller.gd +++ b/Game/scripts/ship_ai_controller.gd @@ -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: