feat(training): add opt-in teamplay evaluation

This commit is contained in:
Josh Creek
2026-09-01 17:30:40 +01:00
parent 9004800326
commit 7b2f9c26f4
8 changed files with 84 additions and 11 deletions
+17 -1
View File
@@ -38,6 +38,10 @@ extends AIController3D
# stone, matching ball_touch_cooldown_ticks's existing "stepping-stone, not
# the objective" framing.
@export_range(0.0, 1.0) var ball_touch_direction_floor := 0.3
# Fraction of a touch payout shared with teammates. Zero preserves all
# existing 1v1/curriculum reward functions; in teamplay the shared amount is
# divided across teammates and never exceeds the touching ship's payout.
@export_range(0.0, 1.0) var team_touch_credit_weight := 0.0
@export var velocity_to_ball_weight := 0.02
# Dense reward for approaching the ball *nose first* near the floor. Unlike
# velocity_to_ball_weight, sideways/reverse closing velocity earns nothing:
@@ -606,6 +610,12 @@ func _on_ship_body_entered(body: Node) -> void:
if air_touch_bonus_weight > 0.0 and ball.global_position.y > AIR_TOUCH_HEIGHT:
touch_payout += air_touch_bonus_weight * alignment
reward += touch_payout
if team_touch_credit_weight > 0.0 and not teammates.is_empty():
var teammate_credit := team_touch_credit(touch_payout, team_touch_credit_weight, teammates.size())
for teammate in teammates:
var teammate_agent := teammate.get_node_or_null("ShipAIController") as ShipAIController
if is_instance_valid(teammate_agent):
teammate_agent.reward += teammate_credit
_ticks_since_ball_touch = 0
# air_touch_fraction/productive_air_touch_fraction (see get_info) share
@@ -616,4 +626,10 @@ func _on_ship_body_entered(body: Node) -> void:
if ball.global_position.y > AIR_TOUCH_HEIGHT:
_air_touches += 1
if alignment >= PRODUCTIVE_AIR_TOUCH_ALIGNMENT:
_productive_air_touches += 1
_productive_air_touches += 1
static func team_touch_credit(touch_payout: float, weight: float, teammate_count: int) -> float:
if touch_payout <= 0.0 or weight <= 0.0 or teammate_count <= 0:
return 0.0
return touch_payout * clampf(weight, 0.0, 1.0) / teammate_count
+10 -6
View File
@@ -141,6 +141,7 @@ var _eval_goals := {0: 0, 1: 0}
var _eval_draws := 0
var _eval_episodes_done := 0
var _episode_ticks := 0
var _eval_team_size := 1
# Curriculum mode state (see _parse_curriculum_args). "self_play" (default)
# is today's only historical behaviour: both ships are live trainees sharing
@@ -176,11 +177,12 @@ func _start() -> void:
spawn_ball()
if _eval:
for team in [0, 1]:
var bot := AIShipController.new()
bot.model_path = _eval_models[team]
bot.allow_vertical = _eval_allow_vertical[team]
bot.allow_pitch_roll = _eval_allow_pitch_roll[team]
spawn_ship(team, 0, bot)
for spawn_index in _eval_team_size:
var bot := AIShipController.new()
bot.model_path = _eval_models[team]
bot.allow_vertical = _eval_allow_vertical[team]
bot.allow_pitch_roll = _eval_allow_pitch_roll[team]
spawn_ship(team, spawn_index, bot)
return
var team0_ships: Array[Ship] = []
@@ -248,6 +250,7 @@ func _parse_eval_args() -> void:
_eval_models[0] = args["eval_model_a"]
_eval_models[1] = args["eval_model_b"]
_eval_episodes = int(args.get("eval_episodes", str(_eval_episodes)))
_eval_team_size = clampi(int(args.get("eval_team_size", str(_eval_team_size))), 1, 2)
_eval_allow_vertical[0] = _typed_like(args.get("eval_allow_vertical_a", "true"), true)
_eval_allow_vertical[1] = _typed_like(args.get("eval_allow_vertical_b", "true"), true)
_eval_allow_pitch_roll[0] = _typed_like(args.get("eval_allow_pitch_roll_a", "true"), true)
@@ -265,7 +268,7 @@ const TRAINING_MODE_OVERRIDES := [
# ShipAIController @export names a curriculum run may override, read as
# --ai_<name>=<value> to avoid colliding with the names above.
const SHIP_AI_OVERRIDES := [
"ball_touch_reward", "ball_touch_cooldown_ticks", "ball_touch_direction_floor",
"ball_touch_reward", "ball_touch_cooldown_ticks", "ball_touch_direction_floor", "team_touch_credit_weight",
"velocity_to_ball_weight", "ball_velocity_to_goal_weight", "ball_distance_penalty",
"forward_velocity_to_ball_weight", "air_approach_weight", "air_touch_bonus_weight", "wall_contact_penalty", "tilt_penalty",
"ground_tilt_penalty", "non_forward_penalty", "grounded_upright_reward",
@@ -323,6 +326,7 @@ func _ai_default(name: String) -> Variant:
"ball_touch_reward": return 0.4
"ball_touch_cooldown_ticks": return 60
"ball_touch_direction_floor": return 0.3
"team_touch_credit_weight": return 0.0
"velocity_to_ball_weight": return 0.02
"forward_velocity_to_ball_weight": return 0.0
"air_approach_weight": return 0.0