cogames 0.3.65__py3-none-any.whl → 0.3.69__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 +201 -495
  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 +420 -84
  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.69.dist-info}/METADATA +45 -29
  124. cogames-0.3.69.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.69.dist-info}/WHEEL +0 -0
  132. {cogames-0.3.65.dist-info → cogames-0.3.69.dist-info}/entry_points.txt +0 -0
  133. {cogames-0.3.65.dist-info → cogames-0.3.69.dist-info}/licenses/LICENSE +0 -0
  134. {cogames-0.3.65.dist-info → cogames-0.3.69.dist-info}/top_level.txt +0 -0
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(
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from typing import Sequence
6
+
7
+ from cogames.cogs_vs_clips.mission import CvCMission
8
+ from cogames.cogs_vs_clips.reward_variants import AVAILABLE_REWARD_VARIANTS
9
+ from cogames.cogs_vs_clips.variants import HIDDEN_VARIANTS, VARIANTS
10
+ from cogames.core import CoGameMissionVariant
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class EventProfile:
15
+ name: str
16
+ clips_overrides: dict[str, object]
17
+ weather_overrides: dict[str, object]
18
+
19
+
20
+ COGSGUARD_FIXED_MAPS: list[str] = [
21
+ "machina_100_stations.map",
22
+ "machina_200_stations.map",
23
+ "cave_base_50.map",
24
+ "vanilla_large.map",
25
+ ]
26
+
27
+ DEFAULT_EVENT_PROFILE = EventProfile("events_baseline", {}, {})
28
+ COGSGUARD_EVENT_PROFILES: list[EventProfile] = [
29
+ DEFAULT_EVENT_PROFILE,
30
+ EventProfile(
31
+ "events_fast_clips_short_day",
32
+ {
33
+ "initial_clips_start": 5,
34
+ "initial_clips_spots": 2,
35
+ "scramble_start": 25,
36
+ "scramble_interval": 50,
37
+ "scramble_radius": 35,
38
+ "align_start": 50,
39
+ "align_interval": 50,
40
+ "align_radius": 35,
41
+ },
42
+ {"day_length": 100},
43
+ ),
44
+ EventProfile(
45
+ "events_slow_clips_long_day",
46
+ {
47
+ "initial_clips_start": 50,
48
+ "initial_clips_spots": 1,
49
+ "scramble_start": 200,
50
+ "scramble_interval": 200,
51
+ "scramble_radius": 15,
52
+ "align_start": 300,
53
+ "align_interval": 200,
54
+ "align_radius": 15,
55
+ },
56
+ {"day_length": 400},
57
+ ),
58
+ EventProfile(
59
+ "events_no_clips",
60
+ {"disabled": True},
61
+ {"day_length": 200},
62
+ ),
63
+ ]
64
+
65
+
66
+ def normalize_variant_names(variants: str | Sequence[str] | None) -> list[str]:
67
+ if variants is None:
68
+ return []
69
+ if isinstance(variants, str):
70
+ if variants.startswith("["):
71
+ parsed = json.loads(variants)
72
+ if isinstance(parsed, list):
73
+ return [str(name) for name in parsed]
74
+ return [variants]
75
+ return list(variants)
76
+
77
+
78
+ def split_variants(
79
+ variants: str | Sequence[str] | None,
80
+ ) -> tuple[list[CoGameMissionVariant], list[str]]:
81
+ if variants is None:
82
+ names: list[str] = []
83
+ else:
84
+ names = normalize_variant_names(variants)
85
+ all_variants = {variant.name: variant for variant in [*VARIANTS, *HIDDEN_VARIANTS]}
86
+ reward_variants = set(AVAILABLE_REWARD_VARIANTS)
87
+
88
+ resolved: list[CoGameMissionVariant] = []
89
+ resolved_rewards: list[str] = []
90
+ unknown: list[str] = []
91
+ for name in names:
92
+ if name in reward_variants:
93
+ resolved_rewards.append(name)
94
+ continue
95
+ variant = all_variants.get(name)
96
+ if variant is None:
97
+ unknown.append(name)
98
+ continue
99
+ resolved.append(variant)
100
+
101
+ if unknown:
102
+ available_mission = ", ".join(v.name for v in VARIANTS)
103
+ available_reward = ", ".join(AVAILABLE_REWARD_VARIANTS)
104
+ missing = ", ".join(unknown)
105
+ raise ValueError(
106
+ f"Unknown variant(s): {missing}. Mission variants: {available_mission}. "
107
+ f"Reward variants: {available_reward}."
108
+ )
109
+
110
+ return resolved, resolved_rewards
111
+
112
+
113
+ def resolve_event_profiles(event_profiles: Sequence[EventProfile] | None) -> list[EventProfile]:
114
+ if event_profiles is None:
115
+ return [DEFAULT_EVENT_PROFILE]
116
+ return list(event_profiles)
117
+
118
+
119
+ def filter_compatible_variants(
120
+ mission: CvCMission, variants: Sequence[CoGameMissionVariant]
121
+ ) -> list[CoGameMissionVariant]:
122
+ return [variant for variant in variants if variant.compat(mission)]
@@ -18,11 +18,11 @@ def make_cogsguard_tutorial_site() -> CoGameSite:
18
18
  hub_height=15,
19
19
  outer_clearance=2,
20
20
  stations=[
21
- "aligner_station",
22
- "scrambler_station",
23
- "miner_station",
24
- "scout_station",
25
- "chest",
21
+ "c:aligner",
22
+ "c:scrambler",
23
+ "c:miner",
24
+ "c:scout",
25
+ "c:chest",
26
26
  ],
27
27
  )
28
28
  map_builder = MapGen.Config(
@@ -32,7 +32,7 @@ CvCConfig = SimpleNamespace(
32
32
  "miner": "⛏️",
33
33
  "scout": "🔭",
34
34
  },
35
- RESOURCES=["energy", "heart", "hp", "influence", *_ELEMENTS, *_GEAR],
35
+ RESOURCES=["energy", "heart", "hp", "influence", "solar", *_ELEMENTS, *_GEAR],
36
36
  VIBES=_VIBES,
37
37
  VIBE_NAMES=[vibe.name for vibe in _VIBES],
38
38
  )
@@ -341,11 +341,10 @@ Included missions and variants:
341
341
  Usage example:
342
342
 
343
343
  ```bash
344
- uv run python packages/cogames/scripts/run_evaluation.py \
345
- --policy thinky \
344
+ uv run cogames diagnose thinky \
346
345
  --mission-set integrated_evals \
347
346
  --cogs 4 \
348
- --repeats 2
347
+ --episodes 2
349
348
  ```
350
349
 
351
350
  Recommendation: When designing new scorable baselines, combine one "shaping" variant (e.g., `HeartChorusVariant`,
@@ -193,49 +193,25 @@ uv run cogames play --mission hello_world.single_use_swarm --cogs 4 -p baseline
193
193
 
194
194
  ## Programmatic Evaluation
195
195
 
196
- ### Using run_evaluation.py
196
+ ### Using cogames diagnose / cogames run
197
197
 
198
198
  For systematic evaluation across multiple missions and configurations:
199
199
 
200
200
  ```bash
201
201
  # Evaluate on integrated eval suite
202
- uv run python packages/cogames/scripts/run_evaluation.py \
203
- --policy thinky \
202
+ uv run cogames diagnose thinky \
204
203
  --mission-set integrated_evals \
205
204
  --cogs 4 \
206
- --repeats 2
205
+ --episodes 2
207
206
 
208
- # Evaluate specific agent
209
- uv run python packages/cogames/scripts/run_evaluation.py \
207
+ # Evaluate specific agent with structured output
208
+ uv run cogames run \
209
+ --mission-set integrated_evals \
210
210
  --policy baseline \
211
- --steps 1000 \
212
- --output eval_baseline.json
211
+ --episodes 10 \
212
+ --format json
213
213
  ```
214
214
 
215
- ### Using in Curriculum Training
216
-
217
- Both diagnostic and integrated missions can be used in curriculum training via `mission_variant_curriculum.py`:
218
-
219
- ```python
220
- from recipes.experiment.cvc import mission_variant_curriculum
221
-
222
- # Train on diagnostic missions
223
- mission_variant_curriculum.train(
224
- base_missions=["diagnostic_missions"],
225
- num_cogs=4,
226
- variants="all"
227
- )
228
-
229
- # Train on specific integrated missions
230
- mission_variant_curriculum.train(
231
- base_missions=["oxygen_bottleneck", "energy_starved"],
232
- num_cogs=4,
233
- variants=["pack_rat", "energized"]
234
- )
235
- ```
236
-
237
- ---
238
-
239
215
  ## Design Philosophy
240
216
 
241
217
  ### Diagnostic Missions
@@ -26,7 +26,6 @@ _GENEROUS_COG = CogConfig(
26
26
  cargo_limit=255,
27
27
  initial_energy=255,
28
28
  initial_hp=100,
29
- energy_regen=255,
30
29
  hp_regen=0,
31
30
  influence_regen=0,
32
31
  )
@@ -37,11 +37,8 @@ class DifficultyLevel(CoGameMissionVariant):
37
37
  name: str = Field(description="Difficulty name (easy, medium, hard, brutal, etc.)")
38
38
  description: str = Field(description="What makes this difficulty challenging", default="")
39
39
 
40
- # Energy regen multiplier (relative to mission baseline)
41
- energy_regen_mult: float = Field(default=1.0)
42
-
43
- # Absolute overrides (if set, ignore multipliers)
44
- energy_regen_override: int | None = Field(default=None)
40
+ # Solar override (if set, overrides weather day/night deltas)
41
+ solar_override: int | None = Field(default=None)
45
42
  move_energy_cost_override: int | None = Field(default=None)
46
43
  energy_capacity_override: int | None = Field(default=None)
47
44
  cargo_capacity_override: int | None = Field(default=None)
@@ -70,20 +67,20 @@ STANDARD = DifficultyLevel(
70
67
  HARD = DifficultyLevel(
71
68
  name="hard",
72
69
  description="Minimal passive regen and higher move cost",
73
- energy_regen_override=1, # Minimal regen prevents deadlock
70
+ solar_override=1, # Minimal regen prevents deadlock
74
71
  move_energy_cost_override=2,
75
72
  )
76
73
 
77
74
  SINGLE_USE = DifficultyLevel(
78
75
  name="single_use",
79
76
  description="Minimal regen - no second chances",
80
- energy_regen_override=1,
77
+ solar_override=1,
81
78
  )
82
79
 
83
80
  SPEED_RUN = DifficultyLevel(
84
81
  name="speed_run",
85
82
  description="Short clock, cheap movement",
86
- energy_regen_override=2,
83
+ solar_override=2,
87
84
  move_energy_cost_override=1,
88
85
  max_steps_override=600,
89
86
  )
@@ -91,7 +88,7 @@ SPEED_RUN = DifficultyLevel(
91
88
  ENERGY_CRISIS = DifficultyLevel(
92
89
  name="energy_crisis",
93
90
  description="Minimal passive regen - plan every move",
94
- energy_regen_override=1, # Minimal regen prevents deadlock
91
+ solar_override=1, # Minimal regen prevents deadlock
95
92
  )
96
93
 
97
94
  # Export variants for use with --variant CLI flag.
@@ -116,7 +113,7 @@ def list_difficulties() -> None:
116
113
  print("=" * 80)
117
114
  for diff in DIFFICULTY_VARIANTS:
118
115
  print(f"\n{diff.name.upper()}: {diff.description}")
119
- print(f" Energy regen mult: {diff.energy_regen_mult}")
116
+ print(f" Solar override: {diff.solar_override}")
120
117
 
121
118
 
122
119
  if __name__ == "__main__":
@@ -15,6 +15,7 @@ from cogames.cogs_vs_clips.stations import (
15
15
  )
16
16
  from cogames.cogs_vs_clips.team import CogTeam
17
17
  from cogames.cogs_vs_clips.variants import NumCogsVariant
18
+ from cogames.cogs_vs_clips.weather import WeatherConfig
18
19
  from cogames.core import (
19
20
  MAP_MISSION_DELIMITER,
20
21
  CoGameMission,
@@ -28,7 +29,7 @@ from mettagrid.config.action_config import (
28
29
  NoopActionConfig,
29
30
  )
30
31
  from mettagrid.config.game_value import inv
31
- from mettagrid.config.mettagrid_config import GameConfig, MettaGridConfig
32
+ from mettagrid.config.mettagrid_config import CollectiveConfig, GameConfig, MettaGridConfig
32
33
  from mettagrid.config.obs_config import GlobalObsConfig, ObsConfig
33
34
  from mettagrid.map_builder.map_builder import AnyMapBuilderConfig
34
35
 
@@ -46,6 +47,7 @@ class CvCMission(CoGameMission):
46
47
  """Mission configuration for CogsGuard game mode."""
47
48
 
48
49
  max_steps: int = Field(default=10000)
50
+ total_junctions: int = Field(default=118, description="Total junctions on the map (for curriculum scaling)")
49
51
 
50
52
  cog: CogConfig = Field(default_factory=lambda: CogConfig())
51
53
  teams: dict[str, CogTeam] = Field(
@@ -55,6 +57,7 @@ class CvCMission(CoGameMission):
55
57
  )
56
58
 
57
59
  clips: ClipsConfig = Field(default_factory=lambda: ClipsConfig())
60
+ weather: WeatherConfig = Field(default_factory=lambda: WeatherConfig())
58
61
 
59
62
  @property
60
63
  def num_agents(self) -> int:
@@ -74,7 +77,7 @@ class CvCMission(CoGameMission):
74
77
  Returns:
75
78
  MettaGridConfig ready for environment creation
76
79
  """
77
-
80
+ team_objs = list(self.teams.values())
78
81
  game = GameConfig(
79
82
  map_builder=self.map_builder(),
80
83
  max_steps=self.max_steps,
@@ -91,24 +94,40 @@ class CvCMission(CoGameMission):
91
94
  noop=NoopActionConfig(),
92
95
  change_vibe=ChangeVibeActionConfig(vibes=CvCConfig.VIBES),
93
96
  ),
94
- agent=self.cog.agent_config(team="cogs", max_steps=self.max_steps),
95
- agents=[self.cog.agent_config(team="cogs", max_steps=self.max_steps) for _ in range(self.num_agents)],
97
+ agents=[
98
+ self.cog.agent_config(team=t.name, max_steps=self.max_steps)
99
+ for t in team_objs
100
+ for _ in range(t.num_agents)
101
+ ],
96
102
  objects={
97
103
  "wall": CvCWallConfig().station_cfg(),
98
- "hub": CvCHubConfig().station_cfg(team="cogs"),
99
104
  "junction": CvCJunctionConfig().station_cfg(),
100
- "chest": CvCChestConfig().station_cfg(team="cogs"),
101
105
  **{
102
106
  f"{resource}_extractor": CvCExtractorConfig(resource=resource).station_cfg()
103
107
  for resource in CvCConfig.ELEMENTS
104
108
  },
105
- **{f"{g}_station": CvCGearStationConfig(gear_type=g).station_cfg(team="cogs") for g in CvCConfig.GEAR},
109
+ **{
110
+ f"{t.short_name}:hub": CvCHubConfig().station_cfg(team=t.short_name, collective=t.name)
111
+ for t in team_objs
112
+ },
113
+ **{
114
+ f"{t.short_name}:chest": CvCChestConfig().station_cfg(team=t.short_name, collective=t.name)
115
+ for t in team_objs
116
+ },
117
+ **{
118
+ f"{t.short_name}:{g}": CvCGearStationConfig(gear_type=g).station_cfg(
119
+ team=t.short_name, collective=t.name
120
+ )
121
+ for t in team_objs
122
+ for g in CvCConfig.GEAR
123
+ },
106
124
  },
107
125
  collectives={
108
- **{team.name: team.collective_config() for team in self.teams.values()},
109
- "clips": self.clips.collective_config(),
126
+ **{t.name: t.collective_config() for t in team_objs},
127
+ **self.clips.collectives(),
128
+ "neutral": CollectiveConfig(name="neutral"),
110
129
  },
111
- events=self.clips.events(max_steps=self.max_steps),
130
+ events=self._merge_events(),
112
131
  )
113
132
 
114
133
  env = MettaGridConfig(game=game)
@@ -122,3 +141,12 @@ class CvCMission(CoGameMission):
122
141
  env.label += f".{variant.name}"
123
142
 
124
143
  return env
144
+
145
+ def _merge_events(self) -> dict:
146
+ """Merge clips and weather events, raising on key conflicts."""
147
+ clips_events = self.clips.events(max_steps=self.max_steps)
148
+ weather_events = self.weather.events(max_steps=self.max_steps)
149
+ overlap = set(clips_events) & set(weather_events)
150
+ if overlap:
151
+ raise ValueError(f"Overlapping event keys between clips and weather: {overlap}")
152
+ return {**clips_events, **weather_events}
@@ -50,7 +50,7 @@ def get_core_missions() -> list[CvCMission]:
50
50
 
51
51
 
52
52
  def _build_eval_missions() -> list[CvCMission]:
53
- from cogames.cogs_vs_clips.evals.integrated_evals import EVAL_MISSIONS as INTEGRATED_EVAL_MISSIONS
53
+ from cogames.cogs_vs_clips.evals.integrated_evals import EVAL_MISSIONS as INTEGRATED_EVAL_MISSIONS # noqa: PLC0415
54
54
 
55
55
  return [
56
56
  *INTEGRATED_EVAL_MISSIONS,