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,476 +0,0 @@
1
- """
2
- UnclippingAgent - Extends SimpleBaselineAgent with unclipping capabilities.
3
-
4
- This agent can detect clipped extractors and craft unclip items to restore them.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from dataclasses import dataclass, field
10
- from typing import TYPE_CHECKING, Optional, cast
11
-
12
- from mettagrid.policy.policy import MultiAgentPolicy, StatefulAgentPolicy
13
- from mettagrid.policy.policy_env_interface import PolicyEnvInterface
14
- from mettagrid.simulator import Action
15
- from mettagrid.simulator.interface import AgentObservation
16
-
17
- from .baseline_agent import BaselineAgentPolicyImpl
18
- from .types import (
19
- BaselineHyperparameters,
20
- ExtractorInfo,
21
- Phase,
22
- SimpleAgentState,
23
- )
24
- from .utils import use_object_at
25
-
26
- if TYPE_CHECKING:
27
- pass
28
-
29
-
30
- @dataclass
31
- class UnclippingHyperparameters(BaselineHyperparameters):
32
- """Extends baseline hyperparameters with unclipping-specific parameters."""
33
-
34
- # Unclipping strategy
35
- unclip_priority_order: tuple[str, ...] = ("oxygen", "silicon", "carbon", "germanium") # Order to unclip resources
36
- craft_unclip_items_early: bool = True # Craft unclip items proactively vs on-demand
37
-
38
-
39
- @dataclass
40
- class UnclippingAgentState(SimpleAgentState):
41
- """Extended state for unclipping agent.
42
-
43
- Note: decoder, modulator, resonator, scrambler are already defined in SimpleAgentState.
44
- """
45
-
46
- # Unclip tracking
47
- blocked_by_clipped_extractor: Optional[tuple[int, int]] = None
48
- unclip_target_resource: Optional[str] = None # Which resource is clipped
49
-
50
- # Discovered unclip recipes from observations (resource_type -> (unclip_item, craft_recipe))
51
- # e.g., "oxygen" -> ("decoder", {"carbon": 1})
52
- unclip_recipes: dict[str, tuple[str, dict[str, int]]] = field(default_factory=dict)
53
-
54
- # Craft recipes for unclip items (item_name -> craft_recipe)
55
- # e.g., "decoder" -> {"carbon": 1}
56
- # Initialized from assembler protocols
57
- unclip_craft_recipes: dict[str, dict[str, int]] = field(default_factory=dict)
58
-
59
-
60
- class UnclippingAgentPolicyImpl(BaselineAgentPolicyImpl):
61
- """
62
- Agent that can unclip extractors by crafting and using unclip items.
63
-
64
- Unclip item mapping:
65
- - decoder (from carbon) unclips oxygen extractors
66
- - modulator (from oxygen) unclips carbon extractors
67
- - resonator (from silicon) unclips germanium extractors
68
- - scrambler (from germanium) unclips silicon extractors
69
- """
70
-
71
- def __init__(
72
- self,
73
- policy_env_info: PolicyEnvInterface,
74
- agent_id: int,
75
- hyperparams: UnclippingHyperparameters,
76
- ):
77
- super().__init__(policy_env_info, agent_id, hyperparams)
78
- self._priority_order = hyperparams.unclip_priority_order
79
-
80
- def initial_agent_state(self) -> UnclippingAgentState:
81
- """Create initial state for unclipping agent."""
82
- # Get the base state from parent class
83
- base_state = super().initial_agent_state()
84
-
85
- # Initialize unclip recipes from assembler protocols
86
- # Map: unclip_item -> craft_recipe (e.g., "decoder" -> {"carbon": 1})
87
- unclip_craft_recipes = {}
88
- for protocol in self._policy_env_info.assembler_protocols:
89
- for output_item, output_amount in protocol.output_resources.items():
90
- if output_amount > 0 and output_item in ("decoder", "modulator", "resonator", "scrambler"):
91
- craft_recipe = dict(protocol.input_resources)
92
- craft_recipe.pop("energy", None)
93
- unclip_craft_recipes[output_item] = craft_recipe
94
-
95
- # Create unclipping state by extending base state
96
- # Convert base_state dict to UnclippingAgentState with additional fields
97
- return UnclippingAgentState(
98
- **base_state.__dict__,
99
- unclip_recipes={}, # Will be discovered from clipped extractor observations
100
- unclip_craft_recipes=unclip_craft_recipes, # Store craft recipes separately
101
- )
102
-
103
- def _discover_unclip_recipe(self, s: UnclippingAgentState, parsed_observation) -> None:
104
- """
105
- Discover unclip recipes from clipped extractor observations.
106
-
107
- When we observe a clipped extractor, its protocol_outputs tell us what unclip item is needed.
108
- We then need to observe the assembler with the correct vibe to learn how to craft that item.
109
- """
110
- for _pos, obj_state in parsed_observation.nearby_objects.items():
111
- obj_name = obj_state.name.lower()
112
- # Check if this is a clipped extractor
113
- if "extractor" in obj_name and obj_state.clipped > 0:
114
- # Extract resource type from name (e.g., "oxygen_extractor" -> "oxygen")
115
- resource_type = obj_name.replace("_extractor", "")
116
- # Check if we already know this recipe
117
- if resource_type in s.unclip_recipes:
118
- continue
119
-
120
- # Read the unclip item from protocol_inputs (unclipping protocols have inputs, not outputs!)
121
- if obj_state.protocol_inputs:
122
- # The clipped extractor's protocol_inputs shows what unclip item is needed
123
- for item_name, amount in obj_state.protocol_inputs.items():
124
- if amount > 0 and item_name in ("decoder", "modulator", "resonator", "scrambler"):
125
- # Get the craft recipe from our initialized recipes
126
- craft_recipe = s.unclip_craft_recipes.get(item_name, {})
127
- s.unclip_recipes[resource_type] = (item_name, craft_recipe)
128
- break
129
-
130
- # No need to discover craft recipes from assembler - we already have them from init!
131
-
132
- def _get_unclip_item_name(self, s: UnclippingAgentState, clipped_resource: str) -> Optional[str]:
133
- recipe = s.unclip_recipes.get(clipped_resource)
134
- if recipe is None:
135
- return None
136
- item_name, _ = recipe
137
- return item_name
138
-
139
- def _has_unclip_item(self, s: UnclippingAgentState) -> bool:
140
- """Check if agent has the unclip item for the blocked extractor."""
141
- if s.blocked_by_clipped_extractor is None:
142
- return False
143
-
144
- if s.unclip_target_resource is None:
145
- return False
146
-
147
- item_name = self._get_unclip_item_name(s, s.unclip_target_resource)
148
- if item_name is None:
149
- return False
150
-
151
- has_item = getattr(s, item_name, 0) > 0
152
- return has_item
153
-
154
- def _get_unclip_info(
155
- self, s: UnclippingAgentState, resource: Optional[str]
156
- ) -> Optional[tuple[str, dict[str, int]]]:
157
- """Get unclip item name and craft recipe for a resource type."""
158
- if resource is None:
159
- return None
160
- return s.unclip_recipes.get(resource)
161
-
162
- def _clear_unclip_state(self, s: UnclippingAgentState) -> None:
163
- s.blocked_by_clipped_extractor = None
164
- s.unclip_target_resource = None
165
-
166
- def _set_unclip_state(self, s: UnclippingAgentState, resource_type: str, extractor: ExtractorInfo) -> None:
167
- # Only set unclip state if we've discovered the recipe for this resource
168
- if resource_type not in s.unclip_recipes:
169
- return
170
- s.blocked_by_clipped_extractor = extractor.position
171
- s.unclip_target_resource = resource_type
172
-
173
- def _get_vibe_for_phase(self, phase: Phase, state: UnclippingAgentState) -> str:
174
- """Override to set correct vibe for CRAFT_UNCLIP and UNCLIP phases."""
175
- # For crafting unclip items at the assembler, use "gear" vibe
176
- if phase == Phase.CRAFT_UNCLIP:
177
- return "gear"
178
-
179
- # For unclipping extractors, use "gear" vibe
180
- if phase == Phase.UNCLIP:
181
- return "gear"
182
-
183
- # Otherwise use baseline logic
184
- return super()._get_vibe_for_phase(phase, state)
185
-
186
- def _update_phase(self, s: UnclippingAgentState) -> None:
187
- """Override to add unclipping phase priorities before gathering."""
188
- old_phase = s.phase
189
-
190
- # Priority 1-2: Recharge and Deliver (handled by parent)
191
- if s.energy < self._hyperparams.recharge_threshold_low or s.phase == Phase.RECHARGE or s.hearts > 0:
192
- super()._update_phase(s)
193
- if old_phase != s.phase:
194
- s.cached_path = None
195
- s.cached_path_target = None
196
- return
197
-
198
- # Priority 3: Assemble if possible (and not currently blocked by clipped resource)
199
- heart_recipe = s.heart_recipe or {}
200
- can_assemble = all(
201
- getattr(s, res, 0) >= heart_recipe.get(res, 0) for res in ("carbon", "oxygen", "germanium", "silicon")
202
- )
203
-
204
- if can_assemble and s.blocked_by_clipped_extractor is None:
205
- if s.phase != Phase.ASSEMBLE:
206
- s.phase = Phase.ASSEMBLE
207
- s.pending_use_resource = None
208
- s.pending_use_amount = 0
209
- s.waiting_at_extractor = None
210
- if old_phase != s.phase:
211
- s.cached_path = None
212
- s.cached_path_target = None
213
- return
214
-
215
- # Priority 4: Unclipping workflow (before gathering)
216
- if s.blocked_by_clipped_extractor is not None and s.unclip_target_resource is not None:
217
- # First, check if the extractor is still clipped
218
- target_pos = s.blocked_by_clipped_extractor
219
- extractors = s.extractors.get(s.unclip_target_resource, [])
220
- target_extractor = None
221
- for ext in extractors:
222
- if ext.position == target_pos:
223
- target_extractor = ext
224
- break
225
-
226
- # If extractor is no longer clipped or not found, clear unclip state
227
- if target_extractor is None or not target_extractor.clipped:
228
- self._clear_unclip_state(s)
229
- # Fall through to normal phase logic
230
- else:
231
- # Still clipped, continue unclipping workflow
232
- info = self._get_unclip_info(s, s.unclip_target_resource)
233
-
234
- if info is not None:
235
- item_name, craft_recipe = info
236
- item_count = getattr(s, item_name, 0)
237
-
238
- if item_count > 0:
239
- if s.phase != Phase.UNCLIP:
240
- s.phase = Phase.UNCLIP
241
- return
242
- elif craft_recipe and all(getattr(s, res, 0) >= amt for res, amt in craft_recipe.items()):
243
- if s.phase != Phase.CRAFT_UNCLIP:
244
- s.phase = Phase.CRAFT_UNCLIP
245
- return
246
- else:
247
- # Need to gather craft resources
248
- if s.phase != Phase.GATHER:
249
- s.phase = Phase.GATHER
250
- if old_phase != s.phase:
251
- s.cached_path = None
252
- s.cached_path_target = None
253
- return
254
-
255
- # Priority 5: Default to GATHER (handled by parent)
256
- super()._update_phase(s)
257
- if old_phase != s.phase:
258
- s.cached_path = None
259
- s.cached_path_target = None
260
-
261
- def _find_any_needed_extractor(self, s: UnclippingAgentState) -> Optional[tuple[ExtractorInfo, str]]:
262
- """
263
- Override to detect clipped extractors and trigger unclipping workflow.
264
- """
265
- # Try baseline logic first
266
- result = super()._find_any_needed_extractor(s)
267
- if result is not None:
268
- self._clear_unclip_state(s)
269
- return result
270
-
271
- deficits = self._calculate_deficits(s)
272
-
273
- def distance(pos: tuple[int, int]) -> int:
274
- return abs(pos[0] - s.row) + abs(pos[1] - s.col)
275
-
276
- # If we are already blocked, ensure we gather craft resources if needed
277
- if s.blocked_by_clipped_extractor is not None:
278
- info = self._get_unclip_info(s, s.unclip_target_resource)
279
- if info is not None:
280
- item_name, craft_recipe = info
281
- if getattr(s, item_name, 0) > 0:
282
- return None # Ready to unclip
283
-
284
- # Need craft resources before crafting
285
- if craft_recipe:
286
- for craft_resource, needed_amount in craft_recipe.items():
287
- if getattr(s, craft_resource, 0) < needed_amount:
288
- craft_extractors = s.extractors.get(craft_resource, [])
289
- available = [e for e in craft_extractors if not e.clipped and e.remaining_uses > 0]
290
- if available:
291
- nearest = min(available, key=lambda e: distance(e.position))
292
- return (nearest, craft_resource)
293
-
294
- clipped = [e for e in craft_extractors if e.clipped and e.remaining_uses > 0]
295
- if clipped:
296
- nearest = min(clipped, key=lambda e: distance(e.position))
297
- self._set_unclip_state(s, craft_resource, nearest)
298
- return None
299
-
300
- # Look for deficits that are blocked by clipped extractors
301
- for resource_type in self._priority_order:
302
- if deficits.get(resource_type, 0) <= 0:
303
- continue
304
-
305
- extractors = s.extractors.get(resource_type, [])
306
- if not extractors:
307
- continue
308
-
309
- available = [e for e in extractors if not e.clipped and e.remaining_uses > 0]
310
- if available:
311
- nearest = min(available, key=lambda e: distance(e.position))
312
- return (nearest, resource_type)
313
-
314
- clipped = [e for e in extractors if e.clipped and e.remaining_uses > 0]
315
- if clipped:
316
- nearest = min(clipped, key=lambda e: distance(e.position))
317
- self._set_unclip_state(s, resource_type, nearest)
318
- return None
319
-
320
- # No special action
321
- self._clear_unclip_state(s)
322
- return None
323
-
324
- def step_with_state(
325
- self, obs: AgentObservation, state: UnclippingAgentState
326
- ) -> tuple[Action, UnclippingAgentState]:
327
- """Override to discover unclip recipes from observations."""
328
- # First, let the base class handle observation parsing and state updates
329
- action, updated_state = super().step_with_state(obs, state)
330
- state = cast(UnclippingAgentState, updated_state)
331
-
332
- # Now discover unclip recipes from the parsed observation
333
- # We need to parse again to get the nearby_objects
334
- parsed = self.parse_observation(state, obs)
335
- self._discover_unclip_recipe(state, parsed)
336
-
337
- return action, state
338
-
339
- def _execute_phase(self, s: UnclippingAgentState) -> Action:
340
- """Override to handle CRAFT_UNCLIP and UNCLIP phases."""
341
- if s.phase == Phase.CRAFT_UNCLIP:
342
- return self._do_craft_unclip(s)
343
- elif s.phase == Phase.UNCLIP:
344
- return self._do_unclip(s)
345
- # All other phases handled by parent (GATHER, ASSEMBLE, DELIVER, RECHARGE)
346
- return super()._execute_phase(s)
347
-
348
- def _do_craft_unclip(self, s: UnclippingAgentState) -> Action:
349
- """Craft unclip item at assembler."""
350
- info = self._get_unclip_info(s, s.unclip_target_resource)
351
- if info is None:
352
- self._clear_unclip_state(s)
353
- s.phase = Phase.GATHER
354
- return self._actions.noop.Noop()
355
-
356
- item_name, craft_recipe = info
357
-
358
- # Check if we have all craft resources
359
- if craft_recipe and not all(getattr(s, res, 0) >= amt for res, amt in craft_recipe.items()):
360
- # Need to gather craft resources first
361
- s.phase = Phase.GATHER
362
- return self._actions.noop.Noop()
363
-
364
- # Explore until we find assembler
365
- explore_action = self._explore_until(
366
- s, condition=lambda: s.stations.get("assembler") is not None, reason="Need assembler for crafting"
367
- )
368
- if explore_action is not None:
369
- return explore_action
370
-
371
- # Vibe is automatically set by _get_vibe_for_phase to the input resource (e.g., "carbon" for decoder)
372
-
373
- assembler = s.stations.get("assembler")
374
- if assembler is None:
375
- return self._actions.noop.Noop()
376
-
377
- ar, ac = assembler
378
- dr = abs(s.row - ar)
379
- dc = abs(s.col - ac)
380
- is_adjacent = (dr == 1 and dc == 0) or (dr == 0 and dc == 1)
381
-
382
- if is_adjacent:
383
- return use_object_at(
384
- s, assembler, actions=self._actions, move_deltas=self._move_deltas, using_for=f"craft_{item_name}"
385
- )
386
-
387
- return self._move_towards(s, assembler, reach_adjacent=True)
388
-
389
- def _do_unclip(self, s: UnclippingAgentState) -> Action:
390
- """Use unclip item on clipped extractor."""
391
- if s.blocked_by_clipped_extractor is None:
392
- s.phase = Phase.GATHER
393
- self._clear_unclip_state(s)
394
- return self._actions.noop.Noop()
395
-
396
- info = self._get_unclip_info(s, s.unclip_target_resource)
397
- if info is None:
398
- self._clear_unclip_state(s)
399
- s.phase = Phase.GATHER
400
- return self._actions.noop.Noop()
401
-
402
- item_name, _ = info
403
- if getattr(s, item_name, 0) <= 0:
404
- # Lost the item before reaching extractor
405
- self._clear_unclip_state(s)
406
- s.phase = Phase.GATHER
407
- return self._actions.noop.Noop()
408
-
409
- # Navigate to clipped extractor
410
- target = s.blocked_by_clipped_extractor
411
- tr, tc = target
412
- dr = abs(s.row - tr)
413
- dc = abs(s.col - tc)
414
- is_at_target = dr == 0 and dc == 0
415
- is_adjacent = (dr == 1 and dc == 0) or (dr == 0 and dc == 1)
416
-
417
- if is_at_target:
418
- # Already on the extractor - it should be unclipped now
419
- # Wait for next step to verify and clear state
420
- return self._actions.noop.Noop()
421
-
422
- if is_adjacent:
423
- # Adjacent to clipped extractor - use it to unclip (like using any other object)
424
- action = use_object_at(
425
- s,
426
- target,
427
- actions=self._actions,
428
- move_deltas=self._move_deltas,
429
- using_for=f"unclip_{s.unclip_target_resource}",
430
- )
431
- # Don't clear unclip state yet - wait until next step to verify it worked
432
- # The state will be cleared in _update_phase when we see the extractor is unclipped
433
- return action
434
-
435
- # Not adjacent yet, move towards it
436
- return self._move_towards(s, target, reach_adjacent=True)
437
-
438
-
439
- # ============================================================================
440
- # Policy Wrapper Class
441
- # ============================================================================
442
-
443
-
444
- class UnclippingPolicy(MultiAgentPolicy):
445
- """Multi-agent policy wrapper for UnclippingAgent.
446
-
447
- This class wraps UnclippingAgent to work with the policy interface.
448
- It handles multiple agents, each with their own UnclippingAgent instance.
449
- """
450
-
451
- short_names = ["ladybug"]
452
-
453
- def __init__(
454
- self,
455
- policy_env_info: PolicyEnvInterface,
456
- device: str = "cpu",
457
- hyperparams: Optional[UnclippingHyperparameters] = None,
458
- ):
459
- super().__init__(policy_env_info, device=device)
460
- self._agent_policies: dict[int, StatefulAgentPolicy[UnclippingAgentState]] = {}
461
- self._hyperparams = hyperparams or UnclippingHyperparameters()
462
-
463
- def agent_policy(self, agent_id: int) -> StatefulAgentPolicy[UnclippingAgentState]:
464
- if agent_id not in self._agent_policies:
465
- # UnclippingAgentPolicyImpl uses UnclippingAgentState but inherits from
466
- # BaselineAgentPolicyImpl typed with SimpleAgentState, requiring a cast
467
- policy = cast(
468
- StatefulAgentPolicy[UnclippingAgentState],
469
- StatefulAgentPolicy(
470
- UnclippingAgentPolicyImpl(self._policy_env_info, agent_id, self._hyperparams),
471
- self._policy_env_info,
472
- agent_id=agent_id,
473
- ),
474
- )
475
- self._agent_policies[agent_id] = policy
476
- return self._agent_policies[agent_id]