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
@@ -0,0 +1,76 @@
1
+ import subprocess
2
+ import tempfile
3
+ from pathlib import Path
4
+
5
+ import jupytext
6
+ import nbformat
7
+ import typer
8
+ from nbstripout import strip_output
9
+
10
+
11
+ def get_cogames_root() -> Path:
12
+ """Get cogames root path by finding pyproject.toml."""
13
+ for parent in Path(__file__).resolve().parents:
14
+ if (parent / "pyproject.toml").exists():
15
+ return parent
16
+ raise RuntimeError("Could not find cogames root (no pyproject.toml found)")
17
+
18
+
19
+ def lint_py_file(path: Path, /) -> None:
20
+ """Run ruff format on a Python file."""
21
+ subprocess.run(["ruff", "format", str(path)], check=True, capture_output=True)
22
+
23
+
24
+ def run_notebook(*, nb_path: Path) -> None:
25
+ """Execute notebook in place."""
26
+ typer.echo(f" Executing {nb_path.name}...")
27
+ result = subprocess.run(
28
+ ["jupyter", "execute", nb_path.name, "--inplace"],
29
+ cwd=nb_path.parent,
30
+ capture_output=True,
31
+ text=True,
32
+ )
33
+ if result.returncode != 0:
34
+ typer.echo(f" Error: notebook execution failed: {result.stderr}", err=True)
35
+ raise typer.Exit(1)
36
+ typer.echo(f" Executed {nb_path.name}")
37
+
38
+
39
+ def clean_notebook_metadata(*, nb_path: Path) -> None:
40
+ """Strip metadata (execution counts, cell IDs, execution timestamps) but keep outputs.
41
+
42
+ Also ensures accelerator is set to GPU for Colab.
43
+ """
44
+ typer.echo(f" Cleaning metadata from {nb_path.name}...")
45
+ nb = nbformat.read(nb_path, as_version=4)
46
+ nb = strip_output(
47
+ nb,
48
+ keep_output=True,
49
+ keep_count=False,
50
+ keep_id=False,
51
+ extra_keys=["cell.metadata.execution"],
52
+ )
53
+ # Ensure GPU accelerator for Colab
54
+ # See: https://github.com/mwouts/jupytext/pull/235#issuecomment-495010137
55
+ nb.metadata["accelerator"] = "GPU"
56
+ nbformat.write(nb, nb_path)
57
+ typer.echo(f" Cleaned metadata from {nb_path.name}")
58
+
59
+
60
+ def files_equal(path1: str | Path, path2: str | Path, /) -> bool:
61
+ """Check if two files have identical content."""
62
+ return Path(path1).read_bytes() == Path(path2).read_bytes()
63
+
64
+
65
+ def py_nb_content_equal(*, py_path: Path, nb_path: Path) -> bool:
66
+ """Check if .py and .ipynb have equivalent content (ignoring outputs and linting)."""
67
+ with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as tmp:
68
+ tmp_py = Path(tmp.name)
69
+ try:
70
+ # Convert current .ipynb to .py and compare with existing .py
71
+ nb = jupytext.read(nb_path)
72
+ jupytext.write(nb, tmp_py, fmt="py:percent")
73
+ lint_py_file(tmp_py) # Format generated .py for fair comparison
74
+ return files_equal(py_path, tmp_py)
75
+ finally:
76
+ tmp_py.unlink(missing_ok=True)
@@ -0,0 +1,156 @@
1
+ """
2
+ `cogames docsync` is a dev-only tool to sync cogames docs between .ipynb, .py, and .md formats.
3
+
4
+ Tests for this module are in tests/docsync/
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from cogames.cli.docsync._nb_md_sync import convert_nb_to_md
13
+ from cogames.cli.docsync._nb_py_sync import convert_nb_to_py, convert_py_to_nb
14
+ from cogames.cli.docsync._three_way_sync import get_stem_paths, is_stem_in_sync_three_way, sync_stem_three_way
15
+ from cogames.cli.docsync._utils import get_cogames_root
16
+
17
+ app = typer.Typer(
18
+ help="""Sync cogames docs between .ipynb, .py, and .md formats
19
+
20
+ Directives (first line of code cell):
21
+ # <<hide>> - skip entire cell (code + output)
22
+ # <<hide-input>> - skip code, show output
23
+ # <<hide-output>> - show code, skip output
24
+ # <<collapse-input>> - wrap code in <details>
25
+ # <<collapse-output>> - wrap output in <details>
26
+
27
+ Placeholders (in markdown cells):
28
+ <<colab-link>> - replaced with Colab URL for this notebook
29
+ """
30
+ )
31
+
32
+
33
+ @app.command()
34
+ def nb_to_md(
35
+ nb_path: Annotated[Path, typer.Argument(help="Path to .ipynb file")],
36
+ skip_rerun: Annotated[bool, typer.Option("--skip-rerun", help="Skip re-running notebook")] = False,
37
+ cogames_root_raw: Annotated[
38
+ Path | None, typer.Option("--cogames-root", hidden=True, help="Override root path (for testing)")
39
+ ] = None,
40
+ ):
41
+ """Convert .ipynb to .md (reruns notebook by default to update outputs)."""
42
+
43
+ if not nb_path.exists():
44
+ typer.echo(f"Error: {nb_path} not found", err=True)
45
+ raise typer.Exit(1)
46
+ if nb_path.suffix != ".ipynb":
47
+ typer.echo(f"Error: expected .ipynb file, got {nb_path.suffix}", err=True)
48
+ raise typer.Exit(1)
49
+
50
+ cogames_root = cogames_root_raw or get_cogames_root()
51
+ typer.echo(f"Exporting {nb_path.name}...")
52
+ convert_nb_to_md(nb_path=nb_path, should_rerun=not skip_rerun, cogames_root=cogames_root)
53
+
54
+
55
+ @app.command()
56
+ def nb_to_py(
57
+ nb_path: Annotated[Path, typer.Argument(help="Path to .ipynb file")],
58
+ ):
59
+ """Convert notebook to .py percent format (ignores outputs, keeps code)."""
60
+
61
+ if not nb_path.exists():
62
+ typer.echo(f"Error: {nb_path} not found", err=True)
63
+ raise typer.Exit(1)
64
+ if nb_path.suffix != ".ipynb":
65
+ typer.echo(f"Error: expected .ipynb file, got {nb_path.suffix}", err=True)
66
+ raise typer.Exit(1)
67
+
68
+ py_path = nb_path.with_suffix(".py")
69
+ typer.echo(f"Converting {nb_path.name} to .py...")
70
+ convert_nb_to_py(nb_path=nb_path, py_path=py_path)
71
+ typer.echo(f"Created {py_path.name}")
72
+
73
+
74
+ @app.command()
75
+ def py_to_nb(
76
+ py_path: Annotated[Path, typer.Argument(help="Path to .py file")],
77
+ skip_rerun: Annotated[bool, typer.Option("--skip-rerun", help="Skip re-running notebook")] = False,
78
+ ):
79
+ """Convert .py percent format to .ipynb (runs notebook by default to populate outputs)."""
80
+
81
+ if not py_path.exists():
82
+ typer.echo(f"Error: {py_path} not found", err=True)
83
+ raise typer.Exit(1)
84
+ if py_path.suffix != ".py":
85
+ typer.echo(f"Error: expected .py file, got {py_path.suffix}", err=True)
86
+ raise typer.Exit(1)
87
+
88
+ nb_path = py_path.with_suffix(".ipynb")
89
+ typer.echo(f"Converting {py_path.name} to .ipynb...")
90
+ convert_py_to_nb(py_path=py_path, nb_path=nb_path, should_rerun=not skip_rerun)
91
+ typer.echo(f"Created {nb_path.name}")
92
+
93
+
94
+ @app.command()
95
+ def check(
96
+ skip_rerun: Annotated[bool, typer.Option("--skip-rerun", help="Skip re-running notebooks")] = False,
97
+ cogames_root_raw: Annotated[
98
+ Path | None, typer.Option("--cogames-root", hidden=True, help="Override root path (for testing)")
99
+ ] = None,
100
+ ):
101
+ """Verify that .py, .ipynb, and .md files are all in sync (without modifying files)."""
102
+ cogames_root = cogames_root_raw or get_cogames_root()
103
+ stem_paths = get_stem_paths(cogames_root=cogames_root)
104
+ should_rerun = not skip_rerun
105
+
106
+ typer.echo(f"Checking {len(stem_paths)} notebook(s)...")
107
+
108
+ errors: list[str] = []
109
+ for stem_path in stem_paths:
110
+ stem_errors = is_stem_in_sync_three_way(
111
+ stem_path=stem_path, should_rerun=should_rerun, cogames_root=cogames_root
112
+ )
113
+ if stem_errors:
114
+ typer.echo(f" ✗ {stem_path.name}")
115
+ else:
116
+ typer.echo(f" ✓ {stem_path.name}")
117
+ errors.extend(stem_errors)
118
+
119
+ if errors:
120
+ typer.echo("\nNotebook documentation is out of sync!", err=True)
121
+ for error in errors:
122
+ typer.echo(f" {error}", err=True)
123
+ typer.echo("\nTo fix: run 'cogames docsync all'", err=True)
124
+ raise typer.Exit(1)
125
+
126
+ typer.echo("All notebooks are in sync!")
127
+
128
+
129
+ @app.command()
130
+ def all(
131
+ skip_rerun: Annotated[bool, typer.Option("--skip-rerun", help="Skip re-running notebooks")] = False,
132
+ cogames_root_raw: Annotated[
133
+ Path | None, typer.Option("--cogames-root", hidden=True, help="Override root path (for testing)")
134
+ ] = None,
135
+ ):
136
+ """Resyncs README and tutorials/ .py and .ipynb files with each other and exports to .md files."""
137
+ cogames_root = cogames_root_raw or get_cogames_root()
138
+ stem_paths = get_stem_paths(cogames_root=cogames_root)
139
+ should_rerun = not skip_rerun
140
+
141
+ typer.echo(f"Found {len(stem_paths)} notebook(s) to process")
142
+
143
+ for stem_path in stem_paths:
144
+ stem_path_relative = stem_path.relative_to(cogames_root)
145
+ typer.echo(f"Processing {stem_path_relative}...")
146
+ sync_stem_three_way(
147
+ stem_path=stem_path,
148
+ should_rerun=should_rerun,
149
+ cogames_root=cogames_root,
150
+ )
151
+
152
+ typer.echo(f"Done! Processed {len(stem_paths)} notebook(s)")
153
+
154
+
155
+ if __name__ == "__main__":
156
+ app()
@@ -36,6 +36,22 @@ def parse_policy_identifier(identifier: str) -> tuple[str, int | None]:
36
36
  return identifier, None
37
37
 
38
38
 
39
+ def parse_season_ref(season_ref: str) -> tuple[str, int | None]:
40
+ if ":v" in season_ref:
41
+ name, version_str = season_ref.rsplit(":v", 1)
42
+ try:
43
+ return name, int(version_str)
44
+ except ValueError:
45
+ return season_ref, None
46
+ if ":" in season_ref:
47
+ name, version_str = season_ref.rsplit(":", 1)
48
+ try:
49
+ return name, int(version_str)
50
+ except ValueError:
51
+ return season_ref, None
52
+ return season_ref, None
53
+
54
+
39
55
  def _format_timestamp(value: Optional[str]) -> str:
40
56
  """Format ISO timestamps for CLI output."""
41
57
  if not value:
@@ -64,40 +80,60 @@ def _get_client(login_server: str, server: str) -> TournamentServerClient | None
64
80
  return TournamentServerClient.from_login(server_url=server, login_server=login_server)
65
81
 
66
82
 
83
+ def _help_callback(ctx: typer.Context, value: bool) -> None:
84
+ """Callback for custom help option."""
85
+ if value:
86
+ console.print(ctx.get_help())
87
+ raise typer.Exit()
88
+
89
+
67
90
  def submissions_cmd(
68
- policy_name: Optional[str] = typer.Argument(
91
+ policy_name: Optional[str] = typer.Option(
69
92
  None,
70
- help="Policy name to filter (e.g., 'my-policy' or 'my-policy:v3')",
93
+ "--policy",
94
+ "-p",
95
+ metavar="POLICY",
96
+ help="Filter by policy name (e.g., 'my-policy' or 'my-policy:v3')",
97
+ rich_help_panel="Filter",
71
98
  ),
72
99
  season: Optional[str] = typer.Option(
73
100
  None,
74
101
  "--season",
102
+ metavar="SEASON",
75
103
  help="Filter by tournament season",
104
+ rich_help_panel="Filter",
76
105
  ),
77
106
  login_server: str = typer.Option(
78
107
  DEFAULT_COGAMES_SERVER,
79
108
  "--login-server",
80
- help="Login/authentication server URL",
109
+ metavar="URL",
110
+ help="Authentication server URL",
111
+ rich_help_panel="Server",
81
112
  ),
82
113
  server: str = typer.Option(
83
114
  DEFAULT_SUBMIT_SERVER,
84
115
  "--server",
85
116
  "-s",
86
- help="Observatory API base URL",
117
+ metavar="URL",
118
+ help="Tournament server URL",
119
+ rich_help_panel="Server",
87
120
  ),
88
121
  json_output: bool = typer.Option(
89
122
  False,
90
123
  "--json",
91
- help="Print the raw JSON response instead of a table",
124
+ help="Print raw JSON instead of table",
125
+ rich_help_panel="Output",
126
+ ),
127
+ _help: bool = typer.Option(
128
+ False,
129
+ "--help",
130
+ "-h",
131
+ help="Show this message and exit",
132
+ is_eager=True,
133
+ callback=_help_callback,
134
+ rich_help_panel="Other",
92
135
  ),
93
136
  ) -> None:
94
- """Show your uploaded policies and tournament submissions.
95
-
96
- Examples:
97
- cogames submissions # All uploads
98
- cogames submissions --season beta # Submissions in beta season
99
- cogames submissions my-policy # Info on a specific policy
100
- """
101
137
  client = _get_client(login_server, server)
102
138
  if not client:
103
139
  return
@@ -167,7 +203,7 @@ def _show_season_submissions(
167
203
  ) -> None:
168
204
  """Show submissions for a specific season."""
169
205
  try:
170
- entries = client.get_season_policies(season, mine=True)
206
+ entries = client.get_season_policies(season, mine=True, include_hidden_seasons=True)
171
207
  except httpx.HTTPStatusError as exc:
172
208
  if exc.response.status_code == 404:
173
209
  console.print(f"[red]Season '{season}' not found[/red]")
@@ -219,39 +255,50 @@ def _show_season_submissions(
219
255
 
220
256
  def leaderboard_cmd(
221
257
  season: str = typer.Option(
222
- ...,
258
+ "beta-cogsguard",
223
259
  "--season",
224
- help="Tournament season name (required)",
260
+ metavar="SEASON",
261
+ help="Tournament season name",
262
+ rich_help_panel="Tournament",
225
263
  ),
226
264
  login_server: str = typer.Option(
227
265
  DEFAULT_COGAMES_SERVER,
228
266
  "--login-server",
229
- help="Login/authentication server URL",
267
+ metavar="URL",
268
+ help="Authentication server URL",
269
+ rich_help_panel="Server",
230
270
  ),
231
271
  server: str = typer.Option(
232
272
  DEFAULT_SUBMIT_SERVER,
233
273
  "--server",
234
274
  "-s",
235
- help="Observatory API base URL",
275
+ metavar="URL",
276
+ help="Tournament server URL",
277
+ rich_help_panel="Server",
236
278
  ),
237
279
  json_output: bool = typer.Option(
238
280
  False,
239
281
  "--json",
240
- help="Print the raw JSON response instead of a table",
282
+ help="Print raw JSON instead of table",
283
+ rich_help_panel="Output",
284
+ ),
285
+ _help: bool = typer.Option(
286
+ False,
287
+ "--help",
288
+ "-h",
289
+ help="Show this message and exit",
290
+ is_eager=True,
291
+ callback=_help_callback,
292
+ rich_help_panel="Other",
241
293
  ),
242
294
  ) -> None:
243
- """Display the tournament leaderboard for a season.
244
-
245
- Example:
246
- cogames leaderboard --season beta
247
- """
248
295
  client = _get_client(login_server, server)
249
296
  if not client:
250
297
  return
251
298
 
252
299
  try:
253
300
  with client:
254
- entries = client.get_leaderboard(season)
301
+ entries = client.get_leaderboard(season, include_hidden_seasons=True)
255
302
  except httpx.HTTPStatusError as exc:
256
303
  if exc.response.status_code == 404:
257
304
  console.print(f"[red]Season '{season}' not found[/red]")
@@ -290,30 +337,67 @@ def leaderboard_cmd(
290
337
 
291
338
 
292
339
  def seasons_cmd(
340
+ season_name: Optional[str] = typer.Argument(None, help="Show versions of a specific season"),
341
+ versions: bool = typer.Option(False, "--versions", "-v", help="List all versions of the season"),
293
342
  login_server: str = typer.Option(
294
343
  DEFAULT_COGAMES_SERVER,
295
344
  "--login-server",
296
- help="Login/authentication server URL",
345
+ metavar="URL",
346
+ help="Authentication server URL",
347
+ rich_help_panel="Server",
297
348
  ),
298
349
  server: str = typer.Option(
299
350
  DEFAULT_SUBMIT_SERVER,
300
351
  "--server",
301
352
  "-s",
302
- help="Observatory API base URL",
353
+ metavar="URL",
354
+ help="Tournament server URL",
355
+ rich_help_panel="Server",
303
356
  ),
304
357
  json_output: bool = typer.Option(
305
358
  False,
306
359
  "--json",
307
- help="Print the raw JSON response instead of a table",
360
+ help="Print raw JSON instead of table",
361
+ rich_help_panel="Output",
362
+ ),
363
+ _help: bool = typer.Option(
364
+ False,
365
+ "--help",
366
+ "-h",
367
+ help="Show this message and exit",
368
+ is_eager=True,
369
+ callback=_help_callback,
370
+ rich_help_panel="Other",
308
371
  ),
309
372
  ) -> None:
310
- """List available tournament seasons."""
311
373
  client = _get_client(login_server, server)
312
374
  if not client:
313
375
  return
314
376
 
315
377
  try:
316
378
  with client:
379
+ if season_name and versions:
380
+ season_versions = client.get_season_versions(season_name)
381
+ if json_output:
382
+ console.print(json.dumps([v.model_dump() for v in season_versions], indent=2))
383
+ return
384
+
385
+ if not season_versions:
386
+ console.print(f"[yellow]No versions found for season '{season_name}'.[/yellow]")
387
+ return
388
+
389
+ table = Table(title=f"Versions: {season_name}", box=box.SIMPLE_HEAVY, show_lines=False, pad_edge=False)
390
+ table.add_column("Version", style="bold cyan")
391
+ table.add_column("Status", style="white")
392
+ table.add_column("Created", style="dim")
393
+
394
+ for v in season_versions:
395
+ status = "[green]canonical[/green]" if v.canonical else "[dim]historical[/dim]"
396
+ table.add_row(f"v{v.version}", status, _format_timestamp(v.created_at))
397
+
398
+ console.print(table)
399
+ return
400
+
317
401
  seasons = client.get_seasons()
318
402
  except httpx.HTTPError as exc:
319
403
  console.print(f"[red]Failed to reach server:[/red] {exc}")