feat(*): Add bot-vs-bot Spectate mode with main-menu entry, entropy-control flags (--ent-coef, --reset-std) for resumed training runs, and a Linux/3090 remote-training guide (TRAINING_LINUX.md)

This commit is contained in:
Josh Creek
2026-07-19 10:10:32 +01:00
parent 379ef9910e
commit 07217c3517
8 changed files with 230 additions and 3 deletions
+6
View File
@@ -39,5 +39,11 @@ custom_minimum_size = Vector2(330, 60)
layout_mode = 2
text = "Match"
[node name="SpectateButton" type="Button" parent="CenterContainer/VBoxContainer"]
custom_minimum_size = Vector2(330, 60)
layout_mode = 2
text = "Spectate (Bot vs Bot)"
[connection signal="pressed" from="CenterContainer/VBoxContainer/FreePlayButton" to="." method="_on_free_play_pressed"]
[connection signal="pressed" from="CenterContainer/VBoxContainer/MatchButton" to="." method="_on_match_pressed"]
[connection signal="pressed" from="CenterContainer/VBoxContainer/SpectateButton" to="." method="_on_spectate_pressed"]
+14
View File
@@ -0,0 +1,14 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://scripts/spectate_mode.gd" id="1_s"]
[ext_resource type="PackedScene" path="res://scenes/arena_01.tscn" id="2_s"]
[ext_resource type="PackedScene" uid="uid://c8kak2l3m4n5" path="res://scenes/HUD.tscn" id="3_s"]
[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"
[node name="Arena" parent="." instance=ExtResource("2_s")]
[node name="HUD" parent="." instance=ExtResource("3_s")]
+4
View File
@@ -10,3 +10,7 @@ func _on_free_play_pressed() -> void:
func _on_match_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/match.tscn")
func _on_spectate_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/spectate.tscn")
+54
View File
@@ -0,0 +1,54 @@
extends GameMode
# Spectate: bot vs bot exhibition — both ships are AI-driven so two trained
# policies can be watched playing each other. Score tracking and kickoff
# resets like Match, but no timer. R resets the ball, Esc returns to the menu.
signal score_changed(score: Dictionary)
@export_group("Team 0 bot")
@export_file("*.json") var bot_a_model_path: String = ""
@export_range(1, 60) var bot_a_reaction_ticks: int = 8
@export_range(0.0, 1.0) var bot_a_action_noise: float = 0.0
@export_group("Team 1 bot")
@export_file("*.json") var bot_b_model_path: String = ""
@export_range(1, 60) var bot_b_reaction_ticks: int = 8
@export_range(0.0, 1.0) var bot_b_action_noise: float = 0.0
var score := {0: 0, 1: 0}
func _start() -> void:
spawn_ball()
var ship_a := spawn_ship(0, 0, _make_bot(bot_a_model_path, bot_a_reaction_ticks, bot_a_action_noise))
spawn_ship(1, 0, _make_bot(bot_b_model_path, bot_b_reaction_ticks, bot_b_action_noise))
spawn_camera_rig(ship_a)
func _make_bot(model_path: String, reaction_ticks: int, action_noise: float) -> ShipController:
if not model_path.is_empty() and FileAccess.file_exists(model_path):
var bot := AIShipController.new()
bot.model_path = model_path
bot.reaction_ticks = reaction_ticks
bot.action_noise = action_noise
return bot
if not model_path.is_empty():
push_warning("SpectateMode: bot model not found at %s, spawning inert ship" % model_path)
return ShipController.new() # inert placeholder
func _on_goal_scored(conceding_team: int) -> void:
var scoring_team := 1 - conceding_team
score[scoring_team] += 1
score_changed.emit(score.duplicate())
print("Goal for team %d! Score: %d - %d" % [scoring_team, score[0], score[1]])
reset_ball()
reset_ships()
func _unhandled_input(event):
if event.is_action_pressed("reset_ball"):
reset_ball()
else:
super(event)
+1
View File
@@ -0,0 +1 @@
uid://r86jia8qr60x