mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 00:14:00 +00:00
feat(training): add airborne_penalty and a stage-6 "unmask" curriculum run
Stage 5 (aggression) passed (41-47 vs grounded curric-s2-defend, within the lenient gate but not yet a clear win). Rather than keep the locomotion mask on indefinitely, stage 6 reopens full 3D controls on top of the aggression retune and pairs it with a new dense airborne_penalty (scaled by height above the floor) so the policy learns to prefer staying grounded through incentives instead of a hard mask — same regime shift that regressed stage 3, but this time with a mitigation and ~12x the training time (~240M timesteps / ~24h vs ~20M / ~2h) to actually re-converge instead of stalling mid-shift. airborne_penalty follows the existing SHIP_AI_OVERRIDES pattern: default 0 (off) on ship_ai_controller.gd, exposed via train.py's new --airborne-penalty flag, added to training_mode.gd's allow-list. Also adds a per-stage timesteps override in curriculum.py (STAGES[n]["timesteps"]) since this is the first stage to need a different budget than the rest.
This commit is contained in:
@@ -70,6 +70,14 @@ extends AIController3D
|
||||
# makes running the clock out strictly worse than scoring as soon as a
|
||||
# chance appears, instead of a free way to keep collecting dense reward.
|
||||
@export var time_penalty := 0.001
|
||||
# Per-tick penalty scaled by height above the floor (0 on the floor, full
|
||||
# value at the arena's ceiling) — distinct from the locomotion mask, which
|
||||
# only discards *thrust*-driven vertical/pitch-roll input; a masked ship can
|
||||
# still be launched airborne by collisions (ball impacts, ship-vs-ship
|
||||
# knockback, the wall/ceiling surface-pull field), and nothing previously
|
||||
# penalized time spent up there. Default 0 (off) so ordinary runs are
|
||||
# unaffected; the floor-lock curriculum stage turns it on.
|
||||
@export var airborne_penalty := 0.0
|
||||
|
||||
# Locomotion curriculum: when false, the corresponding action axes are
|
||||
# discarded in set_action before reaching the ship, so the ship stays
|
||||
@@ -196,6 +204,14 @@ func _physics_process(delta):
|
||||
var uprightness: float = ship.global_transform.basis.y.dot(Vector3.UP)
|
||||
reward -= tilt_penalty * (1.0 - uprightness) * 0.5
|
||||
|
||||
# Dense penalty: height above the floor (see airborne_penalty). The
|
||||
# floor sits at world y = 0 (see training_mode.gd's FIELD_MIN_Y/
|
||||
# _escaped bounds); normalized so the worst case is pinned at the
|
||||
# ceiling.
|
||||
if airborne_penalty > 0.0:
|
||||
var height := maxf(ship.global_position.y, 0.0)
|
||||
reward -= airborne_penalty * height / ArenaBoundary.INNER_HEIGHT
|
||||
|
||||
|
||||
func _wall_or_ceiling_contact() -> bool:
|
||||
var state := PhysicsServer3D.body_get_direct_state(ship.get_rid())
|
||||
|
||||
Reference in New Issue
Block a user