extends Control # 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" # Every tier runs its promoted checkpoint at full trained capability — # difficulty is a genuinely different policy, never the same policy # handicapped with reaction delay or action noise. Easy and Medium are now # distinct models (medium.json beats easy.json 65-22-13 head-to-head); Hard # still points at medium.json, the strongest promoted policy, and stays a # label-only duplicate until a stronger one earns hard.json. Keep the tiers # monotonic: never leave a lower tier pointing at a stronger model than the # one above it. const DIFFICULTIES := [ {"name": "Easy", "model": "res://bots/promoted/easy.json", "reaction_ticks": 8, "action_noise": 0.0}, {"name": "Medium", "model": "res://bots/promoted/medium.json", "reaction_ticks": 8, "action_noise": 0.0}, {"name": "Hard", "model": "res://bots/promoted/medium.json", "reaction_ticks": 8, "action_noise": 0.0}, ] @onready var difficulty_dropdown: OptionButton = %DifficultyDropdown @onready var arena_dropdown: OptionButton = %ArenaDropdown @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: _populate_difficulty_dropdown() _populate_arena_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) # Free Play only — Match/Spectate pick a random arena in code instead. func _populate_arena_dropdown() -> void: arena_dropdown.clear() for tier in ArenaRegistry.ARENAS: arena_dropdown.add_item(tier["name"]) var selected := 0 for i in ArenaRegistry.ARENAS.size(): if ArenaRegistry.ARENAS[i]["path"] == GameSettings.selected_arena_path: selected = i break arena_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) if dir: 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). 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(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) break func _selected_path(dropdown: OptionButton) -> String: if dropdown.item_count > 0 and dropdown.selected >= 0: return dropdown.get_item_metadata(dropdown.selected) return "" func _on_free_play_pressed() -> void: var chosen: Dictionary = ArenaRegistry.ARENAS[0] if arena_dropdown.selected < 0 else ArenaRegistry.ARENAS[arena_dropdown.selected] GameSettings.selected_arena_path = chosen["path"] get_tree().change_scene_to_file("res://scenes/free_play.tscn") func _on_match_pressed() -> void: 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") func _on_settings_pressed() -> void: get_tree().change_scene_to_file("res://scenes/settings.tscn") func _on_spectate_pressed() -> void: GameSettings.spectate_bot_a_path = _selected_path(bot_a_dropdown) GameSettings.spectate_bot_b_path = _selected_path(bot_b_dropdown) get_tree().change_scene_to_file("res://scenes/spectate.tscn")