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
+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": []
}