mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-16 11:52:03 +00:00
Compare commits
3 Commits
2f6e7b1201
...
602fa297d0
| Author | SHA1 | Date | |
|---|---|---|---|
| 602fa297d0 | |||
| 03f49e59c8 | |||
| fde098c6c9 |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -60,6 +60,29 @@ extends AIController3D
|
||||
# vector rather than the planar-only one, since a real aerial requires
|
||||
# pitching away from level.
|
||||
@export var air_approach_weight := 0.0
|
||||
# Event bonus, conjunctive with the same goal-direction alignment already
|
||||
# gating ball_touch_reward: extra payout for a touch that is BOTH genuinely
|
||||
# aerial (ball.y > AIR_TOUCH_HEIGHT, matching the productive_air_touch_
|
||||
# fraction telemetry definition exactly) AND goal-directed. air_approach_
|
||||
# weight alone did not move productive_air_touch_fraction (still 0.0 after
|
||||
# a further 180M steps, 360M cumulative) because nothing in the reward ever
|
||||
# made touching the ball while still airborne worth more than the
|
||||
# alternative every policy already had available for free: let gravity pull
|
||||
# an unredirected air-intercept ball back down (it falls well short of the
|
||||
# goal's ~0-1.5m height band over the required flight distance, so it does
|
||||
# not auto-score) and then collect the same goal_reward/ball_touch_reward
|
||||
# via the already-dominant, already-solved ground game once it lands. This
|
||||
# is deliberately NOT the standalone height-only bonus generation 4 ruled
|
||||
# out (see TRAINING.md's "why no air-touch reward" note, added to close the
|
||||
# RLGym wall-bounce exploit): it only pays scaled by the same alignment
|
||||
# dot-product as the base term, so batting the ball in a non-productive
|
||||
# direction earns nothing extra, same anti-farming shape as ball_touch_
|
||||
# reward itself. Also safe from that specific exploit on distributional
|
||||
# grounds: air-intercept spawns are central (x in [-8,8], z in [-10,10],
|
||||
# arena half-extents 18/27) and air-drill spawns keep AIR_DRILL_BALL_WALL_
|
||||
# CLEARANCE from every wall, so neither state can be solved by bouncing off
|
||||
# one.
|
||||
@export var air_touch_bonus_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
|
||||
@@ -504,14 +527,16 @@ func _on_ship_body_entered(body: Node) -> void:
|
||||
var to_goal := attack_goal_position - ball.global_position
|
||||
if to_goal.length_squared() > 0.0001 and ball.linear_velocity.length_squared() > 0.0001:
|
||||
alignment = clampf(ball.linear_velocity.normalized().dot(to_goal.normalized()), 0.0, 1.0)
|
||||
reward += ball_touch_reward * lerpf(ball_touch_direction_floor, 1.0, alignment)
|
||||
var touch_payout := ball_touch_reward * lerpf(ball_touch_direction_floor, 1.0, alignment)
|
||||
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
|
||||
_ticks_since_ball_touch = 0
|
||||
|
||||
# Telemetry only (see get_info's air_touch_fraction) — not a reward term.
|
||||
# Generation 4 deliberately doesn't reward high touches directly (see
|
||||
# TRAINING.md's "why no air-touch reward" note); this just measures
|
||||
# whether the air-drill state setter is producing genuine aerial
|
||||
# contests, so a future decision to add one is data-driven.
|
||||
# air_touch_fraction/productive_air_touch_fraction (see get_info) share
|
||||
# their AIR_TOUCH_HEIGHT/alignment definitions 1:1 with air_touch_bonus_
|
||||
# weight above by design — the reward now targets exactly the behaviour
|
||||
# the telemetry measures.
|
||||
_touches += 1
|
||||
if ball.global_position.y > AIR_TOUCH_HEIGHT:
|
||||
_air_touches += 1
|
||||
|
||||
@@ -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", "air_approach_weight", "wall_contact_penalty", "tilt_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",
|
||||
"speed_reward_weight", "time_penalty", "airborne_penalty",
|
||||
]
|
||||
@@ -324,6 +324,7 @@ func _ai_default(name: String) -> Variant:
|
||||
"velocity_to_ball_weight": return 0.02
|
||||
"forward_velocity_to_ball_weight": return 0.0
|
||||
"air_approach_weight": return 0.0
|
||||
"air_touch_bonus_weight": return 0.0
|
||||
"ball_velocity_to_goal_weight": return 0.004
|
||||
"ball_distance_penalty": return 0.002
|
||||
"wall_contact_penalty": return 0.0025
|
||||
|
||||
+28
@@ -662,6 +662,34 @@ were deleted and Stage 5 restarts from Stage 4's checkpoint with the new term,
|
||||
same reasoning as every previous mechanism change: don't resume a policy
|
||||
shaped by an absent term into one where it now exists.
|
||||
|
||||
**`air_approach_weight` alone did not fix it.** A further three attempts
|
||||
(180M more steps, 360M cumulative across all six Stage-5 attempts, 2026-08-18
|
||||
to 2026-08-19) closed with `productive_air_touch_fraction` still exactly 0.0
|
||||
and `air_touch_fraction` still at noise level, while `goal_rate` kept clearing
|
||||
its (lower) floor. Working out the actual physics instead of retuning another
|
||||
number found the real gap: an unredirected `_place_air_intercept` ball
|
||||
(spawned 6-12m up, aimed at a goal whose collision box sits at ~0-1.5m) sags
|
||||
well short of the goal from gravity alone over the required flight
|
||||
distance — it does not auto-score — so it just falls to the floor, where the
|
||||
already-solved ground game collects the exact same `goal_reward`/
|
||||
`ball_touch_reward` regardless of whether anything touched it in the air.
|
||||
Nothing in the reward ever made a genuinely aerial touch worth more than
|
||||
waiting the second or two for the ball to land, so `air_approach_weight`'s
|
||||
dense closing-speed shaping had nothing to reinforce — a second,
|
||||
independent missing-incentive gap in the same stage, not a training-duration
|
||||
problem. `air_touch_bonus_weight` (`ship_ai_controller.gd`) closes it
|
||||
directly: an event bonus on top of `ball_touch_reward`, paid only for a
|
||||
touch that is both above `AIR_TOUCH_HEIGHT` and goal-directed, scaled by the
|
||||
exact same alignment factor already gating the base touch reward —
|
||||
conjunctive, not standalone, so it targets exactly the behaviour
|
||||
`productive_air_touch_fraction` measures without reopening the RLGym
|
||||
wall-bounce exploit the original "no standalone air-touch reward" decision
|
||||
(above) was written to avoid: air-intercept/air-drill spawns are kept away
|
||||
from every wall by construction. Set to 0.5 (roughly `ball_touch_reward`'s
|
||||
own magnitude) and folded into `HANDLING_REWARD_FLAGS`. The three blocked
|
||||
attempts were deleted and Stage 5 restarts from Stage 4's checkpoint again
|
||||
with both terms active.
|
||||
|
||||
Stage 6's `league` opponent mode samples a historical exported policy at each
|
||||
episode reset. Each later stage preserves the preceding shaping and adds one
|
||||
new difficulty.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -873,5 +873,32 @@
|
||||
"team_1": 54
|
||||
},
|
||||
"win_rate_a": 0.58
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-08-19T21:34:06+00:00",
|
||||
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260819-1321-gen5-s5-intercepts-retry2.json",
|
||||
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260816-2126-gen5-s4-handling-retry2.json",
|
||||
"seed": 1,
|
||||
"episodes": 100,
|
||||
"wins_a": 56,
|
||||
"wins_b": 31,
|
||||
"draws": 13,
|
||||
"side_results": {
|
||||
"a_team_0": {
|
||||
"wins_a": 25,
|
||||
"wins_b": 16,
|
||||
"draws": 9
|
||||
},
|
||||
"a_team_1": {
|
||||
"wins_a": 31,
|
||||
"wins_b": 15,
|
||||
"draws": 4
|
||||
}
|
||||
},
|
||||
"physical_team_wins": {
|
||||
"team_0": 40,
|
||||
"team_1": 47
|
||||
},
|
||||
"win_rate_a": 0.56
|
||||
}
|
||||
]
|
||||
|
||||
@@ -181,10 +181,40 @@ STANDING_ARGS = ["--ent-coef", "0.01", "--entropy-floor"]
|
||||
# than continuing retry2's, same reasoning as every previous mechanism
|
||||
# change in this file: don't resume a policy shaped by an absent term into
|
||||
# one where it now exists.
|
||||
#
|
||||
# Round 8 (2026-08-19): air_approach_weight alone did not move the needle
|
||||
# either -- another full 180M-step chain (3 more attempts, 360M cumulative
|
||||
# across all six Stage-5 attempts) closed with productive_air_touch_fraction
|
||||
# still exactly 0.0 and air_touch_fraction at noise level, while goal_rate
|
||||
# kept passing its (lower) floor. Working out the physics instead of just
|
||||
# re-tuning a number found why: an unredirected air-intercept ball (spawned
|
||||
# 6-12m up, aimed at a goal whose collision box sits at ~0-1.5m) sags well
|
||||
# short of the goal from gravity alone over the required flight distance --
|
||||
# it does not auto-score -- so it simply falls to the floor, and the
|
||||
# already-solved ground game (forward_velocity_to_ball_weight, ball_touch_
|
||||
# reward, goal_reward) collects the exact same total episode reward either
|
||||
# way. Nothing ever made touching the ball while it was still genuinely
|
||||
# airborne worth more than waiting the second or two for it to land, so
|
||||
# air_approach_weight's dense closing-speed shaping had nothing to reinforce
|
||||
# -- nowhere near a training-duration problem, a second missing-incentive
|
||||
# gap in the same stage.
|
||||
#
|
||||
# air_touch_bonus_weight (ship_ai_controller.gd) closes it directly: an
|
||||
# event bonus on top of ball_touch_reward, paid only for a touch that is
|
||||
# both above AIR_TOUCH_HEIGHT and goal-directed, scaled by the exact same
|
||||
# alignment factor already gating the base touch reward -- conjunctive, not
|
||||
# standalone, so it can't be farmed by batting the ball in a useless
|
||||
# direction, and it targets exactly the behaviour productive_air_touch_
|
||||
# fraction measures instead of only the approach to it. Set to 0.5 (roughly
|
||||
# ball_touch_reward's own magnitude, so a fully-aligned aerial touch pays
|
||||
# ~1.7x a fully-aligned ground one). Also folded into HANDLING_REWARD_FLAGS
|
||||
# so Stage 6 inherits it. Restarts Stage 5 from Stage 4's checkpoint again,
|
||||
# same reasoning as every prior mechanism change here.
|
||||
HANDLING_REWARD_FLAGS = [
|
||||
"--velocity-to-ball-weight", "0.04",
|
||||
"--forward-velocity-to-ball-weight", "0.15",
|
||||
"--air-approach-weight", "0.15",
|
||||
"--air-touch-bonus-weight", "0.5",
|
||||
"--ball-distance-penalty", "0.01",
|
||||
"--ball-touch-reward", "0.7",
|
||||
"--ball-velocity-to-goal-weight", "0.06",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"stage_index": 1,
|
||||
"attempt": 2,
|
||||
"attempt": 0,
|
||||
"status": "in_progress",
|
||||
"log": [
|
||||
{
|
||||
@@ -258,171 +258,6 @@
|
||||
],
|
||||
"reason": "Human override. Only miss was the training goal-rate floor, at 0.7731 vs 0.80; every evaluation gate passed (65-22-13 vs promoted/easy.json, 87% non-draw, 12.6% physical-side imbalance) and the goal rate improved monotonically across all three attempts (0.537 -> 0.683 -> 0.773). Also promoted to Game/bots/promoted/medium.json on the same evidence. Stage 5 resumes from this checkpoint."
|
||||
}
|
||||
},
|
||||
{
|
||||
"stage_index": 1,
|
||||
"stage_number": 5,
|
||||
"stage_name": "intercepts",
|
||||
"experiment": "20260818-1903-gen5-s5-intercepts",
|
||||
"attempt": 0,
|
||||
"telemetry_tail": {
|
||||
"rollout/air_touch_fraction": 0.00011999999731779099,
|
||||
"rollout/airborne_fraction": 0.2547863810658455,
|
||||
"rollout/ep_len_mean": 97.30996008300781,
|
||||
"rollout/ep_rew_mean": 16.281804374694826,
|
||||
"rollout/forward_motion_fraction": 0.33076866614818573,
|
||||
"rollout/goal_rate": 0.8198399951457978,
|
||||
"rollout/grounded_upright_fraction": 0.1814400002658367,
|
||||
"rollout/mean_altitude": 3.249552144050598,
|
||||
"rollout/productive_air_touch_fraction": 0.0,
|
||||
"rollout/upright_fraction": 0.7921435704231262,
|
||||
"rollout/vertical_thrust_mean": 0.06410999808754772
|
||||
},
|
||||
"telemetry_failures": [
|
||||
"rollout/productive_air_touch_fraction=0.0000 < 0.0050"
|
||||
],
|
||||
"evaluation_goal_failures": [],
|
||||
"side_balance_failures": [],
|
||||
"eval": {
|
||||
"timestamp": "2026-08-19T03:12:42+00:00",
|
||||
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260818-1903-gen5-s5-intercepts.json",
|
||||
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260816-2126-gen5-s4-handling-retry2.json",
|
||||
"seed": 1,
|
||||
"episodes": 100,
|
||||
"wins_a": 55,
|
||||
"wins_b": 30,
|
||||
"draws": 15,
|
||||
"side_results": {
|
||||
"a_team_0": {
|
||||
"wins_a": 25,
|
||||
"wins_b": 17,
|
||||
"draws": 8
|
||||
},
|
||||
"a_team_1": {
|
||||
"wins_a": 30,
|
||||
"wins_b": 13,
|
||||
"draws": 7
|
||||
}
|
||||
},
|
||||
"physical_team_wins": {
|
||||
"team_0": 38,
|
||||
"team_1": 47
|
||||
},
|
||||
"win_rate_a": 0.55
|
||||
},
|
||||
"evals": [
|
||||
{
|
||||
"timestamp": "2026-08-19T03:12:42+00:00",
|
||||
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260818-1903-gen5-s5-intercepts.json",
|
||||
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260816-2126-gen5-s4-handling-retry2.json",
|
||||
"seed": 1,
|
||||
"episodes": 100,
|
||||
"wins_a": 55,
|
||||
"wins_b": 30,
|
||||
"draws": 15,
|
||||
"side_results": {
|
||||
"a_team_0": {
|
||||
"wins_a": 25,
|
||||
"wins_b": 17,
|
||||
"draws": 8
|
||||
},
|
||||
"a_team_1": {
|
||||
"wins_a": 30,
|
||||
"wins_b": 13,
|
||||
"draws": 7
|
||||
}
|
||||
},
|
||||
"physical_team_wins": {
|
||||
"team_0": 38,
|
||||
"team_1": 47
|
||||
},
|
||||
"win_rate_a": 0.55
|
||||
}
|
||||
],
|
||||
"decision": "fail"
|
||||
},
|
||||
{
|
||||
"stage_index": 1,
|
||||
"stage_number": 5,
|
||||
"stage_name": "intercepts",
|
||||
"experiment": "20260819-0412-gen5-s5-intercepts-retry1",
|
||||
"attempt": 1,
|
||||
"telemetry_tail": {
|
||||
"rollout/air_touch_fraction": 0.00011999999731779099,
|
||||
"rollout/airborne_fraction": 0.25744442877173424,
|
||||
"rollout/ep_len_mean": 112.18267991638183,
|
||||
"rollout/ep_rew_mean": 20.06156894302368,
|
||||
"rollout/forward_motion_fraction": 0.32834857150912283,
|
||||
"rollout/goal_rate": 0.7362400003671646,
|
||||
"rollout/grounded_upright_fraction": 0.17714000023156404,
|
||||
"rollout/mean_altitude": 3.2925854358673097,
|
||||
"rollout/productive_air_touch_fraction": 0.0,
|
||||
"rollout/upright_fraction": 0.7877685700654984,
|
||||
"rollout/vertical_thrust_mean": 0.09532099807504565
|
||||
},
|
||||
"telemetry_failures": [
|
||||
"rollout/goal_rate=0.7362 < 0.7500",
|
||||
"rollout/productive_air_touch_fraction=0.0000 < 0.0050"
|
||||
],
|
||||
"evaluation_goal_failures": [],
|
||||
"side_balance_failures": [],
|
||||
"eval": {
|
||||
"timestamp": "2026-08-19T12:21:42+00:00",
|
||||
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260819-0412-gen5-s5-intercepts-retry1.json",
|
||||
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260816-2126-gen5-s4-handling-retry2.json",
|
||||
"seed": 1,
|
||||
"episodes": 100,
|
||||
"wins_a": 58,
|
||||
"wins_b": 33,
|
||||
"draws": 9,
|
||||
"side_results": {
|
||||
"a_team_0": {
|
||||
"wins_a": 26,
|
||||
"wins_b": 22,
|
||||
"draws": 2
|
||||
},
|
||||
"a_team_1": {
|
||||
"wins_a": 32,
|
||||
"wins_b": 11,
|
||||
"draws": 7
|
||||
}
|
||||
},
|
||||
"physical_team_wins": {
|
||||
"team_0": 37,
|
||||
"team_1": 54
|
||||
},
|
||||
"win_rate_a": 0.58
|
||||
},
|
||||
"evals": [
|
||||
{
|
||||
"timestamp": "2026-08-19T12:21:42+00:00",
|
||||
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260819-0412-gen5-s5-intercepts-retry1.json",
|
||||
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260816-2126-gen5-s4-handling-retry2.json",
|
||||
"seed": 1,
|
||||
"episodes": 100,
|
||||
"wins_a": 58,
|
||||
"wins_b": 33,
|
||||
"draws": 9,
|
||||
"side_results": {
|
||||
"a_team_0": {
|
||||
"wins_a": 26,
|
||||
"wins_b": 22,
|
||||
"draws": 2
|
||||
},
|
||||
"a_team_1": {
|
||||
"wins_a": 32,
|
||||
"wins_b": 11,
|
||||
"draws": 7
|
||||
}
|
||||
},
|
||||
"physical_team_wins": {
|
||||
"team_0": 37,
|
||||
"team_1": 54
|
||||
},
|
||||
"win_rate_a": 0.58
|
||||
}
|
||||
],
|
||||
"decision": "fail"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -360,6 +360,12 @@ def parse_args():
|
||||
help="Aerial mirror of forward-velocity-to-ball-weight: high-altitude dense reward for "
|
||||
"nose-led 3D closing speed toward the ball",
|
||||
)
|
||||
curriculum.add_argument(
|
||||
"--air-touch-bonus-weight", type=float, default=None,
|
||||
help="Event bonus on top of ball-touch-reward for a touch that is both genuinely aerial "
|
||||
"(ball above AIR_TOUCH_HEIGHT) and goal-directed, scaled by the same alignment factor "
|
||||
"as the base touch reward",
|
||||
)
|
||||
curriculum.add_argument(
|
||||
"--ball-distance-penalty", type=float, default=None,
|
||||
help="Overrides ShipAIController.ball_distance_penalty (dense per-tick cost scaled by distance to the ball)",
|
||||
@@ -425,6 +431,7 @@ def _curriculum_kwargs(args) -> dict:
|
||||
"ai_velocity_to_ball_weight": args.velocity_to_ball_weight,
|
||||
"ai_forward_velocity_to_ball_weight": args.forward_velocity_to_ball_weight,
|
||||
"ai_air_approach_weight": args.air_approach_weight,
|
||||
"ai_air_touch_bonus_weight": args.air_touch_bonus_weight,
|
||||
"ai_ball_distance_penalty": args.ball_distance_penalty,
|
||||
"ai_ball_touch_reward": args.ball_touch_reward,
|
||||
"ai_airborne_penalty": args.airborne_penalty,
|
||||
|
||||
Reference in New Issue
Block a user