Files
CosmicClash/Game/scripts/main_menu.gd
T
Josh Creek 0b6679e84b chore(training): promote stage-4 retry2 to medium and open stage 5
20260816-2126-gen5-s4-handling-retry2 exhausted its three attempts and
missed only the 0.80 training goal-rate floor, at 0.7731. Every
evaluation gate passed: 65-22-13 versus promoted/easy.json, 87% non-draw
against an 80% floor, 12.6% physical-side imbalance against a 20%
ceiling, and both handling telemetry floors clear. The round improved the
goal rate monotonically across attempts (0.537 -> 0.683 -> 0.773) and the
checkpoint plays well by hand, so close Stage 4 by human override.

Promote it to Game/bots/promoted/medium.json. Medium and Hard both point
at the new policy: Hard stays a label-only duplicate until a stronger one
earns hard.json, which keeps the tiers monotonic rather than leaving Hard
weaker than Medium.

generation5_state.json flips that log entry to "pass" with a
decision_override block preserving the original verdict and reasoning,
and advances to Stage 5 attempt 1. This is what passing_entry() needs to
resolve Stage 5's resume checkpoint and evaluation reference, and what
league_pool() will need at Stage 6; --skip-to-next-stage would advance
the stage without marking anything as passing and die immediately.

generation5.sh now pulls before launching. Each stage ends in
commit_progress()'s push, which fails and kills the run hours in if the
box is behind origin.
2026-08-17 07:49:21 +01:00

148 lines
5.9 KiB
GDScript

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")