cogames 0.3.65__py3-none-any.whl → 0.3.68__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 (134) hide show
  1. cogames/cli/client.py +0 -3
  2. cogames/cli/docsync/docsync.py +7 -1
  3. cogames/cli/mission.py +44 -19
  4. cogames/cli/policy.py +26 -10
  5. cogames/cli/submit.py +127 -141
  6. cogames/cli/utils.py +5 -0
  7. cogames/cogs_vs_clips/clip_difficulty.py +57 -0
  8. cogames/cogs_vs_clips/clips.py +23 -6
  9. cogames/cogs_vs_clips/cog.py +16 -5
  10. cogames/cogs_vs_clips/cogsguard_curriculum.py +122 -0
  11. cogames/cogs_vs_clips/cogsguard_tutorial.py +5 -5
  12. cogames/cogs_vs_clips/config.py +1 -1
  13. cogames/cogs_vs_clips/docs/cogs_vs_clips_mapgen.md +2 -3
  14. cogames/cogs_vs_clips/evals/README.md +8 -32
  15. cogames/cogs_vs_clips/evals/diagnostic_evals.py +0 -1
  16. cogames/cogs_vs_clips/evals/difficulty_variants.py +7 -10
  17. cogames/cogs_vs_clips/mission.py +38 -10
  18. cogames/cogs_vs_clips/missions.py +1 -1
  19. cogames/cogs_vs_clips/reward_variants.py +173 -0
  20. cogames/cogs_vs_clips/sites.py +6 -5
  21. cogames/cogs_vs_clips/stations.py +13 -9
  22. cogames/cogs_vs_clips/team.py +3 -1
  23. cogames/cogs_vs_clips/terrain.py +2 -2
  24. cogames/cogs_vs_clips/variants.py +175 -4
  25. cogames/cogs_vs_clips/weather.py +52 -0
  26. cogames/docs/SCRIPTED_AGENT.md +3 -3
  27. cogames/evaluate.py +4 -2
  28. cogames/main.py +357 -51
  29. cogames/maps/canidate1_1000.map +1 -1
  30. cogames/maps/canidate1_1000_stations.map +2 -2
  31. cogames/maps/canidate1_500.map +1 -1
  32. cogames/maps/canidate1_500_stations.map +2 -2
  33. cogames/maps/canidate2_1000.map +1 -1
  34. cogames/maps/canidate2_1000_stations.map +2 -2
  35. cogames/maps/canidate2_500.map +1 -1
  36. cogames/maps/canidate2_500_stations.map +1 -1
  37. cogames/maps/canidate3_1000.map +1 -1
  38. cogames/maps/canidate3_1000_stations.map +2 -2
  39. cogames/maps/canidate3_500.map +1 -1
  40. cogames/maps/canidate3_500_stations.map +2 -2
  41. cogames/maps/canidate4_500.map +1 -1
  42. cogames/maps/canidate4_500_stations.map +2 -2
  43. cogames/maps/cave_base_50.map +2 -2
  44. cogames/maps/diagnostic_evals/diagnostic_agile.map +2 -2
  45. cogames/maps/diagnostic_evals/diagnostic_agile_hard.map +2 -2
  46. cogames/maps/diagnostic_evals/diagnostic_charge_up.map +6 -6
  47. cogames/maps/diagnostic_evals/diagnostic_charge_up_hard.map +6 -6
  48. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1.map +6 -6
  49. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1_hard.map +6 -6
  50. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2.map +6 -6
  51. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2_hard.map +6 -6
  52. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3.map +6 -6
  53. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3_hard.map +6 -6
  54. cogames/maps/diagnostic_evals/diagnostic_chest_near.map +6 -6
  55. cogames/maps/diagnostic_evals/diagnostic_chest_search.map +6 -6
  56. cogames/maps/diagnostic_evals/diagnostic_chest_search_hard.map +6 -6
  57. cogames/maps/diagnostic_evals/diagnostic_extract_lab.map +6 -6
  58. cogames/maps/diagnostic_evals/diagnostic_extract_lab_hard.map +6 -6
  59. cogames/maps/diagnostic_evals/diagnostic_memory.map +6 -6
  60. cogames/maps/diagnostic_evals/diagnostic_memory_hard.map +6 -6
  61. cogames/maps/diagnostic_evals/diagnostic_radial.map +2 -2
  62. cogames/maps/diagnostic_evals/diagnostic_radial_hard.map +2 -2
  63. cogames/maps/diagnostic_evals/diagnostic_resource_lab.map +6 -6
  64. cogames/maps/diagnostic_evals/diagnostic_unclip.map +6 -6
  65. cogames/maps/evals/eval_balanced_spread.map +6 -6
  66. cogames/maps/evals/eval_clip_oxygen.map +6 -6
  67. cogames/maps/evals/eval_collect_resources.map +6 -6
  68. cogames/maps/evals/eval_collect_resources_hard.map +6 -6
  69. cogames/maps/evals/eval_collect_resources_medium.map +6 -6
  70. cogames/maps/evals/eval_divide_and_conquer.map +6 -6
  71. cogames/maps/evals/eval_energy_starved.map +6 -6
  72. cogames/maps/evals/eval_multi_coordinated_collect_hard.map +6 -6
  73. cogames/maps/evals/eval_oxygen_bottleneck.map +6 -6
  74. cogames/maps/evals/eval_single_use_world.map +6 -6
  75. cogames/maps/evals/extractor_hub_100x100.map +6 -6
  76. cogames/maps/evals/extractor_hub_30x30.map +6 -6
  77. cogames/maps/evals/extractor_hub_50x50.map +6 -6
  78. cogames/maps/evals/extractor_hub_70x70.map +6 -6
  79. cogames/maps/evals/extractor_hub_80x80.map +6 -6
  80. cogames/maps/machina_100_stations.map +2 -2
  81. cogames/maps/machina_200_stations.map +2 -2
  82. cogames/maps/machina_200_stations_small.map +2 -2
  83. cogames/maps/machina_eval_exp01.map +2 -2
  84. cogames/maps/machina_eval_template_large.map +2 -2
  85. cogames/maps/machinatrainer4agents.map +2 -2
  86. cogames/maps/machinatrainer4agentsbase.map +2 -2
  87. cogames/maps/machinatrainerbig.map +2 -2
  88. cogames/maps/machinatrainersmall.map +2 -2
  89. cogames/maps/planky_evals/aligner_avoid_aoe.map +6 -6
  90. cogames/maps/planky_evals/aligner_full_cycle.map +6 -6
  91. cogames/maps/planky_evals/aligner_gear.map +6 -6
  92. cogames/maps/planky_evals/aligner_hearts.map +6 -6
  93. cogames/maps/planky_evals/aligner_junction.map +6 -6
  94. cogames/maps/planky_evals/exploration_distant.map +6 -6
  95. cogames/maps/planky_evals/maze.map +6 -6
  96. cogames/maps/planky_evals/miner_best_resource.map +6 -6
  97. cogames/maps/planky_evals/miner_deposit.map +6 -6
  98. cogames/maps/planky_evals/miner_extract.map +6 -6
  99. cogames/maps/planky_evals/miner_full_cycle.map +6 -6
  100. cogames/maps/planky_evals/miner_gear.map +6 -6
  101. cogames/maps/planky_evals/multi_role.map +6 -6
  102. cogames/maps/planky_evals/resource_chain.map +6 -6
  103. cogames/maps/planky_evals/scout_explore.map +6 -6
  104. cogames/maps/planky_evals/scout_gear.map +6 -6
  105. cogames/maps/planky_evals/scrambler_full_cycle.map +6 -6
  106. cogames/maps/planky_evals/scrambler_gear.map +6 -6
  107. cogames/maps/planky_evals/scrambler_target.map +6 -6
  108. cogames/maps/planky_evals/stuck_corridor.map +6 -6
  109. cogames/maps/planky_evals/survive_retreat.map +6 -6
  110. cogames/maps/training_facility_clipped.map +2 -2
  111. cogames/maps/training_facility_open_1.map +2 -2
  112. cogames/maps/training_facility_open_2.map +2 -2
  113. cogames/maps/training_facility_open_3.map +2 -2
  114. cogames/maps/training_facility_tight_4.map +2 -2
  115. cogames/maps/training_facility_tight_5.map +2 -2
  116. cogames/maps/vanilla_large.map +2 -2
  117. cogames/maps/vanilla_small.map +2 -2
  118. cogames/pickup.py +6 -5
  119. cogames/play.py +14 -16
  120. cogames/policy/nim_agents/__init__.py +0 -2
  121. cogames/policy/nim_agents/agents.py +0 -11
  122. cogames/policy/starter_agent.py +4 -1
  123. {cogames-0.3.65.dist-info → cogames-0.3.68.dist-info}/METADATA +45 -29
  124. cogames-0.3.68.dist-info/RECORD +160 -0
  125. metta_alo/scoring.py +7 -7
  126. cogames-0.3.65.dist-info/RECORD +0 -160
  127. metta_alo/job_specs.py +0 -17
  128. metta_alo/policy.py +0 -16
  129. metta_alo/pure_single_episode_runner.py +0 -75
  130. metta_alo/rollout.py +0 -322
  131. {cogames-0.3.65.dist-info → cogames-0.3.68.dist-info}/WHEEL +0 -0
  132. {cogames-0.3.65.dist-info → cogames-0.3.68.dist-info}/entry_points.txt +0 -0
  133. {cogames-0.3.65.dist-info → cogames-0.3.68.dist-info}/licenses/LICENSE +0 -0
  134. {cogames-0.3.65.dist-info → cogames-0.3.68.dist-info}/top_level.txt +0 -0
cogames/cli/submit.py CHANGED
@@ -10,22 +10,21 @@ import uuid
10
10
  import zipfile
11
11
  from dataclasses import dataclass
12
12
  from pathlib import Path
13
- from typing import TYPE_CHECKING
14
13
 
15
14
  import httpx
16
15
  import typer
17
16
  from rich.console import Console
18
17
 
19
18
  from cogames.cli.base import console
19
+ from cogames.cli.client import TournamentServerClient
20
20
  from cogames.cli.login import DEFAULT_COGAMES_SERVER
21
- from cogames.cli.policy import PolicySpec, get_policy_spec
22
- from metta_alo.rollout import run_single_episode
23
-
24
- if TYPE_CHECKING:
25
- from cogames.cli.client import TournamentServerClient
26
-
21
+ from cogames.cli.mission import get_mission
22
+ from cogames.cli.policy import PolicySpec, get_policy_spec, parse_policy_spec
27
23
  from mettagrid.config.mettagrid_config import MettaGridConfig
24
+ from mettagrid.policy.prepare_policy_spec import download_policy_spec_from_s3_as_zip
28
25
  from mettagrid.policy.submission import POLICY_SPEC_FILENAME, SubmissionPolicySpec
26
+ from mettagrid.runner.rollout import run_episode_local
27
+ from mettagrid.util.uri_resolvers.schemes import parse_uri, resolve_uri
29
28
 
30
29
  DEFAULT_SUBMIT_SERVER = "https://api.observatory.softmax-research.net"
31
30
 
@@ -51,12 +50,11 @@ def validate_paths(paths: list[str], console: Console) -> list[Path]:
51
50
  raw_path = Path(path_str).expanduser()
52
51
 
53
52
  # Resolve the path and check it's within CWD
54
- try:
55
- resolved = raw_path.resolve() if raw_path.is_absolute() else (cwd / raw_path).resolve()
56
- relative = resolved.relative_to(cwd)
57
- except ValueError:
53
+ resolved = raw_path.resolve() if raw_path.is_absolute() else (cwd / raw_path).resolve()
54
+ if not resolved.is_relative_to(cwd):
58
55
  console.print(f"[red]Error:[/red] Path must be within the current directory: {path_str}")
59
- raise ValueError(f"Path escapes CWD: {path_str}") from None
56
+ raise ValueError(f"Path escapes CWD: {path_str}")
57
+ relative = resolved.relative_to(cwd)
60
58
 
61
59
  # Check if path exists
62
60
  if not resolved.exists():
@@ -70,9 +68,6 @@ def validate_paths(paths: list[str], console: Console) -> list[Path]:
70
68
 
71
69
  def _maybe_resolve_checkpoint_bundle_uri(policy: str) -> tuple[Path, bool] | None:
72
70
  """Return (local_zip_path, cleanup) if policy points to a checkpoint bundle URI."""
73
- from mettagrid.policy.prepare_policy_spec import download_policy_spec_from_s3_as_zip
74
- from mettagrid.util.uri_resolvers.schemes import parse_uri, resolve_uri
75
-
76
71
  first = policy.split(",", 1)[0].strip()
77
72
  parsed = parse_uri(first, allow_none=True, default_scheme=None)
78
73
  if parsed is None or parsed.scheme not in {"file", "s3"}:
@@ -111,9 +106,8 @@ def _zip_directory_bundle(bundle_dir: Path) -> Path:
111
106
  def validate_bundle_in_isolation(policy_zip: Path, console: Console, *, season: str, server: str) -> bool:
112
107
  console.print("[dim]Testing policy bundle can run 10 steps...[/dim]")
113
108
 
114
- temp_dir = None
109
+ temp_dir = create_temp_validation_env()
115
110
  try:
116
- temp_dir = create_temp_validation_env()
117
111
  bundle_name = policy_zip.name
118
112
  shutil.copy2(policy_zip, temp_dir / bundle_name)
119
113
 
@@ -148,11 +142,8 @@ def validate_bundle_in_isolation(policy_zip: Path, console: Console, *, season:
148
142
 
149
143
  console.print("[green]Validation passed[/green]")
150
144
  return True
151
- except subprocess.TimeoutExpired:
152
- console.print("[red]Validation timed out after 5 minutes[/red]")
153
- return False
154
145
  finally:
155
- if temp_dir and temp_dir.exists():
146
+ if temp_dir.exists():
156
147
  shutil.rmtree(temp_dir)
157
148
 
158
149
 
@@ -214,8 +205,46 @@ def copy_files_maintaining_structure(files: list[Path], dest_dir: Path) -> None:
214
205
  shutil.copy2(file_path, dest_path)
215
206
 
216
207
 
217
- def validate_policy_spec(policy_spec: PolicySpec, env_cfg: MettaGridConfig) -> None:
218
- env_cfg = env_cfg.model_copy()
208
+ _SEASON_VALIDATION_MISSIONS: dict[str, str] = {
209
+ "beta": "training_facility.harvest",
210
+ "beta-cogsguard": "cogsguard_arena.basic",
211
+ }
212
+ _DEFAULT_VALIDATION_MISSION = "cogsguard_arena.basic"
213
+
214
+
215
+ def get_validation_mission_for_season(season: str | None = None) -> str:
216
+ """Get the appropriate mission for validating policies in a given season."""
217
+ if season is None:
218
+ return _DEFAULT_VALIDATION_MISSION
219
+ return _SEASON_VALIDATION_MISSIONS.get(season, _DEFAULT_VALIDATION_MISSION)
220
+
221
+
222
+ def validate_policy_spec(
223
+ policy_spec: PolicySpec,
224
+ env_cfg: MettaGridConfig | None = None,
225
+ *,
226
+ device: str = "cpu",
227
+ season: str | None = None,
228
+ ) -> None:
229
+ """Validate policy works.
230
+
231
+ Runs a single episode (up to 10 steps) using the same alo rollout flow as `cogames eval`.
232
+
233
+ Args:
234
+ policy_spec: The policy to validate.
235
+ env_cfg: Optional environment config to validate against.
236
+ device: Target device for policy evaluation (cpu/cuda/auto).
237
+ season: Optional season name to determine which game to validate against.
238
+ """
239
+ if env_cfg is None:
240
+ mission_name = get_validation_mission_for_season(season)
241
+ # Legacy seasons (e.g., "beta") use legacy missions that require include_legacy=True
242
+ include_legacy = season in _SEASON_VALIDATION_MISSIONS
243
+ _, env_cfg, _ = get_mission(mission_name, include_legacy=include_legacy)
244
+ else:
245
+ env_cfg = env_cfg.model_copy()
246
+
247
+ # Run 1 episode for up to 10 steps to validate the policy works
219
248
  env_cfg.game.max_steps = 10
220
249
  n = env_cfg.game.num_agents
221
250
  n_submitted = min(2, n)
@@ -226,18 +255,21 @@ def validate_policy_spec(policy_spec: PolicySpec, env_cfg: MettaGridConfig) -> N
226
255
  else:
227
256
  policy_specs = [policy_spec]
228
257
  assignments = [0] * n
229
- run_single_episode(
258
+ run_episode_local(
230
259
  policy_specs=policy_specs,
231
260
  assignments=assignments,
232
261
  env=env_cfg,
233
- results_uri=None,
234
- replay_uri=None,
235
262
  seed=42,
236
263
  max_action_time_ms=10000,
237
- device="cpu",
264
+ device=device,
238
265
  )
239
266
 
240
267
 
268
+ def validate_policy_uri(policy_uri: str, env_cfg: MettaGridConfig, *, device: str = "cpu") -> None:
269
+ policy_spec = parse_policy_spec(policy_uri, device=device).to_policy_spec()
270
+ validate_policy_spec(policy_spec, env_cfg, device=device)
271
+
272
+
241
273
  def validate_policy_in_isolation(
242
274
  policy_spec: PolicySpec,
243
275
  include_files: list[Path],
@@ -257,9 +289,8 @@ def validate_policy_in_isolation(
257
289
 
258
290
  console.print("[dim]Testing policy can run 10 steps...[/dim]")
259
291
 
260
- temp_dir = None
292
+ temp_dir = create_temp_validation_env()
261
293
  try:
262
- temp_dir = create_temp_validation_env()
263
294
  copy_files_maintaining_structure(include_files, temp_dir)
264
295
 
265
296
  policy_arg = _format_policy_arg(policy_spec)
@@ -304,17 +335,23 @@ def validate_policy_in_isolation(
304
335
 
305
336
  console.print("[green]Validation passed[/green]")
306
337
  return True
307
-
308
- except subprocess.TimeoutExpired:
309
- console.print("[red]Validation timed out after 5 minutes[/red]")
310
- return False
311
- except Exception:
312
- return False
313
338
  finally:
314
- if temp_dir and temp_dir.exists():
339
+ if temp_dir.exists():
315
340
  shutil.rmtree(temp_dir)
316
341
 
317
342
 
343
+ def _collect_ancestor_init_files(include_files: list[Path]) -> list[Path]:
344
+ found: set[Path] = set()
345
+ for path in include_files:
346
+ parent = path.parent
347
+ while parent != Path(".") and parent != parent.parent:
348
+ init = parent / "__init__.py"
349
+ if init.is_file():
350
+ found.add(init)
351
+ parent = parent.parent
352
+ return sorted(found)
353
+
354
+
318
355
  def create_submission_zip(
319
356
  include_files: list[Path],
320
357
  policy_spec: PolicySpec,
@@ -335,17 +372,22 @@ def create_submission_zip(
335
372
  setup_script=setup_script,
336
373
  )
337
374
 
375
+ all_files: dict[str, Path] = {}
376
+ for init_path in _collect_ancestor_init_files(include_files):
377
+ all_files[str(init_path)] = init_path
378
+ for file_path in include_files:
379
+ if file_path.is_dir():
380
+ for root, _, files in os.walk(file_path):
381
+ for file in files:
382
+ full = Path(root) / file
383
+ all_files[str(full)] = full
384
+ else:
385
+ all_files[str(file_path)] = file_path
386
+
338
387
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
339
388
  zipf.writestr(data=submission_spec.model_dump_json(), zinfo_or_arcname=POLICY_SPEC_FILENAME)
340
-
341
- for file_path in include_files:
342
- if file_path.is_dir():
343
- for root, _, files in os.walk(file_path):
344
- for file in files:
345
- file_full_path = Path(root) / file
346
- zipf.write(file_full_path, arcname=file_full_path)
347
- else:
348
- zipf.write(file_path, arcname=file_path)
389
+ for arcname, path in all_files.items():
390
+ zipf.write(path, arcname=arcname)
349
391
 
350
392
  return Path(zip_path)
351
393
 
@@ -360,82 +402,45 @@ def upload_submission(
360
402
  """Upload submission to CoGames backend using a presigned S3 URL."""
361
403
  console.print("[bold]Uploading[/bold]")
362
404
 
363
- try:
364
- presigned_data = client.get_presigned_upload_url()
365
- upload_url = presigned_data.get("upload_url")
366
- upload_id = presigned_data.get("upload_id")
405
+ presigned_data = client.get_presigned_upload_url()
406
+ upload_url = presigned_data.get("upload_url")
407
+ upload_id = presigned_data.get("upload_id")
367
408
 
368
- if not upload_url or not upload_id:
369
- console.print("[red]Upload URL missing from response[/red]")
370
- return None
371
- except httpx.TimeoutException:
372
- console.print("[red]Timed out while requesting upload URL[/red]")
373
- return None
374
- except httpx.HTTPStatusError as exc:
375
- console.print(f"[red]Failed to get upload URL ({exc.response.status_code})[/red]")
376
- console.print(f"[dim]{exc.response.text}[/dim]")
377
- return None
378
- except Exception as e:
379
- console.print(f"[red]Error requesting upload URL: {e}[/red]")
380
- return None
409
+ if not upload_url or not upload_id:
410
+ raise ValueError("Upload URL missing from response")
381
411
 
382
412
  console.print("[dim]Uploading to storage...[/dim]")
383
413
 
384
- try:
385
- with open(zip_path, "rb") as f:
386
- upload_response = httpx.put(
387
- upload_url,
388
- content=f,
389
- headers={"Content-Type": "application/zip"},
390
- timeout=600.0,
391
- )
392
- upload_response.raise_for_status()
393
- except httpx.TimeoutException:
394
- console.print("[red]Upload timed out after 10 minutes[/red]")
395
- return None
396
- except httpx.HTTPStatusError as exc:
397
- console.print(f"[red]Upload failed with status {exc.response.status_code}[/red]")
398
- console.print(f"[dim]{exc.response.text}[/dim]")
399
- return None
400
- except Exception as e:
401
- console.print(f"[red]Upload error: {e}[/red]")
402
- return None
414
+ with open(zip_path, "rb") as f:
415
+ upload_response = httpx.put(
416
+ upload_url,
417
+ content=f,
418
+ headers={"Content-Type": "application/zip"},
419
+ timeout=600.0,
420
+ )
421
+ upload_response.raise_for_status()
403
422
 
404
423
  if not season:
405
424
  console.print("[dim]Uploading policy...[/dim]")
406
425
  else:
407
426
  console.print(f"[dim]Uploading policy and submitting to season {season}...[/dim]")
408
427
 
428
+ result = client.complete_policy_upload(upload_id, submission_name, season=season)
429
+ submission_id = result.get("id")
430
+ name = result.get("name")
431
+ version = result.get("version")
432
+ pools = result.get("pools")
433
+ if submission_id is None or name is None or version is None:
434
+ raise ValueError("Missing fields in response")
409
435
  try:
410
- result = client.complete_policy_upload(upload_id, submission_name, season=season)
411
- submission_id = result.get("id")
412
- name = result.get("name")
413
- version = result.get("version")
414
- pools = result.get("pools")
415
- if submission_id is not None and name is not None and version is not None:
416
- try:
417
- return UploadResult(
418
- policy_version_id=uuid.UUID(str(submission_id)),
419
- name=name,
420
- version=version,
421
- pools=pools,
422
- )
423
- except ValueError:
424
- console.print(f"[red]Invalid submission ID returned: {submission_id}[/red]")
425
- return None
426
-
427
- console.print("[red]Missing fields in response[/red]")
428
- return None
429
- except httpx.TimeoutException:
430
- console.print("[red]Registration timed out[/red]")
431
- return None
432
- except httpx.HTTPStatusError as exc:
433
- console.print(f"[red]Registration failed with status {exc.response.status_code}[/red]")
434
- console.print(f"[dim]{exc.response.text}[/dim]")
435
- return None
436
- except Exception as e:
437
- console.print(f"[red]Registration error: {e}[/red]")
438
- return None
436
+ return UploadResult(
437
+ policy_version_id=uuid.UUID(str(submission_id)),
438
+ name=name,
439
+ version=version,
440
+ pools=pools,
441
+ )
442
+ except ValueError as exc:
443
+ raise ValueError(f"Invalid submission ID returned: {submission_id}") from exc
439
444
 
440
445
 
441
446
  def _upload_policy_bundle(
@@ -501,8 +506,6 @@ def upload_policy(
501
506
  validation_season: str = "",
502
507
  season: str | None = None,
503
508
  ) -> UploadResult | None:
504
- from cogames.cli.client import TournamentServerClient
505
-
506
509
  if dry_run:
507
510
  console.print("[dim]Dry run mode - no upload[/dim]\n")
508
511
 
@@ -510,11 +513,7 @@ def upload_policy(
510
513
  if not client:
511
514
  return None
512
515
 
513
- try:
514
- bundle_result = _maybe_resolve_checkpoint_bundle_uri(policy)
515
- except Exception as e:
516
- console.print(f"[red]Error resolving checkpoint bundle:[/red] {e}")
517
- return None
516
+ bundle_result = _maybe_resolve_checkpoint_bundle_uri(policy)
518
517
 
519
518
  if bundle_result is not None:
520
519
  return _upload_policy_bundle(
@@ -532,11 +531,7 @@ def upload_policy(
532
531
  season=season,
533
532
  )
534
533
 
535
- try:
536
- policy_spec = get_policy_spec(ctx, policy)
537
- except Exception as e:
538
- console.print(f"[red]Error parsing policy:[/red] {e}")
539
- return None
534
+ policy_spec = get_policy_spec(ctx, policy)
540
535
 
541
536
  if init_kwargs:
542
537
  merged_kwargs = {**policy_spec.init_kwargs, **init_kwargs}
@@ -548,13 +543,12 @@ def upload_policy(
548
543
 
549
544
  cwd = Path.cwd().resolve()
550
545
  if policy_spec.data_path:
551
- try:
552
- resolved = Path(policy_spec.data_path).expanduser().resolve()
553
- data_rel = str(resolved.relative_to(cwd))
554
- except ValueError:
546
+ resolved = Path(policy_spec.data_path).expanduser().resolve()
547
+ if not resolved.is_relative_to(cwd):
555
548
  console.print("[red]Error:[/red] Policy weights path must be within the current directory.")
556
549
  console.print(f"[dim]{policy_spec.data_path}[/dim]")
557
- return None
550
+ raise ValueError("Policy weights path must be within the current directory.")
551
+ data_rel = str(resolved.relative_to(cwd))
558
552
  policy_spec = PolicySpec(
559
553
  class_path=policy_spec.class_path,
560
554
  data_path=data_rel,
@@ -563,13 +557,12 @@ def upload_policy(
563
557
 
564
558
  setup_script_rel: str | None = None
565
559
  if setup_script:
566
- try:
567
- resolved = Path(setup_script).expanduser().resolve()
568
- setup_script_rel = str(resolved.relative_to(cwd))
569
- except ValueError:
560
+ resolved = Path(setup_script).expanduser().resolve()
561
+ if not resolved.is_relative_to(cwd):
570
562
  console.print("[red]Error:[/red] Setup script path must be within the current directory.")
571
563
  console.print(f"[dim]{setup_script}[/dim]")
572
- return None
564
+ raise ValueError("Setup script path must be within the current directory.")
565
+ setup_script_rel = str(resolved.relative_to(cwd))
573
566
 
574
567
  files_to_include = []
575
568
  if policy_spec.data_path:
@@ -581,10 +574,7 @@ def upload_policy(
581
574
 
582
575
  validated_paths: list[Path] = []
583
576
  if files_to_include:
584
- try:
585
- validated_paths = validate_paths(files_to_include, console)
586
- except (ValueError, FileNotFoundError):
587
- return None
577
+ validated_paths = validate_paths(files_to_include, console)
588
578
 
589
579
  if not skip_validation:
590
580
  if not validate_policy_in_isolation(
@@ -600,11 +590,7 @@ def upload_policy(
600
590
  else:
601
591
  console.print("[dim]Skipping validation[/dim]")
602
592
 
603
- try:
604
- zip_path = create_submission_zip(validated_paths, policy_spec, setup_script=setup_script_rel)
605
- except Exception as e:
606
- console.print(f"[red]Error creating zip:[/red] {e}")
607
- return None
593
+ zip_path = create_submission_zip(validated_paths, policy_spec, setup_script=setup_script_rel)
608
594
 
609
595
  if dry_run:
610
596
  console.print("[green]Dry run complete[/green]")
cogames/cli/utils.py CHANGED
@@ -11,6 +11,11 @@ def suppress_noisy_logs() -> None:
11
11
  warnings.filterwarnings("ignore", category=DeprecationWarning, module="pkg_resources")
12
12
  warnings.filterwarnings("ignore", category=DeprecationWarning, module="pygame.pkgdata")
13
13
 
14
+ # Pyro docstrings use LaTeX math notation (\ge for ≥) which Python 3.12+ warns about:
15
+ # pyro/ops/stats.py:527: SyntaxWarning: invalid escape sequence '\g'
16
+ # Note: module= filter doesn't work for SyntaxWarnings (emitted at parse time before module loads)
17
+ warnings.filterwarnings("ignore", category=SyntaxWarning, message=r".*invalid escape sequence.*")
18
+
14
19
  # Silence PyTorch distributed elastic warning about redirects on MacOS/Windows
15
20
  logging.getLogger("torch.distributed.elastic.multiprocessing.redirects").setLevel(logging.ERROR)
16
21
  warnings.filterwarnings(
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Final, Optional
4
+
5
+ from cogames.cogs_vs_clips.mission import CvCMission
6
+ from cogames.core import CoGameMissionVariant
7
+
8
+ MEDIUM_CLIPS_END: Final[int] = 300
9
+
10
+
11
+ class CogsGuardDifficulty(CoGameMissionVariant):
12
+ name: str
13
+ description: str = ""
14
+ disable_clips: bool = False
15
+ scramble_end: Optional[int] = None
16
+ align_end: Optional[int] = None
17
+ presence_end: Optional[int] = None
18
+
19
+ def modify_mission(self, mission: CvCMission) -> None:
20
+ if self.disable_clips:
21
+ mission.clips.disabled = True
22
+ return
23
+
24
+ mission.clips.disabled = False
25
+ mission.clips.scramble_end = self.scramble_end
26
+ mission.clips.align_end = self.align_end
27
+ mission.clips.presence_end = self.presence_end
28
+
29
+
30
+ EASY = CogsGuardDifficulty(
31
+ name="easy",
32
+ description="No clips events.",
33
+ disable_clips=True,
34
+ )
35
+
36
+ MEDIUM = CogsGuardDifficulty(
37
+ name="medium",
38
+ description="A few early clips events, then none.",
39
+ scramble_end=MEDIUM_CLIPS_END,
40
+ align_end=MEDIUM_CLIPS_END,
41
+ presence_end=MEDIUM_CLIPS_END,
42
+ )
43
+
44
+ HARD = CogsGuardDifficulty(
45
+ name="hard",
46
+ description="Standard clips event system.",
47
+ )
48
+
49
+ COGSGUARD_DIFFICULTIES: tuple[CogsGuardDifficulty, ...] = (EASY, MEDIUM, HARD)
50
+
51
+
52
+ def get_cogsguard_difficulty(name: str) -> CogsGuardDifficulty:
53
+ for difficulty in COGSGUARD_DIFFICULTIES:
54
+ if difficulty.name == name:
55
+ return difficulty
56
+ available = ", ".join(d.name for d in COGSGUARD_DIFFICULTIES)
57
+ raise ValueError(f"Unknown difficulty '{name}'. Available difficulties: {available}")
@@ -4,6 +4,8 @@ Clips are a non-player faction that gradually takes over neutral junctions.
4
4
  These events create the spreading/scrambling behavior that pressures players.
5
5
  """
6
6
 
7
+ from typing import Optional
8
+
7
9
  from pydantic import Field
8
10
 
9
11
  from mettagrid.base_config import Config
@@ -18,6 +20,8 @@ from mettagrid.config.tag import typeTag
18
20
  class ClipsConfig(Config):
19
21
  """Configuration for clips behavior in CogsGuard game mode."""
20
22
 
23
+ disabled: bool = Field(default=False)
24
+
21
25
  # Clips Behavior - scramble cogs junctions to neutral
22
26
  initial_clips_start: int = Field(default=10)
23
27
  initial_clips_spots: int = Field(default=1)
@@ -25,11 +29,16 @@ class ClipsConfig(Config):
25
29
  scramble_start: int = Field(default=50)
26
30
  scramble_interval: int = Field(default=100)
27
31
  scramble_radius: int = Field(default=25)
32
+ scramble_end: Optional[int] = Field(default=None)
28
33
 
29
34
  # Clips Behavior - align neutral junctions to clips
30
35
  align_start: int = Field(default=100)
31
36
  align_interval: int = Field(default=100)
32
37
  align_radius: int = Field(default=25)
38
+ align_end: Optional[int] = Field(default=None)
39
+
40
+ # Clips Behavior - presence check for re-invasion
41
+ presence_end: Optional[int] = Field(default=None)
33
42
 
34
43
  def events(self, max_steps: int) -> dict[str, EventConfig]:
35
44
  """Create all clips events for a mission.
@@ -37,6 +46,12 @@ class ClipsConfig(Config):
37
46
  Returns:
38
47
  Dictionary of event name to EventConfig.
39
48
  """
49
+ if self.disabled:
50
+ return {}
51
+ scramble_end = max_steps if self.scramble_end is None else min(self.scramble_end, max_steps)
52
+ align_end = max_steps if self.align_end is None else min(self.align_end, max_steps)
53
+ presence_end = max_steps if self.presence_end is None else min(self.presence_end, max_steps)
54
+
40
55
  return {
41
56
  "initial_clips": EventConfig(
42
57
  name="initial_clips",
@@ -48,7 +63,7 @@ class ClipsConfig(Config):
48
63
  "cogs_to_neutral": EventConfig(
49
64
  name="cogs_to_neutral",
50
65
  target_tag=typeTag("junction"),
51
- timesteps=periodic(start=self.scramble_start, period=self.scramble_interval, end=max_steps),
66
+ timesteps=periodic(start=self.scramble_start, period=self.scramble_interval, end=scramble_end),
52
67
  # near a clips-aligned junction
53
68
  filters=[
54
69
  isNear(typeTag("junction"), [isAlignedTo("clips")], radius=self.scramble_radius),
@@ -61,7 +76,7 @@ class ClipsConfig(Config):
61
76
  "neutral_to_clips": EventConfig(
62
77
  name="neutral_to_clips",
63
78
  target_tag=typeTag("junction"),
64
- timesteps=periodic(start=self.align_start, period=self.align_interval, end=max_steps),
79
+ timesteps=periodic(start=self.align_start, period=self.align_interval, end=align_end),
65
80
  # neutral junctions near a clips-aligned junction
66
81
  filters=[
67
82
  isNear(typeTag("junction"), [isAlignedTo("clips")], radius=self.align_radius),
@@ -74,13 +89,15 @@ class ClipsConfig(Config):
74
89
  "presence_check": EventConfig(
75
90
  name="presence_check",
76
91
  target_tag=typeTag("junction"),
77
- timesteps=periodic(start=self.initial_clips_start, period=self.scramble_interval * 2, end=max_steps),
92
+ timesteps=periodic(start=self.initial_clips_start, period=self.scramble_interval * 2, end=presence_end),
78
93
  filters=[isNear(typeTag("junction"), [isAlignedTo("clips")], radius=1000)],
79
94
  max_targets=1,
80
95
  fallback="initial_clips",
81
96
  ),
82
97
  }
83
98
 
84
- def collective_config(self) -> CollectiveConfig:
85
- """Create a CollectiveConfig for this clips configuration."""
86
- return CollectiveConfig(name="clips")
99
+ def collectives(self) -> dict[str, CollectiveConfig]:
100
+ """Create collectives for clips."""
101
+ if self.disabled:
102
+ return {}
103
+ return {"clips": CollectiveConfig(name="clips")}
@@ -4,6 +4,7 @@ from pydantic import Field
4
4
 
5
5
  from cogames.cogs_vs_clips.config import CvCConfig
6
6
  from mettagrid.base_config import Config
7
+ from mettagrid.config.game_value import InventoryValue
7
8
  from mettagrid.config.game_value import stat as game_stat
8
9
  from mettagrid.config.handler_config import Handler
9
10
  from mettagrid.config.mettagrid_config import (
@@ -11,6 +12,8 @@ from mettagrid.config.mettagrid_config import (
11
12
  InventoryConfig,
12
13
  ResourceLimitsConfig,
13
14
  )
15
+ from mettagrid.config.mutation.game_value_mutation import SetGameValueMutation
16
+ from mettagrid.config.mutation.mutation import EntityTarget
14
17
  from mettagrid.config.mutation.resource_mutation import updateActor
15
18
  from mettagrid.config.reward_config import reward
16
19
 
@@ -35,12 +38,12 @@ class CogConfig(Config):
35
38
  # Initial inventory
36
39
  initial_energy: int = Field(default=100)
37
40
  initial_hp: int = Field(default=50)
41
+ initial_solar: int = Field(default=1)
38
42
 
39
43
  # Regen amounts
40
- energy_regen: int = Field(default=1)
41
44
  hp_regen: int = Field(default=-1)
42
45
  influence_regen: int = Field(default=-1)
43
- action_cost: dict[str, int] = Field(default_factory=lambda: {"energy": 3})
46
+ action_cost: dict[str, int] = Field(default_factory=lambda: {"energy": 4})
44
47
 
45
48
  def agent_config(self, team: str, max_steps: int) -> AgentConfig:
46
49
  """Create an AgentConfig for this cog configuration."""
@@ -62,20 +65,28 @@ class CogConfig(Config):
62
65
  min=self.influence_limit, resources=["influence"], modifiers=self.influence_modifiers
63
66
  ),
64
67
  },
65
- initial={"energy": self.initial_energy, "hp": self.initial_hp},
68
+ initial={"energy": self.initial_energy, "hp": self.initial_hp, "solar": self.initial_solar},
66
69
  ),
67
70
  on_tick={
68
71
  "regen": Handler(
69
72
  mutations=[
70
73
  updateActor(
71
74
  {
72
- "energy": self.energy_regen,
73
75
  "hp": self.hp_regen,
74
76
  "influence": self.influence_regen,
75
77
  }
76
78
  )
77
79
  ]
78
- )
80
+ ),
81
+ "solar_to_energy": Handler(
82
+ mutations=[
83
+ SetGameValueMutation(
84
+ value=InventoryValue(item="energy"),
85
+ source=InventoryValue(item="solar"),
86
+ target=EntityTarget.ACTOR,
87
+ )
88
+ ]
89
+ ),
79
90
  },
80
91
  rewards={
81
92
  "aligned_junction_held": reward(