mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat(*): Fix exported-policy action order to gymnasium's sorted-key layout, add wall-contact penalty and stronger ball-touch reward, and wire Spectate to run01 vs run02
This commit is contained in:
@@ -56,4 +56,4 @@ See `TRAINING.md` for the full workflow (training, exporting, evaluating, diffic
|
||||
- `scripts/ship_observations.gd` is the shared observation builder used by both training and in-game inference — never fork or diverge these two paths. Team 1's observations are mirrored (180° about Y) so one policy plays both sides.
|
||||
- In-game bots: `scripts/ai_ship_controller.gd` (a `ShipController`) runs the exported policy JSON via `scripts/policy_network.gd` (pure-GDScript MLP) — no .NET/ONNX/Python at runtime. Models live in `Game/bots/`; Match mode's `bot_model_path`/`bot_reaction_ticks`/`bot_action_noise` exports configure the opponent.
|
||||
- Python side lives in `training/` (venv, not committed): `train.py` (SB3 PPO, launches parallel headless Godot instances from source), `export_policy.py` (checkpoint → JSON with parity check), `evaluate.py` (head-to-head eval, appends `training/eval_history.json`).
|
||||
- The flattened action space is Box(7): thrust xyz, rotation xyz, turbo (>0 = on) — this is `ShipAction` verbatim; change either only deliberately and together.
|
||||
- The flattened action space is Box(7) in gymnasium's **sorted-key order**: rotation xyz, thrust xyz, turbo (>0 = on). The fields are `ShipAction`'s, but gymnasium alphabetizes Dict spaces, so the flat order is NOT ShipAction's thrust-first declaration order — `AIShipController._decide` consumes exported policies in sorted order; change the action space only deliberately and everywhere together.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,8 +6,8 @@
|
||||
|
||||
[node name="Spectate" type="Node3D"]
|
||||
script = ExtResource("1_s")
|
||||
bot_a_model_path = "res://bots/rookie.json"
|
||||
bot_b_model_path = "res://bots/rookie.json"
|
||||
bot_a_model_path = "res://bots/run02.json"
|
||||
bot_b_model_path = "res://bots/run01.json"
|
||||
|
||||
[node name="Arena" parent="." instance=ExtResource("2_s")]
|
||||
|
||||
|
||||
@@ -49,14 +49,15 @@ func get_action() -> ShipAction:
|
||||
func _decide() -> void:
|
||||
var obs := ShipObservations.build(_ship, _opponent, _ball, _attack_goal_position)
|
||||
var out := _policy.forward(obs)
|
||||
# Output layout matches the flattened training action space (Box(7)):
|
||||
# thrust xyz, rotation xyz, turbo (> 0 means on).
|
||||
_action.thrust = Vector3(
|
||||
# Output layout is the trainer's flattened action space (Box(7)), which
|
||||
# gymnasium orders by SORTED key name — rotation xyz, thrust xyz, turbo
|
||||
# (> 0 means on) — NOT ShipAction's thrust-first declaration order.
|
||||
_action.rotation = Vector3(
|
||||
_axis(out[0]),
|
||||
_axis(out[1]),
|
||||
_axis(out[2])
|
||||
)
|
||||
_action.rotation = Vector3(
|
||||
_action.thrust = Vector3(
|
||||
_axis(out[3]),
|
||||
_axis(out[4]),
|
||||
_axis(out[5])
|
||||
|
||||
@@ -15,9 +15,14 @@ extends AIController3D
|
||||
# Reward shaping weights. Dense terms accrue per physics tick (60 sim-ticks
|
||||
# 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 := 0.1
|
||||
@export var ball_touch_reward := 0.25
|
||||
@export var velocity_to_ball_weight := 0.001
|
||||
@export var ball_velocity_to_goal_weight := 0.004
|
||||
# Per-tick penalty while touching the arena enclosure (walls/floor/ceiling).
|
||||
# At 60 ticks/sim-second this is -0.3/s: a ship parked on a wall for a full
|
||||
# 30 s episode loses ~9 — comparable to conceding — while a brief graze
|
||||
# costs almost nothing.
|
||||
@export var wall_contact_penalty := 0.005
|
||||
|
||||
var ship: Ship
|
||||
var rl_controller: RLShipController
|
||||
@@ -84,6 +89,15 @@ func _physics_process(delta):
|
||||
var ball_progress := ball.linear_velocity.dot(ball_to_goal.normalized())
|
||||
reward += ball_velocity_to_goal_weight * ball_progress / ShipObservations.BALL_SPEED_SCALE
|
||||
|
||||
# Dense penalty: every tick spent in contact with the arena enclosure
|
||||
# (contact monitoring is already on for the ball-touch reward). Ships
|
||||
# bumping each other or the ball is fine — only the boundary counts.
|
||||
if wall_contact_penalty > 0.0:
|
||||
for body in ship.get_colliding_bodies():
|
||||
if body is ArenaBoundary:
|
||||
reward -= wall_contact_penalty
|
||||
break
|
||||
|
||||
|
||||
func _on_ship_body_entered(body: Node) -> void:
|
||||
if body.is_in_group("ball"):
|
||||
|
||||
@@ -8,5 +8,15 @@
|
||||
"wins_b": 0,
|
||||
"draws": 5,
|
||||
"win_rate_a": 0.167
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-19T12:11:30+00:00",
|
||||
"model_a": "/Users/jcreek/Documents/repos/GitHub/CosmicClash/Game/bots/run01.json",
|
||||
"model_b": "/Users/jcreek/Documents/repos/GitHub/CosmicClash/Game/bots/run02.json",
|
||||
"episodes": 40,
|
||||
"wins_a": 4,
|
||||
"wins_b": 1,
|
||||
"draws": 35,
|
||||
"win_rate_a": 0.1
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user