feat(*): Add rounded arena boundaries and reward shaping to curb corner-camping

This commit is contained in:
Josh Creek
2026-07-20 08:20:33 +01:00
parent 6cb5902eb6
commit 3457d4ca84
5 changed files with 376 additions and 13 deletions
+27 -1
View File
@@ -16,6 +16,12 @@ extends AIController3D
# 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 := 1.0
# Ball touches pay out at most once per this many physics ticks (1 sim-
# second at 60). Run07 lesson: body_entered re-fires on every micro-
# separation, so pinning the ball against a surface farmed ~2 touches/s —
# outearning every other term while the goal rate fell. The cooldown keeps
# touches a stepping-stone signal instead of the objective.
@export var ball_touch_cooldown_ticks := 60
@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
@@ -36,6 +42,12 @@ extends AIController3D
# value (-0.12/s) when inverted. A penalty rather than an upright bonus so a
# flat, idle ship farms nothing.
@export var tilt_penalty := 0.002
# Per-tick bonus for own speed: 0 stationary, full value (+0.24/s) at
# max_speed. Run07 lesson: after the kickoff flurry both ships parked next to
# a cornered ball — with every other dense term near zero there, standing
# still was a rest state. Sized well below velocity_to_ball_weight so flying
# fast toward the ball still beats flying fast anywhere else.
@export var speed_reward_weight := 0.004
# Contact normals with y above this are floor contact (exempt from the wall
# penalty); below it they read as wall (sideways) or ceiling (downward).
@@ -55,6 +67,8 @@ var ball: RigidBody3D
var opponent: Ship
var attack_goal_position: Vector3
var _ticks_since_ball_touch := 1 << 30 # large so the first touch always pays
# Wire up references after the ship is spawned. `attack_goal` is the goal
# this ship scores into (goal.team == opponent's team).
@@ -97,10 +111,16 @@ func set_action(action) -> void:
rl_controller.action.turbo = int(action["turbo"]) == 1
func reset():
super()
_ticks_since_ball_touch = 1 << 30
func _physics_process(delta):
super(delta)
if not is_instance_valid(ship) or not is_instance_valid(ball):
return
_ticks_since_ball_touch += 1
# Dense shaping: own velocity toward the ball
var to_ball := ball.global_position - ship.global_position
@@ -113,6 +133,11 @@ func _physics_process(delta):
if ball_distance_penalty > 0.0:
reward -= ball_distance_penalty * to_ball.length() / MAX_BALL_DISTANCE
# Dense bonus: own speed, so hovering in place is never a rest state
# (see speed_reward_weight).
if speed_reward_weight > 0.0:
reward += speed_reward_weight * ship.linear_velocity.length() / ship.max_speed
# 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:
@@ -149,5 +174,6 @@ func _wall_or_ceiling_contact() -> bool:
func _on_ship_body_entered(body: Node) -> void:
if body.is_in_group("ball"):
if body.is_in_group("ball") and _ticks_since_ball_touch >= ball_touch_cooldown_ticks:
reward += ball_touch_reward
_ticks_since_ball_touch = 0