cogames 0.3.64__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 (141) hide show
  1. cogames/cli/client.py +0 -3
  2. cogames/cli/docsync/docsync.py +7 -1
  3. cogames/cli/mission.py +68 -53
  4. cogames/cli/policy.py +26 -10
  5. cogames/cli/submit.py +128 -142
  6. cogames/cli/utils.py +5 -0
  7. cogames/cogs_vs_clips/clip_difficulty.py +57 -0
  8. cogames/cogs_vs_clips/clips.py +103 -0
  9. cogames/cogs_vs_clips/cog.py +29 -11
  10. cogames/cogs_vs_clips/cogsguard_curriculum.py +122 -0
  11. cogames/cogs_vs_clips/cogsguard_tutorial.py +15 -16
  12. cogames/cogs_vs_clips/config.py +38 -0
  13. cogames/cogs_vs_clips/{cogs_vs_clips_mapgen.md → docs/cogs_vs_clips_mapgen.md} +8 -10
  14. cogames/cogs_vs_clips/evals/README.md +11 -35
  15. cogames/cogs_vs_clips/evals/cogsguard_evals.py +21 -6
  16. cogames/cogs_vs_clips/evals/diagnostic_evals.py +13 -101
  17. cogames/cogs_vs_clips/evals/difficulty_variants.py +16 -28
  18. cogames/cogs_vs_clips/evals/integrated_evals.py +8 -60
  19. cogames/cogs_vs_clips/evals/spanning_evals.py +48 -54
  20. cogames/cogs_vs_clips/mission.py +93 -277
  21. cogames/cogs_vs_clips/missions.py +17 -27
  22. cogames/cogs_vs_clips/{cogsguard_reward_variants.py → reward_variants.py} +22 -2
  23. cogames/cogs_vs_clips/sites.py +41 -30
  24. cogames/cogs_vs_clips/stations.py +39 -84
  25. cogames/cogs_vs_clips/team.py +46 -0
  26. cogames/cogs_vs_clips/{procedural.py → terrain.py} +14 -8
  27. cogames/cogs_vs_clips/variants.py +201 -107
  28. cogames/cogs_vs_clips/weather.py +52 -0
  29. cogames/core.py +87 -0
  30. cogames/docs/SCRIPTED_AGENT.md +3 -3
  31. cogames/evaluate.py +4 -2
  32. cogames/main.py +357 -51
  33. cogames/maps/canidate1_1000.map +1 -1
  34. cogames/maps/canidate1_1000_stations.map +2 -2
  35. cogames/maps/canidate1_500.map +1 -1
  36. cogames/maps/canidate1_500_stations.map +2 -2
  37. cogames/maps/canidate2_1000.map +1 -1
  38. cogames/maps/canidate2_1000_stations.map +2 -2
  39. cogames/maps/canidate2_500.map +1 -1
  40. cogames/maps/canidate2_500_stations.map +1 -1
  41. cogames/maps/canidate3_1000.map +1 -1
  42. cogames/maps/canidate3_1000_stations.map +2 -2
  43. cogames/maps/canidate3_500.map +1 -1
  44. cogames/maps/canidate3_500_stations.map +2 -2
  45. cogames/maps/canidate4_500.map +1 -1
  46. cogames/maps/canidate4_500_stations.map +2 -2
  47. cogames/maps/cave_base_50.map +2 -2
  48. cogames/maps/diagnostic_evals/diagnostic_agile.map +2 -2
  49. cogames/maps/diagnostic_evals/diagnostic_agile_hard.map +2 -2
  50. cogames/maps/diagnostic_evals/diagnostic_charge_up.map +6 -6
  51. cogames/maps/diagnostic_evals/diagnostic_charge_up_hard.map +6 -6
  52. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1.map +6 -6
  53. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1_hard.map +6 -6
  54. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2.map +6 -6
  55. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2_hard.map +6 -6
  56. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3.map +6 -6
  57. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3_hard.map +6 -6
  58. cogames/maps/diagnostic_evals/diagnostic_chest_near.map +6 -6
  59. cogames/maps/diagnostic_evals/diagnostic_chest_search.map +6 -6
  60. cogames/maps/diagnostic_evals/diagnostic_chest_search_hard.map +6 -6
  61. cogames/maps/diagnostic_evals/diagnostic_extract_lab.map +6 -6
  62. cogames/maps/diagnostic_evals/diagnostic_extract_lab_hard.map +6 -6
  63. cogames/maps/diagnostic_evals/diagnostic_memory.map +6 -6
  64. cogames/maps/diagnostic_evals/diagnostic_memory_hard.map +6 -6
  65. cogames/maps/diagnostic_evals/diagnostic_radial.map +2 -2
  66. cogames/maps/diagnostic_evals/diagnostic_radial_hard.map +2 -2
  67. cogames/maps/diagnostic_evals/diagnostic_resource_lab.map +6 -6
  68. cogames/maps/diagnostic_evals/diagnostic_unclip.map +6 -6
  69. cogames/maps/evals/eval_balanced_spread.map +6 -6
  70. cogames/maps/evals/eval_clip_oxygen.map +6 -6
  71. cogames/maps/evals/eval_collect_resources.map +6 -6
  72. cogames/maps/evals/eval_collect_resources_hard.map +6 -6
  73. cogames/maps/evals/eval_collect_resources_medium.map +6 -6
  74. cogames/maps/evals/eval_divide_and_conquer.map +6 -6
  75. cogames/maps/evals/eval_energy_starved.map +6 -6
  76. cogames/maps/evals/eval_multi_coordinated_collect_hard.map +6 -6
  77. cogames/maps/evals/eval_oxygen_bottleneck.map +6 -6
  78. cogames/maps/evals/eval_single_use_world.map +6 -6
  79. cogames/maps/evals/extractor_hub_100x100.map +6 -6
  80. cogames/maps/evals/extractor_hub_30x30.map +6 -6
  81. cogames/maps/evals/extractor_hub_50x50.map +6 -6
  82. cogames/maps/evals/extractor_hub_70x70.map +6 -6
  83. cogames/maps/evals/extractor_hub_80x80.map +6 -6
  84. cogames/maps/machina_100_stations.map +2 -2
  85. cogames/maps/machina_200_stations.map +2 -2
  86. cogames/maps/machina_200_stations_small.map +2 -2
  87. cogames/maps/machina_eval_exp01.map +2 -2
  88. cogames/maps/machina_eval_template_large.map +2 -2
  89. cogames/maps/machinatrainer4agents.map +2 -2
  90. cogames/maps/machinatrainer4agentsbase.map +2 -2
  91. cogames/maps/machinatrainerbig.map +2 -2
  92. cogames/maps/machinatrainersmall.map +2 -2
  93. cogames/maps/planky_evals/aligner_avoid_aoe.map +6 -6
  94. cogames/maps/planky_evals/aligner_full_cycle.map +6 -6
  95. cogames/maps/planky_evals/aligner_gear.map +6 -6
  96. cogames/maps/planky_evals/aligner_hearts.map +6 -6
  97. cogames/maps/planky_evals/aligner_junction.map +6 -6
  98. cogames/maps/planky_evals/exploration_distant.map +6 -6
  99. cogames/maps/planky_evals/maze.map +6 -6
  100. cogames/maps/planky_evals/miner_best_resource.map +6 -6
  101. cogames/maps/planky_evals/miner_deposit.map +6 -6
  102. cogames/maps/planky_evals/miner_extract.map +6 -6
  103. cogames/maps/planky_evals/miner_full_cycle.map +6 -6
  104. cogames/maps/planky_evals/miner_gear.map +6 -6
  105. cogames/maps/planky_evals/multi_role.map +6 -6
  106. cogames/maps/planky_evals/resource_chain.map +6 -6
  107. cogames/maps/planky_evals/scout_explore.map +6 -6
  108. cogames/maps/planky_evals/scout_gear.map +6 -6
  109. cogames/maps/planky_evals/scrambler_full_cycle.map +6 -6
  110. cogames/maps/planky_evals/scrambler_gear.map +6 -6
  111. cogames/maps/planky_evals/scrambler_target.map +6 -6
  112. cogames/maps/planky_evals/stuck_corridor.map +6 -6
  113. cogames/maps/planky_evals/survive_retreat.map +6 -6
  114. cogames/maps/training_facility_clipped.map +2 -2
  115. cogames/maps/training_facility_open_1.map +2 -2
  116. cogames/maps/training_facility_open_2.map +2 -2
  117. cogames/maps/training_facility_open_3.map +2 -2
  118. cogames/maps/training_facility_tight_4.map +2 -2
  119. cogames/maps/training_facility_tight_5.map +2 -2
  120. cogames/maps/vanilla_large.map +2 -2
  121. cogames/maps/vanilla_small.map +2 -2
  122. cogames/pickup.py +6 -5
  123. cogames/play.py +14 -16
  124. cogames/policy/nim_agents/__init__.py +0 -2
  125. cogames/policy/nim_agents/agents.py +0 -11
  126. cogames/policy/starter_agent.py +4 -1
  127. cogames/verbose.py +2 -2
  128. {cogames-0.3.64.dist-info → cogames-0.3.68.dist-info}/METADATA +45 -29
  129. cogames-0.3.68.dist-info/RECORD +160 -0
  130. metta_alo/scoring.py +7 -7
  131. cogames/cogs_vs_clips/mission_utils.py +0 -19
  132. cogames/cogs_vs_clips/tutorial_missions.py +0 -25
  133. cogames-0.3.64.dist-info/RECORD +0 -159
  134. metta_alo/job_specs.py +0 -17
  135. metta_alo/policy.py +0 -16
  136. metta_alo/pure_single_episode_runner.py +0 -75
  137. metta_alo/rollout.py +0 -322
  138. {cogames-0.3.64.dist-info → cogames-0.3.68.dist-info}/WHEEL +0 -0
  139. {cogames-0.3.64.dist-info → cogames-0.3.68.dist-info}/entry_points.txt +0 -0
  140. {cogames-0.3.64.dist-info → cogames-0.3.68.dist-info}/licenses/LICENSE +0 -0
  141. {cogames-0.3.64.dist-info → cogames-0.3.68.dist-info}/top_level.txt +0 -0
@@ -1,95 +1,189 @@
1
- from typing import override
1
+ from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING, override
4
+
5
+ from pydantic import Field
6
+
7
+ from cogames.cogs_vs_clips.config import CvCConfig
3
8
  from cogames.cogs_vs_clips.evals.difficulty_variants import DIFFICULTY_VARIANTS
4
- from cogames.cogs_vs_clips.mission import Mission, MissionVariant
5
- from cogames.cogs_vs_clips.procedural import BaseHubVariant, MachinaArenaVariant
6
- from mettagrid.config.action_config import VibeTransfer
7
- from mettagrid.config.game_value import stat
8
- from mettagrid.config.reward_config import reward
9
+ from cogames.cogs_vs_clips.terrain import BaseHubVariant, MachinaArenaVariant
10
+ from cogames.core import CoGameMissionVariant
11
+ from mettagrid.config.mettagrid_config import MettaGridConfig
9
12
  from mettagrid.map_builder.map_builder import MapBuilderConfig
10
13
  from mettagrid.mapgen.mapgen import MapGen
11
14
  from mettagrid.mapgen.scenes.base_hub import DEFAULT_EXTRACTORS as HUB_EXTRACTORS
12
15
  from mettagrid.mapgen.scenes.building_distributions import DistributionConfig, DistributionType
13
16
 
17
+ if TYPE_CHECKING:
18
+ from cogames.cogs_vs_clips.mission import CvCMission
19
+
20
+
21
+ def _apply_clips_settings(
22
+ mission: CvCMission,
23
+ *,
24
+ initial_clips_start: int | None = None,
25
+ initial_clips_spots: int | None = None,
26
+ scramble_start: int | None = None,
27
+ scramble_interval: int | None = None,
28
+ scramble_radius: int | None = None,
29
+ align_start: int | None = None,
30
+ align_interval: int | None = None,
31
+ align_radius: int | None = None,
32
+ ) -> None:
33
+ clips = mission.clips
34
+ if initial_clips_start is not None:
35
+ clips.initial_clips_start = initial_clips_start
36
+ if initial_clips_spots is not None:
37
+ clips.initial_clips_spots = initial_clips_spots
38
+ if scramble_start is not None:
39
+ clips.scramble_start = scramble_start
40
+ if scramble_interval is not None:
41
+ clips.scramble_interval = scramble_interval
42
+ if scramble_radius is not None:
43
+ clips.scramble_radius = scramble_radius
44
+ if align_start is not None:
45
+ clips.align_start = align_start
46
+ if align_interval is not None:
47
+ clips.align_interval = align_interval
48
+ if align_radius is not None:
49
+ clips.align_radius = align_radius
50
+
51
+
52
+ class NumCogsVariant(CoGameMissionVariant):
53
+ name: str = "num_cogs"
54
+ description: str = "Set the number of cogs for the mission."
55
+ num_cogs: int
14
56
 
15
- class DarkSideVariant(MissionVariant):
16
- name: str = "dark_side"
17
- description: str = "You're on the dark side of the asteroid. You recharge slower."
57
+ @override
58
+ def modify_mission(self, mission: CvCMission) -> None:
59
+ if self.num_cogs < mission.site.min_cogs or self.num_cogs > mission.site.max_cogs:
60
+ raise ValueError(
61
+ f"Invalid number of cogs for {mission.site.name}: {self.num_cogs}. "
62
+ + f"Must be between {mission.site.min_cogs} and {mission.site.max_cogs}"
63
+ )
64
+
65
+ mission.num_cogs = self.num_cogs
66
+
67
+
68
+ class ClipsEasyVariant(CoGameMissionVariant):
69
+ name: str = "clips_easy"
70
+ description: str = "Slow clips expansion with late pressure."
18
71
 
19
72
  @override
20
- def modify_mission(self, mission):
21
- assert isinstance(mission, Mission)
22
- mission.cog.energy_regen = 0
73
+ def modify_mission(self, mission: CvCMission) -> None:
74
+ _apply_clips_settings(
75
+ mission,
76
+ initial_clips_start=50,
77
+ initial_clips_spots=1,
78
+ scramble_start=250,
79
+ scramble_interval=250,
80
+ scramble_radius=15,
81
+ align_start=300,
82
+ align_interval=250,
83
+ align_radius=15,
84
+ )
23
85
 
24
86
 
25
- class SuperChargedVariant(MissionVariant):
26
- name: str = "super_charged"
27
- description: str = "The sun is shining on you. You recharge faster."
87
+ class ClipsMediumVariant(CoGameMissionVariant):
88
+ name: str = "clips_medium"
89
+ description: str = "Baseline clips pressure (Machina1 default)."
28
90
 
29
91
  @override
30
- def modify_mission(self, mission):
31
- assert isinstance(mission, Mission)
32
- mission.cog.energy_regen += 2
92
+ def modify_mission(self, mission: CvCMission) -> None:
93
+ _apply_clips_settings(
94
+ mission,
95
+ initial_clips_start=10,
96
+ initial_clips_spots=1,
97
+ scramble_start=50,
98
+ scramble_interval=100,
99
+ scramble_radius=25,
100
+ align_start=100,
101
+ align_interval=100,
102
+ align_radius=25,
103
+ )
33
104
 
34
105
 
35
- class RoughTerrainVariant(MissionVariant):
36
- name: str = "rough_terrain"
37
- description: str = "The terrain is rough. Moving is more energy intensive."
106
+ class ClipsHardVariant(CoGameMissionVariant):
107
+ name: str = "clips_hard"
108
+ description: str = "Fast clips pressure with wider influence."
38
109
 
39
110
  @override
40
- def modify_mission(self, mission):
41
- assert isinstance(mission, Mission)
42
- mission.cog.move_energy_cost += 2
111
+ def modify_mission(self, mission: CvCMission) -> None:
112
+ _apply_clips_settings(
113
+ mission,
114
+ initial_clips_start=5,
115
+ initial_clips_spots=2,
116
+ scramble_start=25,
117
+ scramble_interval=50,
118
+ scramble_radius=35,
119
+ align_start=50,
120
+ align_interval=50,
121
+ align_radius=35,
122
+ )
43
123
 
44
124
 
45
- class PackRatVariant(MissionVariant):
46
- name: str = "pack_rat"
47
- description: str = "Raise heart, cargo, energy, and gear caps to 255."
125
+ class ClipsWaveOnlyVariant(CoGameMissionVariant):
126
+ name: str = "clips_wave_only"
127
+ description: str = "Initial clips wave only, no further spread."
48
128
 
49
129
  @override
50
- def modify_mission(self, mission):
51
- assert isinstance(mission, Mission)
52
- mission.cog.heart_limit = max(mission.cog.heart_limit, 255)
53
- mission.cog.energy_limit = max(mission.cog.energy_limit, 255)
54
- mission.cog.cargo_limit = max(mission.cog.cargo_limit, 255)
55
- mission.cog.gear_limit = max(mission.cog.gear_limit, 255)
130
+ def modify_mission(self, mission: CvCMission) -> None:
131
+ disable_start = mission.max_steps + 1
132
+ _apply_clips_settings(
133
+ mission,
134
+ initial_clips_start=10,
135
+ initial_clips_spots=3,
136
+ scramble_start=disable_start,
137
+ scramble_interval=disable_start,
138
+ align_start=disable_start,
139
+ align_interval=disable_start,
140
+ scramble_radius=25,
141
+ align_radius=25,
142
+ )
56
143
 
57
144
 
58
- class EnergizedVariant(MissionVariant):
59
- name: str = "energized"
60
- description: str = "Max energy and full regen so agents never run dry."
145
+ class DarkSideVariant(CoGameMissionVariant):
146
+ name: str = "dark_side"
147
+ description: str = "You're on the dark side of the asteroid. You recharge slower."
61
148
 
62
149
  @override
63
- def modify_mission(self, mission):
64
- assert isinstance(mission, Mission)
65
- mission.cog.energy_limit = max(mission.cog.energy_limit, 255)
66
- mission.cog.energy_regen = mission.cog.energy_limit
150
+ def modify_mission(self, mission: CvCMission) -> None:
151
+ mission.weather.day_deltas = {"solar": 0}
152
+ mission.weather.night_deltas = {"solar": 0}
153
+
154
+
155
+ class SuperChargedVariant(CoGameMissionVariant):
156
+ name: str = "super_charged"
157
+ description: str = "The sun is shining on you. You recharge faster."
158
+
159
+ @override
160
+ def modify_mission(self, mission: CvCMission) -> None:
161
+ mission.weather.day_deltas = {k: v + 2 for k, v in mission.weather.day_deltas.items()}
162
+ mission.weather.night_deltas = {k: v + 2 for k, v in mission.weather.night_deltas.items()}
67
163
 
68
164
 
69
- class CompassVariant(MissionVariant):
70
- name: str = "compass"
71
- description: str = "Enable compass observation."
165
+ class EnergizedVariant(CoGameMissionVariant):
166
+ name: str = "energized"
167
+ description: str = "Max energy and full regen so agents never run dry."
72
168
 
73
169
  @override
74
- def modify_env(self, mission, env):
75
- env.game.obs.global_obs.compass = True
170
+ def modify_mission(self, mission: CvCMission) -> None:
171
+ mission.cog.energy_limit = max(mission.cog.energy_limit, 255)
172
+ mission.weather.day_deltas = {"solar": 255}
173
+ mission.weather.night_deltas = {"solar": 255}
76
174
 
77
175
 
78
- class Small50Variant(MissionVariant):
176
+ class Small50Variant(CoGameMissionVariant):
79
177
  name: str = "small_50"
80
178
  description: str = "Set map size to 50x50 for quick runs."
81
179
 
82
180
  def modify_env(self, mission, env) -> None:
83
181
  map_builder = env.game.map_builder
84
- # Only set width/height if instance is a SceneConfig, not a MapBuilderConfig
85
- # When instance is a MapBuilderConfig, width and height must be None
86
182
  if isinstance(map_builder, MapGen.Config) and isinstance(map_builder.instance, MapBuilderConfig):
87
- # Skip setting width/height for MapBuilderConfig instances
88
183
  return
89
184
  env.game.map_builder = map_builder.model_copy(update={"width": 50, "height": 50})
90
185
 
91
186
 
92
- # Biome variants (weather) for procedural maps
93
187
  class DesertVariant(MachinaArenaVariant):
94
188
  name: str = "desert"
95
189
  description: str = "The desert sands make navigation challenging."
@@ -117,7 +211,6 @@ class CityVariant(MachinaArenaVariant):
117
211
  def modify_node(self, node):
118
212
  node.biome_weights = {"city": 1.0, "caves": 0.0, "desert": 0.0, "forest": 0.0}
119
213
  node.base_biome = "city"
120
- # Fill almost the entire map with the city layer
121
214
  node.density_scale = 1.0
122
215
  node.biome_count = 1
123
216
  node.max_biome_zone_fraction = 0.95
@@ -140,32 +233,29 @@ class DistantResourcesVariant(MachinaArenaVariant):
140
233
 
141
234
  @override
142
235
  def modify_node(self, node):
143
- # Bias buildings toward the map edges using bimodal clusters centered at
144
236
  node.building_coverage = 0.01
145
237
 
146
238
  vertical_edges = DistributionConfig(
147
239
  type=DistributionType.BIMODAL,
148
- center1_x=0.92, # top right corner
240
+ center1_x=0.92,
149
241
  center1_y=0.08,
150
- center2_x=0.08, # bottom left corner
242
+ center2_x=0.08,
151
243
  center2_y=0.92,
152
244
  cluster_std=0.18,
153
245
  )
154
246
  horizontal_edges = DistributionConfig(
155
247
  type=DistributionType.BIMODAL,
156
- center1_x=0.08, # top left corner
248
+ center1_x=0.08,
157
249
  center1_y=0.08,
158
- center2_x=0.92, # bottom right corner
250
+ center2_x=0.92,
159
251
  center2_y=0.92,
160
252
  cluster_std=0.18,
161
253
  )
162
254
 
163
- # Apply edge-biased distributions to extractors; other buildings follow the global distribution
164
255
  names = list(self.building_names)
165
256
  node.building_distributions = {
166
257
  name: (vertical_edges if i % 2 == 0 else horizontal_edges) for i, name in enumerate(names)
167
258
  }
168
- # Fallback for any unspecified building types
169
259
  node.distribution = DistributionConfig(type=DistributionType.UNIFORM)
170
260
 
171
261
 
@@ -180,10 +270,10 @@ class QuadrantBuildingsVariant(MachinaArenaVariant):
180
270
 
181
271
  names = list(node.building_names or self.building_names)
182
272
  centers = [
183
- (0.25, 0.25), # top-left
184
- (0.75, 0.25), # top-right
185
- (0.25, 0.75), # bottom-left
186
- (0.75, 0.75), # bottom-right
273
+ (0.25, 0.25),
274
+ (0.75, 0.25),
275
+ (0.25, 0.75),
276
+ (0.75, 0.75),
187
277
  ]
188
278
  dists: dict[str, DistributionConfig] = {}
189
279
  for i, name in enumerate(names):
@@ -206,8 +296,6 @@ class SingleResourceUniformVariant(MachinaArenaVariant):
206
296
 
207
297
  @override
208
298
  def modify_node(self, node):
209
- # Resolve resource to a concrete building name
210
- # Restrict building set to only the chosen building and enforce uniform distribution
211
299
  node.building_names = [self.building_name]
212
300
  node.building_weights = {self.building_name: 1.0}
213
301
  node.building_distributions = None
@@ -217,12 +305,10 @@ class SingleResourceUniformVariant(MachinaArenaVariant):
217
305
  class EmptyBaseVariant(BaseHubVariant):
218
306
  name: str = "empty_base"
219
307
  description: str = "Base hub with extractors removed from the four corners."
220
- # Extractor object names to remove, e.g., ["oxygen_extractor"]
221
308
  missing: list[str] = list(HUB_EXTRACTORS)
222
309
 
223
310
  @override
224
311
  def modify_node(self, node):
225
- # Use the default extractor order and blank out any that are missing
226
312
  missing_set = set(self.missing or [])
227
313
  corner_objects = [name if name not in missing_set else "" for name in HUB_EXTRACTORS]
228
314
  node.corner_objects = corner_objects
@@ -230,8 +316,6 @@ class EmptyBaseVariant(BaseHubVariant):
230
316
 
231
317
 
232
318
  class BalancedCornersVariant(MachinaArenaVariant):
233
- """Enable corner balancing to ensure fair spawn distances."""
234
-
235
319
  name: str = "balanced_corners"
236
320
  description: str = "Balance path distances from center to corners for fair spawns."
237
321
  balance_tolerance: float = 1.5
@@ -244,65 +328,75 @@ class BalancedCornersVariant(MachinaArenaVariant):
244
328
  node.max_balance_shortcuts = self.max_balance_shortcuts
245
329
 
246
330
 
247
- class TraderVariant(MissionVariant):
248
- name: str = "trader"
249
- description: str = "Agents can trade resources with each other."
331
+ class MultiTeamVariant(CoGameMissionVariant):
332
+ """Split the map into multiple team instances, each with their own hub and resources."""
333
+
334
+ name: str = "multi_team"
335
+ description: str = "Split map into separate team instances with independent hubs."
336
+ num_teams: int = Field(default=2, ge=2, le=2, description="Number of teams (max 2 supported)")
250
337
 
251
338
  @override
252
- def modify_env(self, mission, env):
253
- # Define vibe transfers for trading resources (actor gives, target receives)
254
- trade_transfers = [
255
- VibeTransfer(vibe="carbon_a", target={"carbon": 1}, actor={"carbon": -1}),
256
- VibeTransfer(vibe="carbon_b", target={"carbon": 10}, actor={"carbon": -10}),
257
- VibeTransfer(vibe="oxygen_a", target={"oxygen": 1}, actor={"oxygen": -1}),
258
- VibeTransfer(vibe="oxygen_b", target={"oxygen": 10}, actor={"oxygen": -10}),
259
- VibeTransfer(vibe="germanium_a", target={"germanium": 1}, actor={"germanium": -1}),
260
- VibeTransfer(vibe="germanium_b", target={"germanium": 4}, actor={"germanium": -4}),
261
- VibeTransfer(vibe="silicon_a", target={"silicon": 10}, actor={"silicon": -10}),
262
- VibeTransfer(vibe="silicon_b", target={"silicon": 50}, actor={"silicon": -50}),
263
- VibeTransfer(vibe="heart_a", target={"heart": 1}, actor={"heart": -1}),
264
- VibeTransfer(vibe="heart_b", target={"heart": 4}, actor={"heart": -4}),
265
- ]
266
- # Enable transfer action with these vibes
267
- env.game.actions.transfer.enabled = True
268
- env.game.actions.transfer.vibe_transfers.extend(trade_transfers)
339
+ def modify_mission(self, mission: CvCMission) -> None:
340
+ team = next(iter(mission.teams.values()))
341
+ # Each team gets the original agent count; clear num_cogs so total is derived from teams
342
+ original_agents = mission.num_agents
343
+ mission.teams = {
344
+ name: team.model_copy(update={"name": name, "short_name": name, "num_agents": original_agents})
345
+ for name in ["cogs_green", "cogs_blue"][: self.num_teams]
346
+ }
347
+ mission.num_cogs = None
348
+
349
+ def modify_env(self, mission: CvCMission, env: MettaGridConfig) -> None:
350
+ original_builder = env.game.map_builder
351
+ # Shrink inner instance borders so teams are close together
352
+ if isinstance(original_builder, MapGen.Config):
353
+ original_builder.border_width = 1
354
+ env.game.map_builder = MapGen.Config(
355
+ instance=original_builder,
356
+ instances=self.num_teams,
357
+ set_team_by_instance=True,
358
+ instance_names=[t.short_name for t in mission.teams.values()],
359
+ instance_object_remap={
360
+ "c:hub": "{instance_name}:hub",
361
+ "c:chest": "{instance_name}:chest",
362
+ **{f"c:{g}": f"{{instance_name}}:{g}" for g in CvCConfig.GEAR},
363
+ },
364
+ # Connect instances: no added borders, clear walls at boundary
365
+ border_width=0, # No outer border (inner instances have their own)
366
+ instance_border_width=0, # No border between instances
367
+ instance_border_clear_radius=3, # Clear walls near instance boundary
368
+ )
269
369
 
270
370
 
271
- class SharedRewardsVariant(MissionVariant):
272
- name: str = "shared_rewards"
273
- description: str = "Rewards for deposited hearts are shared among all agents."
371
+ class NoClipsVariant(CoGameMissionVariant):
372
+ name: str = "no_clips"
373
+ description: str = "Disable clips behavior entirely."
274
374
 
275
375
  @override
276
- def modify_env(self, mission, env):
277
- num_cogs = mission.num_cogs if mission.num_cogs is not None else mission.site.min_cogs
278
- env.game.agent.rewards["chest_heart_deposited_by_agent"] = reward(
279
- stat("chest.heart.deposited_by_agent"), weight=0
280
- )
281
- env.game.agent.rewards["chest_heart_amount"] = reward(stat("chest.heart.amount"), weight=1 / num_cogs)
376
+ def modify_mission(self, mission: CvCMission) -> None:
377
+ mission.clips.disabled = True
282
378
 
283
379
 
284
- # TODO - validate that all variant names are unique
285
- VARIANTS: list[MissionVariant] = [
380
+ VARIANTS: list[CoGameMissionVariant] = [
286
381
  CavesVariant(),
287
382
  CityVariant(),
288
- CompassVariant(),
289
383
  DarkSideVariant(),
384
+ NoClipsVariant(),
290
385
  DesertVariant(),
291
386
  EmptyBaseVariant(),
292
387
  EnergizedVariant(),
293
388
  ForestVariant(),
294
- PackRatVariant(),
389
+ MultiTeamVariant(),
295
390
  QuadrantBuildingsVariant(),
296
- RoughTerrainVariant(),
297
- SharedRewardsVariant(),
298
391
  SingleResourceUniformVariant(),
299
392
  Small50Variant(),
300
393
  SuperChargedVariant(),
301
- TraderVariant(),
302
394
  *DIFFICULTY_VARIANTS,
303
395
  ]
304
396
 
305
- # Hidden variants registry: Remains usable but will NOT appear in `cogames variants` listing
306
- HIDDEN_VARIANTS: list[MissionVariant] = [
307
- # Example: ExperimentalVariant(), # keep empty by default
397
+ HIDDEN_VARIANTS: list[CoGameMissionVariant] = [
398
+ ClipsEasyVariant(),
399
+ ClipsMediumVariant(),
400
+ ClipsHardVariant(),
401
+ ClipsWaveOnlyVariant(),
308
402
  ]
@@ -0,0 +1,52 @@
1
+ """Weather system events for CogsGuard missions.
2
+
3
+ Day/night cycle that applies resource deltas to entities at regular intervals.
4
+ """
5
+
6
+ from pydantic import Field
7
+
8
+ from mettagrid.base_config import Config
9
+ from mettagrid.config.event_config import EventConfig, periodic
10
+ from mettagrid.config.mutation import updateTarget
11
+ from mettagrid.config.tag import typeTag
12
+
13
+
14
+ class WeatherConfig(Config):
15
+ """Configuration for day/night weather cycle."""
16
+
17
+ day_length: int = Field(default=200)
18
+ day_deltas: dict[str, int] = Field(default_factory=lambda: {"solar": 3})
19
+ night_deltas: dict[str, int] = Field(default_factory=lambda: {"solar": 1})
20
+ target_tag: str = Field(default="agent")
21
+
22
+ def events(self, max_steps: int) -> dict[str, EventConfig]:
23
+ """Create weather events for a mission.
24
+
25
+ Returns:
26
+ Dictionary of event name to EventConfig.
27
+ """
28
+ events: dict[str, EventConfig] = {}
29
+ tag = typeTag(self.target_tag)
30
+ half = self.day_length // 2
31
+
32
+ def _merge(apply: dict[str, int], reverse: dict[str, int]) -> dict[str, int]:
33
+ keys = set(apply) | set(reverse)
34
+ return {k: apply.get(k, 0) - reverse.get(k, 0) for k in keys}
35
+
36
+ # Dawn: reverse night deltas, apply day deltas
37
+ events["weather_day"] = EventConfig(
38
+ name="weather_day",
39
+ target_tag=tag,
40
+ timesteps=periodic(start=0, period=self.day_length, end=max_steps),
41
+ mutations=[updateTarget(_merge(self.day_deltas, self.night_deltas))],
42
+ )
43
+
44
+ # Dusk: reverse day deltas, apply night deltas
45
+ events["weather_night"] = EventConfig(
46
+ name="weather_night",
47
+ target_tag=tag,
48
+ timesteps=periodic(start=half, period=self.day_length, end=max_steps),
49
+ mutations=[updateTarget(_merge(self.night_deltas, self.day_deltas))],
50
+ )
51
+
52
+ return events
cogames/core.py ADDED
@@ -0,0 +1,87 @@
1
+ """Core base classes for CoGame missions and variants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC
6
+ from typing import TypeVar
7
+
8
+ from pydantic import Field
9
+ from typing_extensions import Self
10
+
11
+ from mettagrid.base_config import Config
12
+ from mettagrid.config.mettagrid_config import MettaGridConfig
13
+ from mettagrid.map_builder.map_builder import AnyMapBuilderConfig
14
+
15
+ # Type variable for mission types
16
+ TMission = TypeVar("TMission", bound="CoGameMission")
17
+
18
+ MAP_MISSION_DELIMITER = "."
19
+
20
+
21
+ class CoGameMissionVariant(Config, ABC):
22
+ # Note: we could derive the name from the class name automatically, but it would make it
23
+ # harder to find the variant source code based on CLI interactions.
24
+ name: str
25
+ description: str = Field(default="")
26
+
27
+ def modify_mission(self, mission: CoGameMission) -> None:
28
+ # Override this method to modify the mission.
29
+ # Variants are allowed to modify the mission in-place - it's guaranteed to be a one-time only instance.
30
+ pass
31
+
32
+ def modify_env(self, mission: CoGameMission, env: MettaGridConfig) -> None:
33
+ # Override this method to modify the produced environment.
34
+ # Variants are allowed to modify the environment in-place.
35
+ pass
36
+
37
+ def compat(self, mission: CoGameMission) -> bool:
38
+ """Check if this variant is compatible with the given mission.
39
+
40
+ Returns True if the variant can be safely applied to the mission.
41
+ Override this method to add compatibility checks.
42
+ """
43
+ return True
44
+
45
+ def apply(self, mission: TMission) -> TMission:
46
+ mission = mission.model_copy(deep=True)
47
+ mission.variants.append(self)
48
+ self.modify_mission(mission)
49
+ return mission
50
+
51
+
52
+ class CoGameSite(Config):
53
+ name: str
54
+ description: str
55
+ map_builder: AnyMapBuilderConfig
56
+
57
+ min_cogs: int = Field(default=1, ge=1)
58
+ max_cogs: int = Field(default=1000, ge=1)
59
+
60
+
61
+ class CoGameMission(Config, ABC):
62
+ """Base class for Mission configurations with common fields and methods."""
63
+
64
+ name: str
65
+ description: str
66
+ site: CoGameSite
67
+ num_cogs: int | None = None
68
+
69
+ # Variants are applied to the mission immediately, and to its env when make_env is called
70
+ variants: list[CoGameMissionVariant] = Field(default_factory=list)
71
+
72
+ max_steps: int = Field(default=10000)
73
+
74
+ def __init__(self, **kwargs):
75
+ super().__init__(**kwargs)
76
+ # Can't call `variant.apply` here because it will create a new mission instance
77
+ for variant in self.variants:
78
+ variant.modify_mission(self)
79
+
80
+ def with_variants(self, variants: list[CoGameMissionVariant]) -> Self:
81
+ mission = self
82
+ for variant in variants:
83
+ mission = variant.apply(mission)
84
+ return mission
85
+
86
+ def full_name(self) -> str:
87
+ return f"{self.site.name}{MAP_MISSION_DELIMITER}{self.name}"
@@ -261,11 +261,11 @@ uv run cogames play --mission evals.diagnostic_assemble_seeded_search -p baselin
261
261
 
262
262
  ```bash
263
263
  # Run full evaluation suite
264
- uv run python packages/cogames/scripts/run_evaluation.py
264
+ uv run cogames diagnose ladybug -S all
265
265
 
266
266
  # Evaluate specific agent
267
- uv run python packages/cogames/scripts/run_evaluation.py --policy baseline
268
- uv run python packages/cogames/scripts/run_evaluation.py --policy ladybug
267
+ uv run cogames diagnose baseline
268
+ uv run cogames diagnose ladybug
269
269
  ```
270
270
 
271
271
  ## Evaluation Results
cogames/evaluate.py CHANGED
@@ -13,10 +13,10 @@ from pydantic import BaseModel, ConfigDict
13
13
  from rich.console import Console
14
14
  from rich.table import Table
15
15
 
16
- from metta_alo.rollout import run_multi_episode_rollout
17
16
  from metta_alo.scoring import allocate_counts, validate_proportions
18
17
  from mettagrid import MettaGridConfig
19
18
  from mettagrid.policy.policy import PolicySpec
19
+ from mettagrid.runner.rollout import run_multi_episode_rollout
20
20
  from mettagrid.simulator.multi_episode.rollout import MultiEpisodeRolloutResult
21
21
  from mettagrid.simulator.multi_episode.summary import MultiEpisodeRolloutSummary, build_multi_episode_rollout_summaries
22
22
 
@@ -46,6 +46,7 @@ def evaluate(
46
46
  episodes: int,
47
47
  action_timeout_ms: int,
48
48
  seed: int = 42,
49
+ device: Optional[str] = None,
49
50
  output_format: Optional[Literal["yaml", "json"]] = None,
50
51
  save_replay: Optional[str] = None,
51
52
  ) -> MissionResultsSummary:
@@ -70,7 +71,7 @@ def evaluate(
70
71
  all_replay_paths: list[str] = []
71
72
  for mission_name, env_cfg in missions:
72
73
  counts = allocate_counts(env_cfg.game.num_agents, proportions)
73
- assignments = np.repeat(np.arange(len(counts), dtype=int), counts)
74
+ assignments = [i for i, c in enumerate(counts) for _ in range(c)]
74
75
 
75
76
  progress_label = f"Simulating ({mission_name})"
76
77
  with typer.progressbar(length=episodes, label=progress_label) as progress:
@@ -83,6 +84,7 @@ def evaluate(
83
84
  max_action_time_ms=action_timeout_ms,
84
85
  replay_dir=save_replay,
85
86
  create_replay_dir=save_replay is not None,
87
+ device=device,
86
88
  on_progress=lambda _episode_idx, _result: progress.update(1),
87
89
  )
88
90