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,81 +1,25 @@
1
- from typing import Iterable, Sequence, override
1
+ from typing import override
2
2
 
3
3
  from cogames.cogs_vs_clips.evals.difficulty_variants import DIFFICULTY_VARIANTS
4
- from cogames.cogs_vs_clips.mission import MissionVariant
4
+ from cogames.cogs_vs_clips.mission import Mission, MissionVariant
5
5
  from cogames.cogs_vs_clips.procedural import BaseHubVariant, MachinaArenaVariant
6
- from mettagrid.config.mettagrid_config import (
7
- AssemblerConfig,
8
- ChestConfig,
9
- ProtocolConfig,
10
- ResourceLimitsConfig,
11
- VibeTransfer,
12
- )
6
+ from mettagrid.config.action_config import VibeTransfer
7
+ from mettagrid.config.game_value import stat
8
+ from mettagrid.config.reward_config import reward
13
9
  from mettagrid.map_builder.map_builder import MapBuilderConfig
14
10
  from mettagrid.mapgen.mapgen import MapGen
15
11
  from mettagrid.mapgen.scenes.base_hub import DEFAULT_EXTRACTORS as HUB_EXTRACTORS
16
12
  from mettagrid.mapgen.scenes.building_distributions import DistributionConfig, DistributionType
17
13
 
18
14
 
19
- class MinedOutVariant(MissionVariant):
20
- name: str = "mined_out"
21
- description: str = "All resources are depleted. You must be efficient to survive."
22
-
23
- @override
24
- def modify_mission(self, mission):
25
- # Clamp efficiency to minimum of 50 to prevent negative values
26
- mission.carbon_extractor.max_uses = 2
27
- mission.oxygen_extractor.max_uses = 2
28
- mission.silicon_extractor.max_uses = 2
29
-
30
-
31
15
  class DarkSideVariant(MissionVariant):
32
16
  name: str = "dark_side"
33
17
  description: str = "You're on the dark side of the asteroid. You recharge slower."
34
18
 
35
19
  @override
36
20
  def modify_mission(self, mission):
37
- mission.energy_regen_amount = 0
38
-
39
-
40
- class LonelyHeartVariant(MissionVariant):
41
- name: str = "lonely_heart"
42
- description: str = "Making hearts for one agent is easy."
43
-
44
- @override
45
- def modify_mission(self, mission):
46
- mission.assembler.first_heart_cost = 1
47
- mission.assembler.additional_heart_cost = 0
48
- mission.heart_capacity = max(mission.heart_capacity, 255)
49
-
50
- @override
51
- def modify_env(self, mission, env):
52
- simplified_inputs = {"carbon": 1, "oxygen": 1, "germanium": 1, "silicon": 1, "energy": 1}
53
-
54
- assembler = env.game.objects["assembler"]
55
- if not isinstance(assembler, AssemblerConfig):
56
- raise TypeError("Expected 'assembler' to be AssemblerConfig")
57
-
58
- for idx, proto in enumerate(assembler.protocols):
59
- if proto.output_resources.get("heart", 0) == 0:
60
- continue
61
- updated = proto.model_copy(deep=True)
62
- updated.input_resources = dict(simplified_inputs)
63
- assembler.protocols[idx] = updated
64
-
65
- germanium = env.game.objects["germanium_extractor"]
66
- if not isinstance(germanium, AssemblerConfig):
67
- raise TypeError("Expected 'germanium_extractor' to be AssemblerConfig")
68
- germanium.max_uses = 0
69
- updated_protocols: list[ProtocolConfig] = []
70
- for proto in germanium.protocols:
71
- new_proto = proto.model_copy(deep=True)
72
- output = dict(new_proto.output_resources)
73
- output["germanium"] = max(output.get("germanium", 0), 1)
74
- new_proto.output_resources = output
75
- new_proto.cooldown = max(new_proto.cooldown, 1)
76
- updated_protocols.append(new_proto)
77
- if updated_protocols:
78
- germanium.protocols = updated_protocols
21
+ assert isinstance(mission, Mission)
22
+ mission.cog.energy_regen = 0
79
23
 
80
24
 
81
25
  class SuperChargedVariant(MissionVariant):
@@ -84,7 +28,8 @@ class SuperChargedVariant(MissionVariant):
84
28
 
85
29
  @override
86
30
  def modify_mission(self, mission):
87
- mission.energy_regen_amount += 2
31
+ assert isinstance(mission, Mission)
32
+ mission.cog.energy_regen += 2
88
33
 
89
34
 
90
35
  class RoughTerrainVariant(MissionVariant):
@@ -93,54 +38,8 @@ class RoughTerrainVariant(MissionVariant):
93
38
 
94
39
  @override
95
40
  def modify_mission(self, mission):
96
- mission.move_energy_cost += 2
97
-
98
-
99
- class SolarFlareVariant(MissionVariant):
100
- name: str = "solar_flare"
101
- description: str = "Chargers have been damaged by the solar flare."
102
-
103
- @override
104
- def modify_mission(self, mission):
105
- # Clamp efficiency to minimum of 1 to prevent negative values
106
- mission.charger.efficiency = max(1, mission.charger.efficiency - 50)
107
-
108
-
109
- class TrainingVariant(MissionVariant):
110
- name: str = "training"
111
- description: str = "Training-friendly: max cargo, fast extractors, chest only deposits hearts."
112
-
113
- @override
114
- def modify_mission(self, mission):
115
- mission.cargo_capacity = 255 # Maximum cargo for easier resource collection
116
-
117
- @override
118
- def modify_env(self, mission, env):
119
- # Set all extractor cooldowns to 5ms (fast)
120
- for extractor_name in ["carbon_extractor", "oxygen_extractor", "germanium_extractor", "silicon_extractor"]:
121
- extractor = env.game.objects.get(extractor_name)
122
- if isinstance(extractor, AssemblerConfig):
123
- updated_protocols = []
124
- for proto in extractor.protocols:
125
- updated_proto = proto.model_copy(deep=True)
126
- updated_proto.cooldown = 5
127
- updated_protocols.append(updated_proto)
128
- extractor.protocols = updated_protocols
129
-
130
- # Modify chest to only deposit hearts by default (not all resources)
131
- chest = env.game.objects.get("chest")
132
- if isinstance(chest, ChestConfig):
133
- chest.vibe_transfers = {
134
- "heart_b": {"heart": 1},
135
- "carbon_a": {"carbon": -10},
136
- "carbon_b": {"carbon": 10},
137
- "oxygen_a": {"oxygen": -10},
138
- "oxygen_b": {"oxygen": 10},
139
- "germanium_a": {"germanium": -1},
140
- "germanium_b": {"germanium": 1},
141
- "silicon_a": {"silicon": -25},
142
- "silicon_b": {"silicon": 25},
143
- }
41
+ assert isinstance(mission, Mission)
42
+ mission.cog.move_energy_cost += 2
144
43
 
145
44
 
146
45
  class PackRatVariant(MissionVariant):
@@ -149,10 +48,11 @@ class PackRatVariant(MissionVariant):
149
48
 
150
49
  @override
151
50
  def modify_mission(self, mission):
152
- mission.heart_capacity = max(mission.heart_capacity, 255)
153
- mission.energy_capacity = max(mission.energy_capacity, 255)
154
- mission.cargo_capacity = max(mission.cargo_capacity, 255)
155
- mission.gear_capacity = max(mission.gear_capacity, 255)
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)
156
56
 
157
57
 
158
58
  class EnergizedVariant(MissionVariant):
@@ -161,148 +61,18 @@ class EnergizedVariant(MissionVariant):
161
61
 
162
62
  @override
163
63
  def modify_mission(self, mission):
164
- mission.energy_capacity = max(mission.energy_capacity, 255)
165
- mission.energy_regen_amount = mission.energy_capacity
166
-
167
-
168
- class ResourceBottleneckVariant(MissionVariant):
169
- name: str = "resource_bottleneck"
170
- description: str = "A resource is the limiting factor. Agents must prioritize it over other resources."
171
- resource: Sequence[str] | str = ("oxygen", "germanium", "silicon", "carbon")
172
-
173
- @override
174
- def modify_mission(self, mission):
175
- # Accept either a single resource or an iterable of resources to bottleneck
176
- if isinstance(self.resource, str):
177
- resources: Iterable[str] = [self.resource]
178
- else:
179
- resources = list(self.resource)
180
-
181
- for resource in resources:
182
- if resource in {"carbon", "oxygen", "germanium", "silicon"}:
183
- extractor_attr = f"{resource}_extractor"
184
- elif resource == "energy":
185
- extractor_attr = "charger"
186
- else:
187
- raise ValueError(f"Unsupported resource for bottleneck: {resource}")
188
-
189
- extractor = getattr(mission, extractor_attr, None)
190
- if extractor is None:
191
- raise AttributeError(f"Mission has no extractor attribute '{extractor_attr}'")
192
-
193
- # Clamp efficiency to minimum of 1 to prevent negative values
194
- extractor.efficiency = max(1, int(extractor.efficiency) - 50)
195
-
196
-
197
- class SingleToolUnclipVariant(MissionVariant):
198
- name: str = "single_tool_unclip"
199
- description: str = "Only one tool is available: the decoder."
200
- resource: str = "carbon"
201
-
202
- @override
203
- def modify_env(self, mission, env):
204
- # Restrict assembler to a single generic gear recipe: carbon -> decoder (no vibes required)
205
- # Since the protocol doesn't require vibes, agents won't need to change vibes
206
- assembler = env.game.objects.get("assembler")
207
- if isinstance(assembler, AssemblerConfig):
208
- assembler.protocols = [
209
- ProtocolConfig(vibes=[], input_resources={self.resource: 1}, output_resources={"decoder": 1})
210
- ]
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
211
67
 
212
68
 
213
69
  class CompassVariant(MissionVariant):
214
70
  name: str = "compass"
215
- description: str = "Enable compass observation pointing toward the assembler."
216
-
217
- @override
218
- def modify_mission(self, mission):
219
- mission.compass_enabled = True
220
-
221
-
222
- class HeartChorusVariant(MissionVariant):
223
- name: str = "heart_chorus"
224
- description: str = "Heart-centric reward shaping with gentle resource bonuses."
71
+ description: str = "Enable compass observation."
225
72
 
226
73
  @override
227
74
  def modify_env(self, mission, env):
228
- # Supplemental shaping: focus rewards on the acting agent for heart progress.
229
- rewards = dict(env.game.agent.rewards.stats)
230
- rewards.update(
231
- {
232
- "assembler.heart.created": 1.0,
233
- "chest.heart.deposited_by_agent": 1.0,
234
- "chest.heart.withdrawn_by_agent": -1.0,
235
- "inventory.diversity.ge.2": 0.17,
236
- "inventory.diversity.ge.3": 0.18,
237
- "inventory.diversity.ge.4": 0.60,
238
- "inventory.diversity.ge.5": 0.97,
239
- }
240
- )
241
- env.game.agent.rewards.stats = rewards
242
-
243
-
244
- class TinyHeartProtocolsVariant(MissionVariant):
245
- """Prepend low-cost heart/red-heart assembler protocols for easy hearts."""
246
-
247
- name: str = "tiny_heart_protocols"
248
- description: str = "Prepend low-cost heart/red-heart assembler protocols."
249
-
250
- # Allow customization if ever needed; defaults match prior inline block.
251
- carbon_cost: int = 2
252
- oxygen_cost: int = 2
253
- germanium_cost: int = 1
254
- silicon_cost: int = 3
255
- energy_cost: int = 2
256
-
257
- @override
258
- def modify_env(self, mission, env) -> None:
259
- assembler = env.game.objects.get("assembler")
260
- if not isinstance(assembler, AssemblerConfig):
261
- raise TypeError("Expected 'assembler' to be AssemblerConfig")
262
-
263
- tiny_inputs = {
264
- "carbon": self.carbon_cost,
265
- "oxygen": self.oxygen_cost,
266
- "germanium": self.germanium_cost,
267
- "silicon": self.silicon_cost,
268
- "energy": self.energy_cost,
269
- }
270
-
271
- tiny_protocols = [
272
- ProtocolConfig(
273
- vibes=[vibe] * (i + 1),
274
- input_resources=tiny_inputs,
275
- output_resources={"heart": i + 1},
276
- )
277
- for vibe in ("heart_a", "red-heart")
278
- for i in range(4)
279
- ]
280
- tiny_keys = {(tuple(p.vibes), p.min_agents) for p in tiny_protocols}
281
- existing = [p for p in assembler.protocols if (tuple(p.vibes), p.min_agents) not in tiny_keys]
282
- assembler.protocols = [*tiny_protocols, *existing]
283
-
284
-
285
- class VibeCheckMin2Variant(MissionVariant):
286
- name: str = "vibe_check_min_2"
287
- description: str = "Require at least 2 heart vibes to craft a heart."
288
- min_vibes: int = 2
289
-
290
- @override
291
- def modify_env(self, mission, env):
292
- assembler = env.game.objects["assembler"]
293
- if not isinstance(assembler, AssemblerConfig):
294
- raise TypeError("Expected 'assembler' to be AssemblerConfig")
295
-
296
- filtered: list[ProtocolConfig] = []
297
- for proto in assembler.protocols:
298
- # Keep non-heart protocols as-is (e.g., gear recipes)
299
- if proto.output_resources.get("heart", 0) == 0:
300
- filtered.append(proto)
301
- continue
302
- # Keep only heart protocols that require >= 2 'heart' vibes
303
- if len(proto.vibes) >= 2 and all(v == "heart_a" for v in proto.vibes):
304
- filtered.append(proto)
305
- assembler.protocols = filtered
75
+ env.game.obs.global_obs.compass = True
306
76
 
307
77
 
308
78
  class Small50Variant(MissionVariant):
@@ -319,153 +89,6 @@ class Small50Variant(MissionVariant):
319
89
  env.game.map_builder = map_builder.model_copy(update={"width": 50, "height": 50})
320
90
 
321
91
 
322
- class InventoryHeartTuneVariant(MissionVariant):
323
- name: str = "inventory_heart_tune"
324
- description: str = "Tune starting agent inventory to N hearts worth of inputs; optional heart capacity."
325
- hearts: int = 1
326
- heart_capacity: int | None = None
327
-
328
- @override
329
- def modify_env(self, mission, env) -> None:
330
- hearts = max(0, int(self.hearts))
331
- if hearts == 0 and self.heart_capacity is None:
332
- return
333
-
334
- heart_cost = mission.assembler.first_heart_cost
335
- per_heart = {
336
- "carbon": heart_cost,
337
- "oxygen": heart_cost,
338
- "germanium": max(heart_cost // 10, 1),
339
- "silicon": 3 * heart_cost,
340
- "energy": 0,
341
- }
342
-
343
- if hearts > 0:
344
- agent_cfg = env.game.agent
345
- agent_cfg.inventory.initial = dict(agent_cfg.inventory.initial)
346
-
347
- def _limit_for(resource: str) -> int:
348
- return agent_cfg.inventory.get_limit(resource)
349
-
350
- for resource_name, per_heart_value in per_heart.items():
351
- current = int(agent_cfg.inventory.initial.get(resource_name, 0))
352
- target = current + per_heart_value * hearts
353
- cap = _limit_for(resource_name)
354
- agent_cfg.inventory.initial[resource_name] = min(cap, target)
355
-
356
- if self.heart_capacity is not None:
357
- agent_cfg = env.game.agent
358
- hearts_limit = agent_cfg.inventory.limits.get("heart")
359
- if hearts_limit is None:
360
- hearts_limit = ResourceLimitsConfig(limit=self.heart_capacity, resources=["heart"])
361
- hearts_limit.limit = max(int(hearts_limit.limit), int(self.heart_capacity))
362
- agent_cfg.inventory.limits["heart"] = hearts_limit
363
-
364
-
365
- class ChestHeartTuneVariant(MissionVariant):
366
- name: str = "chest_heart_tune"
367
- description: str = "Tune chest starting inventory to N hearts worth of inputs."
368
- hearts: int = 2
369
-
370
- @override
371
- def modify_env(self, mission, env) -> None:
372
- hearts = max(0, int(self.hearts))
373
- if hearts == 0:
374
- return
375
- heart_cost = mission.assembler.first_heart_cost
376
- per_heart = {
377
- "carbon": heart_cost,
378
- "oxygen": heart_cost,
379
- "germanium": max(heart_cost // 10, 1),
380
- "silicon": 3 * heart_cost,
381
- }
382
- chest_cfg = env.game.objects["chest"]
383
- if not isinstance(chest_cfg, ChestConfig):
384
- raise TypeError("Expected 'chest' to be ChestConfig")
385
- start = dict(chest_cfg.inventory.initial)
386
- for k, v in per_heart.items():
387
- start[k] = start.get(k, 0) + v * hearts
388
- chest_cfg.inventory.initial = start
389
-
390
-
391
- class ExtractorHeartTuneVariant(MissionVariant):
392
- name: str = "extractor_heart_tune"
393
- description: str = "Tune extractors for N hearts production capability."
394
- hearts: int = 1
395
-
396
- @override
397
- def modify_mission(self, mission):
398
- hearts = max(0, int(self.hearts))
399
- if hearts == 0:
400
- return
401
- heart_cost = mission.assembler.first_heart_cost
402
- one_heart = {
403
- "carbon": heart_cost,
404
- "oxygen": heart_cost,
405
- "germanium": max(heart_cost // 10, 1),
406
- "silicon": 3 * heart_cost,
407
- }
408
-
409
- # Carbon per-use depends on efficiency
410
- carbon_per_use = max(1, 4 * mission.carbon_extractor.efficiency // 100)
411
- carbon_needed = one_heart["carbon"] * hearts
412
- mission.carbon_extractor.max_uses = (carbon_needed + carbon_per_use - 1) // carbon_per_use
413
-
414
- # Oxygen is 20 per use
415
- oxygen_per_use = 20
416
- oxygen_needed = one_heart["oxygen"] * hearts
417
- mission.oxygen_extractor.max_uses = (oxygen_needed + oxygen_per_use - 1) // oxygen_per_use
418
-
419
- # Silicon is ~25 per use (scaled by efficiency); silicon extractor divides by 10 internally
420
- silicon_per_use = max(1, int(25 * mission.silicon_extractor.efficiency // 100))
421
- silicon_needed = one_heart["silicon"] * hearts
422
- silicon_uses = (silicon_needed + silicon_per_use - 1) // silicon_per_use
423
- mission.silicon_extractor.max_uses = max(1, silicon_uses * 10)
424
-
425
- # Germanium: fixed one use producing all required
426
- mission.germanium_extractor.efficiency = int(one_heart["germanium"] * hearts)
427
-
428
-
429
- class CyclicalUnclipVariant(MissionVariant):
430
- name: str = "cyclical_unclip"
431
- description: str = "Required resources for unclipping recipes are cyclical. \
432
- So Germanium extractors require silicon-based unclipping recipes."
433
-
434
- @override
435
- def modify_env(self, mission, env):
436
- if env.game.clipper is not None:
437
- env.game.clipper.unclipping_protocols = [
438
- ProtocolConfig(input_resources={"scrambler": 1}, cooldown=1),
439
- ProtocolConfig(input_resources={"resonator": 1}, cooldown=1),
440
- ProtocolConfig(input_resources={"modulator": 1}, cooldown=1),
441
- ProtocolConfig(input_resources={"decoder": 1}, cooldown=1),
442
- ]
443
-
444
-
445
- class ClipHubStationsVariant(MissionVariant):
446
- name: str = "clip_hub_stations"
447
- description: str = "Clip the specified base stations (by name)."
448
- # Valid names: "carbon_extractor", "oxygen_extractor", "germanium_extractor", "silicon_extractor", "charger"
449
- clip: list[str] = ["carbon_extractor", "oxygen_extractor", "germanium_extractor", "silicon_extractor", "charger"]
450
-
451
- @override
452
- def modify_mission(self, mission):
453
- for station_name in self.clip:
454
- station = getattr(mission, station_name, None)
455
- if station is not None:
456
- station.start_clipped = True
457
-
458
-
459
- class ClipPeriodOnVariant(MissionVariant):
460
- name: str = "clip_period_on"
461
- description: str = "Enable global clipping with a small non-zero clip period."
462
- clip_period: int = 50
463
-
464
- @override
465
- def modify_mission(self, mission):
466
- mission.clip_period = self.clip_period
467
-
468
-
469
92
  # Biome variants (weather) for procedural maps
470
93
  class DesertVariant(MachinaArenaVariant):
471
94
  name: str = "desert"
@@ -498,7 +121,6 @@ class CityVariant(MachinaArenaVariant):
498
121
  node.density_scale = 1.0
499
122
  node.biome_count = 1
500
123
  node.max_biome_zone_fraction = 0.95
501
- # Tighten the city grid itself
502
124
 
503
125
 
504
126
  class CavesVariant(MachinaArenaVariant):
@@ -547,34 +169,6 @@ class DistantResourcesVariant(MachinaArenaVariant):
547
169
  node.distribution = DistributionConfig(type=DistributionType.UNIFORM)
548
170
 
549
171
 
550
- class SingleUseSwarmVariant(MissionVariant):
551
- name: str = "single_use_swarm"
552
- description: str = "Everything is single use; agents must fan out and reconverge."
553
- building_coverage: float = 0.03
554
-
555
- @override
556
- def modify_mission(self, mission):
557
- # Make each extractor single-use
558
- for res in ("carbon", "oxygen", "silicon"):
559
- extractor = getattr(mission, f"{res}_extractor", None)
560
- if extractor is not None:
561
- extractor.max_uses = 1
562
-
563
- @override
564
- def modify_env(self, mission, env):
565
- # Ensure charger is also single-use (its Config defaults to unlimited)
566
- charger = env.game.objects.get("charger")
567
- if isinstance(charger, AssemblerConfig):
568
- charger.max_uses = 1
569
-
570
- # Increase building coverage a bit to create many single-use points
571
- map_builder = getattr(env.game, "map_builder", None)
572
- instance = getattr(map_builder, "instance", None)
573
- if instance is not None and hasattr(instance, "building_coverage"):
574
- current = float(getattr(instance, "building_coverage", 0.01))
575
- instance.building_coverage = max(current, float(self.building_coverage))
576
-
577
-
578
172
  class QuadrantBuildingsVariant(MachinaArenaVariant):
579
173
  name: str = "quadrant_buildings"
580
174
  description: str = "Place buildings in the four quadrants of the map."
@@ -635,35 +229,6 @@ class EmptyBaseVariant(BaseHubVariant):
635
229
  node.corner_bundle = "custom"
636
230
 
637
231
 
638
- class AssemblerDrawsFromChestsVariant(BaseHubVariant):
639
- name: str = "assembler_draws_from_chests"
640
- description: str = "Assembler draws from chests."
641
-
642
- # It would be better if this were configurable, but we use variants in places where that's hard.
643
- # This needs to not overlap with the default (heart) chest.
644
- chest_distance: int = 2
645
-
646
- @override
647
- def modify_node(self, node):
648
- node.cross_objects = ["chest_carbon", "chest_oxygen", "chest_germanium", "chest_silicon"]
649
- node.cross_bundle = "custom"
650
- node.cross_distance = self.chest_distance
651
-
652
- @override
653
- def modify_env(self, mission, env):
654
- super().modify_env(mission, env)
655
- assembler = env.game.objects["assembler"]
656
- assert isinstance(assembler, AssemblerConfig)
657
- assembler.chest_search_distance = self.chest_distance
658
- chest = env.game.objects["chest"]
659
- assert isinstance(chest, ChestConfig)
660
- chest.vibe_transfers = {
661
- "default": {
662
- "heart": 255,
663
- }
664
- }
665
-
666
-
667
232
  class BalancedCornersVariant(MachinaArenaVariant):
668
233
  """Enable corner balancing to ensure fair spawn distances."""
669
234
 
@@ -710,46 +275,30 @@ class SharedRewardsVariant(MissionVariant):
710
275
  @override
711
276
  def modify_env(self, mission, env):
712
277
  num_cogs = mission.num_cogs if mission.num_cogs is not None else mission.site.min_cogs
713
- rewards = dict(env.game.agent.rewards.stats)
714
- rewards["chest.heart.deposited_by_agent"] = 0
715
- rewards["chest.heart.amount"] = 1 / num_cogs
716
- env.game.agent.rewards.stats = rewards
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)
717
282
 
718
283
 
719
284
  # TODO - validate that all variant names are unique
720
285
  VARIANTS: list[MissionVariant] = [
721
- AssemblerDrawsFromChestsVariant(),
722
286
  CavesVariant(),
723
- ChestHeartTuneVariant(),
724
287
  CityVariant(),
725
- ClipHubStationsVariant(),
726
- ClipPeriodOnVariant(),
727
288
  CompassVariant(),
728
- CyclicalUnclipVariant(),
729
289
  DarkSideVariant(),
730
290
  DesertVariant(),
731
291
  EmptyBaseVariant(),
732
292
  EnergizedVariant(),
733
- ExtractorHeartTuneVariant(),
734
293
  ForestVariant(),
735
- HeartChorusVariant(),
736
- InventoryHeartTuneVariant(),
737
- LonelyHeartVariant(),
738
- MinedOutVariant(),
739
294
  PackRatVariant(),
740
295
  QuadrantBuildingsVariant(),
741
- ResourceBottleneckVariant(),
742
296
  RoughTerrainVariant(),
743
297
  SharedRewardsVariant(),
744
298
  SingleResourceUniformVariant(),
745
- SingleToolUnclipVariant(),
746
299
  Small50Variant(),
747
- SolarFlareVariant(),
748
300
  SuperChargedVariant(),
749
301
  TraderVariant(),
750
- TinyHeartProtocolsVariant(),
751
- TrainingVariant(),
752
- VibeCheckMin2Variant(),
753
302
  *DIFFICULTY_VARIANTS,
754
303
  ]
755
304
 
cogames/device.py CHANGED
@@ -15,10 +15,18 @@ def resolve_training_device(console: Console, requested: str) -> torch.device:
15
15
  return False
16
16
  return torch.cuda.is_available()
17
17
 
18
+ def mps_usable() -> bool:
19
+ mps_backend = getattr(torch.backends, "mps", None)
20
+ if mps_backend is None or not mps_backend.is_built():
21
+ return False
22
+ return mps_backend.is_available()
23
+
18
24
  if normalized == "auto":
19
25
  if cuda_usable():
20
26
  return torch.device("cuda")
21
- console.print("[yellow]CUDA not available; falling back to CPU for training.[/yellow]")
27
+ if mps_usable():
28
+ return torch.device("mps")
29
+ console.print("[yellow]CUDA/MPS not available; falling back to CPU for training.[/yellow]")
22
30
  return torch.device("cpu")
23
31
 
24
32
  try:
@@ -31,4 +39,8 @@ def resolve_training_device(console: Console, requested: str) -> torch.device:
31
39
  console.print("[yellow]CUDA requested but unavailable. Training will run on CPU instead.[/yellow]")
32
40
  return torch.device("cpu")
33
41
 
42
+ if candidate.type == "mps" and not mps_usable():
43
+ console.print("[yellow]MPS requested but unavailable. Training will run on CPU instead.[/yellow]")
44
+ return torch.device("cpu")
45
+
34
46
  return candidate