mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat: add main-menu difficulty picker for Match mode
Replace the raw checkpoint dropdown with curated Easy/Medium/Hard presets that drive GameSettings' bot model/reaction_ticks/action_noise overrides. Move raw-checkpoint testing and Spectate mode into a dev-only section hidden via OS.is_debug_build() so they disappear from release exports.
This commit is contained in:
+45
-12
@@ -71,11 +71,11 @@ text = "A 2:30 match — you vs a trained bot"
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="OpponentLabel" type="Label" parent="CenterContainer/VBoxContainer/MatchRow"]
|
||||
[node name="DifficultyLabel" type="Label" parent="CenterContainer/VBoxContainer/MatchRow"]
|
||||
layout_mode = 2
|
||||
text = "Opponent"
|
||||
text = "Difficulty"
|
||||
|
||||
[node name="BotDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/MatchRow"]
|
||||
[node name="DifficultyDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/MatchRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
layout_mode = 2
|
||||
@@ -86,45 +86,78 @@ custom_minimum_size = Vector2(0, 56)
|
||||
layout_mode = 2
|
||||
text = "Play Match"
|
||||
|
||||
[node name="SpectateSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer"]
|
||||
[node name="DevSection" type="VBoxContainer" parent="CenterContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="DevSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer/DevSection"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SpectateHeader" type="Label" parent="CenterContainer/VBoxContainer"]
|
||||
[node name="DevHeader" type="Label" parent="CenterContainer/VBoxContainer/DevSection"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "Developer"
|
||||
|
||||
[node name="DevHint" type="Label" parent="CenterContainer/VBoxContainer/DevSection"]
|
||||
modulate = Color(1, 1, 1, 0.55)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "Dev-only — hidden in release builds"
|
||||
|
||||
[node name="DevOpponentRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer/DevSection"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="DevOpponentLabel" type="Label" parent="CenterContainer/VBoxContainer/DevSection/DevOpponentRow"]
|
||||
layout_mode = 2
|
||||
text = "Opponent override"
|
||||
|
||||
[node name="DevBotDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/DevSection/DevOpponentRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="SpectateSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer/DevSection"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SpectateHeader" type="Label" parent="CenterContainer/VBoxContainer/DevSection"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "Spectate"
|
||||
|
||||
[node name="SpectateHint" type="Label" parent="CenterContainer/VBoxContainer"]
|
||||
[node name="SpectateHint" type="Label" parent="CenterContainer/VBoxContainer/DevSection"]
|
||||
modulate = Color(1, 1, 1, 0.55)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "Watch two bots play each other"
|
||||
|
||||
[node name="SpectateRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"]
|
||||
[node name="SpectateRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer/DevSection"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="BotADropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/SpectateRow"]
|
||||
[node name="BotADropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/DevSection/SpectateRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="VsLabel" type="Label" parent="CenterContainer/VBoxContainer/SpectateRow"]
|
||||
[node name="VsLabel" type="Label" parent="CenterContainer/VBoxContainer/DevSection/SpectateRow"]
|
||||
layout_mode = 2
|
||||
text = "vs"
|
||||
|
||||
[node name="BotBDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/SpectateRow"]
|
||||
[node name="BotBDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/DevSection/SpectateRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="SpectateButton" type="Button" parent="CenterContainer/VBoxContainer"]
|
||||
[node name="SpectateButton" type="Button" parent="CenterContainer/VBoxContainer/DevSection"]
|
||||
custom_minimum_size = Vector2(0, 56)
|
||||
layout_mode = 2
|
||||
text = "Watch Match"
|
||||
|
||||
[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"]
|
||||
[connection signal="pressed" from="CenterContainer/VBoxContainer/DevSection/SpectateButton" to="." method="_on_spectate_pressed"]
|
||||
|
||||
@@ -5,6 +5,18 @@ extends Node
|
||||
|
||||
# Opponent policy chosen in the main menu; empty = use the mode's own export.
|
||||
var selected_bot_path: String = ""
|
||||
# Difficulty handicaps chosen alongside selected_bot_path; -1/-1.0 = use the
|
||||
# mode's own export.
|
||||
var selected_bot_reaction_ticks: int = -1
|
||||
var selected_bot_action_noise: float = -1.0
|
||||
# Name of the chosen difficulty tier, kept only to reselect the right
|
||||
# dropdown item when returning to the menu.
|
||||
var selected_difficulty_name: String = ""
|
||||
# Dev-only: raw checkpoint override for Match's opponent; empty = use the
|
||||
# difficulty tier above instead. Kept separate from selected_bot_path so the
|
||||
# dev dropdown's own selection survives independently of which difficulty is
|
||||
# picked.
|
||||
var dev_bot_override_path: String = ""
|
||||
|
||||
# Spectate (bot vs bot) policies chosen in the main menu; empty = mode export.
|
||||
var spectate_bot_a_path: String = ""
|
||||
|
||||
+64
-15
@@ -1,26 +1,54 @@
|
||||
extends Control
|
||||
|
||||
# Main menu: one handler per game mode. Bot dropdowns are populated from
|
||||
# res://bots at runtime so newly trained bots appear automatically — never
|
||||
# maintain a hardcoded list. Selections are pushed into the GameSettings
|
||||
# autoload for the target mode to read, and restored when returning to the
|
||||
# menu within a session.
|
||||
# Main menu: one handler per game mode. Match's opponent is a curated
|
||||
# Easy/Medium/Hard difficulty picker (DIFFICULTIES) rather than a raw
|
||||
# checkpoint list — see TRAINING.md for promoting new tiers into
|
||||
# res://bots/promoted/. Raw-checkpoint testing and Spectate (bot vs bot) are
|
||||
# dev-only tools, grouped under DevSection and hidden outside debug builds so
|
||||
# they disappear automatically from release exports. Selections are pushed
|
||||
# into the GameSettings autoload for the target mode to read, and restored
|
||||
# when returning to the menu within a session.
|
||||
|
||||
const BOTS_DIR := "res://bots"
|
||||
|
||||
@onready var bot_dropdown: OptionButton = %BotDropdown
|
||||
const DIFFICULTIES := [
|
||||
{"name": "Easy", "model": "res://bots/promoted/easy.json", "reaction_ticks": 24, "action_noise": 0.35},
|
||||
{"name": "Medium", "model": "res://bots/promoted/easy.json", "reaction_ticks": 14, "action_noise": 0.15},
|
||||
{"name": "Hard", "model": "res://bots/promoted/easy.json", "reaction_ticks": 8, "action_noise": 0.0},
|
||||
]
|
||||
|
||||
@onready var difficulty_dropdown: OptionButton = %DifficultyDropdown
|
||||
@onready var dev_section: Control = %DevSection
|
||||
@onready var dev_bot_dropdown: OptionButton = %DevBotDropdown
|
||||
@onready var bot_a_dropdown: OptionButton = %BotADropdown
|
||||
@onready var bot_b_dropdown: OptionButton = %BotBDropdown
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var bots := _list_bots()
|
||||
_populate_dropdown(bot_dropdown, bots, GameSettings.selected_bot_path)
|
||||
_populate_dropdown(bot_a_dropdown, bots, GameSettings.spectate_bot_a_path)
|
||||
_populate_dropdown(bot_b_dropdown, bots, GameSettings.spectate_bot_b_path)
|
||||
_populate_difficulty_dropdown()
|
||||
dev_section.visible = OS.is_debug_build()
|
||||
if dev_section.visible:
|
||||
var bots := _list_bots()
|
||||
_populate_dropdown(dev_bot_dropdown, bots, GameSettings.dev_bot_override_path, true)
|
||||
_populate_dropdown(bot_a_dropdown, bots, GameSettings.spectate_bot_a_path)
|
||||
_populate_dropdown(bot_b_dropdown, bots, GameSettings.spectate_bot_b_path)
|
||||
$CenterContainer/VBoxContainer/FreePlayButton.grab_focus()
|
||||
|
||||
|
||||
func _populate_difficulty_dropdown() -> void:
|
||||
difficulty_dropdown.clear()
|
||||
for tier in DIFFICULTIES:
|
||||
difficulty_dropdown.add_item(tier["name"])
|
||||
var selected := 0
|
||||
for i in DIFFICULTIES.size():
|
||||
if DIFFICULTIES[i]["name"] == GameSettings.selected_difficulty_name:
|
||||
selected = i
|
||||
break
|
||||
difficulty_dropdown.select(selected)
|
||||
|
||||
|
||||
# Lists bot checkpoints as paths relative to BOTS_DIR, including the
|
||||
# top-level curriculum checkpoints and anything under promoted/.
|
||||
func _list_bots() -> Array[String]:
|
||||
var files: Array[String] = []
|
||||
var dir := DirAccess.open(BOTS_DIR)
|
||||
@@ -28,20 +56,30 @@ func _list_bots() -> Array[String]:
|
||||
for f in dir.get_files():
|
||||
if f.get_extension() == "json":
|
||||
files.append(f)
|
||||
var promoted_dir := DirAccess.open(BOTS_DIR + "/promoted")
|
||||
if promoted_dir:
|
||||
for f in promoted_dir.get_files():
|
||||
if f.get_extension() == "json":
|
||||
files.append("promoted/" + f)
|
||||
files.sort()
|
||||
return files
|
||||
|
||||
|
||||
# Fills a dropdown with bot names (metadata = full model path). Reselects
|
||||
# `preferred_path` if it's still on disk, else the newest (last) bot.
|
||||
func _populate_dropdown(dropdown: OptionButton, bots: Array[String], preferred_path: String) -> void:
|
||||
# Fills a dropdown with bot names (metadata = full model path). When
|
||||
# include_none is true, prepends a "(Use difficulty)" sentinel (metadata "")
|
||||
# and defaults selection to it. Reselects `preferred_path` if it's still on
|
||||
# disk, else the newest (last) bot.
|
||||
func _populate_dropdown(dropdown: OptionButton, bots: Array[String], preferred_path: String, include_none: bool = false) -> void:
|
||||
dropdown.clear()
|
||||
if include_none:
|
||||
dropdown.add_item("(Use difficulty)")
|
||||
dropdown.set_item_metadata(0, "")
|
||||
for f in bots:
|
||||
dropdown.add_item(f.get_basename())
|
||||
dropdown.set_item_metadata(dropdown.item_count - 1, BOTS_DIR + "/" + f)
|
||||
if dropdown.item_count == 0:
|
||||
return
|
||||
dropdown.select(dropdown.item_count - 1)
|
||||
dropdown.select(0 if include_none else dropdown.item_count - 1)
|
||||
for i in dropdown.item_count:
|
||||
if dropdown.get_item_metadata(i) == preferred_path:
|
||||
dropdown.select(i)
|
||||
@@ -59,7 +97,18 @@ func _on_free_play_pressed() -> void:
|
||||
|
||||
|
||||
func _on_match_pressed() -> void:
|
||||
GameSettings.selected_bot_path = _selected_path(bot_dropdown)
|
||||
var tier: Dictionary = DIFFICULTIES[difficulty_dropdown.selected] if difficulty_dropdown.selected >= 0 else DIFFICULTIES[0]
|
||||
var override_path := _selected_path(dev_bot_dropdown) if dev_section.visible else ""
|
||||
GameSettings.dev_bot_override_path = override_path
|
||||
GameSettings.selected_difficulty_name = tier["name"]
|
||||
if override_path.is_empty():
|
||||
GameSettings.selected_bot_path = tier["model"]
|
||||
GameSettings.selected_bot_reaction_ticks = tier["reaction_ticks"]
|
||||
GameSettings.selected_bot_action_noise = tier["action_noise"]
|
||||
else:
|
||||
GameSettings.selected_bot_path = override_path
|
||||
GameSettings.selected_bot_reaction_ticks = -1
|
||||
GameSettings.selected_bot_action_noise = -1.0
|
||||
get_tree().change_scene_to_file("res://scenes/match.tscn")
|
||||
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ func _make_opponent_controller() -> ShipController:
|
||||
if not path.is_empty() and FileAccess.file_exists(path):
|
||||
var bot := AIShipController.new()
|
||||
bot.model_path = path
|
||||
bot.reaction_ticks = bot_reaction_ticks
|
||||
bot.action_noise = bot_action_noise
|
||||
bot.reaction_ticks = bot_reaction_ticks if GameSettings.selected_bot_reaction_ticks < 0 else GameSettings.selected_bot_reaction_ticks
|
||||
bot.action_noise = bot_action_noise if GameSettings.selected_bot_action_noise < 0.0 else GameSettings.selected_bot_action_noise
|
||||
return bot
|
||||
if not path.is_empty():
|
||||
push_warning("MatchMode: bot model not found at %s, spawning inert opponent" % path)
|
||||
|
||||
@@ -11,7 +11,7 @@ The training pipeline is built — see `TRAINING.md` (self-play PPO via the vend
|
||||
- [x] Staged curriculum (score → defend → avoid draws → full mechanics) via `train.py`'s `--opponent-mode`/`--draw-penalty`/`--attack-goal-bias`/`--vertical-ramp`/`--pitch-roll-ramp` flags — see TRAINING.md's "Curriculum training" section. `--opponent-mode=frozen` is a single-fixed-model slice of the league idea below, not the full sampled pool.
|
||||
- [ ] Frozen-opponent league: train the live policy against a *pool* of past exported checkpoints, sampled per-episode (today's `--opponent-mode=frozen` only supports one fixed model per run) to prevent self-play strategy collapse on long runs.
|
||||
- [ ] Richer state setter / curriculum: aerial states, wall plays, rebound scenarios as skill grows (beyond the score/defend/draw staging already in place).
|
||||
- [ ] Main-menu difficulty picker (Match already takes `bot_model_path`/`bot_reaction_ticks`/`bot_action_noise` exports).
|
||||
- [x] Main-menu difficulty picker: Easy/Medium/Hard presets in `main_menu.gd` set `bot_model_path`/`bot_reaction_ticks`/`bot_action_noise` via `GameSettings`. Raw-checkpoint testing and Spectate mode moved into a `DevSection` hidden outside debug builds. All three tiers currently reuse `promoted/easy.json` with different handicaps until `medium`/`hard` checkpoints are promoted.
|
||||
|
||||
## Multiplayer (long term)
|
||||
|
||||
@@ -22,5 +22,5 @@ The training pipeline is built — see `TRAINING.md` (self-play PPO via the vend
|
||||
|
||||
## General
|
||||
|
||||
- [ ] No autoloads yet by design — add a singleton only when cross-scene state is actually needed (e.g. passing match settings/results between menu, match, and results screens).
|
||||
- [x] `GameSettings` autoload (`scripts/game_settings.gd`) added for exactly the cross-scene case this item anticipated — passing menu selections (bot/difficulty, spectate matchup) into match/spectate modes. Still the only autoload; keep it that way unless another cross-scene need shows up.
|
||||
- [ ] More arenas: `arena_01.tscn` is the template — an arena is a setting (sky, lighting, decoration) + two team-tagged goals on the standard goal lines + spawn markers + an instance of the shared `objects/arena_boundary.tscn`, with no rules or state. All arenas share the standard play volume defined by `ArenaBoundary`'s constants.
|
||||
|
||||
Reference in New Issue
Block a user