fix(physics): make upright a real state, and actually start ships on the floor

Six rounds of reward shaping (~700M steps) failed to produce upright ground
driving. A critical review of the simulation rather than the reward found
why:

1. The hull was a 1x1x4 box with inertia (1,1,1) and no restoring torque
   anywhere, so belly-down and rolled-90 were geometrically identical
   resting states. "Upright" was not a physically distinguished state at
   all - the reward was paying for a property the simulation did not have.
2. ~65% of episodes spawned ships via _random_position, which samples Y
   uniformly over the full 18m volume (mean ~8.7m). The measured
   airborne_fraction ~0.44 was largely that spawn distribution, and every
   ground-handling term fades out above 3m, so the shaping being tuned
   barely ever applied.
3. air_drill_chance 0.20 spawned deliberately unreachable-without-climbing
   states in the stage meant to teach ground driving, and its own
   air_touch_fraction (0.0002) shows the drills were never solved.

Fixes land in the physics and the task distribution, not the reward:
- ship.tscn: hull 1x1x4 -> 1.6x0.6x4 so it has one stable resting face;
  inertia (1,1,1) -> (7,1,7), physically correct for the hull, making
  tumbling reluctant while keeping yaw snappy.
- ship.gd: new altitude-faded righting torque (spring-damper toward
  belly-down, faded out by 3m so aerials keep full attitude freedom).
  This is the grav-plating analogue of Rocket League's auto-righting and
  helps human pilots land cleanly too.
- training_mode.gd: new ground_start_chance branch spawning ships level and
  resting on the floor with a floor-level ball - the state the handling
  stage's rewards are actually written for.
- generation5.py: ground-start-chance 0.50, air-drill-chance 0.20 -> 0.0.

Reward terms are left exactly as they were; they should finally pull in a
direction the ship can go.
This commit is contained in:
Josh Creek
2026-08-16 08:17:16 +01:00
parent 033a774c92
commit ea756bd5ba
15 changed files with 146 additions and 270 deletions
+31 -1
View File
@@ -122,6 +122,35 @@ STANDING_ARGS = ["--ent-coef", "0.01", "--entropy-floor"]
# so it likely cannot measure the behaviour the 0.45 floor was meant to
# capture. Re-baseline that floor from what the new signal reports rather
# than from another round of reshaping.
#
# Round 6 (2026-08-16): round 5 read grounded_upright_fraction 0.050 /
# 0.069 / 0.052 — when the ship touches the floor it is upright about 1
# time in 17 — and the user's own observation was "it spends the vast
# majority of the time on its side, driving upwards towards the ball". A
# critical review of the *simulation* rather than the reward found why six
# rounds of shaping could never work:
#
# 1. The hull was a 1x1x4 box with inertia (1,1,1) and no restoring
# torque anywhere, so belly-down and rolled-90 were geometrically
# identical resting states. "Upright" was not a physically
# distinguished state at all — the reward was paying for a property
# the simulation did not have.
# 2. ~65% of episodes spawned ships from _random_position, which samples
# Y uniformly over the full 18m volume (mean ~8.7m). The measured
# airborne_fraction ~0.44 was largely that spawn distribution, and
# every ground-handling term fades out above 3m, so the shaping
# being tuned barely ever applied.
# 3. air_drill_chance 0.20 spawned deliberately unreachable-without-
# climbing states in the stage meant to teach ground driving, and its
# own air_touch_fraction (0.0002) shows the drills were never solved.
#
# Fixes land in the physics and the task distribution instead of the
# reward: an altitude-faded righting torque plus a flat-bottomed hull and
# realistic inertia (ship.gd / ship.tscn) make belly-down a genuine
# attractor, ground_start_chance 0.50 actually starts the ship on the
# floor, and air-drill-chance goes to 0. The reward terms already built
# are left exactly as they were — they should finally pull in a direction
# the ship can go.
HANDLING_REWARD_FLAGS = [
"--velocity-to-ball-weight", "0.04",
"--forward-velocity-to-ball-weight", "0.15",
@@ -145,8 +174,9 @@ STAGES = [
"--opponent-mode", "self_play",
"--kickoff-chance", "0.15",
"--near-goal-chance", "0.25",
"--air-drill-chance", "0.20",
"--air-drill-chance", "0.0",
"--air-intercept-chance", "0.0",
"--ground-start-chance", "0.50",
*HANDLING_REWARD_FLAGS,
],
# Conservative catastrophe floors, not claims of mastery. Tail values
+3 -260
View File
@@ -1,263 +1,6 @@
{
"stage_index": 0,
"attempt": 2,
"status": "blocked",
"log": [
{
"stage_index": 0,
"stage_number": 4,
"stage_name": "handling",
"experiment": "20260814-1939-gen5-s4-handling",
"attempt": 0,
"telemetry_tail": {
"rollout/air_touch_fraction": 0.0004599999897181988,
"rollout/airborne_fraction": 0.43958361899852755,
"rollout/ep_len_mean": 137.3482799835205,
"rollout/ep_rew_mean": -9.079601858139037,
"rollout/forward_motion_fraction": 0.2240593806952238,
"rollout/goal_rate": 0.5860000020265579,
"rollout/grounded_upright_fraction": 0.049944999784231184,
"rollout/mean_altitude": 4.377092701435089,
"rollout/productive_air_touch_fraction": 3.999999910593033e-05,
"rollout/upright_fraction": 0.2692298571616411,
"rollout/vertical_thrust_mean": 0.11654999937745743
},
"telemetry_failures": [
"rollout/goal_rate=0.5860 < 0.8000",
"rollout/upright_fraction=0.2692 < 0.4500",
"rollout/forward_motion_fraction=0.2241 < 0.2500"
],
"evaluation_goal_failures": [
"easy.json: goal_rate=0.780 < 0.800"
],
"side_balance_failures": [],
"eval": {
"timestamp": "2026-08-15T00:56:25+00:00",
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260814-1939-gen5-s4-handling.json",
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/promoted/easy.json",
"seed": 1,
"episodes": 100,
"wins_a": 47,
"wins_b": 31,
"draws": 22,
"side_results": {
"a_team_0": {
"wins_a": 26,
"wins_b": 16,
"draws": 8
},
"a_team_1": {
"wins_a": 21,
"wins_b": 15,
"draws": 14
}
},
"physical_team_wins": {
"team_0": 41,
"team_1": 37
},
"win_rate_a": 0.47
},
"evals": [
{
"timestamp": "2026-08-15T00:56:25+00:00",
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260814-1939-gen5-s4-handling.json",
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/promoted/easy.json",
"seed": 1,
"episodes": 100,
"wins_a": 47,
"wins_b": 31,
"draws": 22,
"side_results": {
"a_team_0": {
"wins_a": 26,
"wins_b": 16,
"draws": 8
},
"a_team_1": {
"wins_a": 21,
"wins_b": 15,
"draws": 14
}
},
"physical_team_wins": {
"team_0": 41,
"team_1": 37
},
"win_rate_a": 0.47
}
],
"decision": "fail"
},
{
"stage_index": 0,
"stage_number": 4,
"stage_name": "handling",
"experiment": "20260815-0156-gen5-s4-handling-retry1",
"attempt": 1,
"telemetry_tail": {
"rollout/air_touch_fraction": 0.0001599999964237213,
"rollout/airborne_fraction": 0.4403798085451126,
"rollout/ep_len_mean": 142.34064038085938,
"rollout/ep_rew_mean": -5.766011684894562,
"rollout/forward_motion_fraction": 0.22267090499401093,
"rollout/goal_rate": 0.5380800016522408,
"rollout/grounded_upright_fraction": 0.06886999988555909,
"rollout/mean_altitude": 4.340883392333985,
"rollout/productive_air_touch_fraction": 3.999999910593033e-05,
"rollout/upright_fraction": 0.300410619109869,
"rollout/vertical_thrust_mean": 0.060687998470733875
},
"telemetry_failures": [
"rollout/goal_rate=0.5381 < 0.8000",
"rollout/upright_fraction=0.3004 < 0.4500",
"rollout/forward_motion_fraction=0.2227 < 0.2500"
],
"evaluation_goal_failures": [],
"side_balance_failures": [],
"eval": {
"timestamp": "2026-08-15T07:12:56+00:00",
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260815-0156-gen5-s4-handling-retry1.json",
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/promoted/easy.json",
"seed": 1,
"episodes": 100,
"wins_a": 48,
"wins_b": 32,
"draws": 20,
"side_results": {
"a_team_0": {
"wins_a": 22,
"wins_b": 17,
"draws": 11
},
"a_team_1": {
"wins_a": 26,
"wins_b": 15,
"draws": 9
}
},
"physical_team_wins": {
"team_0": 37,
"team_1": 43
},
"win_rate_a": 0.48
},
"evals": [
{
"timestamp": "2026-08-15T07:12:56+00:00",
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260815-0156-gen5-s4-handling-retry1.json",
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/promoted/easy.json",
"seed": 1,
"episodes": 100,
"wins_a": 48,
"wins_b": 32,
"draws": 20,
"side_results": {
"a_team_0": {
"wins_a": 22,
"wins_b": 17,
"draws": 11
},
"a_team_1": {
"wins_a": 26,
"wins_b": 15,
"draws": 9
}
},
"physical_team_wins": {
"team_0": 37,
"team_1": 43
},
"win_rate_a": 0.48
}
],
"decision": "fail"
},
{
"stage_index": 0,
"stage_number": 4,
"stage_name": "handling",
"experiment": "20260815-0812-gen5-s4-handling-retry2",
"attempt": 2,
"telemetry_tail": {
"rollout/air_touch_fraction": 0.0002199999950826168,
"rollout/airborne_fraction": 0.4502894285917282,
"rollout/ep_len_mean": 141.1317198638916,
"rollout/ep_rew_mean": -4.833696460247039,
"rollout/forward_motion_fraction": 0.2373267139494419,
"rollout/goal_rate": 0.5526000013351441,
"rollout/grounded_upright_fraction": 0.051729999739676714,
"rollout/mean_altitude": 4.42696008014679,
"rollout/productive_air_touch_fraction": 7.999999821186065e-05,
"rollout/upright_fraction": 0.2735673332810402,
"rollout/vertical_thrust_mean": 0.08471799899730831
},
"telemetry_failures": [
"rollout/goal_rate=0.5526 < 0.8000",
"rollout/upright_fraction=0.2736 < 0.4500",
"rollout/forward_motion_fraction=0.2373 < 0.2500"
],
"evaluation_goal_failures": [
"easy.json: goal_rate=0.740 < 0.800"
],
"side_balance_failures": [],
"eval": {
"timestamp": "2026-08-15T13:29:47+00:00",
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260815-0812-gen5-s4-handling-retry2.json",
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/promoted/easy.json",
"seed": 1,
"episodes": 100,
"wins_a": 44,
"wins_b": 30,
"draws": 26,
"side_results": {
"a_team_0": {
"wins_a": 21,
"wins_b": 18,
"draws": 11
},
"a_team_1": {
"wins_a": 23,
"wins_b": 12,
"draws": 15
}
},
"physical_team_wins": {
"team_0": 33,
"team_1": 41
},
"win_rate_a": 0.44
},
"evals": [
{
"timestamp": "2026-08-15T13:29:47+00:00",
"model_a": "/home/jcreek/ai-training/CosmicClash/Game/bots/20260815-0812-gen5-s4-handling-retry2.json",
"model_b": "/home/jcreek/ai-training/CosmicClash/Game/bots/promoted/easy.json",
"seed": 1,
"episodes": 100,
"wins_a": 44,
"wins_b": 30,
"draws": 26,
"side_results": {
"a_team_0": {
"wins_a": 21,
"wins_b": 18,
"draws": 11
},
"a_team_1": {
"wins_a": 23,
"wins_b": 12,
"draws": 15
}
},
"physical_team_wins": {
"team_0": 33,
"team_1": 41
},
"win_rate_a": 0.44
}
],
"decision": "fail"
}
]
"attempt": 0,
"status": "in_progress",
"log": []
}
+6
View File
@@ -334,6 +334,11 @@ def parse_args():
"--air-intercept-chance", type=float, default=None,
help="Moving high-ball interception starts aimed at a real goal (generation-5 aerial stage)",
)
curriculum.add_argument(
"--ground-start-chance", type=float, default=None,
help="Fraction of resets that spawn ships level and resting on the floor with a floor-level "
"ball — the state the handling stage's ground rewards are written for",
)
curriculum.add_argument(
"--team-size", type=int, choices=range(1, 6), default=None,
help="Ships per team (1-5); generation-5 automated stages remain 1v1 until 2v2 evaluation exists",
@@ -406,6 +411,7 @@ def _curriculum_kwargs(args) -> dict:
"ball_near_goal_chance": args.near_goal_chance,
"air_drill_chance": args.air_drill_chance,
"air_intercept_chance": args.air_intercept_chance,
"ground_start_chance": args.ground_start_chance,
"team_size": args.team_size,
"ai_tilt_penalty": args.tilt_penalty,
"ai_ground_tilt_penalty": args.ground_tilt_penalty,