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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. cogames/cli/client.py +60 -6
  2. cogames/cli/docsync/__init__.py +0 -0
  3. cogames/cli/docsync/_nb_md_directive_processing.py +180 -0
  4. cogames/cli/docsync/_nb_md_sync.py +103 -0
  5. cogames/cli/docsync/_nb_py_sync.py +122 -0
  6. cogames/cli/docsync/_three_way_sync.py +115 -0
  7. cogames/cli/docsync/_utils.py +76 -0
  8. cogames/cli/docsync/docsync.py +156 -0
  9. cogames/cli/leaderboard.py +112 -28
  10. cogames/cli/mission.py +64 -53
  11. cogames/cli/policy.py +46 -10
  12. cogames/cli/submit.py +268 -67
  13. cogames/cogs_vs_clips/cog.py +79 -0
  14. cogames/cogs_vs_clips/cogs_vs_clips_mapgen.md +19 -16
  15. cogames/cogs_vs_clips/cogsguard_reward_variants.py +153 -0
  16. cogames/cogs_vs_clips/cogsguard_tutorial.py +56 -0
  17. cogames/cogs_vs_clips/evals/README.md +10 -16
  18. cogames/cogs_vs_clips/evals/cogsguard_evals.py +81 -0
  19. cogames/cogs_vs_clips/evals/diagnostic_evals.py +49 -444
  20. cogames/cogs_vs_clips/evals/difficulty_variants.py +13 -326
  21. cogames/cogs_vs_clips/evals/integrated_evals.py +5 -45
  22. cogames/cogs_vs_clips/evals/spanning_evals.py +9 -180
  23. cogames/cogs_vs_clips/mission.py +187 -146
  24. cogames/cogs_vs_clips/missions.py +46 -137
  25. cogames/cogs_vs_clips/procedural.py +8 -8
  26. cogames/cogs_vs_clips/sites.py +107 -3
  27. cogames/cogs_vs_clips/stations.py +198 -186
  28. cogames/cogs_vs_clips/tutorial_missions.py +1 -1
  29. cogames/cogs_vs_clips/variants.py +25 -476
  30. cogames/device.py +13 -1
  31. cogames/{policy/scripted_agent/README.md → docs/SCRIPTED_AGENT.md} +82 -58
  32. cogames/evaluate.py +18 -30
  33. cogames/main.py +1434 -243
  34. cogames/maps/canidate1_1000.map +1 -1
  35. cogames/maps/canidate1_1000_stations.map +2 -2
  36. cogames/maps/canidate1_500.map +1 -1
  37. cogames/maps/canidate1_500_stations.map +2 -2
  38. cogames/maps/canidate2_1000.map +1 -1
  39. cogames/maps/canidate2_1000_stations.map +2 -2
  40. cogames/maps/canidate2_500.map +1 -1
  41. cogames/maps/canidate2_500_stations.map +2 -2
  42. cogames/maps/canidate3_1000.map +1 -1
  43. cogames/maps/canidate3_1000_stations.map +2 -2
  44. cogames/maps/canidate3_500.map +1 -1
  45. cogames/maps/canidate3_500_stations.map +2 -2
  46. cogames/maps/canidate4_500.map +1 -1
  47. cogames/maps/canidate4_500_stations.map +2 -2
  48. cogames/maps/cave_base_50.map +2 -2
  49. cogames/maps/diagnostic_evals/diagnostic_agile.map +2 -2
  50. cogames/maps/diagnostic_evals/diagnostic_agile_hard.map +2 -2
  51. cogames/maps/diagnostic_evals/diagnostic_charge_up.map +2 -2
  52. cogames/maps/diagnostic_evals/diagnostic_charge_up_hard.map +2 -2
  53. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1.map +2 -2
  54. cogames/maps/diagnostic_evals/diagnostic_chest_navigation1_hard.map +2 -2
  55. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2.map +2 -2
  56. cogames/maps/diagnostic_evals/diagnostic_chest_navigation2_hard.map +2 -2
  57. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3.map +2 -2
  58. cogames/maps/diagnostic_evals/diagnostic_chest_navigation3_hard.map +2 -2
  59. cogames/maps/diagnostic_evals/diagnostic_chest_near.map +2 -2
  60. cogames/maps/diagnostic_evals/diagnostic_chest_search.map +2 -2
  61. cogames/maps/diagnostic_evals/diagnostic_chest_search_hard.map +2 -2
  62. cogames/maps/diagnostic_evals/diagnostic_extract_lab.map +2 -2
  63. cogames/maps/diagnostic_evals/diagnostic_extract_lab_hard.map +2 -2
  64. cogames/maps/diagnostic_evals/diagnostic_memory.map +2 -2
  65. cogames/maps/diagnostic_evals/diagnostic_memory_hard.map +2 -2
  66. cogames/maps/diagnostic_evals/diagnostic_radial.map +2 -2
  67. cogames/maps/diagnostic_evals/diagnostic_radial_hard.map +2 -2
  68. cogames/maps/diagnostic_evals/diagnostic_resource_lab.map +2 -2
  69. cogames/maps/diagnostic_evals/diagnostic_unclip.map +2 -2
  70. cogames/maps/evals/eval_balanced_spread.map +9 -5
  71. cogames/maps/evals/eval_clip_oxygen.map +9 -5
  72. cogames/maps/evals/eval_collect_resources.map +9 -5
  73. cogames/maps/evals/eval_collect_resources_hard.map +9 -5
  74. cogames/maps/evals/eval_collect_resources_medium.map +9 -5
  75. cogames/maps/evals/eval_divide_and_conquer.map +9 -5
  76. cogames/maps/evals/eval_energy_starved.map +9 -5
  77. cogames/maps/evals/eval_multi_coordinated_collect_hard.map +9 -5
  78. cogames/maps/evals/eval_oxygen_bottleneck.map +9 -5
  79. cogames/maps/evals/eval_single_use_world.map +9 -5
  80. cogames/maps/evals/extractor_hub_100x100.map +9 -5
  81. cogames/maps/evals/extractor_hub_30x30.map +9 -5
  82. cogames/maps/evals/extractor_hub_50x50.map +9 -5
  83. cogames/maps/evals/extractor_hub_70x70.map +9 -5
  84. cogames/maps/evals/extractor_hub_80x80.map +9 -5
  85. cogames/maps/machina_100_stations.map +2 -2
  86. cogames/maps/machina_200_stations.map +2 -2
  87. cogames/maps/machina_200_stations_small.map +2 -2
  88. cogames/maps/machina_eval_exp01.map +2 -2
  89. cogames/maps/machina_eval_template_large.map +2 -2
  90. cogames/maps/machinatrainer4agents.map +2 -2
  91. cogames/maps/machinatrainer4agentsbase.map +2 -2
  92. cogames/maps/machinatrainerbig.map +2 -2
  93. cogames/maps/machinatrainersmall.map +2 -2
  94. cogames/maps/planky_evals/aligner_avoid_aoe.map +28 -0
  95. cogames/maps/planky_evals/aligner_full_cycle.map +28 -0
  96. cogames/maps/planky_evals/aligner_gear.map +24 -0
  97. cogames/maps/planky_evals/aligner_hearts.map +24 -0
  98. cogames/maps/planky_evals/aligner_junction.map +26 -0
  99. cogames/maps/planky_evals/exploration_distant.map +28 -0
  100. cogames/maps/planky_evals/maze.map +32 -0
  101. cogames/maps/planky_evals/miner_best_resource.map +26 -0
  102. cogames/maps/planky_evals/miner_deposit.map +24 -0
  103. cogames/maps/planky_evals/miner_extract.map +26 -0
  104. cogames/maps/planky_evals/miner_full_cycle.map +28 -0
  105. cogames/maps/planky_evals/miner_gear.map +24 -0
  106. cogames/maps/planky_evals/multi_role.map +28 -0
  107. cogames/maps/planky_evals/resource_chain.map +30 -0
  108. cogames/maps/planky_evals/scout_explore.map +32 -0
  109. cogames/maps/planky_evals/scout_gear.map +24 -0
  110. cogames/maps/planky_evals/scrambler_full_cycle.map +28 -0
  111. cogames/maps/planky_evals/scrambler_gear.map +24 -0
  112. cogames/maps/planky_evals/scrambler_target.map +26 -0
  113. cogames/maps/planky_evals/stuck_corridor.map +32 -0
  114. cogames/maps/planky_evals/survive_retreat.map +26 -0
  115. cogames/maps/training_facility_clipped.map +2 -2
  116. cogames/maps/training_facility_open_1.map +2 -2
  117. cogames/maps/training_facility_open_2.map +2 -2
  118. cogames/maps/training_facility_open_3.map +2 -2
  119. cogames/maps/training_facility_tight_4.map +2 -2
  120. cogames/maps/training_facility_tight_5.map +2 -2
  121. cogames/maps/vanilla_large.map +2 -2
  122. cogames/maps/vanilla_small.map +2 -2
  123. cogames/pickup.py +183 -0
  124. cogames/play.py +166 -33
  125. cogames/policy/chaos_monkey.py +54 -0
  126. cogames/policy/nim_agents/__init__.py +27 -10
  127. cogames/policy/nim_agents/agents.py +121 -60
  128. cogames/policy/nim_agents/thinky_eval.py +35 -222
  129. cogames/policy/pufferlib_policy.py +67 -32
  130. cogames/policy/starter_agent.py +184 -0
  131. cogames/policy/trainable_policy_template.py +4 -1
  132. cogames/train.py +51 -13
  133. cogames/verbose.py +2 -2
  134. cogames-0.3.64.dist-info/METADATA +1842 -0
  135. cogames-0.3.64.dist-info/RECORD +159 -0
  136. cogames-0.3.64.dist-info/licenses/LICENSE +21 -0
  137. cogames-0.3.64.dist-info/top_level.txt +2 -0
  138. metta_alo/__init__.py +0 -0
  139. metta_alo/job_specs.py +17 -0
  140. metta_alo/policy.py +16 -0
  141. metta_alo/pure_single_episode_runner.py +75 -0
  142. metta_alo/py.typed +0 -0
  143. metta_alo/rollout.py +322 -0
  144. metta_alo/scoring.py +168 -0
  145. cogames/maps/diagnostic_evals/diagnostic_assembler_near.map +0 -49
  146. cogames/maps/diagnostic_evals/diagnostic_assembler_search.map +0 -49
  147. cogames/maps/diagnostic_evals/diagnostic_assembler_search_hard.map +0 -89
  148. cogames/policy/nim_agents/common.nim +0 -887
  149. cogames/policy/nim_agents/install.sh +0 -1
  150. cogames/policy/nim_agents/ladybug_agent.nim +0 -984
  151. cogames/policy/nim_agents/nim_agents.nim +0 -55
  152. cogames/policy/nim_agents/nim_agents.nims +0 -14
  153. cogames/policy/nim_agents/nimby.lock +0 -3
  154. cogames/policy/nim_agents/racecar_agents.nim +0 -884
  155. cogames/policy/nim_agents/random_agents.nim +0 -68
  156. cogames/policy/nim_agents/test_agents.py +0 -53
  157. cogames/policy/nim_agents/thinky_agents.nim +0 -717
  158. cogames/policy/scripted_agent/baseline_agent.py +0 -1049
  159. cogames/policy/scripted_agent/demo_policy.py +0 -244
  160. cogames/policy/scripted_agent/pathfinding.py +0 -126
  161. cogames/policy/scripted_agent/starter_agent.py +0 -136
  162. cogames/policy/scripted_agent/types.py +0 -235
  163. cogames/policy/scripted_agent/unclipping_agent.py +0 -476
  164. cogames/policy/scripted_agent/utils.py +0 -385
  165. cogames-0.3.49.dist-info/METADATA +0 -406
  166. cogames-0.3.49.dist-info/RECORD +0 -136
  167. cogames-0.3.49.dist-info/top_level.txt +0 -1
  168. {cogames-0.3.49.dist-info → cogames-0.3.64.dist-info}/WHEEL +0 -0
  169. {cogames-0.3.49.dist-info → cogames-0.3.64.dist-info}/entry_points.txt +0 -0
@@ -1,18 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
- from typing import Callable, Dict
4
+ from typing import Dict
5
5
 
6
6
  from pydantic import Field
7
7
 
8
- from cogames.cogs_vs_clips.mission import Mission, MissionVariant, Site
8
+ from cogames.cogs_vs_clips.cog import CogConfig
9
+ from cogames.cogs_vs_clips.mission import Mission, Site
10
+ from mettagrid.config.game_value import stat
11
+ from mettagrid.config.handler_config import Handler
9
12
  from mettagrid.config.mettagrid_config import (
10
- AssemblerConfig,
11
13
  ChestConfig,
12
14
  MettaGridConfig,
13
- ProtocolConfig,
14
15
  ResourceLimitsConfig,
15
16
  )
17
+ from mettagrid.config.mutation.resource_mutation import updateActor
18
+ from mettagrid.config.reward_config import reward
16
19
  from mettagrid.map_builder.map_builder import MapBuilderConfig
17
20
  from mettagrid.mapgen.mapgen import MapGen
18
21
 
@@ -38,19 +41,6 @@ def get_map(map_name: str) -> MapBuilderConfig:
38
41
  )
39
42
 
40
43
 
41
- def _add_make_env_modifier(mission: Mission, modifier: Callable[[MettaGridConfig], None]) -> Mission:
42
- """Attach a post-make_env modifier to an instantiated mission."""
43
- original_make_env = mission.make_env
44
-
45
- def wrapped_make_env() -> MettaGridConfig:
46
- env_cfg = original_make_env()
47
- modifier(env_cfg)
48
- return env_cfg
49
-
50
- object.__setattr__(mission, "make_env", wrapped_make_env)
51
- return mission
52
-
53
-
54
44
  EVALS = Site(
55
45
  name="evals",
56
46
  description="Diagnostic evaluation arenas.",
@@ -60,10 +50,40 @@ EVALS = Site(
60
50
  )
61
51
 
62
52
 
53
+ # Generous cog config for diagnostic missions: high limits and full energy regen
54
+ _GENEROUS_COG = CogConfig(
55
+ gear_limit=255,
56
+ hp_limit=255,
57
+ heart_limit=255,
58
+ energy_limit=255,
59
+ cargo_limit=255,
60
+ initial_energy=255,
61
+ initial_hp=100,
62
+ energy_regen=255,
63
+ hp_regen=0,
64
+ influence_regen=0,
65
+ )
66
+
67
+ # Same but without generous energy regen (for charge-up diagnostics)
68
+ _MODEST_COG = CogConfig(
69
+ gear_limit=255,
70
+ hp_limit=255,
71
+ heart_limit=255,
72
+ energy_limit=255,
73
+ cargo_limit=255,
74
+ initial_energy=255,
75
+ initial_hp=100,
76
+ energy_regen=1,
77
+ hp_regen=0,
78
+ influence_regen=0,
79
+ )
80
+
81
+
63
82
  class _DiagnosticMissionBase(Mission):
64
83
  """Base class for minimal diagnostic evaluation missions."""
65
84
 
66
85
  site: Site = EVALS
86
+ cog: CogConfig = Field(default_factory=lambda: _GENEROUS_COG.model_copy())
67
87
 
68
88
  map_name: str = Field(default="evals/diagnostic_eval_template.map")
69
89
  max_steps: int = Field(default=250)
@@ -72,27 +92,16 @@ class _DiagnosticMissionBase(Mission):
72
92
  inventory_seed: Dict[str, int] = Field(default_factory=dict)
73
93
  communal_chest_hearts: int | None = Field(default=None)
74
94
  resource_chest_stock: Dict[str, int] = Field(default_factory=dict)
75
- clip_extractors: set[str] = Field(default_factory=set)
76
- extractor_max_uses: Dict[str, int] = Field(default_factory=dict)
77
- assembler_heart_chorus: int = Field(default=1)
78
- # If True, set assembler heart chorus to the number of agents in the environment
79
- dynamic_assembler_chorus: bool = Field(default=False)
80
95
  # If True, give agents high energy capacity and regen (overridden by specific missions)
81
96
  generous_energy: bool = Field(default=True)
82
97
 
98
+ # Disable clips events for diagnostic evals
99
+ clips_scramble_start: int = Field(default=99999)
100
+ clips_align_start: int = Field(default=99999)
101
+
83
102
  def configure_env(self, cfg: MettaGridConfig) -> None: # pragma: no cover - hook for subclasses
84
103
  """Hook for mission-specific environment alterations."""
85
104
 
86
- def configure(self) -> None:
87
- # Defaults per spec: large capacities, high regen unless mission disables it
88
- self.heart_capacity = max(self.heart_capacity, 255)
89
- self.cargo_capacity = max(self.cargo_capacity, 255)
90
- self.gear_capacity = max(self.gear_capacity, 255)
91
- self.energy_capacity = max(self.energy_capacity, 255)
92
- if self.generous_energy:
93
- # Full energy each step; effectively "lots of charge"
94
- self.energy_regen_amount = self.energy_capacity
95
-
96
105
  def make_env(self) -> MettaGridConfig:
97
106
  """Override make_env to use the mission's map_name instead of site.map_builder."""
98
107
  forced_map = get_map(self.map_name)
@@ -107,15 +116,6 @@ class _DiagnosticMissionBase(Mission):
107
116
  self._apply_inventory_seed(cfg)
108
117
  self._apply_communal_chest(cfg)
109
118
  self._apply_resource_chests(cfg)
110
- self._apply_extractor_settings(cfg)
111
- # Apply assembler requirements (may be overridden by dynamic chorus below)
112
- self._apply_assembler_requirements(cfg)
113
- # Zero out cooldowns everywhere to keep interactions snappy
114
- self._zero_all_protocol_cooldowns(cfg)
115
- # If required, set heart chorus to the number of agents after env is created
116
- if self.dynamic_assembler_chorus:
117
- self.assembler_heart_chorus = max(1, int(cfg.game.num_agents))
118
- self._apply_assembler_requirements(cfg)
119
119
  # Finally, normalize rewards so a single deposited heart yields at most 1 reward.
120
120
  self._apply_heart_reward_cap(cfg)
121
121
  self.configure_env(cfg)
@@ -124,41 +124,6 @@ class _DiagnosticMissionBase(Mission):
124
124
  # Restore original map_builder
125
125
  self.site.map_builder = original_map_builder
126
126
 
127
- def instantiate(
128
- self,
129
- map_builder: MapBuilderConfig,
130
- num_cogs: int,
131
- variant: MissionVariant | None = None,
132
- *,
133
- cli_override: bool = False,
134
- ) -> "Mission":
135
- forced_map = get_map(self.map_name)
136
- # TODO: Mission doesn't have instantiate() - this code path appears unused
137
- mission = super().instantiate(forced_map, num_cogs, variant, cli_override=cli_override) # type: ignore[attr-defined]
138
- if not cli_override and self.required_agents is not None:
139
- mission.num_cogs = self.required_agents
140
-
141
- def _post(cfg: MettaGridConfig) -> None:
142
- cfg.game.map_builder = forced_map
143
- cfg.game.max_steps = self.max_steps
144
- self._apply_inventory_seed(cfg)
145
- self._apply_communal_chest(cfg)
146
- self._apply_resource_chests(cfg)
147
- self._apply_extractor_settings(cfg)
148
- # Apply assembler requirements (may be overridden by dynamic chorus below)
149
- self._apply_assembler_requirements(cfg)
150
- # Zero out cooldowns everywhere to keep interactions snappy
151
- self._zero_all_protocol_cooldowns(cfg)
152
- # If required, set heart chorus to the number of agents after env is created
153
- if self.dynamic_assembler_chorus:
154
- self.assembler_heart_chorus = max(1, int(cfg.game.num_agents))
155
- self._apply_assembler_requirements(cfg)
156
- # Finally, normalize rewards so a single deposited heart yields at most 1 reward.
157
- self._apply_heart_reward_cap(cfg)
158
- self.configure_env(cfg)
159
-
160
- return _add_make_env_modifier(mission, _post)
161
-
162
127
  # ------------------------------------------------------------------
163
128
  # Helpers
164
129
  # ------------------------------------------------------------------
@@ -185,54 +150,6 @@ class _DiagnosticMissionBase(Mission):
185
150
  if isinstance(chest_cfg, ChestConfig):
186
151
  chest_cfg.inventory.initial = {resource: amount}
187
152
 
188
- def _apply_extractor_settings(self, cfg: MettaGridConfig) -> None:
189
- for resource in RESOURCE_NAMES:
190
- extractor = cfg.game.objects.get(f"{resource}_extractor")
191
- if not isinstance(extractor, AssemblerConfig):
192
- continue
193
- if resource in self.clip_extractors:
194
- extractor.start_clipped = True
195
- if resource in self.extractor_max_uses:
196
- extractor.max_uses = self.extractor_max_uses[resource]
197
-
198
- def _apply_assembler_requirements(self, cfg: MettaGridConfig) -> None:
199
- assembler = cfg.game.objects.get("assembler")
200
- if not isinstance(assembler, AssemblerConfig):
201
- return
202
- self._ensure_minimal_heart_recipe(assembler)
203
- if self.assembler_heart_chorus <= 1:
204
- return
205
- # Use a valid heart vibe from the CVC vibe set.
206
- chorus = ["heart_a"] * self.assembler_heart_chorus
207
- updated: list[ProtocolConfig] = []
208
- heart_protocol_applied = False
209
- for proto in assembler.protocols:
210
- if proto.output_resources.get("heart", 0) > 0:
211
- if heart_protocol_applied:
212
- # Drop duplicate heart protocols to avoid vibe collisions.
213
- continue
214
- updated_proto = proto.model_copy(update={"vibes": chorus})
215
- updated.append(updated_proto)
216
- heart_protocol_applied = True
217
- else:
218
- updated.append(proto)
219
- assembler.protocols = updated
220
-
221
- def _zero_all_protocol_cooldowns(self, cfg: MettaGridConfig) -> None:
222
- # Zero cooldowns on assembler/extractor protocols and unclipping protocols
223
- for _name, obj in list(cfg.game.objects.items()):
224
- if not isinstance(obj, AssemblerConfig):
225
- continue
226
- updated: list[ProtocolConfig] = []
227
- for proto in obj.protocols:
228
- updated.append(proto.model_copy(update={"cooldown": 0}))
229
- obj.protocols = updated
230
- if cfg.game.clipper is not None:
231
- new_up: list[ProtocolConfig] = []
232
- for proto in cfg.game.clipper.unclipping_protocols:
233
- new_up.append(proto.model_copy(update={"cooldown": 0}))
234
- cfg.game.clipper.unclipping_protocols = new_up
235
-
236
153
  def _apply_heart_reward_cap(self, cfg: MettaGridConfig) -> None:
237
154
  """Normalize diagnostics so a single deposited heart yields at most 1 reward per episode.
238
155
 
@@ -240,66 +157,22 @@ class _DiagnosticMissionBase(Mission):
240
157
  - Ensure all chests can store at most 1 heart so total reward per episode cannot exceed 1.
241
158
  """
242
159
  agent_cfg = cfg.game.agent
243
- rewards = agent_cfg.rewards
244
- stats = dict(rewards.stats or {})
245
- stats["chest.heart.deposited_by_agent"] = 1.0
246
- agent_cfg.rewards = rewards.model_copy(update={"stats": stats})
160
+ rewards = dict(agent_cfg.rewards)
161
+ rewards["chest_heart_deposited_by_agent"] = reward(stat("chest.heart.deposited_by_agent"))
162
+ agent_cfg.rewards = rewards
247
163
 
248
164
  # Cap heart capacity for every chest used in diagnostics (communal or resource-specific).
249
165
  for _name, obj in cfg.game.objects.items():
250
166
  if not isinstance(obj, ChestConfig):
251
167
  continue
252
168
  # Find existing heart limit or create new one
253
- heart_limit = obj.inventory.limits.get("heart", ResourceLimitsConfig(limit=1, resources=["heart"]))
254
- heart_limit.limit = 1
169
+ heart_limit = obj.inventory.limits.get("heart", ResourceLimitsConfig(min=1, resources=["heart"]))
170
+ heart_limit.min = 1
255
171
  obj.inventory.limits["heart"] = heart_limit
256
172
 
257
- def _ensure_minimal_heart_recipe(self, assembler: AssemblerConfig) -> None:
258
- minimal_inputs = {
259
- "carbon": 2,
260
- "oxygen": 2,
261
- "germanium": 1,
262
- "silicon": 3,
263
- "energy": 2,
264
- }
265
-
266
- updated_protocols: list[ProtocolConfig] = []
267
- heart_recipe_applied = False
268
-
269
- for proto in assembler.protocols:
270
- if proto.output_resources.get("heart", 0) > 0:
271
- if heart_recipe_applied:
272
- # Drop duplicate heart recipes to avoid conflicting requirements.
273
- continue
274
- updated_proto = proto.model_copy(
275
- update={
276
- "vibes": ["heart_a"],
277
- "input_resources": minimal_inputs,
278
- "cooldown": 0,
279
- "output_resources": {"heart": 1},
280
- }
281
- )
282
- updated_protocols.append(updated_proto)
283
- heart_recipe_applied = True
284
- else:
285
- updated_protocols.append(proto)
286
-
287
- if not heart_recipe_applied:
288
- updated_protocols.insert(
289
- 0,
290
- ProtocolConfig(
291
- vibes=["heart_a"],
292
- input_resources=minimal_inputs,
293
- output_resources={"heart": 1},
294
- cooldown=0,
295
- ),
296
- )
297
-
298
- assembler.protocols = updated_protocols
299
-
300
173
 
301
174
  # ----------------------------------------------------------------------
302
- # New diagnostics per spec
175
+ # Diagnostics (non-hub)
303
176
  # ----------------------------------------------------------------------
304
177
 
305
178
 
@@ -311,7 +184,6 @@ class DiagnosticChestNavigation1(_DiagnosticMissionBase):
311
184
  inventory_seed: Dict[str, int] = Field(default_factory=lambda: {"heart": 1})
312
185
  max_steps: int = Field(default=250)
313
186
  required_agents: int | None = 1
314
- # 1-4 agents by default; no forced agent count
315
187
 
316
188
 
317
189
  class DiagnosticChestNavigation2(_DiagnosticMissionBase):
@@ -351,138 +223,6 @@ class DiagnosticChestDepositSearch(_DiagnosticMissionBase):
351
223
  max_steps: int = Field(default=250)
352
224
 
353
225
 
354
- # Assemble seeded: 1-4 agents, assembler requires exactly num agents to chorus
355
- class DiagnosticAssembleSeededNear(_DiagnosticMissionBase):
356
- name: str = "diagnostic_assemble_seeded_near"
357
- description: str = "Agents are pre-seeded; chorus glyph HEART near the assembler."
358
- map_name: str = "evals/diagnostic_assembler_near.map"
359
- dynamic_assembler_chorus: bool = True
360
- inventory_seed: Dict[str, int] = Field(
361
- default_factory=lambda: {"carbon": 2, "oxygen": 2, "germanium": 1, "silicon": 3}
362
- )
363
- max_steps: int = Field(default=50)
364
-
365
-
366
- class DiagnosticAssembleSeededSearch(_DiagnosticMissionBase):
367
- name: str = "diagnostic_assemble_seeded_search"
368
- description: str = "Agents are pre-seeded; locate the assembler and chorus glyph HEART."
369
- map_name: str = "evals/diagnostic_assembler_search.map"
370
- dynamic_assembler_chorus: bool = True
371
- inventory_seed: Dict[str, int] = Field(
372
- default_factory=lambda: {"carbon": 2, "oxygen": 2, "germanium": 1, "silicon": 3}
373
- )
374
- max_steps: int = Field(default=150)
375
-
376
-
377
- # Extract mission set: missing one resource; 1-4 agents; chorus required
378
- class DiagnosticExtractMissingCarbon(_DiagnosticMissionBase):
379
- name: str = "diagnostic_extract_missing_carbon"
380
- description: str = "All agents start around the assembler; carbon must be extracted."
381
- map_name: str = "evals/diagnostic_extract_lab.map"
382
- dynamic_assembler_chorus: bool = True
383
- inventory_seed: Dict[str, int] = Field(default_factory=lambda: {"oxygen": 2, "germanium": 1, "silicon": 3})
384
- max_steps: int = Field(default=130)
385
-
386
-
387
- class DiagnosticExtractMissingOxygen(_DiagnosticMissionBase):
388
- name: str = "diagnostic_extract_missing_oxygen"
389
- description: str = "Gather oxygen from the extractor to complete a heart."
390
- map_name: str = "evals/diagnostic_extract_lab.map"
391
- inventory_seed: Dict[str, int] = Field(default_factory=lambda: {"carbon": 2, "germanium": 1, "silicon": 3})
392
- max_steps: int = Field(default=130)
393
-
394
-
395
- class DiagnosticExtractMissingGermanium(_DiagnosticMissionBase):
396
- name: str = "diagnostic_extract_missing_germanium"
397
- description: str = "Gather germanium from the extractor to complete a heart."
398
- map_name: str = "evals/diagnostic_extract_lab.map"
399
- inventory_seed: Dict[str, int] = Field(default_factory=lambda: {"carbon": 2, "oxygen": 2, "silicon": 3})
400
- max_steps: int = Field(default=130)
401
-
402
-
403
- class DiagnosticExtractMissingSilicon(_DiagnosticMissionBase):
404
- name: str = "diagnostic_extract_missing_silicon"
405
- description: str = "Gather silicon from the extractor to complete a heart."
406
- map_name: str = "evals/diagnostic_extract_lab.map"
407
- inventory_seed: Dict[str, int] = Field(default_factory=lambda: {"carbon": 2, "oxygen": 2, "germanium": 1})
408
- max_steps: int = Field(default=130)
409
-
410
-
411
- class _UnclipBase(_DiagnosticMissionBase):
412
- map_name: str = "evals/diagnostic_unclip.map"
413
- dynamic_assembler_chorus: bool = True
414
-
415
- def configure_env(self, cfg: MettaGridConfig) -> None:
416
- # Determine how many extractors to clip based on number of agents (1..4)
417
- num_agents = max(1, int(cfg.game.num_agents))
418
- resources = list(RESOURCE_NAMES)
419
- to_clip = resources[: min(num_agents, len(resources))]
420
- # Clip the chosen extractors
421
- for res in to_clip:
422
- station = cfg.game.objects.get(f"{res}_extractor")
423
- if isinstance(station, AssemblerConfig):
424
- station.start_clipped = True
425
- # Configure unclipping to require the other three resources of the clipped station (no gear)
426
- unclipping_protos: list[ProtocolConfig] = []
427
- for res in to_clip:
428
- others = {r: 1 for r in resources if r != res}
429
- unclipping_protos.append(ProtocolConfig(input_resources=others, cooldown=0))
430
- if cfg.game.clipper is not None:
431
- cfg.game.clipper.unclipping_protocols = unclipping_protos
432
-
433
- non_clipped = [res for res in resources if res not in to_clip]
434
-
435
- assembler = cfg.game.objects.get("assembler")
436
- if isinstance(assembler, AssemblerConfig):
437
- updated_protocols: list[ProtocolConfig] = []
438
- for proto in assembler.protocols:
439
- if proto.output_resources.get("decoder", 0) > 0:
440
- inputs = {res: 1 for res in non_clipped}
441
- updated_proto = proto.model_copy(update={"vibes": ["gear"], "input_resources": inputs})
442
- updated_protocols.append(updated_proto)
443
- else:
444
- updated_protocols.append(proto)
445
- assembler.protocols = updated_protocols
446
-
447
- agent_cfg = cfg.game.agent
448
- inventory = dict(agent_cfg.inventory.initial)
449
- for res in resources:
450
- inventory.pop(res, None)
451
-
452
- base_amount = 2 if num_agents > 1 else 1
453
- for res in non_clipped:
454
- inventory[res] = max(inventory.get(res, 0), base_amount)
455
- for res in to_clip:
456
- inventory.pop(res, None)
457
-
458
- if num_agents > 1:
459
- inventory["decoder"] = max(inventory.get("decoder", 0), len(to_clip))
460
- else:
461
- inventory.pop("decoder", None)
462
-
463
- agent_cfg.inventory.initial = inventory
464
-
465
-
466
- class DiagnosticUnclipCraft(_UnclipBase):
467
- name: str = "diagnostic_unclip_craft"
468
- description: str = "Craft to unclip extractors and complete a heart chorus."
469
- # No preseeded tools; agents have only basic resources below
470
- inventory_seed: Dict[str, int] = Field(
471
- default_factory=lambda: {"carbon": 1, "oxygen": 1, "germanium": 1, "silicon": 1}
472
- )
473
- max_steps: int = Field(default=250)
474
-
475
-
476
- class DiagnosticUnclipPreseed(_UnclipBase):
477
- name: str = "diagnostic_unclip_preseed"
478
- description: str = "Preseeded for unclipping; number of clipped extractors equals number of agents."
479
- # Preseed with a mix of resources to allow immediate unclipping
480
- inventory_seed: Dict[str, int] = Field(
481
- default_factory=lambda: {"carbon": 2, "oxygen": 2, "germanium": 2, "silicon": 2}
482
- )
483
- max_steps: int = Field(default=250)
484
-
485
-
486
226
  class DiagnosticChargeUp(_DiagnosticMissionBase):
487
227
  name: str = "diagnostic_charge_up"
488
228
  description: str = "Agent starts low on energy and must charge to proceed."
@@ -498,29 +238,7 @@ class DiagnosticChargeUp(_DiagnosticMissionBase):
498
238
  agent = cfg.game.agent
499
239
  agent.inventory.initial = dict(agent.inventory.initial)
500
240
  agent.inventory.initial["energy"] = 60
501
- agent.inventory.regen_amounts = {"default": {"energy": 0}}
502
-
503
-
504
- class DiagnosticAgile(_DiagnosticMissionBase):
505
- name: str = "diagnostic_agile"
506
- description: str = "Navigation agility challenge; 1-4 agents."
507
- map_name: str = "evals/diagnostic_agile.map"
508
- max_steps: int = Field(default=250)
509
-
510
- def configure_env(self, cfg: MettaGridConfig) -> None:
511
- required = {"carbon": 2, "oxygen": 2, "germanium": 1, "silicon": 3}
512
- for resource, needed in required.items():
513
- station = cfg.game.objects.get(f"{resource}_extractor")
514
- if isinstance(station, AssemblerConfig):
515
- station.max_uses = 1
516
- updated: list[ProtocolConfig] = []
517
- for proto in station.protocols:
518
- outputs = dict(proto.output_resources)
519
- if resource in outputs:
520
- outputs = {resource: needed}
521
- proto = proto.model_copy(update={"output_resources": outputs})
522
- updated.append(proto)
523
- station.protocols = updated
241
+ agent.on_tick = {"regen": Handler(mutations=[updateActor({"energy": 0})])}
524
242
 
525
243
 
526
244
  class DiagnosticMemory(_DiagnosticMissionBase):
@@ -532,21 +250,6 @@ class DiagnosticMemory(_DiagnosticMissionBase):
532
250
  max_steps: int = Field(default=110)
533
251
 
534
252
 
535
- class DiagnosticRadial(_DiagnosticMissionBase):
536
- name: str = "diagnostic_radial"
537
- description: str = "Radial resource layout; gather all four ingredients and chorus assemble."
538
- map_name: str = "evals/diagnostic_radial.map"
539
- dynamic_assembler_chorus: bool = True
540
- max_steps: int = Field(default=250)
541
-
542
- def configure_env(self, cfg: MettaGridConfig) -> None:
543
- agent = cfg.game.agent
544
- inventory = dict(agent.inventory.initial)
545
- inventory["energy"] = 255
546
- agent.inventory.initial = inventory
547
- agent.inventory.regen_amounts = {"default": {"energy": 255}}
548
-
549
-
550
253
  # ----------------------------------------------------------------------
551
254
  # Hard versions of diagnostics (same maps, more time)
552
255
  # ----------------------------------------------------------------------
@@ -603,7 +306,7 @@ class DiagnosticChargeUpHard(_DiagnosticMissionBase):
603
306
  agent = cfg.game.agent
604
307
  agent.inventory.initial = dict(agent.inventory.initial)
605
308
  agent.inventory.initial["energy"] = 60
606
- agent.inventory.regen_amounts = {"default": {"energy": 0}}
309
+ agent.on_tick = {"regen": Handler(mutations=[updateActor({"energy": 0})])}
607
310
 
608
311
 
609
312
  class DiagnosticMemoryHard(_DiagnosticMissionBase):
@@ -615,87 +318,6 @@ class DiagnosticMemoryHard(_DiagnosticMissionBase):
615
318
  max_steps: int = Field(default=170)
616
319
 
617
320
 
618
- class DiagnosticAssembleSeededSearchHard(_DiagnosticMissionBase):
619
- name: str = "diagnostic_assemble_seeded_search_hard"
620
- description: str = "Agents are pre-seeded; locate the assembler and chorus glyph HEART (hard)."
621
- map_name: str = "evals/diagnostic_assembler_search_hard.map"
622
- dynamic_assembler_chorus: bool = True
623
- inventory_seed: Dict[str, int] = Field(
624
- default_factory=lambda: {"carbon": 2, "oxygen": 2, "germanium": 1, "silicon": 3}
625
- )
626
- max_steps: int = Field(default=250)
627
-
628
-
629
- class DiagnosticExtractMissingCarbonHard(_DiagnosticMissionBase):
630
- name: str = "diagnostic_extract_missing_carbon_hard"
631
- description: str = "All agents start around the assembler; carbon must be extracted (hard)."
632
- map_name: str = "evals/diagnostic_extract_lab_hard.map"
633
- dynamic_assembler_chorus: bool = True
634
- inventory_seed: Dict[str, int] = Field(default_factory=lambda: {"oxygen": 2, "germanium": 1, "silicon": 3})
635
- max_steps: int = Field(default=230)
636
-
637
-
638
- class DiagnosticExtractMissingOxygenHard(_DiagnosticMissionBase):
639
- name: str = "diagnostic_extract_missing_oxygen_hard"
640
- description: str = "Gather oxygen from the extractor to complete a heart (hard)."
641
- map_name: str = "evals/diagnostic_extract_lab_hard.map"
642
- inventory_seed: Dict[str, int] = Field(default_factory=lambda: {"carbon": 2, "germanium": 1, "silicon": 3})
643
- max_steps: int = Field(default=230)
644
-
645
-
646
- class DiagnosticExtractMissingGermaniumHard(_DiagnosticMissionBase):
647
- name: str = "diagnostic_extract_missing_germanium_hard"
648
- description: str = "Gather germanium from the extractor to complete a heart (hard)."
649
- map_name: str = "evals/diagnostic_extract_lab_hard.map"
650
- inventory_seed: Dict[str, int] = Field(default_factory=lambda: {"carbon": 2, "oxygen": 2, "silicon": 3})
651
- max_steps: int = Field(default=230)
652
-
653
-
654
- class DiagnosticExtractMissingSiliconHard(_DiagnosticMissionBase):
655
- name: str = "diagnostic_extract_missing_silicon_hard"
656
- description: str = "Gather silicon from the extractor to complete a heart (hard)."
657
- map_name: str = "evals/diagnostic_extract_lab_hard.map"
658
- inventory_seed: Dict[str, int] = Field(default_factory=lambda: {"carbon": 2, "oxygen": 2, "germanium": 1})
659
- max_steps: int = Field(default=230)
660
-
661
-
662
- class DiagnosticAgileHard(_DiagnosticMissionBase):
663
- name: str = "diagnostic_agile_hard"
664
- description: str = "Navigation agility challenge; 1-4 agents (hard)."
665
- map_name: str = "evals/diagnostic_agile_hard.map"
666
- max_steps: int = Field(default=350)
667
-
668
- def configure_env(self, cfg: MettaGridConfig) -> None:
669
- required = {"carbon": 2, "oxygen": 2, "germanium": 1, "silicon": 3}
670
- for resource, needed in required.items():
671
- station = cfg.game.objects.get(f"{resource}_extractor")
672
- if isinstance(station, AssemblerConfig):
673
- station.max_uses = 1
674
- updated: list[ProtocolConfig] = []
675
- for proto in station.protocols:
676
- outputs = dict(proto.output_resources)
677
- if resource in outputs:
678
- outputs = {resource: needed}
679
- proto = proto.model_copy(update={"output_resources": outputs})
680
- updated.append(proto)
681
- station.protocols = updated
682
-
683
-
684
- class DiagnosticRadialHard(_DiagnosticMissionBase):
685
- name: str = "diagnostic_radial_hard"
686
- description: str = "Radial resource layout; gather all four ingredients and chorus assemble (hard)."
687
- map_name: str = "evals/diagnostic_radial_hard.map"
688
- dynamic_assembler_chorus: bool = True
689
- max_steps: int = Field(default=350)
690
-
691
- def configure_env(self, cfg: MettaGridConfig) -> None:
692
- agent = cfg.game.agent
693
- inventory = dict(agent.inventory.initial)
694
- inventory["energy"] = 255
695
- agent.inventory.initial = inventory
696
- agent.inventory.regen_amounts = {"default": {"energy": 255}}
697
-
698
-
699
321
  DIAGNOSTIC_EVALS: list[type[_DiagnosticMissionBase]] = [
700
322
  DiagnosticChestNavigation1,
701
323
  DiagnosticChestNavigation2,
@@ -704,16 +326,6 @@ DIAGNOSTIC_EVALS: list[type[_DiagnosticMissionBase]] = [
704
326
  DiagnosticChestDepositSearch,
705
327
  DiagnosticChargeUp,
706
328
  DiagnosticMemory,
707
- DiagnosticAssembleSeededNear,
708
- DiagnosticAssembleSeededSearch,
709
- DiagnosticExtractMissingCarbon,
710
- DiagnosticExtractMissingOxygen,
711
- DiagnosticExtractMissingGermanium,
712
- DiagnosticExtractMissingSilicon,
713
- DiagnosticUnclipCraft,
714
- DiagnosticUnclipPreseed,
715
- DiagnosticAgile,
716
- DiagnosticRadial,
717
329
  # Hard versions
718
330
  DiagnosticChestNavigation1Hard,
719
331
  DiagnosticChestNavigation2Hard,
@@ -721,11 +333,4 @@ DIAGNOSTIC_EVALS: list[type[_DiagnosticMissionBase]] = [
721
333
  DiagnosticChestDepositSearchHard,
722
334
  DiagnosticChargeUpHard,
723
335
  DiagnosticMemoryHard,
724
- DiagnosticAssembleSeededSearchHard,
725
- DiagnosticExtractMissingCarbonHard,
726
- DiagnosticExtractMissingOxygenHard,
727
- DiagnosticExtractMissingGermaniumHard,
728
- DiagnosticExtractMissingSiliconHard,
729
- DiagnosticAgileHard,
730
- DiagnosticRadialHard,
731
336
  ]