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:
Josh Creek
2026-08-05 09:17:56 +01:00
parent 18fdb0f232
commit 3049c42867
7 changed files with 164 additions and 60 deletions
+10 -1
View File
@@ -20,6 +20,12 @@ const KICKOFF_COUNTDOWN_SECONDS := 3
@export var match_length_seconds := 150.0
# Ships per team. The player always controls one ship on team 0; every other
# ship (teammates and the whole opposing team) is AI-controlled — no local
# multiplayer input. Plumbing only for this pass: there's no menu UI yet to
# pick a team size above 1.
@export_range(1, 5) var team_size: int = 1
@export_group("AI opponent")
# Trained policy for the opponent; empty = inert placeholder ship.
@export_file("*.json") var bot_model_path: String = ""
@@ -41,7 +47,10 @@ func _start() -> void:
spawn_ball()
var player_ship := spawn_ship(0, 0, PlayerShipController.new())
spawn_camera_rig(player_ship)
spawn_ship(1, 0, _make_opponent_controller())
for i in range(1, team_size):
spawn_ship(0, i, _make_opponent_controller())
for i in team_size:
spawn_ship(1, i, _make_opponent_controller())
await _run_kickoff_countdown()