cogames 0.3.49__py3-none-any.whl → 0.3.64__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. cogames/cli/client.py +60 -6
  2. cogames/cli/docsync/__init__.py +0 -0
  3. cogames/cli/docsync/_nb_md_directive_processing.py +180 -0
  4. cogames/cli/docsync/_nb_md_sync.py +103 -0
  5. cogames/cli/docsync/_nb_py_sync.py +122 -0
  6. cogames/cli/docsync/_three_way_sync.py +115 -0
  7. cogames/cli/docsync/_utils.py +76 -0
  8. cogames/cli/docsync/docsync.py +156 -0
  9. cogames/cli/leaderboard.py +112 -28
  10. cogames/cli/mission.py +64 -53
  11. cogames/cli/policy.py +46 -10
  12. cogames/cli/submit.py +268 -67
  13. cogames/cogs_vs_clips/cog.py +79 -0
  14. cogames/cogs_vs_clips/cogs_vs_clips_mapgen.md +19 -16
  15. cogames/cogs_vs_clips/cogsguard_reward_variants.py +153 -0
  16. cogames/cogs_vs_clips/cogsguard_tutorial.py +56 -0
  17. cogames/cogs_vs_clips/evals/README.md +10 -16
  18. cogames/cogs_vs_clips/evals/cogsguard_evals.py +81 -0
  19. cogames/cogs_vs_clips/evals/diagnostic_evals.py +49 -444
  20. cogames/cogs_vs_clips/evals/difficulty_variants.py +13 -326
  21. cogames/cogs_vs_clips/evals/integrated_evals.py +5 -45
  22. cogames/cogs_vs_clips/evals/spanning_evals.py +9 -180
  23. cogames/cogs_vs_clips/mission.py +187 -146
  24. cogames/cogs_vs_clips/missions.py +46 -137
  25. cogames/cogs_vs_clips/procedural.py +8 -8
  26. cogames/cogs_vs_clips/sites.py +107 -3
  27. cogames/cogs_vs_clips/stations.py +198 -186
  28. cogames/cogs_vs_clips/tutorial_missions.py +1 -1
  29. cogames/cogs_vs_clips/variants.py +25 -476
  30. cogames/device.py +13 -1
  31. cogames/{policy/scripted_agent/README.md → docs/SCRIPTED_AGENT.md} +82 -58
  32. cogames/evaluate.py +18 -30
  33. cogames/main.py +1434 -243
  34. cogames/maps/canidate1_1000.map +1 -1
  35. cogames/maps/canidate1_1000_stations.map +2 -2
  36. cogames/maps/canidate1_500.map +1 -1
  37. cogames/maps/canidate1_500_stations.map +2 -2
  38. cogames/maps/canidate2_1000.map +1 -1
  39. cogames/maps/canidate2_1000_stations.map +2 -2
  40. cogames/maps/canidate2_500.map +1 -1
  41. cogames/maps/canidate2_500_stations.map +2 -2
  42. cogames/maps/canidate3_1000.map +1 -1
  43. cogames/maps/canidate3_1000_stations.map +2 -2
  44. cogames/maps/canidate3_500.map +1 -1
  45. cogames/maps/canidate3_500_stations.map +2 -2
  46. cogames/maps/canidate4_500.map +1 -1
  47. cogames/maps/canidate4_500_stations.map +2 -2
  48. cogames/maps/cave_base_50.map +2 -2
  49. cogames/maps/diagnostic_evals/diagnostic_agile.map +2 -2
  50. cogames/maps/diagnostic_evals/diagnostic_agile_hard.map +2 -2
  51. cogames/maps/diagnostic_evals/diagnostic_charge_up.map +2 -2
  52. cogames/maps/diagnostic_evals/diagnostic_charge_up_hard.map +2 -2
  53. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1.map +2 -2
  54. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1_hard.map +2 -2
  55. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2.map +2 -2
  56. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2_hard.map +2 -2
  57. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3.map +2 -2
  58. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3_hard.map +2 -2
  59. cogames/maps/diagnostic_evals/diagnostic_chest_near.map +2 -2
  60. cogames/maps/diagnostic_evals/diagnostic_chest_search.map +2 -2
  61. cogames/maps/diagnostic_evals/diagnostic_chest_search_hard.map +2 -2
  62. cogames/maps/diagnostic_evals/diagnostic_extract_lab.map +2 -2
  63. cogames/maps/diagnostic_evals/diagnostic_extract_lab_hard.map +2 -2
  64. cogames/maps/diagnostic_evals/diagnostic_memory.map +2 -2
  65. cogames/maps/diagnostic_evals/diagnostic_memory_hard.map +2 -2
  66. cogames/maps/diagnostic_evals/diagnostic_radial.map +2 -2
  67. cogames/maps/diagnostic_evals/diagnostic_radial_hard.map +2 -2
  68. cogames/maps/diagnostic_evals/diagnostic_resource_lab.map +2 -2
  69. cogames/maps/diagnostic_evals/diagnostic_unclip.map +2 -2
  70. cogames/maps/evals/eval_balanced_spread.map +9 -5
  71. cogames/maps/evals/eval_clip_oxygen.map +9 -5
  72. cogames/maps/evals/eval_collect_resources.map +9 -5
  73. cogames/maps/evals/eval_collect_resources_hard.map +9 -5
  74. cogames/maps/evals/eval_collect_resources_medium.map +9 -5
  75. cogames/maps/evals/eval_divide_and_conquer.map +9 -5
  76. cogames/maps/evals/eval_energy_starved.map +9 -5
  77. cogames/maps/evals/eval_multi_coordinated_collect_hard.map +9 -5
  78. cogames/maps/evals/eval_oxygen_bottleneck.map +9 -5
  79. cogames/maps/evals/eval_single_use_world.map +9 -5
  80. cogames/maps/evals/extractor_hub_100x100.map +9 -5
  81. cogames/maps/evals/extractor_hub_30x30.map +9 -5
  82. cogames/maps/evals/extractor_hub_50x50.map +9 -5
  83. cogames/maps/evals/extractor_hub_70x70.map +9 -5
  84. cogames/maps/evals/extractor_hub_80x80.map +9 -5
  85. cogames/maps/machina_100_stations.map +2 -2
  86. cogames/maps/machina_200_stations.map +2 -2
  87. cogames/maps/machina_200_stations_small.map +2 -2
  88. cogames/maps/machina_eval_exp01.map +2 -2
  89. cogames/maps/machina_eval_template_large.map +2 -2
  90. cogames/maps/machinatrainer4agents.map +2 -2
  91. cogames/maps/machinatrainer4agentsbase.map +2 -2
  92. cogames/maps/machinatrainerbig.map +2 -2
  93. cogames/maps/machinatrainersmall.map +2 -2
  94. cogames/maps/planky_evals/aligner_avoid_aoe.map +28 -0
  95. cogames/maps/planky_evals/aligner_full_cycle.map +28 -0
  96. cogames/maps/planky_evals/aligner_gear.map +24 -0
  97. cogames/maps/planky_evals/aligner_hearts.map +24 -0
  98. cogames/maps/planky_evals/aligner_junction.map +26 -0
  99. cogames/maps/planky_evals/exploration_distant.map +28 -0
  100. cogames/maps/planky_evals/maze.map +32 -0
  101. cogames/maps/planky_evals/miner_best_resource.map +26 -0
  102. cogames/maps/planky_evals/miner_deposit.map +24 -0
  103. cogames/maps/planky_evals/miner_extract.map +26 -0
  104. cogames/maps/planky_evals/miner_full_cycle.map +28 -0
  105. cogames/maps/planky_evals/miner_gear.map +24 -0
  106. cogames/maps/planky_evals/multi_role.map +28 -0
  107. cogames/maps/planky_evals/resource_chain.map +30 -0
  108. cogames/maps/planky_evals/scout_explore.map +32 -0
  109. cogames/maps/planky_evals/scout_gear.map +24 -0
  110. cogames/maps/planky_evals/scrambler_full_cycle.map +28 -0
  111. cogames/maps/planky_evals/scrambler_gear.map +24 -0
  112. cogames/maps/planky_evals/scrambler_target.map +26 -0
  113. cogames/maps/planky_evals/stuck_corridor.map +32 -0
  114. cogames/maps/planky_evals/survive_retreat.map +26 -0
  115. cogames/maps/training_facility_clipped.map +2 -2
  116. cogames/maps/training_facility_open_1.map +2 -2
  117. cogames/maps/training_facility_open_2.map +2 -2
  118. cogames/maps/training_facility_open_3.map +2 -2
  119. cogames/maps/training_facility_tight_4.map +2 -2
  120. cogames/maps/training_facility_tight_5.map +2 -2
  121. cogames/maps/vanilla_large.map +2 -2
  122. cogames/maps/vanilla_small.map +2 -2
  123. cogames/pickup.py +183 -0
  124. cogames/play.py +166 -33
  125. cogames/policy/chaos_monkey.py +54 -0
  126. cogames/policy/nim_agents/__init__.py +27 -10
  127. cogames/policy/nim_agents/agents.py +121 -60
  128. cogames/policy/nim_agents/thinky_eval.py +35 -222
  129. cogames/policy/pufferlib_policy.py +67 -32
  130. cogames/policy/starter_agent.py +184 -0
  131. cogames/policy/trainable_policy_template.py +4 -1
  132. cogames/train.py +51 -13
  133. cogames/verbose.py +2 -2
  134. cogames-0.3.64.dist-info/METADATA +1842 -0
  135. cogames-0.3.64.dist-info/RECORD +159 -0
  136. cogames-0.3.64.dist-info/licenses/LICENSE +21 -0
  137. cogames-0.3.64.dist-info/top_level.txt +2 -0
  138. metta_alo/__init__.py +0 -0
  139. metta_alo/job_specs.py +17 -0
  140. metta_alo/policy.py +16 -0
  141. metta_alo/pure_single_episode_runner.py +75 -0
  142. metta_alo/py.typed +0 -0
  143. metta_alo/rollout.py +322 -0
  144. metta_alo/scoring.py +168 -0
  145. cogames/maps/diagnostic_evals/diagnostic_assembler_near.map +0 -49
  146. cogames/maps/diagnostic_evals/diagnostic_assembler_search.map +0 -49
  147. cogames/maps/diagnostic_evals/diagnostic_assembler_search_hard.map +0 -89
  148. cogames/policy/nim_agents/common.nim +0 -887
  149. cogames/policy/nim_agents/install.sh +0 -1
  150. cogames/policy/nim_agents/ladybug_agent.nim +0 -984
  151. cogames/policy/nim_agents/nim_agents.nim +0 -55
  152. cogames/policy/nim_agents/nim_agents.nims +0 -14
  153. cogames/policy/nim_agents/nimby.lock +0 -3
  154. cogames/policy/nim_agents/racecar_agents.nim +0 -884
  155. cogames/policy/nim_agents/random_agents.nim +0 -68
  156. cogames/policy/nim_agents/test_agents.py +0 -53
  157. cogames/policy/nim_agents/thinky_agents.nim +0 -717
  158. cogames/policy/scripted_agent/baseline_agent.py +0 -1049
  159. cogames/policy/scripted_agent/demo_policy.py +0 -244
  160. cogames/policy/scripted_agent/pathfinding.py +0 -126
  161. cogames/policy/scripted_agent/starter_agent.py +0 -136
  162. cogames/policy/scripted_agent/types.py +0 -235
  163. cogames/policy/scripted_agent/unclipping_agent.py +0 -476
  164. cogames/policy/scripted_agent/utils.py +0 -385
  165. cogames-0.3.49.dist-info/METADATA +0 -406
  166. cogames-0.3.49.dist-info/RECORD +0 -136
  167. cogames-0.3.49.dist-info/top_level.txt +0 -1
  168. {cogames-0.3.49.dist-info → cogames-0.3.64.dist-info}/WHEEL +0 -0
  169. {cogames-0.3.49.dist-info → cogames-0.3.64.dist-info}/entry_points.txt +0 -0
cogames/main.py CHANGED
@@ -18,7 +18,7 @@ import sys
18
18
  import threading
19
19
  import time
20
20
  from pathlib import Path
21
- from typing import Any, Literal, Optional, TypeVar
21
+ from typing import Literal, Optional, TypeVar
22
22
 
23
23
  import typer
24
24
  import yaml # type: ignore[import]
@@ -29,14 +29,16 @@ from rich.panel import Panel
29
29
  from rich.prompt import Prompt
30
30
  from rich.table import Table
31
31
 
32
- import cogames.policy.scripted_agent.starter_agent as starter_agent
32
+ import cogames.policy.starter_agent as starter_agent
33
33
  import cogames.policy.trainable_policy_template as trainable_policy_template
34
34
  from cogames import evaluate as evaluate_module
35
35
  from cogames import game, verbose
36
+ from cogames import pickup as pickup_module
36
37
  from cogames import play as play_module
37
38
  from cogames import train as train_module
38
39
  from cogames.cli.base import console
39
- from cogames.cli.client import TournamentServerClient
40
+ from cogames.cli.client import SeasonInfo, TournamentServerClient, fetch_default_season, fetch_season_info
41
+ from cogames.cli.docsync import docsync
40
42
  from cogames.cli.leaderboard import (
41
43
  leaderboard_cmd,
42
44
  parse_policy_identifier,
@@ -53,14 +55,17 @@ from cogames.cli.mission import (
53
55
  list_variants,
54
56
  )
55
57
  from cogames.cli.policy import (
58
+ _translate_error,
56
59
  get_policy_spec,
57
60
  get_policy_specs_with_proportions,
61
+ parse_policy_spec,
58
62
  policy_arg_example,
59
63
  policy_arg_w_proportion_example,
60
64
  )
61
- from cogames.cli.submit import DEFAULT_SUBMIT_SERVER, upload_policy, validate_policy_spec
65
+ from cogames.cli.submit import DEFAULT_SUBMIT_SERVER, results_url_for_season, upload_policy, validate_policy_spec
62
66
  from cogames.curricula import make_rotation
63
67
  from cogames.device import resolve_training_device
68
+ from mettagrid.config.mettagrid_config import MettaGridConfig
64
69
  from mettagrid.mapgen.mapgen import MapGen
65
70
  from mettagrid.policy.loader import discover_and_register_policies
66
71
  from mettagrid.policy.policy_registry import get_policy_registry
@@ -100,13 +105,21 @@ def _resolve_mettascope_script() -> Path:
100
105
  )
101
106
 
102
107
 
108
+ def _register_policies() -> None:
109
+ discover_and_register_policies()
110
+
111
+
112
+ def _register_policies_callback() -> None:
113
+ _register_policies()
114
+
115
+
103
116
  app = typer.Typer(
104
117
  help="CoGames - Multi-agent cooperative and competitive games",
105
118
  context_settings={"help_option_names": ["-h", "--help"]},
106
119
  no_args_is_help=True,
107
120
  rich_markup_mode="rich",
108
121
  pretty_exceptions_show_locals=False,
109
- callback=lambda: discover_and_register_policies("cogames.policy"),
122
+ callback=_register_policies_callback,
110
123
  )
111
124
 
112
125
  tutorial_app = typer.Typer(
@@ -119,8 +132,12 @@ tutorial_app = typer.Typer(
119
132
  if register_tribal_cli is not None:
120
133
  register_tribal_cli(app)
121
134
 
135
+ app.add_typer(docsync.app, name="docsync", hidden=True)
122
136
 
123
- @tutorial_app.command(name="play", help="Interactive tutorial - learn to play Cogs vs Clips")
137
+
138
+ @tutorial_app.command(
139
+ name="play", help="Interactive tutorial - learn to play Cogs vs Clips", rich_help_panel="Tutorial"
140
+ )
124
141
  def tutorial_cmd(
125
142
  ctx: typer.Context,
126
143
  ) -> None:
@@ -130,8 +147,8 @@ def tutorial_cmd(
130
147
 
131
148
  console.print(
132
149
  Panel.fit(
133
- "[bold cyan]MISSION BRIEFING: Tutorial Sector[/bold cyan]\n\n"
134
- "Welcome, Cognitive. This simulation mirrors frontline HEART ops.\n"
150
+ "[bold cyan]MISSION BRIEFING: CogsGuard Training Sector[/bold cyan]\n\n"
151
+ "Welcome, Cognitive. This simulation mirrors frontline CogsGuard ops.\n"
135
152
  "We will launch the Mettascope visual interface now.\n\n"
136
153
  "When you are ready to deploy, press Enter below and then return here to receive instructions.",
137
154
  title="Mission Briefing",
@@ -142,13 +159,11 @@ def tutorial_cmd(
142
159
  Prompt.ask("[dim]Press Enter to launch simulation[/dim]", default="", show_default=False)
143
160
  console.print("[dim]Initializing Mettascope...[/dim]")
144
161
 
145
- # Load tutorial mission
146
- from cogames.cogs_vs_clips.tutorial_missions import TutorialMission
162
+ # Load tutorial mission (CogsGuard)
163
+ from cogames.cogs_vs_clips.missions import make_cogsguard_mission
147
164
 
148
165
  # Create environment config
149
- env_cfg = TutorialMission.make_env()
150
- # Force 1 agent for tutorial
151
- env_cfg.game.num_agents = 1
166
+ env_cfg = make_cogsguard_mission(num_agents=1, max_steps=1000).make_env()
152
167
 
153
168
  stop_event = threading.Event()
154
169
 
@@ -174,7 +189,7 @@ def tutorial_cmd(
174
189
  "Right Pane (Vibe Deck): Select icons here to change your Cog's broadcast resonance.",
175
190
  "Zoom/Pan: Scroll or pinch to zoom the arena; drag to pan.",
176
191
  "Click various buildings to view their details in the Left Pane.",
177
- "Look for the Chest, Assembler, Charger, and Extractor stations.",
192
+ "Look for the Hub (Hub), Junctions, Gear Stations, and Extractors.",
178
193
  "Click your Cog to assume control.",
179
194
  ),
180
195
  },
@@ -182,46 +197,46 @@ def tutorial_cmd(
182
197
  "title": "Step 2 — Movement & Energy",
183
198
  "lines": (
184
199
  "Use WASD or Arrow Keys to move your Cog.",
185
- "Every move costs Energy, every time step recovers Energy.",
200
+ "Every move costs Energy, and aligned hubs/junctions recharge you.",
186
201
  "Watch your battery bar on the Cog or in the HUD.",
187
- "If low, rest (skip turn), lean against a wall (walk into it), vibe, or",
188
- "find a Charger [yellow]+[/yellow].",
202
+ "If low, rest (skip turn), lean against a wall (walk into it), or",
203
+ "stand near the Hub or an aligned Junction.",
189
204
  ),
190
205
  },
191
206
  {
192
- "title": "Step 3 — Extraction",
207
+ "title": "Step 3 — Gear Up",
193
208
  "lines": (
194
209
  "Primary interaction mode is WALKING INTO things.",
195
- "Locate an Extractor station:",
196
- " [yellow]C[/yellow] (Carbon), [yellow]O[/yellow] (Oxygen),",
197
- " [yellow]G[/yellow] (Germanium), [yellow]S[/yellow] (Silicon).",
198
- "Walk into it to extract resources.",
199
- "Note: Silicon ([yellow]S[/yellow]) costs 20 energy!",
210
+ "Locate a Gear Station and walk into it to equip a role:",
211
+ " [yellow]⛏ Miner[/yellow], [yellow]🔭 Scout[/yellow],",
212
+ " [yellow]🔗 Aligner[/yellow], [yellow]🌀 Scrambler[/yellow].",
213
+ "Gear costs are paid from the team commons.",
200
214
  ),
201
215
  },
202
216
  {
203
- "title": "Step 4 — Crafting (Assembler)",
217
+ "title": "Step 4 — Resources & Hearts",
204
218
  "lines": (
205
- "Click the central Assembler [yellow]&[/yellow] to see the HEART recipe in the Left Pane.",
206
- "Set your Vibe (Right Pane) to match the requirement (usually [red]heart_a[/red]).",
207
- "Walk into the Assembler to craft. Inputs are taken from your inventory instantly.",
219
+ "Find an Extractor station to gather elements:",
220
+ " [yellow]C[/yellow] (Carbon), [yellow]O[/yellow] (Oxygen),",
221
+ " [yellow]G[/yellow] (Germanium), [yellow]S[/yellow] (Silicon).",
222
+ "Visit the Chest to assemble or withdraw Hearts from the commons.",
208
223
  ),
209
224
  },
210
225
  {
211
- "title": "Step 5 — Deposit (Chest)",
226
+ "title": "Step 5 — Junction Control",
212
227
  "lines": (
213
- "Go to the Chest [yellow]C[/yellow] (usually near the center).",
214
- "Switch your Vibe to [red]heart_b[/red] (Deposit Mode).",
215
- "Walk into the Chest to deposit the HEART and complete the objective.",
216
- "Note: To pull resources out of the Chest, you must vibe the matching resource *_a protocol.",
228
+ "Junctions (junctions) can be aligned to your team.",
229
+ "As an Aligner: get Influence (stand near the Hub) + a Heart, then bump a neutral junction.",
230
+ "As a Scrambler: get a Heart, then bump an enemy-aligned junction to neutralize it.",
231
+ "Aligned junctions recharge energy for your team.",
217
232
  ),
218
233
  },
219
234
  {
220
235
  "title": "Step 6 — Objective Complete",
221
236
  "lines": (
222
237
  "[bold green]🎉 Congratulations![/bold green] You have completed the tutorial.",
223
- "You've mastered extraction, crafting, and resource management.",
224
- "[bold cyan]You're now ready to tackle the full mission![/bold cyan]",
238
+ "You've mastered movement, gear, resources, and junction control.",
239
+ "[bold cyan]You're now ready to tackle the full CogsGuard arena![/bold cyan]",
225
240
  ),
226
241
  },
227
242
  )
@@ -241,7 +256,7 @@ def tutorial_cmd(
241
256
 
242
257
  console.print(
243
258
  "[bold green]REFERENCE DOSSIERS[/bold green]\n"
244
- "- [link=packages/cogames/MISSION.md]MISSION.md[/link]: Machina VII deployment orders.\n"
259
+ "- [link=packages/cogames/MISSION.md]MISSION.md[/link]: CogsGuard deployment orders.\n"
245
260
  "- [link=packages/cogames/README.md]README.md[/link]: System overview and CLI quick start.\n"
246
261
  "- [link=packages/cogames/TECHNICAL_MANUAL.md]TECHNICAL_MANUAL.md[/link]: FACE sensor/command schematics."
247
262
  )
@@ -268,40 +283,297 @@ def tutorial_cmd(
268
283
  stop_event.set()
269
284
 
270
285
 
271
- app.add_typer(tutorial_app, name="tutorial")
286
+ @tutorial_app.command(
287
+ name="cogsguard",
288
+ help="Interactive CogsGuard tutorial - learn roles and territory control",
289
+ rich_help_panel="Tutorial",
290
+ )
291
+ def cogsguard_tutorial_cmd(
292
+ ctx: typer.Context,
293
+ ) -> None:
294
+ """Run the CogsGuard tutorial."""
295
+ # Suppress logs during tutorial to keep instructions visible
296
+ logging.getLogger().setLevel(logging.ERROR)
297
+
298
+ console.print(
299
+ Panel.fit(
300
+ "[bold cyan]MISSION BRIEFING: CogsGuard Training[/bold cyan]\n\n"
301
+ "Welcome, Cognitive. This simulation introduces you to CogsGuard operations.\n"
302
+ "You will learn about specialized gear, resource management, and territory control.\n\n"
303
+ "When you are ready to deploy, press Enter below and then return here to receive instructions.",
304
+ title="CogsGuard Briefing",
305
+ border_style="green",
306
+ )
307
+ )
308
+
309
+ Prompt.ask("[dim]Press Enter to launch simulation[/dim]", default="", show_default=False)
310
+ console.print("[dim]Initializing Mettascope...[/dim]")
311
+
312
+ # Load CogsGuard tutorial mission
313
+ from cogames.cogs_vs_clips.cogsguard_tutorial import CogsGuardTutorialMission
314
+
315
+ # Create environment config
316
+ env_cfg = CogsGuardTutorialMission.make_env()
317
+
318
+ stop_event = threading.Event()
319
+
320
+ def _wait_for_enter(prompt: str) -> bool:
321
+ if stop_event.is_set():
322
+ return False
323
+ try:
324
+ Prompt.ask(prompt, default="", show_default=False)
325
+ except (KeyboardInterrupt, EOFError):
326
+ stop_event.set()
327
+ return False
328
+ return True
329
+
330
+ def run_cogsguard_tutorial_steps():
331
+ # Wait a moment for the window to appear
332
+ time.sleep(3)
333
+
334
+ tutorial_steps = (
335
+ {
336
+ "title": "Step 1 — Objective & Scoring",
337
+ "lines": (
338
+ "CogsGuard is a territory control game. Your team earns points by holding junctions.",
339
+ "[bold]Reward per tick[/bold] = junctions held / max_steps / num_cogs",
340
+ "Control more junctions, earn more points. You start in your Hub (center).",
341
+ ),
342
+ "task": "Click your Cog to select it, then explore your Hub and familiarize yourself with the area.",
343
+ },
344
+ {
345
+ "title": "Step 2 — The Clips Threat",
346
+ "lines": (
347
+ "[bold red]WARNING:[/bold red] Clips are automated enemies that expand territory!",
348
+ "Every ~300 steps, Clips [yellow]scramble[/yellow] nearby Cog junctions to neutral.",
349
+ "Every ~300 steps, Clips [yellow]capture[/yellow] nearby neutral junctions.",
350
+ "Clips expansion has a 25-cell radius. You must actively defend or be overrun!",
351
+ ),
352
+ },
353
+ {
354
+ "title": "Step 3 — Territory & Resources",
355
+ "lines": (
356
+ "Junctions and Hubs project effects in a [bold]10-cell radius[/bold]:",
357
+ "[green]Friendly territory:[/green] Restores +100 HP, +100 energy, +10 influence per tick.",
358
+ "[red]Enemy territory:[/red] Drains -1 HP and -100 influence per tick.",
359
+ "[bold]HP:[/bold] Base 100. You lose -1 HP/tick outside friendly territory.",
360
+ " At 0 HP, gear and hearts are [bold red]destroyed[/bold red].",
361
+ "[bold]Energy:[/bold] Base 20. Moving costs [yellow]3 energy[/yellow]. Regens +1/tick.",
362
+ "[yellow]Key insight:[/yellow] Aligners can't capture in enemy AOE (influence drains too fast).",
363
+ ),
364
+ "task": "Walk outside your Hub, watch your HP drain, then return to heal.",
365
+ },
366
+ {
367
+ "title": "Step 4 — Gear Stations",
368
+ "lines": (
369
+ "Equip gear at stations. Each costs 6 collective resources (different mixes):",
370
+ "[yellow]Miner[/yellow]: +40 cargo, 10x extraction. Cost: 1C/1O/[bold]3G[/bold]/1S",
371
+ "[yellow]Aligner[/yellow]: +20 influence cap, captures territory. Cost: [bold]3C[/bold]/1O/1G/1S",
372
+ "[yellow]Scrambler[/yellow]: +200 HP, disrupts enemy junctions. Cost: 1C/[bold]3O[/bold]/1G/1S",
373
+ "[yellow]Scout[/yellow]: +400 HP, +100 energy, mobile recon. Cost: 1C/1O/1G/[bold]3S[/bold]",
374
+ "Switching gear replaces your current gear (only hold one at a time).",
375
+ ),
376
+ "task": "Find a Gear Station in your base and equip Miner gear (walk into it).",
377
+ },
378
+ {
379
+ "title": "Step 5 — Capturing & Scrambling",
380
+ "lines": (
381
+ "[bold]To capture a neutral junction (Aligner only):[/bold]",
382
+ " • Requires: Aligner gear + [yellow]1 heart[/yellow] + [yellow]1 influence[/yellow]",
383
+ " • Must NOT be in enemy AOE (influence would be drained)",
384
+ "[bold]To scramble an enemy junction (Scrambler only):[/bold]",
385
+ " • Requires: Scrambler gear + [yellow]1 heart[/yellow]",
386
+ " • Converts enemy junction to neutral (then Aligners can capture it)",
387
+ ),
388
+ },
389
+ {
390
+ "title": "Step 6 — Resources & Hearts",
391
+ "lines": (
392
+ "[bold]Extractors:[/bold] Walk into them to gather resources (1 per use, 10 with Miner gear).",
393
+ "[bold]Deposit:[/bold] Walk into the Hub (center of Hub) to deposit resources.",
394
+ "[bold]Hearts:[/bold] At the Chest, convert [yellow]1C + 1O + 1G + 1S[/yellow] into 1 heart.",
395
+ " Hearts are spent to capture/scramble junctions.",
396
+ "[bold]Aligning:[/bold] Switch to Aligner gear, then walk into a neutral junction to capture it.",
397
+ "Team coordination: Miners gather → deposit → make hearts → Aligners/Scramblers use them.",
398
+ ),
399
+ "task": (
400
+ "Extract resources (C/O/G/S), deposit at the Hub, craft a heart, "
401
+ "then switch to Aligner and capture a junction."
402
+ ),
403
+ },
404
+ {
405
+ "title": "Step 7 — Tutorial Complete",
406
+ "lines": (
407
+ "[bold green]Congratulations![/bold green] You've completed the CogsGuard tutorial.",
408
+ "",
409
+ "[bold]Remember the core loop:[/bold]",
410
+ " 1. Miners gather resources and deposit at the Hub",
411
+ " 2. Convert resources to hearts at the Chest",
412
+ " 3. Scramblers neutralize enemy junctions (1 heart each)",
413
+ " 4. Aligners capture neutral junctions (1 heart + 1 influence each)",
414
+ " 5. Defend against Clips expansion!",
415
+ "",
416
+ "[bold cyan]You're ready for full CogsGuard missions![/bold cyan]",
417
+ ),
418
+ },
419
+ )
420
+
421
+ for idx, step in enumerate(tutorial_steps):
422
+ if stop_event.is_set():
423
+ return
424
+ console.print()
425
+ console.print(f"[bold cyan]{step['title']}[/bold cyan]")
426
+ console.print()
427
+ for line in step["lines"]:
428
+ console.print(f" • {line}")
429
+ # Display task if present
430
+ if "task" in step:
431
+ console.print()
432
+ console.print(f" [bold yellow]TASK:[/bold yellow] {step['task']}")
433
+ console.print()
434
+ if idx < len(tutorial_steps) - 1:
435
+ console.print("[dim]Press Enter to continue...[/dim]")
436
+ if not _wait_for_enter(""):
437
+ return
438
+
439
+ console.print("[dim]CogsGuard tutorial briefing complete. Good luck, Cognitive.[/dim]")
440
+ console.print("[dim]Close the Mettascope window to exit the tutorial.[/dim]")
441
+
442
+ # Start tutorial interaction in a background thread
443
+ tutorial_thread = threading.Thread(target=run_cogsguard_tutorial_steps, daemon=True)
444
+ tutorial_thread.start()
445
+
446
+ # Run play (blocks main thread)
447
+ try:
448
+ play_module.play(
449
+ console,
450
+ env_cfg=env_cfg,
451
+ policy_spec=get_policy_spec(ctx, "class=noop"),
452
+ game_name="cogsguard_tutorial",
453
+ render_mode="gui",
454
+ )
455
+ except KeyboardInterrupt:
456
+ logger.info("CogsGuard tutorial interrupted; exiting.")
457
+ finally:
458
+ stop_event.set()
459
+
460
+
461
+ app.add_typer(tutorial_app, name="tutorial", rich_help_panel="Tutorials")
462
+
463
+
464
+ def _help_callback(ctx: typer.Context, value: bool) -> None:
465
+ """Callback for custom help option."""
466
+ if value:
467
+ console.print(ctx.get_help())
468
+ raise typer.Exit()
469
+
470
+
471
+ @app.command(
472
+ name="missions",
473
+ help="""List available missions.
474
+
475
+ This command has three modes:
476
+
477
+ [bold]1. List sites:[/bold] Run with no arguments to see all available sites.
272
478
 
479
+ [bold]2. List missions at a site:[/bold] Pass a site name (e.g., 'cogsguard_machina_1') to see its missions.
273
480
 
274
- @app.command("missions", help="List all available missions, or describe a specific mission")
481
+ [bold]3. Describe a mission:[/bold] Use -m to describe a specific mission. Only in this mode do \
482
+ --cogs, --variant, --format, and --save have any effect.""",
483
+ rich_help_panel="Missions",
484
+ epilog="""[dim]Examples:[/dim]
485
+
486
+ [cyan]cogames missions[/cyan] List all sites
487
+
488
+ [cyan]cogames missions cogsguard_machina_1[/cyan] List missions at site
489
+
490
+ [cyan]cogames missions -m cogsguard_machina_1.basic[/cyan] Describe a mission
491
+
492
+ [cyan]cogames missions -m arena --format json[/cyan] Output as JSON""",
493
+ add_help_option=False,
494
+ )
275
495
  @app.command("games", hidden=True)
276
496
  @app.command("mission", hidden=True)
277
497
  def games_cmd(
278
498
  ctx: typer.Context,
279
- mission: Optional[str] = typer.Option(None, "--mission", "-m", help="Name of the mission"),
280
- cogs: Optional[int] = typer.Option(None, "--cogs", "-c", help="Number of cogs (agents)"),
499
+ # --- List ---
500
+ site: Optional[str] = typer.Argument(
501
+ None,
502
+ metavar="SITE",
503
+ help="Filter by site (e.g., cogsguard_machina_1)",
504
+ ),
505
+ # --- Describe (requires -m) ---
506
+ mission: Optional[str] = typer.Option(
507
+ None,
508
+ "--mission",
509
+ "-m",
510
+ metavar="MISSION",
511
+ help="Mission to describe",
512
+ rich_help_panel="Describe",
513
+ ),
514
+ cogs: Optional[int] = typer.Option(
515
+ None,
516
+ "--cogs",
517
+ "-c",
518
+ help="Override agent count (requires -m)",
519
+ rich_help_panel="Describe",
520
+ ),
281
521
  variant: Optional[list[str]] = typer.Option( # noqa: B008
282
522
  None,
283
523
  "--variant",
284
524
  "-v",
285
- help="Mission variant (can be used multiple times, e.g., --variant solar_flare --variant dark_side)",
525
+ metavar="VARIANT",
526
+ help="Apply variant (requires -m, repeatable)",
527
+ rich_help_panel="Describe",
286
528
  ),
287
529
  format_: Optional[Literal["yaml", "json"]] = typer.Option(
288
- None, "--format", help="Output mission configuration in YAML or JSON."
530
+ None,
531
+ "--format",
532
+ help="Output format (requires -m)",
533
+ rich_help_panel="Describe",
289
534
  ),
290
535
  save: Optional[Path] = typer.Option( # noqa: B008
291
536
  None,
292
537
  "--save",
293
538
  "-s",
294
- help="Save mission configuration to file (YAML or JSON)",
539
+ metavar="PATH",
540
+ help="Save config to file (requires -m)",
541
+ rich_help_panel="Describe",
542
+ ),
543
+ # --- Debug ---
544
+ print_cvc_config: bool = typer.Option(
545
+ False,
546
+ "--print-cvc-config",
547
+ help="Print CVC mission config (requires -m)",
548
+ hidden=True,
549
+ ),
550
+ print_mg_config: bool = typer.Option(
551
+ False,
552
+ "--print-mg-config",
553
+ help="Print MettaGrid config (requires -m)",
554
+ hidden=True,
555
+ ),
556
+ # --- Help ---
557
+ _help: bool = typer.Option(
558
+ False,
559
+ "--help",
560
+ "-h",
561
+ help="Show this message and exit",
562
+ is_eager=True,
563
+ callback=_help_callback,
564
+ rich_help_panel="Other",
295
565
  ),
296
- print_cvc_config: bool = typer.Option(False, "--print-cvc-config", help="Print Mission config (CVC config)"),
297
- print_mg_config: bool = typer.Option(False, "--print-mg-config", help="Print MettaGridConfig"),
298
- site: Optional[str] = typer.Argument(None, help="Site to list missions for (e.g., training_facility)"),
299
566
  ) -> None:
300
567
  if mission is None:
301
568
  list_missions(site)
302
569
  return
303
570
 
304
- resolved_mission, env_cfg, mission_cfg = get_mission_name_and_config(ctx, mission, variant, cogs)
571
+ try:
572
+ resolved_mission, env_cfg, mission_cfg = get_mission_name_and_config(ctx, mission, variant, cogs)
573
+ except typer.Exit as exc:
574
+ if exc.exit_code != 1:
575
+ raise
576
+ return
305
577
 
306
578
  if print_cvc_config or print_mg_config:
307
579
  try:
@@ -338,64 +610,190 @@ def games_cmd(
338
610
  raise typer.Exit(1) from exc
339
611
 
340
612
 
341
- @app.command("evals", help="List all eval missions")
613
+ @app.command("evals", help="List all eval missions", rich_help_panel="Missions")
342
614
  def evals_cmd() -> None:
343
615
  list_evals()
344
616
 
345
617
 
346
- @app.command("variants", help="List all available mission variants")
618
+ @app.command("variants", help="List all available mission variants", rich_help_panel="Missions")
347
619
  def variants_cmd() -> None:
348
620
  list_variants()
349
621
 
350
622
 
351
- @app.command(name="describe", help="Describe a mission and its configuration")
623
+ @app.command(
624
+ name="describe",
625
+ help="Describe a mission and its configuration",
626
+ rich_help_panel="Missions",
627
+ epilog="""[dim]Examples:[/dim]
628
+
629
+ [cyan]cogames describe hello_world.open_world[/cyan] Describe mission
630
+
631
+ [cyan]cogames describe arena -c 4 -v dark_side[/cyan] With 4 cogs and variant""",
632
+ add_help_option=False,
633
+ )
352
634
  def describe_cmd(
353
635
  ctx: typer.Context,
354
- mission: str = typer.Argument(..., help="Mission name (e.g., hello_world.open_world)"),
355
- cogs: Optional[int] = typer.Option(None, "--cogs", "-c", help="Number of cogs (agents)"),
636
+ mission: str = typer.Argument(
637
+ ...,
638
+ metavar="MISSION",
639
+ help="Mission name (e.g., hello_world.open_world)",
640
+ ),
641
+ cogs: Optional[int] = typer.Option(
642
+ None,
643
+ "--cogs",
644
+ "-c",
645
+ help="Number of cogs (agents)",
646
+ rich_help_panel="Configuration",
647
+ ),
356
648
  variant: Optional[list[str]] = typer.Option( # noqa: B008
357
649
  None,
358
650
  "--variant",
359
651
  "-v",
360
- help="Mission variant (can be used multiple times, e.g., --variant solar_flare --variant dark_side)",
652
+ metavar="VARIANT",
653
+ help="Apply variant (repeatable)",
654
+ rich_help_panel="Configuration",
655
+ ),
656
+ _help: bool = typer.Option(
657
+ False,
658
+ "--help",
659
+ "-h",
660
+ help="Show this message and exit",
661
+ is_eager=True,
662
+ callback=_help_callback,
663
+ rich_help_panel="Other",
361
664
  ),
362
665
  ) -> None:
363
666
  resolved_mission, env_cfg, mission_cfg = get_mission_name_and_config(ctx, mission, variant, cogs)
364
667
  describe_mission(resolved_mission, env_cfg, mission_cfg)
365
668
 
366
669
 
367
- @app.command(name="play", help="Play a game")
670
+ @app.command(
671
+ name="play",
672
+ rich_help_panel="Play",
673
+ help="""Play a game interactively.
674
+
675
+ This runs a single episode of the game using the specified policy.
676
+
677
+ By default, the policy is 'noop', so agents won't move unless manually controlled.
678
+ To see agents move by themselves, use `--policy class=random` or `--policy class=baseline`.
679
+
680
+ You can manually control the actions of a specific cog by clicking on a cog
681
+ in GUI mode or pressing M in unicode mode and using your arrow or WASD keys.
682
+ Log mode is non-interactive and doesn't support manual control.
683
+ """,
684
+ epilog="""[dim]Examples:[/dim]
685
+
686
+ [cyan]cogames play -m cogsguard_machina_1.basic[/cyan] Interactive
687
+
688
+ [cyan]cogames play -m cogsguard_machina_1.basic -p class=random[/cyan] Random policy
689
+
690
+ [cyan]cogames play -m cogsguard_machina_1.basic -c 4 -p class=baseline[/cyan] Baseline, 4 cogs
691
+
692
+ [cyan]cogames play -m cogsguard_machina_1 -r unicode[/cyan] Terminal mode""",
693
+ add_help_option=False,
694
+ )
368
695
  def play_cmd(
369
696
  ctx: typer.Context,
370
- mission: Optional[str] = typer.Option(None, "--mission", "-m", help="Name of the mission"),
371
- cogs: Optional[int] = typer.Option(None, "--cogs", "-c", help="Number of cogs (agents)"),
697
+ # --- Game Setup ---
698
+ mission: Optional[str] = typer.Option(
699
+ None,
700
+ "--mission",
701
+ "-m",
702
+ metavar="MISSION",
703
+ help="Mission to play (run [bold]cogames missions[/bold] to list)",
704
+ rich_help_panel="Game Setup",
705
+ ),
372
706
  variant: Optional[list[str]] = typer.Option( # noqa: B008
373
707
  None,
374
708
  "--variant",
375
709
  "-v",
376
- help="Mission variant (can be used multiple times, e.g., --variant solar_flare --variant dark_side)",
710
+ metavar="VARIANT",
711
+ help="Apply variant modifier (repeatable)",
712
+ rich_help_panel="Game Setup",
713
+ ),
714
+ cogs: Optional[int] = typer.Option(
715
+ None,
716
+ "--cogs",
717
+ "-c",
718
+ metavar="N",
719
+ help="Number of cogs/agents",
720
+ show_default="from mission",
721
+ rich_help_panel="Game Setup",
722
+ ),
723
+ # --- Policy ---
724
+ policy: str = typer.Option(
725
+ "class=noop",
726
+ "--policy",
727
+ "-p",
728
+ metavar="POLICY",
729
+ help="Policy controlling cogs ([bold]noop[/bold], [bold]random[/bold], [bold]lstm[/bold], or path)",
730
+ rich_help_panel="Policy",
731
+ ),
732
+ # --- Simulation ---
733
+ steps: int = typer.Option(
734
+ 1000,
735
+ "--steps",
736
+ "-s",
737
+ metavar="N",
738
+ help="Max steps per episode",
739
+ rich_help_panel="Simulation",
740
+ ),
741
+ render: RenderMode = typer.Option( # noqa: B008
742
+ "gui",
743
+ "--render",
744
+ "-r",
745
+ help=(
746
+ "[bold]gui[/bold]=MettaScope, [bold]vibescope[/bold]=VibeScope, "
747
+ "[bold]unicode[/bold]=terminal, [bold]log[/bold]=metrics only"
748
+ ),
749
+ rich_help_panel="Simulation",
750
+ ),
751
+ seed: int = typer.Option(
752
+ 42,
753
+ "--seed",
754
+ help="RNG seed for reproducibility",
755
+ rich_help_panel="Simulation",
377
756
  ),
378
- policy: str = typer.Option("class=noop", "--policy", "-p", help=f"Policy ({policy_arg_example})"),
379
- steps: int = typer.Option(1000, "--steps", "-s", help="Number of steps to run", min=1),
380
- render: RenderMode = typer.Option("gui", "--render", "-r", help="Render mode"), # noqa: B008
381
- seed: int = typer.Option(42, "--seed", help="Seed for the simulator and policy", min=0),
382
757
  map_seed: Optional[int] = typer.Option(
383
758
  None,
384
759
  "--map-seed",
385
- help="Override MapGen seed for procedural maps (defaults to --seed if not set)",
386
- min=0,
387
- ),
388
- print_cvc_config: bool = typer.Option(
389
- False, "--print-cvc-config", help="Print Mission config (CVC config) and exit"
760
+ metavar="SEED",
761
+ help="Separate seed for procedural map generation",
762
+ show_default="same as --seed",
763
+ rich_help_panel="Simulation",
390
764
  ),
391
- print_mg_config: bool = typer.Option(False, "--print-mg-config", help="Print MettaGridConfig and exit"),
765
+ # --- Output ---
392
766
  save_replay_dir: Optional[Path] = typer.Option( # noqa: B008
393
767
  None,
394
768
  "--save-replay-dir",
395
- help=(
396
- "Directory to save replay. Directory will be created if it doesn't exist. "
397
- "Replay will be saved with a unique UUID-based filename."
398
- ),
769
+ metavar="DIR",
770
+ help="Save replay file for later viewing with [bold]cogames replay[/bold]",
771
+ rich_help_panel="Output",
772
+ ),
773
+ # --- Debug (hidden from casual users) ---
774
+ print_cvc_config: bool = typer.Option(
775
+ False,
776
+ "--print-cvc-config",
777
+ help="Print mission config and exit",
778
+ rich_help_panel="Debug",
779
+ hidden=True,
780
+ ),
781
+ print_mg_config: bool = typer.Option(
782
+ False,
783
+ "--print-mg-config",
784
+ help="Print MettaGrid config and exit",
785
+ rich_help_panel="Debug",
786
+ hidden=True,
787
+ ),
788
+ # --- Help at end ---
789
+ _help: bool = typer.Option(
790
+ False,
791
+ "--help",
792
+ "-h",
793
+ help="Show this message and exit",
794
+ is_eager=True,
795
+ callback=_help_callback,
796
+ rich_help_panel="Other",
399
797
  ),
400
798
  ) -> None:
401
799
  resolved_mission, env_cfg, mission_cfg = get_mission_name_and_config(ctx, mission, variant, cogs)
@@ -407,15 +805,11 @@ def play_cmd(
407
805
  console.print(f"[red]Error printing config: {exc}[/red]")
408
806
  raise typer.Exit(1) from exc
409
807
 
410
- # Optionally override MapGen seed so maps are reproducible across runs.
411
- # This uses --map-seed if provided, otherwise reuses the main --seed.
412
- from mettagrid.mapgen.mapgen import MapGen
413
-
414
- effective_map_seed: Optional[int] = map_seed if map_seed is not None else seed
415
- if effective_map_seed is not None:
808
+ # Optional MapGen seed override for procedural maps.
809
+ if map_seed is not None:
416
810
  map_builder = getattr(env_cfg.game, "map_builder", None)
417
- if isinstance(map_builder, MapGen.Config) and map_builder.seed is None:
418
- map_builder.seed = effective_map_seed
811
+ if isinstance(map_builder, MapGen.Config):
812
+ map_builder.seed = map_seed
419
813
 
420
814
  policy_spec = get_policy_spec(ctx, policy)
421
815
  console.print(f"[cyan]Playing {resolved_mission}[/cyan]")
@@ -439,11 +833,32 @@ def play_cmd(
439
833
  )
440
834
 
441
835
 
442
- @app.command(name="replay", help="Replay a saved game using MettaScope")
836
+ @app.command(
837
+ name="replay",
838
+ help="Replay a saved game episode from a file in the GUI",
839
+ rich_help_panel="Play",
840
+ epilog="""[dim]Examples:[/dim]
841
+
842
+ [cyan]cogames replay ./replays/game.replay[/cyan] Replay a saved game
843
+
844
+ [cyan]cogames replay ./train_dir/my_run/replay.bin[/cyan] Replay from training run""",
845
+ add_help_option=False,
846
+ )
443
847
  def replay_cmd(
444
- replay_path: Path = typer.Argument(..., help="Path to the replay file"), # noqa: B008
848
+ replay_path: Path = typer.Argument( # noqa: B008
849
+ ...,
850
+ metavar="FILE",
851
+ help="Path to the replay file (.replay or .bin)",
852
+ ),
853
+ _help: bool = typer.Option(
854
+ False,
855
+ "--help",
856
+ "-h",
857
+ help="Show this message and exit",
858
+ is_eager=True,
859
+ callback=_help_callback,
860
+ ),
445
861
  ) -> None:
446
- """Replay a saved game using MettaScope visualization tool."""
447
862
  if not replay_path.exists():
448
863
  console.print(f"[red]Error: Replay file not found: {replay_path}[/red]")
449
864
  raise typer.Exit(1)
@@ -468,15 +883,73 @@ def replay_cmd(
468
883
  raise typer.Exit(1) from exc
469
884
 
470
885
 
471
- @app.command("make-mission", help="Create a new mission configuration")
886
+ @app.command(
887
+ name="make-mission",
888
+ help="Create a custom mission from a base template",
889
+ rich_help_panel="Missions",
890
+ epilog="""[dim]Examples:[/dim]
891
+
892
+ [cyan]cogames make-mission -m hello_world -c 8 -o my_mission.yml[/cyan] 8 cogs
893
+
894
+ [cyan]cogames make-mission -m arena --width 64 --height 64 -o big.yml[/cyan] 64x64 map
895
+
896
+ [cyan]cogames play -m my_mission.yml[/cyan] Use custom mission""",
897
+ add_help_option=False,
898
+ )
472
899
  @app.command("make-game", hidden=True)
473
900
  def make_mission(
474
901
  ctx: typer.Context,
475
- base_mission: Optional[str] = typer.Option(None, "--mission", "-m", help="Base mission to start configuring from"),
476
- cogs: Optional[int] = typer.Option(None, "--cogs", "-c", help="Number of cogs (agents)", min=1),
477
- width: Optional[int] = typer.Option(None, "--width", "-w", help="Map width", min=1),
478
- height: Optional[int] = typer.Option(None, "--height", "-h", help="Map height", min=1),
479
- output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path (yml or json)"), # noqa: B008
902
+ # --- Mission ---
903
+ base_mission: Optional[str] = typer.Option(
904
+ None,
905
+ "--mission",
906
+ "-m",
907
+ metavar="MISSION",
908
+ help="Base mission to start from",
909
+ rich_help_panel="Mission",
910
+ ),
911
+ # --- Customization ---
912
+ cogs: Optional[int] = typer.Option(
913
+ None,
914
+ "--cogs",
915
+ "-c",
916
+ help="Number of cogs (agents)",
917
+ min=1,
918
+ rich_help_panel="Customization",
919
+ ),
920
+ width: Optional[int] = typer.Option(
921
+ None,
922
+ "--width",
923
+ help="Map width",
924
+ min=1,
925
+ rich_help_panel="Customization",
926
+ ),
927
+ height: Optional[int] = typer.Option(
928
+ None,
929
+ "--height",
930
+ help="Map height",
931
+ min=1,
932
+ rich_help_panel="Customization",
933
+ ),
934
+ # --- Output ---
935
+ output: Optional[Path] = typer.Option( # noqa: B008
936
+ None,
937
+ "--output",
938
+ "-o",
939
+ metavar="PATH",
940
+ help="Output file path (.yml or .json)",
941
+ rich_help_panel="Output",
942
+ ),
943
+ # --- Help ---
944
+ _help: bool = typer.Option(
945
+ False,
946
+ "--help",
947
+ "-h",
948
+ help="Show this message and exit",
949
+ is_eager=True,
950
+ callback=_help_callback,
951
+ rich_help_panel="Other",
952
+ ),
480
953
  ) -> None:
481
954
  try:
482
955
  resolved_mission, env_cfg, _ = get_mission_name_and_config(ctx, base_mission)
@@ -514,13 +987,52 @@ def make_mission(
514
987
  raise typer.Exit(1) from exc
515
988
 
516
989
 
517
- @tutorial_app.command("make-policy", help="Create a new policy from a template")
990
+ # TODO (cogsguard migration): Verify make-policy templates work with CogsGuard game mechanics
991
+ @tutorial_app.command(
992
+ name="make-policy",
993
+ help="Create a new policy from a template. Requires --trainable or --scripted.",
994
+ rich_help_panel="Tutorial",
995
+ epilog="""[dim]Examples:[/dim]
996
+
997
+ [cyan]cogames tutorial make-policy -t -o my_nn_policy.py[/cyan] Trainable (neural network)
998
+
999
+ [cyan]cogames tutorial make-policy -s -o my_scripted_policy.py[/cyan] Scripted (rule-based)""",
1000
+ add_help_option=False,
1001
+ )
518
1002
  def make_policy(
519
- output: Path = typer.Option("my_policy.py", "--output", "-o", help="Output file path"), # noqa: B008
520
- trainable: bool = typer.Option(False, "--trainable", "-t", help="Create a trainable (neural network) policy"),
521
- scripted: bool = typer.Option(False, "--scripted", "-s", help="Create a scripted (rule-based) policy"),
1003
+ # --- Policy Type ---
1004
+ trainable: bool = typer.Option(
1005
+ False,
1006
+ "--trainable",
1007
+ help="Create a trainable (neural network) policy",
1008
+ rich_help_panel="Policy Type",
1009
+ ),
1010
+ scripted: bool = typer.Option(
1011
+ False,
1012
+ "--scripted",
1013
+ help="Create a scripted (rule-based) policy",
1014
+ rich_help_panel="Policy Type",
1015
+ ),
1016
+ # --- Output ---
1017
+ output: Path = typer.Option( # noqa: B008
1018
+ "my_policy.py",
1019
+ "--output",
1020
+ "-o",
1021
+ metavar="FILE",
1022
+ help="Output file path",
1023
+ rich_help_panel="Output",
1024
+ ),
1025
+ # --- Help ---
1026
+ _help: bool = typer.Option(
1027
+ False,
1028
+ "--help",
1029
+ "-h",
1030
+ help="Show this message and exit",
1031
+ is_eager=True,
1032
+ callback=_help_callback,
1033
+ rich_help_panel="Other",
1034
+ ),
522
1035
  ) -> None:
523
- """Create a new policy from a template. Requires either --trainable or --scripted."""
524
1036
  if trainable == scripted:
525
1037
  console.print("[red]Error: Specify exactly one of --trainable or --scripted[/red]")
526
1038
  console.print("[dim]Examples:[/dim]")
@@ -550,14 +1062,20 @@ def make_policy(
550
1062
  shutil.copy2(template_path, dest_path)
551
1063
  console.print(f"[green]{policy_type} policy template copied to: {dest_path}[/green]")
552
1064
 
1065
+ if not trainable:
1066
+ content = dest_path.read_text()
1067
+ lines = content.splitlines()
1068
+ lines = [line for line in lines if not line.strip().startswith("short_names =")]
1069
+ dest_path.write_text("\n".join(lines) + "\n")
1070
+
553
1071
  if trainable:
554
1072
  console.print(
555
- "[dim]Train with: cogames tutorial train -m training_facility.harvest -p class="
1073
+ "[dim]Train with: cogames tutorial train -m cogsguard_machina_1.basic -p class="
556
1074
  f"{dest_path.stem}.{policy_class}[/dim]"
557
1075
  )
558
1076
  else:
559
1077
  console.print(
560
- "[dim]Play with: cogames play -m training_facility.harvest -p class="
1078
+ "[dim]Play with: cogames play -m cogsguard_machina_1.basic -p class="
561
1079
  f"{dest_path.stem}.{policy_class}[/dim]"
562
1080
  )
563
1081
 
@@ -569,57 +1087,179 @@ def make_policy(
569
1087
  app.command(name="make-policy", hidden=True)(make_policy)
570
1088
 
571
1089
 
572
- @tutorial_app.command(name="train", help="Train a policy on a mission")
1090
+ @tutorial_app.command(
1091
+ name="train",
1092
+ help="""Train a policy on one or more missions.
1093
+
1094
+ By default, our 'lstm' policy architecture is used. You can select a different architecture
1095
+ (like 'stateless' or 'baseline'), or define your own implementing the MultiAgentPolicy
1096
+ interface with a trainable network() method (see mettagrid/policy/policy.py).
1097
+
1098
+ Continue training from a checkpoint using URI format, or load weights into an explicit class
1099
+ with class=...,data=... syntax.
1100
+
1101
+ Supply repeated -m flags to create a training curriculum that rotates through missions.
1102
+ Use wildcards (*) in mission names to match multiple missions at once.""",
1103
+ rich_help_panel="Tutorial",
1104
+ epilog="""[dim]Examples:[/dim]
1105
+
1106
+ [cyan]cogames tutorial train -m cogsguard_machina_1.basic[/cyan] Basic training
1107
+
1108
+ [cyan]cogames tutorial train -m cogsguard_machina_1.basic -p class=baseline[/cyan]
1109
+ Train baseline policy
1110
+
1111
+ [cyan]cogames tutorial train -p ./train_dir/my_run:v5[/cyan] Continue from checkpoint
1112
+
1113
+ [cyan]cogames tutorial train -p class=lstm,data=./weights.safetensors[/cyan] Load weights into class
1114
+
1115
+ [cyan]cogames tutorial train -m mission_1 -m mission_2[/cyan] Curriculum (rotates)
1116
+
1117
+ [dim]Wildcard patterns:[/dim]
1118
+
1119
+ [cyan]cogames tutorial train -m 'machina_2_bigger:*'[/cyan] All missions on machina_2_bigger
1120
+
1121
+ [cyan]cogames tutorial train -m '*:shaped'[/cyan] All "shaped" missions
1122
+
1123
+ [cyan]cogames tutorial train -m 'machina*:shaped'[/cyan] All "shaped" on machina maps""",
1124
+ add_help_option=False,
1125
+ )
573
1126
  def train_cmd(
574
1127
  ctx: typer.Context,
575
- missions: Optional[list[str]] = typer.Option(None, "--mission", "-m", help="Missions to train on"), # noqa: B008
576
- cogs: Optional[int] = typer.Option(None, "--cogs", "-c", help="Number of cogs (agents)"),
1128
+ # --- Mission Setup ---
1129
+ missions: Optional[list[str]] = typer.Option( # noqa: B008
1130
+ None,
1131
+ "--mission",
1132
+ "-m",
1133
+ metavar="MISSION",
1134
+ help="Missions to train on (wildcards supported, repeatable for curriculum)",
1135
+ rich_help_panel="Mission Setup",
1136
+ ),
1137
+ cogs: Optional[int] = typer.Option(
1138
+ None,
1139
+ "--cogs",
1140
+ "-c",
1141
+ metavar="N",
1142
+ help="Number of cogs (agents)",
1143
+ show_default="from mission",
1144
+ rich_help_panel="Mission Setup",
1145
+ ),
577
1146
  variant: Optional[list[str]] = typer.Option( # noqa: B008
578
1147
  None,
579
1148
  "--variant",
580
1149
  "-v",
581
- help="Mission variant (can be used multiple times, e.g., --variant solar_flare --variant dark_side)",
1150
+ metavar="VARIANT",
1151
+ help="Mission variant (repeatable)",
1152
+ rich_help_panel="Mission Setup",
582
1153
  ),
583
- policy: str = typer.Option("class=lstm", "--policy", "-p", help=f"Policy ({policy_arg_example})"),
584
- checkpoints_path: str = typer.Option(
585
- "./train_dir",
586
- "--checkpoints",
587
- help="Path to save training data",
1154
+ # --- Policy ---
1155
+ policy: str = typer.Option(
1156
+ "class=lstm",
1157
+ "--policy",
1158
+ "-p",
1159
+ metavar="POLICY",
1160
+ help=f"Policy to train ({policy_arg_example})",
1161
+ rich_help_panel="Policy",
588
1162
  ),
589
- steps: int = typer.Option(10_000_000_000, "--steps", "-s", help="Number of training steps", min=1),
590
- device: str = typer.Option(
591
- "auto",
592
- "--device",
593
- help="Device to train on (e.g. 'auto', 'cpu', 'cuda')",
1163
+ # --- Training ---
1164
+ steps: int = typer.Option(
1165
+ 10_000_000_000,
1166
+ "--steps",
1167
+ metavar="N",
1168
+ help="Number of training steps",
1169
+ min=1,
1170
+ rich_help_panel="Training",
594
1171
  ),
595
- seed: int = typer.Option(42, "--seed", help="Seed for training", min=0),
596
- map_seed: Optional[int] = typer.Option(
597
- None,
598
- "--map-seed",
599
- help="Optional MapGen seed override for procedural maps (for deterministic map layouts)",
600
- min=0,
1172
+ batch_size: int = typer.Option(
1173
+ 4096,
1174
+ "--batch-size",
1175
+ metavar="N",
1176
+ help="Batch size for training",
1177
+ min=1,
1178
+ rich_help_panel="Training",
1179
+ ),
1180
+ minibatch_size: int = typer.Option(
1181
+ 4096,
1182
+ "--minibatch-size",
1183
+ metavar="N",
1184
+ help="Minibatch size for training",
1185
+ min=1,
1186
+ rich_help_panel="Training",
1187
+ ),
1188
+ # --- Hardware ---
1189
+ device: str = typer.Option(
1190
+ "cpu",
1191
+ "--device",
1192
+ metavar="DEVICE",
1193
+ help="Device to train on (auto, cpu, cuda, mps)",
1194
+ rich_help_panel="Hardware",
601
1195
  ),
602
- batch_size: int = typer.Option(4096, "--batch-size", help="Batch size for training", min=1),
603
- minibatch_size: int = typer.Option(4096, "--minibatch-size", help="Minibatch size for training", min=1),
604
1196
  num_workers: Optional[int] = typer.Option(
605
1197
  None,
606
1198
  "--num-workers",
607
- help="Number of worker processes (defaults to number of CPU cores)",
1199
+ metavar="N",
1200
+ help="Number of worker processes",
1201
+ show_default="CPU cores",
608
1202
  min=1,
1203
+ rich_help_panel="Hardware",
609
1204
  ),
610
1205
  parallel_envs: Optional[int] = typer.Option(
611
1206
  None,
612
1207
  "--parallel-envs",
1208
+ metavar="N",
613
1209
  help="Number of parallel environments",
614
1210
  min=1,
1211
+ rich_help_panel="Hardware",
615
1212
  ),
616
1213
  vector_batch_size: Optional[int] = typer.Option(
617
1214
  None,
618
1215
  "--vector-batch-size",
619
- help="Override vectorized environment batch size",
1216
+ metavar="N",
1217
+ help="Vectorized environment batch size",
620
1218
  min=1,
1219
+ rich_help_panel="Hardware",
1220
+ ),
1221
+ # --- Reproducibility ---
1222
+ seed: int = typer.Option(
1223
+ 42,
1224
+ "--seed",
1225
+ metavar="N",
1226
+ help="Seed for training RNG",
1227
+ min=0,
1228
+ rich_help_panel="Reproducibility",
1229
+ ),
1230
+ map_seed: Optional[int] = typer.Option(
1231
+ None,
1232
+ "--map-seed",
1233
+ metavar="N",
1234
+ help="MapGen seed for procedural map layout",
1235
+ show_default="same as --seed",
1236
+ min=0,
1237
+ rich_help_panel="Reproducibility",
1238
+ ),
1239
+ # --- Output ---
1240
+ checkpoints_path: str = typer.Option(
1241
+ "./train_dir",
1242
+ "--checkpoints",
1243
+ metavar="DIR",
1244
+ help="Path to save training checkpoints",
1245
+ rich_help_panel="Output",
1246
+ ),
1247
+ log_outputs: bool = typer.Option(
1248
+ False,
1249
+ "--log-outputs",
1250
+ help="Log training outputs",
1251
+ rich_help_panel="Output",
1252
+ ),
1253
+ # --- Help ---
1254
+ _help: bool = typer.Option(
1255
+ False,
1256
+ "--help",
1257
+ "-h",
1258
+ help="Show this message and exit",
1259
+ is_eager=True,
1260
+ callback=_help_callback,
1261
+ rich_help_panel="Other",
621
1262
  ),
622
- log_outputs: bool = typer.Option(False, "--log-outputs", help="Log training outputs"),
623
1263
  ) -> None:
624
1264
  selected_missions = get_mission_names_and_configs(ctx, missions, variants_arg=variant, cogs=cogs)
625
1265
  if len(selected_missions) == 1:
@@ -637,29 +1277,6 @@ def train_cmd(
637
1277
  policy_spec = get_policy_spec(ctx, policy)
638
1278
  torch_device = resolve_training_device(console, device)
639
1279
 
640
- # Optional MapGen seed override for deterministic procedural maps during training.
641
- # We keep this opt-in (via --map-seed) to avoid reducing map diversity by default.
642
-
643
- if map_seed is not None:
644
-
645
- def _maybe_seed(cfg: Any) -> None:
646
- mb = getattr(cfg.game, "map_builder", None)
647
- if isinstance(mb, MapGen.Config) and mb.seed is None:
648
- mb.seed = map_seed
649
-
650
- if env_cfg is not None:
651
- _maybe_seed(env_cfg)
652
-
653
- if supplier is not None:
654
- base_supplier = supplier
655
-
656
- def _seeded_supplier() -> Any:
657
- cfg = base_supplier()
658
- _maybe_seed(cfg)
659
- return cfg
660
-
661
- supplier = _seeded_supplier
662
-
663
1280
  try:
664
1281
  train_module.train(
665
1282
  env_cfg=env_cfg,
@@ -669,6 +1286,7 @@ def train_cmd(
669
1286
  num_steps=steps,
670
1287
  checkpoints_path=Path(checkpoints_path),
671
1288
  seed=seed,
1289
+ map_seed=map_seed,
672
1290
  batch_size=batch_size,
673
1291
  minibatch_size=minibatch_size,
674
1292
  vector_num_workers=num_workers,
@@ -691,64 +1309,154 @@ app.command(name="train", hidden=True)(train_cmd)
691
1309
 
692
1310
  @app.command(
693
1311
  name="run",
694
- help="Evaluate one or more policies on one or more missions",
1312
+ help="""Evaluate one or more policies on missions.
1313
+
1314
+ With multiple policies (e.g., 2 policies, 4 agents), each policy always controls 2 agents,
1315
+ but which agents swap between policies each episode.
1316
+
1317
+ With one policy, this command is equivalent to `cogames scrimmage`.
1318
+ """,
1319
+ rich_help_panel="Evaluate",
1320
+ epilog="""[dim]Examples:[/dim]
1321
+
1322
+ [cyan]cogames run -m cogsguard_machina_1.basic -p lstm[/cyan] Evaluate single policy
1323
+
1324
+ [cyan]cogames run -m cogsguard_machina_1 -p ./train_dir/my_run:v5[/cyan] Evaluate a checkpoint bundle
1325
+
1326
+ [cyan]cogames run -S integrated_evals -p ./train_dir/my_run:v5[/cyan] Evaluate on mission set
1327
+
1328
+ [cyan]cogames run -m 'arena.*' -p lstm -p random -e 20[/cyan] Evaluate multiple policies together
1329
+
1330
+ [cyan]cogames run -m cogsguard_machina_1 -p ./train_dir/my_run:v5,proportion=3 -p class=random,proportion=5[/cyan]
1331
+ Evaluate policies in 3:5 mix""",
1332
+ add_help_option=False,
1333
+ )
1334
+ @app.command(
1335
+ name="scrimmage",
1336
+ help="""Evaluate a single policy controlling all agents.
1337
+
1338
+ This command is equivalent to running `cogames run` with a single policy.
1339
+ """,
1340
+ rich_help_panel="Evaluate",
1341
+ epilog="""[dim]Examples:[/dim]
1342
+
1343
+ [cyan]cogames scrimmage -m arena.battle -p lstm[/cyan] Single policy eval""",
1344
+ add_help_option=False,
695
1345
  )
696
1346
  @app.command("eval", hidden=True)
697
1347
  @app.command("evaluate", hidden=True)
698
1348
  def run_cmd(
699
1349
  ctx: typer.Context,
1350
+ # --- Mission ---
700
1351
  missions: Optional[list[str]] = typer.Option( # noqa: B008
701
1352
  None,
702
1353
  "--mission",
703
1354
  "-m",
704
- help="Missions to evaluate (supports wildcards, e.g., --mission training_facility.*)",
1355
+ metavar="MISSION",
1356
+ help="Missions to evaluate (supports wildcards)",
1357
+ rich_help_panel="Mission",
705
1358
  ),
706
1359
  mission_set: Optional[str] = typer.Option(
707
1360
  None,
708
1361
  "--mission-set",
709
1362
  "-S",
710
- help="Predefined mission set: eval_missions, integrated_evals, spanning_evals, diagnostic_evals, all",
1363
+ metavar="SET",
1364
+ help="Predefined set: integrated_evals, spanning_evals, diagnostic_evals, all",
1365
+ rich_help_panel="Mission",
1366
+ ),
1367
+ cogs: Optional[int] = typer.Option(
1368
+ None,
1369
+ "--cogs",
1370
+ "-c",
1371
+ metavar="N",
1372
+ help="Number of cogs (agents)",
1373
+ rich_help_panel="Mission",
711
1374
  ),
712
- cogs: Optional[int] = typer.Option(None, "--cogs", "-c", help="Number of cogs (agents)"),
713
1375
  variant: Optional[list[str]] = typer.Option( # noqa: B008
714
1376
  None,
715
1377
  "--variant",
716
1378
  "-v",
717
- help="Mission variant (can be used multiple times, e.g., --variant solar_flare --variant dark_side)",
1379
+ metavar="VARIANT",
1380
+ help="Mission variant (repeatable)",
1381
+ rich_help_panel="Mission",
718
1382
  ),
1383
+ # --- Policy ---
719
1384
  policies: Optional[list[str]] = typer.Option( # noqa: B008
720
1385
  None,
721
1386
  "--policy",
722
1387
  "-p",
1388
+ metavar="POLICY",
723
1389
  help=f"Policies to evaluate: ({policy_arg_w_proportion_example}...)",
1390
+ rich_help_panel="Policy",
724
1391
  ),
725
- episodes: int = typer.Option(10, "--episodes", "-e", help="Number of evaluation episodes", min=1),
726
- action_timeout_ms: int = typer.Option(
727
- 250,
728
- "--action-timeout-ms",
729
- help="Max milliseconds afforded to generate each action before noop is used by default",
1392
+ # --- Simulation ---
1393
+ episodes: int = typer.Option(
1394
+ 10,
1395
+ "--episodes",
1396
+ "-e",
1397
+ metavar="N",
1398
+ help="Number of evaluation episodes",
1399
+ min=1,
1400
+ rich_help_panel="Simulation",
1401
+ ),
1402
+ steps: Optional[int] = typer.Option(
1403
+ 1000,
1404
+ "--steps",
1405
+ "-s",
1406
+ metavar="N",
1407
+ help="Max steps per episode",
730
1408
  min=1,
1409
+ rich_help_panel="Simulation",
1410
+ ),
1411
+ seed: int = typer.Option(
1412
+ 42,
1413
+ "--seed",
1414
+ metavar="N",
1415
+ help="Seed for evaluation RNG",
1416
+ min=0,
1417
+ rich_help_panel="Simulation",
731
1418
  ),
732
- steps: Optional[int] = typer.Option(1000, "--steps", "-s", help="Max steps per episode", min=1),
733
- seed: int = typer.Option(42, "--seed", help="Base random seed for evaluation", min=0),
734
1419
  map_seed: Optional[int] = typer.Option(
735
1420
  None,
736
1421
  "--map-seed",
737
- help="Override MapGen seed for procedural maps (defaults to --seed if not set)",
1422
+ metavar="N",
1423
+ help="MapGen seed for procedural maps",
738
1424
  min=0,
1425
+ show_default="same as --seed",
1426
+ rich_help_panel="Simulation",
739
1427
  ),
1428
+ action_timeout_ms: int = typer.Option(
1429
+ 250,
1430
+ "--action-timeout-ms",
1431
+ metavar="MS",
1432
+ help="Max ms per action before noop",
1433
+ min=1,
1434
+ rich_help_panel="Simulation",
1435
+ ),
1436
+ # --- Output ---
740
1437
  format_: Optional[Literal["yaml", "json"]] = typer.Option(
741
1438
  None,
742
1439
  "--format",
743
- help="Output results in YAML or JSON format",
1440
+ metavar="FMT",
1441
+ help="Output format: yaml or json",
1442
+ rich_help_panel="Output",
744
1443
  ),
745
1444
  save_replay_dir: Optional[Path] = typer.Option( # noqa: B008
746
1445
  None,
747
1446
  "--save-replay-dir",
748
- help=(
749
- "Directory to save replays. Directory will be created if it doesn't exist. "
750
- "Each replay will be saved with a unique UUID-based filename."
751
- ),
1447
+ metavar="DIR",
1448
+ help="Directory to save replays",
1449
+ rich_help_panel="Output",
1450
+ ),
1451
+ # --- Help ---
1452
+ _help: bool = typer.Option(
1453
+ False,
1454
+ "--help",
1455
+ "-h",
1456
+ help="Show this message and exit",
1457
+ is_eager=True,
1458
+ callback=_help_callback,
1459
+ rich_help_panel="Other",
752
1460
  ),
753
1461
  ) -> None:
754
1462
  # Handle mission set expansion
@@ -773,19 +1481,23 @@ def run_cmd(
773
1481
 
774
1482
  selected_missions = get_mission_names_and_configs(ctx, missions, variants_arg=variant, cogs=cogs, steps=steps)
775
1483
 
776
- # Optionally override MapGen seed so maps are reproducible across runs.
777
- # This uses --map-seed if provided, otherwise reuses the main --seed.
778
- from mettagrid.mapgen.mapgen import MapGen
779
-
780
- effective_map_seed: Optional[int] = map_seed if map_seed is not None else seed
781
- if effective_map_seed is not None:
1484
+ # Optional MapGen seed override for procedural maps.
1485
+ if map_seed is not None:
782
1486
  for _, env_cfg in selected_missions:
783
1487
  map_builder = getattr(env_cfg.game, "map_builder", None)
784
1488
  if isinstance(map_builder, MapGen.Config):
785
- map_builder.seed = effective_map_seed
1489
+ map_builder.seed = map_seed
786
1490
 
787
1491
  policy_specs = get_policy_specs_with_proportions(ctx, policies)
788
1492
 
1493
+ if ctx.info_name == "scrimmage":
1494
+ if len(policy_specs) != 1:
1495
+ console.print("[red]Error: scrimmage accepts exactly one --policy / -p value.[/red]")
1496
+ raise typer.Exit(1)
1497
+ if policy_specs[0].proportion != 1.0:
1498
+ console.print("[red]Error: scrimmage does not support policy proportions.[/red]")
1499
+ raise typer.Exit(1)
1500
+
789
1501
  console.print(
790
1502
  f"[cyan]Preparing evaluation for {len(policy_specs)} policies across {len(selected_missions)} mission(s)[/cyan]"
791
1503
  )
@@ -803,7 +1515,170 @@ def run_cmd(
803
1515
  )
804
1516
 
805
1517
 
806
- @app.command(name="version", help="Show version information")
1518
+ @app.command(
1519
+ name="pickup",
1520
+ help="Evaluate a policy against a pool of other policies and compute VOR",
1521
+ rich_help_panel="Evaluate",
1522
+ epilog="""[dim]Examples:[/dim]
1523
+
1524
+ [cyan]cogames pickup -p greedy --pool random[/cyan] Test greedy against pool of random""",
1525
+ add_help_option=False,
1526
+ )
1527
+ def pickup_cmd(
1528
+ ctx: typer.Context,
1529
+ # --- Mission ---
1530
+ mission: str = typer.Option(
1531
+ "cogsguard_machina_1.basic",
1532
+ "--mission",
1533
+ "-m",
1534
+ metavar="MISSION",
1535
+ help="Mission to evaluate on",
1536
+ rich_help_panel="Mission",
1537
+ ),
1538
+ cogs: int = typer.Option(
1539
+ 4,
1540
+ "--cogs",
1541
+ "-c",
1542
+ metavar="N",
1543
+ help="Number of cogs (agents)",
1544
+ min=1,
1545
+ rich_help_panel="Mission",
1546
+ ),
1547
+ variant: Optional[list[str]] = typer.Option( # noqa: B008
1548
+ None,
1549
+ "--variant",
1550
+ "-v",
1551
+ metavar="VARIANT",
1552
+ help="Mission variant (repeatable)",
1553
+ rich_help_panel="Mission",
1554
+ ),
1555
+ # --- Policy ---
1556
+ policy: Optional[str] = typer.Option(
1557
+ None,
1558
+ "--policy",
1559
+ "-p",
1560
+ metavar="POLICY",
1561
+ help="Candidate policy to evaluate",
1562
+ rich_help_panel="Policy",
1563
+ ),
1564
+ pool: Optional[list[str]] = typer.Option( # noqa: B008
1565
+ None,
1566
+ "--pool",
1567
+ metavar="POLICY",
1568
+ help="Pool policy (repeatable)",
1569
+ rich_help_panel="Policy",
1570
+ ),
1571
+ # --- Simulation ---
1572
+ episodes: int = typer.Option(
1573
+ 1,
1574
+ "--episodes",
1575
+ "-e",
1576
+ metavar="N",
1577
+ help="Episodes per scenario",
1578
+ min=1,
1579
+ rich_help_panel="Simulation",
1580
+ ),
1581
+ steps: Optional[int] = typer.Option(
1582
+ 1000,
1583
+ "--steps",
1584
+ "-s",
1585
+ metavar="N",
1586
+ help="Max steps per episode",
1587
+ min=1,
1588
+ rich_help_panel="Simulation",
1589
+ ),
1590
+ seed: int = typer.Option(
1591
+ 50,
1592
+ "--seed",
1593
+ metavar="N",
1594
+ help="Base random seed",
1595
+ min=0,
1596
+ rich_help_panel="Simulation",
1597
+ ),
1598
+ map_seed: Optional[int] = typer.Option(
1599
+ None,
1600
+ "--map-seed",
1601
+ metavar="N",
1602
+ help="MapGen seed for procedural maps",
1603
+ min=0,
1604
+ show_default="same as --seed",
1605
+ rich_help_panel="Simulation",
1606
+ ),
1607
+ action_timeout_ms: int = typer.Option(
1608
+ 250,
1609
+ "--action-timeout-ms",
1610
+ metavar="MS",
1611
+ help="Max ms per action before noop",
1612
+ min=1,
1613
+ rich_help_panel="Simulation",
1614
+ ),
1615
+ # --- Output ---
1616
+ save_replay_dir: Optional[Path] = typer.Option( # noqa: B008
1617
+ None,
1618
+ "--save-replay-dir",
1619
+ metavar="DIR",
1620
+ help="Directory to save replays",
1621
+ rich_help_panel="Output",
1622
+ ),
1623
+ # --- Help ---
1624
+ _help: bool = typer.Option(
1625
+ False,
1626
+ "--help",
1627
+ "-h",
1628
+ help="Show this message and exit",
1629
+ is_eager=True,
1630
+ callback=_help_callback,
1631
+ rich_help_panel="Other",
1632
+ ),
1633
+ ) -> None:
1634
+ import httpx
1635
+
1636
+ if policy is None:
1637
+ console.print(ctx.get_help())
1638
+ console.print("[yellow]Missing: --policy / -p[/yellow]\n")
1639
+ raise typer.Exit(1)
1640
+
1641
+ if not pool:
1642
+ console.print(ctx.get_help())
1643
+ console.print("[yellow]Supply at least one: --pool[/yellow]\n")
1644
+ raise typer.Exit(1)
1645
+
1646
+ # Resolve mission
1647
+ resolved_mission, env_cfg, _ = get_mission_name_and_config(ctx, mission, variants_arg=variant, cogs=cogs)
1648
+ if steps is not None:
1649
+ env_cfg.game.max_steps = steps
1650
+
1651
+ candidate_label = policy
1652
+ pool_labels = pool
1653
+ candidate_spec = get_policy_spec(ctx, policy)
1654
+ try:
1655
+ pool_specs = [parse_policy_spec(spec).to_policy_spec() for spec in pool]
1656
+ except (ValueError, ModuleNotFoundError, httpx.HTTPError) as exc:
1657
+ translated = _translate_error(exc)
1658
+ console.print(f"[yellow]Error parsing pool policy: {translated}[/yellow]\n")
1659
+ raise typer.Exit(1) from exc
1660
+
1661
+ pickup_module.pickup(
1662
+ console,
1663
+ candidate_spec,
1664
+ pool_specs,
1665
+ env_cfg=env_cfg,
1666
+ mission_name=resolved_mission,
1667
+ episodes=episodes,
1668
+ seed=seed,
1669
+ map_seed=map_seed,
1670
+ action_timeout_ms=action_timeout_ms,
1671
+ save_replay_dir=save_replay_dir,
1672
+ candidate_label=candidate_label,
1673
+ pool_labels=pool_labels,
1674
+ )
1675
+
1676
+
1677
+ @app.command(
1678
+ name="version",
1679
+ help="Show version information for cogames and dependencies",
1680
+ rich_help_panel="Info",
1681
+ )
807
1682
  def version_cmd() -> None:
808
1683
  def public_version(dist_name: str) -> str:
809
1684
  return str(Version(importlib.metadata.version(dist_name)).public)
@@ -818,7 +1693,18 @@ def version_cmd() -> None:
818
1693
  console.print(table)
819
1694
 
820
1695
 
821
- @app.command(name="policies", help="Show default policies and their shorthand names")
1696
+ @app.command(
1697
+ name="policies",
1698
+ help="Show available policy shorthand names",
1699
+ rich_help_panel="Policies",
1700
+ epilog="""[dim]Usage:[/dim]
1701
+
1702
+ Use these shorthand names with [cyan]--policy[/cyan] or [cyan]-p[/cyan]:
1703
+
1704
+ [cyan]cogames play -m arena -p class=random[/cyan] Use random policy
1705
+
1706
+ [cyan]cogames play -m arena -p class=baseline[/cyan] Use baseline policy""",
1707
+ )
822
1708
  def policies_cmd() -> None:
823
1709
  policy_registry = get_policy_registry()
824
1710
  table = Table(show_header=False, box=None, show_lines=False, pad_edge=False)
@@ -832,26 +1718,48 @@ def policies_cmd() -> None:
832
1718
  console.print(table)
833
1719
 
834
1720
 
835
- @app.command(name="login", help="Authenticate with CoGames server")
1721
+ @app.command(
1722
+ name="login",
1723
+ help="Authenticate with CoGames server",
1724
+ rich_help_panel="Tournament",
1725
+ epilog="""[dim]Examples:[/dim]
1726
+
1727
+ [cyan]cogames login[/cyan] Authenticate with default server
1728
+
1729
+ [cyan]cogames login --force[/cyan] Re-authenticate even if already logged in""",
1730
+ add_help_option=False,
1731
+ )
836
1732
  def login_cmd(
837
1733
  server: str = typer.Option(
838
1734
  DEFAULT_COGAMES_SERVER,
839
- "--server",
840
- "-s",
841
- help="CoGames server URL",
1735
+ "--login-server",
1736
+ metavar="URL",
1737
+ help="Authentication server URL",
1738
+ rich_help_panel="Server",
842
1739
  ),
843
1740
  force: bool = typer.Option(
844
1741
  False,
845
1742
  "--force",
846
1743
  "-f",
847
- help="Get a new token even if one already exists",
1744
+ help="Re-authenticate even if already logged in",
1745
+ rich_help_panel="Options",
848
1746
  ),
849
1747
  timeout: int = typer.Option(
850
1748
  300,
851
1749
  "--timeout",
852
1750
  "-t",
1751
+ metavar="SECS",
853
1752
  help="Authentication timeout in seconds",
854
- min=1,
1753
+ rich_help_panel="Options",
1754
+ ),
1755
+ _help: bool = typer.Option(
1756
+ False,
1757
+ "--help",
1758
+ "-h",
1759
+ help="Show this message and exit",
1760
+ is_eager=True,
1761
+ callback=_help_callback,
1762
+ rich_help_panel="Other",
855
1763
  ),
856
1764
  ) -> None:
857
1765
  from urllib.parse import urlparse
@@ -877,29 +1785,211 @@ def login_cmd(
877
1785
  raise typer.Exit(1)
878
1786
 
879
1787
 
880
- app.command(name="submissions", help="Show your uploaded policies and tournament submissions")(submissions_cmd)
1788
+ app.command(
1789
+ name="submissions",
1790
+ help="Show your uploads and tournament submissions",
1791
+ rich_help_panel="Tournament",
1792
+ epilog="""[dim]Examples:[/dim]
1793
+
1794
+ [cyan]cogames submissions[/cyan] All your uploads
1795
+
1796
+ [cyan]cogames submissions --season beta-cogsguard[/cyan] Submissions in a season
881
1797
 
882
- app.command(name="seasons", help="List available tournament seasons")(seasons_cmd)
1798
+ [cyan]cogames submissions -p my-policy[/cyan] Info on a specific policy""",
1799
+ add_help_option=False,
1800
+ )(submissions_cmd)
1801
+
1802
+ app.command(
1803
+ name="seasons",
1804
+ help="List currently running tournament seasons",
1805
+ rich_help_panel="Tournament",
1806
+ add_help_option=False,
1807
+ )(seasons_cmd)
883
1808
 
884
1809
  app.command(
885
1810
  name="leaderboard",
886
1811
  help="Show tournament leaderboard for a season",
1812
+ rich_help_panel="Tournament",
1813
+ epilog="""[dim]Examples:[/dim]
1814
+
1815
+ [cyan]cogames leaderboard --season beta-cogsguard[/cyan] View rankings""",
1816
+ add_help_option=False,
887
1817
  )(leaderboard_cmd)
888
1818
 
889
1819
 
890
- @app.command(name="validate-policy", help="Validate the policy loads and runs a single step")
1820
+ @app.command(
1821
+ name="diagnose",
1822
+ help="Run diagnostic evals for a policy checkpoint",
1823
+ rich_help_panel="Evaluate",
1824
+ epilog="""[dim]Examples:[/dim]
1825
+
1826
+ [cyan]cogames diagnose ./train_dir/my_run[/cyan] Default diagnostics
1827
+
1828
+ [cyan]cogames diagnose lstm -S tournament[/cyan] Tournament suite
1829
+
1830
+ [cyan]cogames diagnose lstm -c 4 -c 8 -e 5[/cyan] Custom cog counts""",
1831
+ add_help_option=False,
1832
+ )
1833
+ def diagnose_cmd(
1834
+ policy: str = typer.Argument(
1835
+ ...,
1836
+ metavar="POLICY",
1837
+ help=f"Policy specification: {policy_arg_example}",
1838
+ ),
1839
+ # --- Evaluation ---
1840
+ mission_set: Literal[
1841
+ "diagnostic_evals",
1842
+ "integrated_evals",
1843
+ "spanning_evals",
1844
+ "thinky_evals",
1845
+ "tournament",
1846
+ "all",
1847
+ ] = typer.Option(
1848
+ "diagnostic_evals",
1849
+ "--mission-set",
1850
+ "-S",
1851
+ metavar="SET",
1852
+ help="Eval suite to run",
1853
+ rich_help_panel="Evaluation",
1854
+ ),
1855
+ experiments: Optional[list[str]] = typer.Option( # noqa: B008
1856
+ None,
1857
+ "--experiments",
1858
+ metavar="NAME",
1859
+ help="Specific experiments (subset of mission set)",
1860
+ rich_help_panel="Evaluation",
1861
+ ),
1862
+ cogs: Optional[list[int]] = typer.Option( # noqa: B008
1863
+ None,
1864
+ "--cogs",
1865
+ "-c",
1866
+ metavar="N",
1867
+ help="Agent counts to test (repeatable)",
1868
+ rich_help_panel="Evaluation",
1869
+ ),
1870
+ # --- Simulation ---
1871
+ steps: int = typer.Option(
1872
+ 1000,
1873
+ "--steps",
1874
+ "-s",
1875
+ metavar="N",
1876
+ help="Max steps per episode",
1877
+ rich_help_panel="Simulation",
1878
+ ),
1879
+ episodes: int = typer.Option(
1880
+ 3,
1881
+ "--episodes",
1882
+ "-e",
1883
+ metavar="N",
1884
+ help="Episodes per case",
1885
+ rich_help_panel="Simulation",
1886
+ ),
1887
+ # --- Help ---
1888
+ _help: bool = typer.Option(
1889
+ False,
1890
+ "--help",
1891
+ "-h",
1892
+ help="Show this message and exit",
1893
+ is_eager=True,
1894
+ callback=_help_callback,
1895
+ rich_help_panel="Other",
1896
+ ),
1897
+ ) -> None:
1898
+ script_path = Path(__file__).resolve().parents[2] / "scripts" / "run_evaluation.py"
1899
+
1900
+ cmd = [sys.executable, str(script_path)]
1901
+ cmd.extend(["--mission-set", mission_set])
1902
+
1903
+ if experiments:
1904
+ cmd.append("--experiments")
1905
+ cmd.extend(experiments)
1906
+
1907
+ if cogs:
1908
+ cmd.append("--cogs")
1909
+ cmd.extend(str(c) for c in cogs)
1910
+
1911
+ cmd.extend(["--steps", str(steps)])
1912
+ cmd.extend(["--repeats", str(episodes)])
1913
+ cmd.append("--no-plots")
1914
+
1915
+ cmd.extend(["--policy", policy])
1916
+
1917
+ console.print("[cyan]Running diagnostic evaluation...[/cyan]")
1918
+ console.print(f"[dim]{' '.join(cmd)}[/dim]")
1919
+ subprocess.run(cmd, check=True)
1920
+
1921
+
1922
+ def _resolve_season(server: str, season_name: str | None = None) -> SeasonInfo:
1923
+ try:
1924
+ if season_name is not None:
1925
+ info = fetch_season_info(server, season_name)
1926
+ console.print(f"[dim]Using season: {info.name}[/dim]")
1927
+ else:
1928
+ info = fetch_default_season(server)
1929
+ console.print(f"[dim]Using default season: {info.name}[/dim]")
1930
+ return info
1931
+ except Exception as e:
1932
+ console.print(f"[red]Could not fetch season from server:[/red] {e}")
1933
+ console.print("Specify a season explicitly with [cyan]--season[/cyan]")
1934
+ raise typer.Exit(1) from None
1935
+
1936
+
1937
+ @app.command(
1938
+ name="validate-policy",
1939
+ help="Validate the policy loads and runs for at least a single step",
1940
+ rich_help_panel="Policies",
1941
+ add_help_option=False,
1942
+ )
891
1943
  def validate_policy_cmd(
892
1944
  ctx: typer.Context,
893
- policy: str = typer.Argument(
1945
+ policy: str = typer.Option(
894
1946
  ...,
1947
+ "--policy",
1948
+ "-p",
1949
+ metavar="POLICY",
895
1950
  help=f"Policy specification: {policy_arg_example}",
1951
+ rich_help_panel="Policy",
896
1952
  ),
897
1953
  setup_script: Optional[str] = typer.Option(
898
1954
  None,
899
1955
  "--setup-script",
900
1956
  help="Path to a Python setup script to run before loading the policy",
1957
+ rich_help_panel="Policy",
1958
+ ),
1959
+ season: Optional[str] = typer.Option(
1960
+ None,
1961
+ "--season",
1962
+ metavar="SEASON",
1963
+ help="Tournament season (determines which game to validate against)",
1964
+ rich_help_panel="Tournament",
1965
+ ),
1966
+ server: str = typer.Option(
1967
+ DEFAULT_SUBMIT_SERVER,
1968
+ "--server",
1969
+ metavar="URL",
1970
+ help="Tournament server URL (used to resolve default season)",
1971
+ rich_help_panel="Server",
1972
+ ),
1973
+ _help: bool = typer.Option(
1974
+ False,
1975
+ "--help",
1976
+ "-h",
1977
+ help="Show this message and exit",
1978
+ is_eager=True,
1979
+ callback=_help_callback,
1980
+ rich_help_panel="Other",
901
1981
  ),
902
1982
  ) -> None:
1983
+ season_info = _resolve_season(server, season)
1984
+ entry_pool_info = next((p for p in season_info.pools if p.name == season_info.entry_pool), None)
1985
+ if not entry_pool_info or not entry_pool_info.config_id:
1986
+ console.print("[red]No entry config found for season[/red]")
1987
+ raise typer.Exit(1)
1988
+
1989
+ with TournamentServerClient(server_url=server) as client:
1990
+ config_data = client.get_config(entry_pool_info.config_id)
1991
+ env_cfg = MettaGridConfig.model_validate(config_data)
1992
+
903
1993
  if setup_script:
904
1994
  import subprocess
905
1995
  import sys
@@ -923,7 +2013,7 @@ def validate_policy_cmd(
923
2013
  console.print("[green]Setup script completed[/green]")
924
2014
 
925
2015
  policy_spec = get_policy_spec(ctx, policy)
926
- validate_policy_spec(policy_spec)
2016
+ validate_policy_spec(policy_spec, env_cfg)
927
2017
  console.print("[green]Policy validated successfully[/green]")
928
2018
  raise typer.Exit(0)
929
2019
 
@@ -936,66 +2026,125 @@ def _parse_init_kwarg(value: str) -> tuple[str, str]:
936
2026
  return key.replace("-", "_"), val
937
2027
 
938
2028
 
939
- @app.command(name="upload", help="Upload a policy to CoGames")
2029
+ @app.command(
2030
+ name="upload",
2031
+ help="Upload a policy to CoGames",
2032
+ rich_help_panel="Tournament",
2033
+ epilog="""[dim]Examples:[/dim]
2034
+
2035
+ [cyan]cogames upload -p ./train_dir/my_run -n my-policy[/cyan] Upload and submit to default season
2036
+
2037
+ [cyan]cogames upload -p ./run -n my-policy --season beta-cvc[/cyan] Upload and submit to specific season
2038
+
2039
+ [cyan]cogames upload -p ./run -n my-policy --no-submit[/cyan] Upload without submitting
2040
+
2041
+ [cyan]cogames upload -p lstm -n my-lstm --dry-run[/cyan] Validate only""",
2042
+ add_help_option=False,
2043
+ )
940
2044
  def upload_cmd(
941
2045
  ctx: typer.Context,
2046
+ # --- Upload ---
2047
+ name: str = typer.Option(
2048
+ ...,
2049
+ "--name",
2050
+ "-n",
2051
+ metavar="NAME",
2052
+ help="Name for your uploaded policy",
2053
+ rich_help_panel="Upload",
2054
+ ),
2055
+ # --- Policy ---
942
2056
  policy: str = typer.Option(
943
2057
  ...,
944
2058
  "--policy",
945
2059
  "-p",
2060
+ metavar="POLICY",
946
2061
  help=f"Policy specification: {policy_arg_example}",
947
- ),
948
- name: str = typer.Option(
949
- ...,
950
- "--name",
951
- "-n",
952
- help="Policy name for the upload",
2062
+ rich_help_panel="Policy",
953
2063
  ),
954
2064
  init_kwarg: Optional[list[str]] = typer.Option( # noqa: B008
955
2065
  None,
956
2066
  "--init-kwarg",
957
2067
  "-k",
958
- help="Policy init kwargs as key=value (can be repeated)",
2068
+ metavar="KEY=VAL",
2069
+ help="Policy init kwargs (can be repeated)",
2070
+ rich_help_panel="Policy",
959
2071
  ),
2072
+ # --- Files ---
960
2073
  include_files: Optional[list[str]] = typer.Option( # noqa: B008
961
2074
  None,
962
2075
  "--include-files",
963
2076
  "-f",
964
- help="Files or directories to include (can be specified multiple times)",
2077
+ metavar="PATH",
2078
+ help="Files or directories to include (can be repeated)",
2079
+ rich_help_panel="Files",
965
2080
  ),
966
- login_server: str = typer.Option(
967
- DEFAULT_COGAMES_SERVER,
968
- "--login-server",
969
- help="Login/authentication server URL",
2081
+ setup_script: Optional[str] = typer.Option(
2082
+ None,
2083
+ "--setup-script",
2084
+ metavar="PATH",
2085
+ help="Python setup script to run before loading the policy",
2086
+ rich_help_panel="Files",
970
2087
  ),
971
- server: str = typer.Option(
972
- DEFAULT_SUBMIT_SERVER,
973
- "--server",
974
- "-s",
975
- help="Server URL",
2088
+ # --- Tournament ---
2089
+ season: Optional[str] = typer.Option(
2090
+ None,
2091
+ "--season",
2092
+ metavar="SEASON",
2093
+ help="Tournament season (default: server's default season)",
2094
+ rich_help_panel="Tournament",
2095
+ ),
2096
+ no_submit: bool = typer.Option(
2097
+ False,
2098
+ "--no-submit",
2099
+ help="Upload without submitting to a season",
2100
+ rich_help_panel="Tournament",
976
2101
  ),
2102
+ # --- Validation ---
977
2103
  dry_run: bool = typer.Option(
978
2104
  False,
979
2105
  "--dry-run",
980
2106
  help="Run validation only without uploading",
2107
+ rich_help_panel="Validation",
981
2108
  ),
982
2109
  skip_validation: bool = typer.Option(
983
2110
  False,
984
2111
  "--skip-validation",
985
2112
  help="Skip policy validation in isolated environment",
2113
+ rich_help_panel="Validation",
986
2114
  ),
987
- setup_script: Optional[str] = typer.Option(
988
- None,
989
- "--setup-script",
990
- help="Path to a Python setup script to run before loading the policy",
2115
+ # --- Server ---
2116
+ login_server: str = typer.Option(
2117
+ DEFAULT_COGAMES_SERVER,
2118
+ "--login-server",
2119
+ metavar="URL",
2120
+ help="Authentication server URL",
2121
+ rich_help_panel="Server",
2122
+ ),
2123
+ server: str = typer.Option(
2124
+ DEFAULT_SUBMIT_SERVER,
2125
+ "--server",
2126
+ metavar="URL",
2127
+ help="Tournament server URL",
2128
+ rich_help_panel="Server",
2129
+ ),
2130
+ # --- Help ---
2131
+ _help: bool = typer.Option(
2132
+ False,
2133
+ "--help",
2134
+ "-h",
2135
+ help="Show this message and exit",
2136
+ is_eager=True,
2137
+ callback=_help_callback,
2138
+ rich_help_panel="Other",
991
2139
  ),
992
2140
  ) -> None:
993
- """Upload a policy to CoGames.
2141
+ season_info = _resolve_season(server, season)
2142
+
2143
+ has_entry_config = any(p.config_id for p in season_info.pools if p.name == season_info.entry_pool)
2144
+ if not has_entry_config and not skip_validation:
2145
+ console.print("[yellow]Warning: No entry config found for season. Skipping validation.[/yellow]")
2146
+ skip_validation = True
994
2147
 
995
- This command validates your policy, creates an upload package,
996
- and uploads it to the CoGames server. You can then submit it
997
- to tournaments using 'cogames submit'.
998
- """
999
2148
  init_kwargs: dict[str, str] = {}
1000
2149
  if init_kwarg:
1001
2150
  for kv in init_kwarg:
@@ -1013,47 +2162,73 @@ def upload_cmd(
1013
2162
  skip_validation=skip_validation,
1014
2163
  init_kwargs=init_kwargs if init_kwargs else None,
1015
2164
  setup_script=setup_script,
2165
+ validation_season=season_info.name,
2166
+ season=season_info.name if not no_submit else None,
1016
2167
  )
1017
2168
 
1018
2169
  if result:
1019
2170
  console.print(f"[green]Upload complete: {result.name}:v{result.version}[/green]")
1020
- console.print(f"\nTo submit to a tournament: cogames submit {result.name}:v{result.version} --season <name>")
2171
+ if result.pools:
2172
+ console.print(f"[dim]Added to pools: {', '.join(result.pools)}[/dim]")
2173
+ console.print(f"[dim]Results:[/dim] {results_url_for_season(server, season_info.name)}")
2174
+ elif no_submit:
2175
+ console.print(f"\nTo submit to a tournament: cogames submit {result.name}:v{result.version}")
1021
2176
 
1022
2177
 
1023
- @app.command(name="submit", help="Submit an uploaded policy to a tournament season")
2178
+ @app.command(
2179
+ name="submit",
2180
+ help="Submit a policy to a tournament season",
2181
+ rich_help_panel="Tournament",
2182
+ epilog="""[dim]Examples:[/dim]
2183
+
2184
+ [cyan]cogames submit my-policy[/cyan] Submit to default season
2185
+
2186
+ [cyan]cogames submit my-policy:v3 --season beta-cvc[/cyan] Submit specific version to specific season""",
2187
+ add_help_option=False,
2188
+ )
1024
2189
  def submit_cmd(
1025
2190
  policy_name: str = typer.Argument(
1026
2191
  ...,
2192
+ metavar="POLICY",
1027
2193
  help="Policy name (e.g., 'my-policy' or 'my-policy:v3' for specific version)",
1028
2194
  ),
1029
- season: str = typer.Option(
1030
- ...,
2195
+ season: Optional[str] = typer.Option(
2196
+ None,
1031
2197
  "--season",
1032
- help="Tournament season name (required)",
2198
+ metavar="SEASON",
2199
+ help="Tournament season name",
2200
+ rich_help_panel="Tournament",
1033
2201
  ),
1034
2202
  login_server: str = typer.Option(
1035
2203
  DEFAULT_COGAMES_SERVER,
1036
2204
  "--login-server",
1037
- help="Login/authentication server URL",
2205
+ metavar="URL",
2206
+ help="Authentication server URL",
2207
+ rich_help_panel="Server",
1038
2208
  ),
1039
2209
  server: str = typer.Option(
1040
2210
  DEFAULT_SUBMIT_SERVER,
1041
2211
  "--server",
1042
2212
  "-s",
1043
- help="Server URL",
2213
+ metavar="URL",
2214
+ help="Tournament server URL",
2215
+ rich_help_panel="Server",
2216
+ ),
2217
+ _help: bool = typer.Option(
2218
+ False,
2219
+ "--help",
2220
+ "-h",
2221
+ help="Show this message and exit",
2222
+ is_eager=True,
2223
+ callback=_help_callback,
2224
+ rich_help_panel="Other",
1044
2225
  ),
1045
2226
  ) -> None:
1046
- """Submit an uploaded policy to a tournament season.
1047
-
1048
- First upload your policy with 'cogames upload', then submit it to
1049
- a tournament season with this command.
1050
-
1051
- Examples:
1052
- cogames submit my-policy --season beta
1053
- cogames submit my-policy:v3 --season beta
1054
- """
1055
2227
  import httpx
1056
2228
 
2229
+ season_info = _resolve_season(server, season)
2230
+ season_name = season_info.name
2231
+
1057
2232
  client = TournamentServerClient.from_login(server_url=server, login_server=login_server)
1058
2233
  if not client:
1059
2234
  raise typer.Exit(1)
@@ -1065,7 +2240,7 @@ def submit_cmd(
1065
2240
  raise typer.Exit(1) from None
1066
2241
 
1067
2242
  version_str = f"[dim]:v{version}[/dim]" if version is not None else "[dim] (latest)[/dim]"
1068
- console.print(f"[bold]Submitting {name}[/bold]{version_str} to season '{season}'\n")
2243
+ console.print(f"[bold]Submitting {name}[/bold]{version_str} to season '{season_name}'\n")
1069
2244
 
1070
2245
  with client:
1071
2246
  pv = client.lookup_policy_version(name=name, version=version)
@@ -1076,12 +2251,12 @@ def submit_cmd(
1076
2251
  raise typer.Exit(1)
1077
2252
 
1078
2253
  try:
1079
- result = client.submit_to_season(season, pv.id)
2254
+ result = client.submit_to_season(season_name, pv.id)
1080
2255
  except httpx.HTTPStatusError as exc:
1081
2256
  if exc.response.status_code == 404:
1082
- console.print(f"[red]Season '{season}' not found[/red]")
2257
+ console.print(f"[red]Season '{season_name}' not found[/red]")
1083
2258
  elif exc.response.status_code == 409:
1084
- console.print(f"[red]Policy already submitted to season '{season}'[/red]")
2259
+ console.print(f"[red]Policy already submitted to season '{season_name}'[/red]")
1085
2260
  else:
1086
2261
  console.print(f"[red]Submit failed with status {exc.response.status_code}[/red]")
1087
2262
  console.print(f"[dim]{exc.response.text}[/dim]")
@@ -1090,33 +2265,49 @@ def submit_cmd(
1090
2265
  console.print(f"[red]Submit failed:[/red] {exc}")
1091
2266
  raise typer.Exit(1) from exc
1092
2267
 
1093
- console.print(f"\n[bold green]Submitted to season '{season}'[/bold green]")
2268
+ console.print(f"\n[bold green]Submitted to season '{season_name}'[/bold green]")
1094
2269
  if result.pools:
1095
- console.print(f"[dim]Pools: {', '.join(result.pools)}[/dim]")
2270
+ console.print(f"[dim]Added to pools: {', '.join(result.pools)}[/dim]")
2271
+ console.print(f"[dim]Results:[/dim] {results_url_for_season(server, season_name)}")
2272
+ console.print(f"[dim]CLI:[/dim] cogames leaderboard --season {season_name}")
2273
+
1096
2274
 
2275
+ @app.command(
2276
+ name="docs",
2277
+ help="Print documentation (run without arguments to see available docs)",
2278
+ rich_help_panel="Info",
2279
+ epilog="""[dim]Examples:[/dim]
2280
+
2281
+ [cyan]cogames docs[/cyan] List available documents
1097
2282
 
1098
- @app.command(name="docs", help="Print documentation")
2283
+ [cyan]cogames docs readme[/cyan] Print README
2284
+
2285
+ [cyan]cogames docs mission[/cyan] Print mission briefing""",
2286
+ add_help_option=False,
2287
+ )
1099
2288
  def docs_cmd(
1100
- doc_name: Optional[str] = typer.Argument(None, help="Document name to print"),
2289
+ doc_name: Optional[str] = typer.Argument(
2290
+ None,
2291
+ metavar="DOC",
2292
+ help="Document name (readme, mission, technical_manual, scripted_agent, evals, mapgen)",
2293
+ ),
2294
+ _help: bool = typer.Option(
2295
+ False,
2296
+ "--help",
2297
+ "-h",
2298
+ help="Show this message and exit",
2299
+ is_eager=True,
2300
+ callback=_help_callback,
2301
+ ),
1101
2302
  ) -> None:
1102
- """Print a documentation file.
1103
-
1104
- Available documents:
1105
- - readme: README.md - CoGames overview and documentation
1106
- - mission: MISSION.md - Mission briefing for Machina VII Deployment
1107
- - technical_manual: TECHNICAL_MANUAL.md - Technical manual for Cogames
1108
- - scripted_agent: Scripted agent policy documentation
1109
- - evals: Evaluation missions documentation
1110
- - mapgen: Cogs vs Clips map generation documentation
1111
- """
1112
2303
  # Hardcoded mapping of document names to file paths and descriptions
1113
2304
  package_root = Path(__file__).parent.parent.parent
1114
2305
  docs_map: dict[str, tuple[Path, str]] = {
1115
2306
  "readme": (package_root / "README.md", "CoGames overview and documentation"),
1116
- "mission": (package_root / "MISSION.md", "Mission briefing for Machina VII Deployment"),
2307
+ "mission": (package_root / "MISSION.md", "Mission briefing for CogsGuard Deployment"),
1117
2308
  "technical_manual": (package_root / "TECHNICAL_MANUAL.md", "Technical manual for Cogames"),
1118
2309
  "scripted_agent": (
1119
- Path(__file__).parent / "policy" / "scripted_agent" / "README.md",
2310
+ Path(__file__).parent / "docs" / "SCRIPTED_AGENT.md",
1120
2311
  "Scripted agent policy documentation",
1121
2312
  ),
1122
2313
  "evals": (