mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 18:13:42 +00:00
feat(training): support N-vs-M matches with persistent per-ship spawn IDs
Extends ShipObservations beyond the old self+1-opponent layout to padded teammate/opponent arrays (MAX_TEAMMATES=4, MAX_OPPONENTS=5, SIZE=83), zero-filling slots past the real roster size the same way the old single- opponent slot was zero-filled when absent. Slot stability across ticks requires a persistent identity: Ship gains spawn_index (set once by GameMode.spawn_ship, never reassigned — there's no despawn path anywhere in this codebase, so a roster is fixed for the whole episode/match). ai_ship_controller.gd's opponent discovery is rewritten from "first non-self ship" to classify every other ship by team and sort by spawn_index; training_mode.gd/ship_ai_controller.gd carry the equivalent sorted lists through the training path so both agree on slot assignment for the same roster. training_mode.gd and match_mode.gd both gain a team_size export (default 1, so every existing curriculum script and match keeps today's 1v1 behaviour unchanged). This is plumbing only: no 2v2+ curriculum or reward design, and no match-mode UI to pick team size, has been done yet. The two checkpoints in Game/bots/promoted/ are fitted to the old 35-float layout and are not migrated — expected to go stale until the next training run.
This commit is contained in:
@@ -35,7 +35,8 @@ var _action := ShipAction.new()
|
||||
var _ticks_until_decision := 0
|
||||
|
||||
var _ship: Ship
|
||||
var _opponent: Ship
|
||||
var _teammates: Array[Ship] = []
|
||||
var _opponents: Array[Ship] = []
|
||||
var _ball: RigidBody3D
|
||||
var _attack_goal_position: Vector3
|
||||
var _scene_refs_ready := false
|
||||
@@ -60,7 +61,7 @@ func get_action() -> ShipAction:
|
||||
|
||||
|
||||
func _decide() -> void:
|
||||
var obs := ShipObservations.build(_ship, _opponent, _ball, _attack_goal_position)
|
||||
var obs := ShipObservations.build(_ship, _teammates, _opponents, _ball, _attack_goal_position)
|
||||
var out := _policy.forward(obs)
|
||||
# See ShipActionCodec for the decode — the single source of truth shared
|
||||
# with the training side, so this must never reimplement layout/ordering
|
||||
@@ -76,21 +77,40 @@ func _decide() -> void:
|
||||
_action = ShipActionCodec.from_logits(out, action_noise)
|
||||
|
||||
|
||||
# Find ship/ball/opponent/goal once everything is spawned. ShipAction axes
|
||||
# are body-frame so only observations need team context (ShipObservations).
|
||||
# Find ship/ball/teammates/opponents/goal once everything is spawned.
|
||||
# ShipAction axes are body-frame so only observations need team context
|
||||
# (ShipObservations). Rosters never change mid-match (no despawn path exists
|
||||
# anywhere in this codebase), so this only needs to run once — sorted by
|
||||
# spawn_index so a given ship keeps the same observation slot for the whole
|
||||
# match, matching TrainingMode's identically-sorted lists.
|
||||
func _discover_scene_refs() -> bool:
|
||||
_ship = get_parent() as Ship
|
||||
if _ship == null or not is_inside_tree():
|
||||
return false
|
||||
_ball = get_tree().get_first_node_in_group("ball")
|
||||
if _ball == null:
|
||||
return false
|
||||
# Cleared, not just appended to: if an earlier call reached this point but
|
||||
# a later check still failed, a retry must not re-append onto whatever it
|
||||
# already collected — that would duplicate every ship in the roster.
|
||||
_teammates.clear()
|
||||
_opponents.clear()
|
||||
for node in get_tree().get_nodes_in_group("ship"):
|
||||
if node != _ship:
|
||||
_opponent = node
|
||||
break
|
||||
var other := node as Ship
|
||||
if other == _ship:
|
||||
continue
|
||||
if other.team == _ship.team:
|
||||
_teammates.append(other)
|
||||
else:
|
||||
_opponents.append(other)
|
||||
_teammates.sort_custom(_by_spawn_index)
|
||||
_opponents.sort_custom(_by_spawn_index)
|
||||
for goal in get_tree().get_nodes_in_group("goal"):
|
||||
if goal.team == 1 - _ship.team:
|
||||
_attack_goal_position = goal.global_position
|
||||
if _ball == null:
|
||||
return false
|
||||
_scene_refs_ready = true
|
||||
return true
|
||||
|
||||
|
||||
static func _by_spawn_index(a: Ship, b: Ship) -> bool:
|
||||
return a.spawn_index < b.spawn_index
|
||||
|
||||
Reference in New Issue
Block a user