cogames-agents 0.0.0.7__cp312-cp312-macosx_11_0_arm64.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 (128) hide show
  1. cogames_agents/__init__.py +0 -0
  2. cogames_agents/evals/__init__.py +5 -0
  3. cogames_agents/evals/planky_evals.py +415 -0
  4. cogames_agents/policy/__init__.py +0 -0
  5. cogames_agents/policy/evolution/__init__.py +0 -0
  6. cogames_agents/policy/evolution/cogsguard/__init__.py +0 -0
  7. cogames_agents/policy/evolution/cogsguard/evolution.py +695 -0
  8. cogames_agents/policy/evolution/cogsguard/evolutionary_coordinator.py +540 -0
  9. cogames_agents/policy/nim_agents/__init__.py +20 -0
  10. cogames_agents/policy/nim_agents/agents.py +98 -0
  11. cogames_agents/policy/nim_agents/bindings/generated/libnim_agents.dylib +0 -0
  12. cogames_agents/policy/nim_agents/bindings/generated/nim_agents.py +215 -0
  13. cogames_agents/policy/nim_agents/cogsguard_agents.nim +555 -0
  14. cogames_agents/policy/nim_agents/cogsguard_align_all_agents.nim +569 -0
  15. cogames_agents/policy/nim_agents/common.nim +1054 -0
  16. cogames_agents/policy/nim_agents/install.sh +1 -0
  17. cogames_agents/policy/nim_agents/ladybug_agent.nim +954 -0
  18. cogames_agents/policy/nim_agents/nim_agents.nim +68 -0
  19. cogames_agents/policy/nim_agents/nim_agents.nims +14 -0
  20. cogames_agents/policy/nim_agents/nimby.lock +3 -0
  21. cogames_agents/policy/nim_agents/racecar_agents.nim +844 -0
  22. cogames_agents/policy/nim_agents/random_agents.nim +68 -0
  23. cogames_agents/policy/nim_agents/test_agents.py +53 -0
  24. cogames_agents/policy/nim_agents/thinky_agents.nim +677 -0
  25. cogames_agents/policy/nim_agents/thinky_eval.py +230 -0
  26. cogames_agents/policy/scripted_agent/README.md +360 -0
  27. cogames_agents/policy/scripted_agent/__init__.py +0 -0
  28. cogames_agents/policy/scripted_agent/baseline_agent.py +1031 -0
  29. cogames_agents/policy/scripted_agent/cogas/__init__.py +5 -0
  30. cogames_agents/policy/scripted_agent/cogas/context.py +68 -0
  31. cogames_agents/policy/scripted_agent/cogas/entity_map.py +152 -0
  32. cogames_agents/policy/scripted_agent/cogas/goal.py +115 -0
  33. cogames_agents/policy/scripted_agent/cogas/goals/__init__.py +27 -0
  34. cogames_agents/policy/scripted_agent/cogas/goals/aligner.py +160 -0
  35. cogames_agents/policy/scripted_agent/cogas/goals/gear.py +197 -0
  36. cogames_agents/policy/scripted_agent/cogas/goals/miner.py +441 -0
  37. cogames_agents/policy/scripted_agent/cogas/goals/scout.py +40 -0
  38. cogames_agents/policy/scripted_agent/cogas/goals/scrambler.py +174 -0
  39. cogames_agents/policy/scripted_agent/cogas/goals/shared.py +160 -0
  40. cogames_agents/policy/scripted_agent/cogas/goals/stem.py +60 -0
  41. cogames_agents/policy/scripted_agent/cogas/goals/survive.py +100 -0
  42. cogames_agents/policy/scripted_agent/cogas/navigator.py +401 -0
  43. cogames_agents/policy/scripted_agent/cogas/obs_parser.py +238 -0
  44. cogames_agents/policy/scripted_agent/cogas/policy.py +525 -0
  45. cogames_agents/policy/scripted_agent/cogas/trace.py +69 -0
  46. cogames_agents/policy/scripted_agent/cogsguard/CLAUDE.md +517 -0
  47. cogames_agents/policy/scripted_agent/cogsguard/README.md +252 -0
  48. cogames_agents/policy/scripted_agent/cogsguard/__init__.py +74 -0
  49. cogames_agents/policy/scripted_agent/cogsguard/aligned_junction_held_investigation.md +152 -0
  50. cogames_agents/policy/scripted_agent/cogsguard/aligner.py +333 -0
  51. cogames_agents/policy/scripted_agent/cogsguard/behavior_hooks.py +44 -0
  52. cogames_agents/policy/scripted_agent/cogsguard/control_agent.py +323 -0
  53. cogames_agents/policy/scripted_agent/cogsguard/debug_agent.py +533 -0
  54. cogames_agents/policy/scripted_agent/cogsguard/miner.py +589 -0
  55. cogames_agents/policy/scripted_agent/cogsguard/options.py +67 -0
  56. cogames_agents/policy/scripted_agent/cogsguard/parity_metrics.py +36 -0
  57. cogames_agents/policy/scripted_agent/cogsguard/policy.py +1967 -0
  58. cogames_agents/policy/scripted_agent/cogsguard/prereq_trace.py +33 -0
  59. cogames_agents/policy/scripted_agent/cogsguard/role_trace.py +50 -0
  60. cogames_agents/policy/scripted_agent/cogsguard/roles.py +31 -0
  61. cogames_agents/policy/scripted_agent/cogsguard/rollout_trace.py +40 -0
  62. cogames_agents/policy/scripted_agent/cogsguard/scout.py +69 -0
  63. cogames_agents/policy/scripted_agent/cogsguard/scrambler.py +350 -0
  64. cogames_agents/policy/scripted_agent/cogsguard/targeted_agent.py +418 -0
  65. cogames_agents/policy/scripted_agent/cogsguard/teacher.py +224 -0
  66. cogames_agents/policy/scripted_agent/cogsguard/types.py +381 -0
  67. cogames_agents/policy/scripted_agent/cogsguard/v2_agent.py +49 -0
  68. cogames_agents/policy/scripted_agent/common/__init__.py +0 -0
  69. cogames_agents/policy/scripted_agent/common/geometry.py +24 -0
  70. cogames_agents/policy/scripted_agent/common/roles.py +34 -0
  71. cogames_agents/policy/scripted_agent/common/tag_utils.py +48 -0
  72. cogames_agents/policy/scripted_agent/demo_policy.py +242 -0
  73. cogames_agents/policy/scripted_agent/pathfinding.py +126 -0
  74. cogames_agents/policy/scripted_agent/pinky/DESIGN.md +317 -0
  75. cogames_agents/policy/scripted_agent/pinky/__init__.py +5 -0
  76. cogames_agents/policy/scripted_agent/pinky/behaviors/__init__.py +17 -0
  77. cogames_agents/policy/scripted_agent/pinky/behaviors/aligner.py +400 -0
  78. cogames_agents/policy/scripted_agent/pinky/behaviors/base.py +119 -0
  79. cogames_agents/policy/scripted_agent/pinky/behaviors/miner.py +632 -0
  80. cogames_agents/policy/scripted_agent/pinky/behaviors/scout.py +138 -0
  81. cogames_agents/policy/scripted_agent/pinky/behaviors/scrambler.py +433 -0
  82. cogames_agents/policy/scripted_agent/pinky/policy.py +570 -0
  83. cogames_agents/policy/scripted_agent/pinky/services/__init__.py +7 -0
  84. cogames_agents/policy/scripted_agent/pinky/services/map_tracker.py +808 -0
  85. cogames_agents/policy/scripted_agent/pinky/services/navigator.py +864 -0
  86. cogames_agents/policy/scripted_agent/pinky/services/safety.py +189 -0
  87. cogames_agents/policy/scripted_agent/pinky/state.py +299 -0
  88. cogames_agents/policy/scripted_agent/pinky/types.py +138 -0
  89. cogames_agents/policy/scripted_agent/planky/CLAUDE.md +124 -0
  90. cogames_agents/policy/scripted_agent/planky/IMPROVEMENTS.md +160 -0
  91. cogames_agents/policy/scripted_agent/planky/NOTES.md +153 -0
  92. cogames_agents/policy/scripted_agent/planky/PLAN.md +254 -0
  93. cogames_agents/policy/scripted_agent/planky/README.md +214 -0
  94. cogames_agents/policy/scripted_agent/planky/STRATEGY.md +100 -0
  95. cogames_agents/policy/scripted_agent/planky/__init__.py +5 -0
  96. cogames_agents/policy/scripted_agent/planky/context.py +68 -0
  97. cogames_agents/policy/scripted_agent/planky/entity_map.py +152 -0
  98. cogames_agents/policy/scripted_agent/planky/goal.py +107 -0
  99. cogames_agents/policy/scripted_agent/planky/goals/__init__.py +27 -0
  100. cogames_agents/policy/scripted_agent/planky/goals/aligner.py +168 -0
  101. cogames_agents/policy/scripted_agent/planky/goals/gear.py +179 -0
  102. cogames_agents/policy/scripted_agent/planky/goals/miner.py +416 -0
  103. cogames_agents/policy/scripted_agent/planky/goals/scout.py +40 -0
  104. cogames_agents/policy/scripted_agent/planky/goals/scrambler.py +174 -0
  105. cogames_agents/policy/scripted_agent/planky/goals/shared.py +160 -0
  106. cogames_agents/policy/scripted_agent/planky/goals/stem.py +49 -0
  107. cogames_agents/policy/scripted_agent/planky/goals/survive.py +96 -0
  108. cogames_agents/policy/scripted_agent/planky/navigator.py +388 -0
  109. cogames_agents/policy/scripted_agent/planky/obs_parser.py +238 -0
  110. cogames_agents/policy/scripted_agent/planky/policy.py +485 -0
  111. cogames_agents/policy/scripted_agent/planky/tests/__init__.py +0 -0
  112. cogames_agents/policy/scripted_agent/planky/tests/conftest.py +66 -0
  113. cogames_agents/policy/scripted_agent/planky/tests/helpers.py +152 -0
  114. cogames_agents/policy/scripted_agent/planky/tests/test_aligner.py +24 -0
  115. cogames_agents/policy/scripted_agent/planky/tests/test_miner.py +30 -0
  116. cogames_agents/policy/scripted_agent/planky/tests/test_scout.py +15 -0
  117. cogames_agents/policy/scripted_agent/planky/tests/test_scrambler.py +29 -0
  118. cogames_agents/policy/scripted_agent/planky/tests/test_stem.py +36 -0
  119. cogames_agents/policy/scripted_agent/planky/trace.py +69 -0
  120. cogames_agents/policy/scripted_agent/types.py +239 -0
  121. cogames_agents/policy/scripted_agent/unclipping_agent.py +461 -0
  122. cogames_agents/policy/scripted_agent/utils.py +381 -0
  123. cogames_agents/policy/scripted_registry.py +80 -0
  124. cogames_agents/py.typed +0 -0
  125. cogames_agents-0.0.0.7.dist-info/METADATA +98 -0
  126. cogames_agents-0.0.0.7.dist-info/RECORD +128 -0
  127. cogames_agents-0.0.0.7.dist-info/WHEEL +6 -0
  128. cogames_agents-0.0.0.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,632 @@
1
+ """
2
+ Miner behavior for Pinky policy.
3
+
4
+ Miners gather resources from extractors and deposit at aligned buildings.
5
+ Strategy: Mine aggressively, deposit when full, only retreat when critical.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Optional
11
+
12
+ from cogames_agents.policy.scripted_agent.pinky.behaviors.base import (
13
+ Services,
14
+ explore_for_station,
15
+ is_adjacent,
16
+ manhattan_distance,
17
+ )
18
+ from cogames_agents.policy.scripted_agent.pinky.types import (
19
+ DEBUG,
20
+ ROLE_TO_STATION,
21
+ DebugInfo,
22
+ RiskTolerance,
23
+ Role,
24
+ StructureInfo,
25
+ StructureType,
26
+ )
27
+ from mettagrid.simulator import Action
28
+
29
+ if TYPE_CHECKING:
30
+ from cogames_agents.policy.scripted_agent.pinky.state import AgentState
31
+
32
+
33
+ class MinerBehavior:
34
+ """Miner agent: gather resources and deposit at aligned buildings."""
35
+
36
+ role = Role.MINER
37
+ risk_tolerance = RiskTolerance.CONSERVATIVE
38
+
39
+ # How many steps to explore before mining without gear
40
+ EXPLORATION_STEPS = 100
41
+
42
+ # How many ticks to mine/explore before retrying gear station
43
+ GEAR_RETRY_INTERVAL = 100
44
+
45
+ # How often to clear the failed extractors list (allow retry)
46
+ FAILED_EXTRACTOR_RETRY_INTERVAL = 200
47
+
48
+ def act(self, state: AgentState, services: Services) -> Action:
49
+ """Execute miner behavior - move toward extractors.
50
+
51
+ Mining happens automatically when walking into extractor cells.
52
+ Uses observation-relative coordinates to avoid position drift issues.
53
+ """
54
+
55
+ # Periodically clear failed extractors to allow retry (they may have replenished)
56
+ if state.step % self.FAILED_EXTRACTOR_RETRY_INTERVAL == 0:
57
+ state.nav.failed_extractors.clear()
58
+
59
+ # Track cargo changes to detect when extraction stops working (inventory full).
60
+ # If cargo doesn't increase for several steps while we have energy to move,
61
+ # the inventory is likely full. This is more robust than tracking cargo capacity.
62
+ cargo_gained = state.total_cargo > state.prev_total_cargo
63
+ if cargo_gained:
64
+ # Cargo increased - reset counter and clear current target (successfully mined)
65
+ state.steps_without_cargo_gain = 0
66
+ state.nav.steps_at_current_extractor = 0
67
+ # Clear the current target - we'll pick a new one next step
68
+ state.nav.current_extractor_target = None
69
+ elif state.total_cargo == state.prev_total_cargo and state.total_cargo > 0:
70
+ # Cargo unchanged but we have some cargo - increment counter
71
+ state.steps_without_cargo_gain += 1
72
+ elif state.total_cargo < state.prev_total_cargo:
73
+ # Cargo decreased (deposit) - reset counter
74
+ state.steps_without_cargo_gain = 0
75
+
76
+ # Track time at current extractor target to detect empty/stuck extractors
77
+ if state.nav.current_extractor_target is not None:
78
+ target = state.nav.current_extractor_target
79
+ dist = manhattan_distance(state.pos, target)
80
+ if dist <= 1: # At or adjacent to target
81
+ state.nav.steps_at_current_extractor += 1
82
+ # If we've been at this extractor for 5+ steps without cargo gain, mark it as failed
83
+ if state.nav.steps_at_current_extractor >= 5 and not cargo_gained:
84
+ if DEBUG:
85
+ print(
86
+ f"[A{state.agent_id}] MINER: Extractor at {target} appears empty/blocked, "
87
+ f"marking as failed after {state.nav.steps_at_current_extractor} steps"
88
+ )
89
+ state.nav.failed_extractors.add(target)
90
+ state.nav.current_extractor_target = None
91
+ state.nav.steps_at_current_extractor = 0
92
+
93
+ # Update prev_total_cargo for next step
94
+ state.prev_total_cargo = state.total_cargo
95
+
96
+ # Priority 0: Check for stuck patterns and handle escape mode (via navigator)
97
+ escape_action = services.navigator.check_and_handle_escape(state)
98
+ if escape_action:
99
+ debug_info = services.navigator.get_escape_debug_info(state)
100
+ state.debug_info = DebugInfo(**debug_info)
101
+ return escape_action
102
+
103
+ # Priority 1: Critical HP retreat
104
+ if state.hp <= 15:
105
+ if DEBUG:
106
+ print(f"[A{state.agent_id}] MINER: CRITICAL HP={state.hp}, retreating!")
107
+ state.debug_info = DebugInfo(mode="retreat", goal="safety", target_object="safe_zone", signal="hp_critical")
108
+ return self._retreat_to_safety(state, services)
109
+
110
+ # Priority 2: Get miner gear if we don't have it
111
+ # Track gear state for detecting gear loss
112
+ has_gear_now = state.miner_gear
113
+ just_lost_gear = state.had_gear_last_step and not has_gear_now
114
+ state.had_gear_last_step = has_gear_now
115
+
116
+ if not state.miner_gear:
117
+ # Check if we should try to get gear now
118
+ ticks_since_last_attempt = state.step - state.last_gear_attempt_step
119
+
120
+ # Try to get gear if:
121
+ # 1. Just lost gear (immediately go home)
122
+ # 2. Initial exploration period
123
+ # 3. 200 ticks have passed since last attempt
124
+ should_try_gear = (
125
+ just_lost_gear
126
+ or state.step < self.EXPLORATION_STEPS
127
+ or ticks_since_last_attempt >= self.GEAR_RETRY_INTERVAL
128
+ )
129
+
130
+ if should_try_gear:
131
+ # First try visible miner_station in current observation
132
+ if state.last_obs is not None:
133
+ result = services.map_tracker.get_direction_to_nearest(
134
+ state, state.last_obs, frozenset({"miner_station"})
135
+ )
136
+ if result:
137
+ direction, target_pos = result
138
+ if DEBUG:
139
+ print(f"[A{state.agent_id}] MINER: Station visible at {target_pos}, moving {direction}")
140
+ state.debug_info = DebugInfo(
141
+ mode="get_gear", goal="miner_station", target_object="miner_station", target_pos=target_pos
142
+ )
143
+ return Action(name=f"move_{direction}")
144
+
145
+ # Use accumulated map knowledge if station was found
146
+ gear_action = self._get_gear(state, services)
147
+ if gear_action:
148
+ return gear_action
149
+
150
+ # Station not found - explore for it
151
+ if DEBUG and state.step % 10 == 0:
152
+ print(f"[A{state.agent_id}] MINER: Exploring for station (step {state.step})")
153
+ state.debug_info = DebugInfo(mode="explore", goal="find_station", target_object="miner_station")
154
+ return self._explore_for_station(state, services)
155
+
156
+ # Priority 3: Deposit when cargo is FULL
157
+ cargo_full_reason = self._cargo_full_reason(state)
158
+ if cargo_full_reason:
159
+ if DEBUG:
160
+ print(
161
+ f"[A{state.agent_id}] MINER: Cargo FULL {state.total_cargo}/{state.cargo_capacity}, "
162
+ "returning to depot"
163
+ )
164
+ state.debug_info = DebugInfo(
165
+ mode="deposit", goal=f"drop_cargo({state.total_cargo})", target_object="depot", signal=cargo_full_reason
166
+ )
167
+ return self._deposit_resources(state, services)
168
+
169
+ # Priority 4: Keep mining - move toward extractors
170
+ # First try to find extractor in current observation (most accurate)
171
+ # Build set of positions to exclude: known-empty + recently failed extractors
172
+ excluded_extractors: set[tuple[int, int]] = set(state.nav.failed_extractors)
173
+ for pos, struct in state.map.structures.items():
174
+ if struct.structure_type == StructureType.EXTRACTOR and not struct.is_usable_extractor():
175
+ excluded_extractors.add(pos)
176
+
177
+ if state.last_obs is not None:
178
+ # Get the resource type with the lowest communal amount
179
+ lowest_resource = self._get_lowest_communal_resource(state)
180
+ all_extractor_types = {"carbon_extractor", "oxygen_extractor", "germanium_extractor", "silicon_extractor"}
181
+
182
+ # Build preferred types (lowest communal resource)
183
+ if lowest_resource:
184
+ preferred_types = {f"{lowest_resource}_extractor"}
185
+ else:
186
+ preferred_types = set()
187
+
188
+ # Try preferred types first (lowest communal resource)
189
+ result = None
190
+ if preferred_types:
191
+ result = services.map_tracker.get_direction_to_nearest(
192
+ state,
193
+ state.last_obs,
194
+ frozenset(preferred_types),
195
+ exclude_positions=excluded_extractors,
196
+ )
197
+
198
+ # Fall back to any extractor type
199
+ if not result:
200
+ result = services.map_tracker.get_direction_to_nearest(
201
+ state,
202
+ state.last_obs,
203
+ frozenset(all_extractor_types),
204
+ exclude_positions=excluded_extractors,
205
+ )
206
+
207
+ if result:
208
+ direction, target_pos = result
209
+ # Track this as our current target
210
+ if state.nav.current_extractor_target != target_pos:
211
+ state.nav.current_extractor_target = target_pos
212
+ state.nav.steps_at_current_extractor = 0
213
+ # Found a visible mineral - remember this step
214
+ state.nav.explore_last_mineral_step = state.step
215
+ state.debug_info = DebugInfo(
216
+ mode="mine", goal="find_extractor", target_object="extractor", target_pos=target_pos
217
+ )
218
+ return Action(name=f"move_{direction}")
219
+
220
+ # No extractor visible - use internal map knowledge to navigate to known extractors
221
+ known_extractor = self._find_nearest_extractor(state, services, excluded_extractors)
222
+ if known_extractor:
223
+ # Track this as our current target
224
+ if state.nav.current_extractor_target != known_extractor.position:
225
+ state.nav.current_extractor_target = known_extractor.position
226
+ state.nav.steps_at_current_extractor = 0
227
+ # Found a mineral - remember this step
228
+ state.nav.explore_last_mineral_step = state.step
229
+
230
+ # Track this resource type for rotation
231
+ res_type = known_extractor.resource_type
232
+ if res_type:
233
+ self._record_resource_gathered(state, res_type)
234
+
235
+ if DEBUG:
236
+ print(
237
+ f"[A{state.agent_id}] MINER: No extractor visible, navigating to known {known_extractor.name} "
238
+ f"at {known_extractor.position}"
239
+ )
240
+ # Format: "carbon:100" or "extractor" if no resource type
241
+ res_type_name = res_type or "extractor"
242
+ inv_amt = known_extractor.inventory_amount
243
+ target_name = f"{res_type_name}:{inv_amt}" if inv_amt >= 0 else res_type_name
244
+ state.debug_info = DebugInfo(
245
+ mode="mine",
246
+ goal="navigate_to_known_extractor",
247
+ target_object=target_name,
248
+ target_pos=known_extractor.position,
249
+ )
250
+ return services.navigator.move_to(state, known_extractor.position, reach_adjacent=True)
251
+
252
+ # No minerals found - explore with expanding radius
253
+ return self._explore_for_minerals(state, services)
254
+
255
+ def needs_gear(self, state: AgentState) -> bool:
256
+ """Miners need miner gear for +40 cargo capacity."""
257
+ return not state.miner_gear
258
+
259
+ def has_resources_to_act(self, state: AgentState) -> bool:
260
+ """Miners don't need resources to mine."""
261
+ return True
262
+
263
+ def _record_resource_gathered(self, state: AgentState, resource_type: str) -> None:
264
+ """Record that we gathered a resource type for rotation tracking.
265
+
266
+ Maintains a list of the last 4 resource types gathered.
267
+ When the miner fills up on one type, it will prefer other types.
268
+ """
269
+ recent = state.nav.last_resource_types
270
+ # Only add if different from the most recent one (avoid duplicates from same extractor)
271
+ if not recent or recent[0] != resource_type:
272
+ recent.insert(0, resource_type)
273
+ # Keep only the last 4 types
274
+ if len(recent) > 4:
275
+ recent.pop()
276
+
277
+ # How many steps without cargo gain before assuming inventory is full
278
+ STEPS_TO_ASSUME_FULL = 3
279
+
280
+ def _cargo_full_reason(self, state: AgentState) -> str:
281
+ """Check if cargo is full and return the reason signal.
282
+
283
+ Returns:
284
+ - "extract_failed_cargo_full" if no cargo gain for several steps (extraction stopped working)
285
+ - "cargo_at_capacity" if cargo >= computed capacity
286
+ - "" if cargo is not full
287
+ """
288
+ # If we have cargo and haven't gained any for several steps, assume full
289
+ if state.total_cargo > 0 and state.steps_without_cargo_gain >= self.STEPS_TO_ASSUME_FULL:
290
+ if DEBUG:
291
+ print(
292
+ f"[A{state.agent_id}] MINER: No cargo gain for {state.steps_without_cargo_gain} steps, "
293
+ f"assuming full (cargo={state.total_cargo})"
294
+ )
295
+ return "extract_failed_cargo_full"
296
+ # Fallback to capacity check
297
+ if state.total_cargo >= state.cargo_capacity:
298
+ return "cargo_at_capacity"
299
+ return ""
300
+
301
+ def _retreat_to_safety(self, state: AgentState, services: Services) -> Action:
302
+ """Return to nearest safe zone."""
303
+ safe_pos = services.safety.nearest_safe_zone(state)
304
+ if safe_pos is None:
305
+ # No known safe zone, just try to find any junction
306
+ for junction in state.map.get_junctions():
307
+ safe_pos = junction.position
308
+ break
309
+
310
+ if safe_pos is None:
311
+ state.debug_info = DebugInfo(mode="retreat", goal="explore_for_safety", target_object="-")
312
+ return services.navigator.explore(state)
313
+
314
+ # If we have cargo and are adjacent to safe building, deposit
315
+ if state.total_cargo > 0 and is_adjacent(state.pos, safe_pos):
316
+ state.debug_info = DebugInfo(
317
+ mode="retreat", goal="deposit_and_heal", target_object="safe_zone", target_pos=safe_pos
318
+ )
319
+ return services.navigator.use_object_at(state, safe_pos)
320
+
321
+ state.debug_info = DebugInfo(
322
+ mode="retreat", goal="reach_safety", target_object="safe_zone", target_pos=safe_pos
323
+ )
324
+ return services.navigator.move_to(state, safe_pos, reach_adjacent=True)
325
+
326
+ def _explore_for_station(self, state: AgentState, services: Services) -> Action:
327
+ """Explore to find the miner station."""
328
+ # Miners explore south since stations are south of spawn in the hub
329
+ return explore_for_station(state, services, primary_direction="south")
330
+
331
+ def _explore_for_minerals(self, state: AgentState, services: Services) -> Action:
332
+ """Explore to find new mineral extractors.
333
+
334
+ Uses the navigator's explore method with direction bias to spread out
335
+ from the base and find new resources. Tracks exploration direction to
336
+ avoid circling.
337
+ """
338
+ # Pick a consistent exploration direction based on agent ID to spread miners out
339
+ # Agent 0 explores south, 1 explores east, 2 explores west, etc.
340
+ directions = ["south", "east", "west", "north"]
341
+ base_direction = directions[state.agent_id % len(directions)]
342
+
343
+ # If we've been exploring the same direction for a while without finding minerals,
344
+ # switch to a different direction
345
+ steps_since_mineral = state.step - state.nav.explore_last_mineral_step
346
+ if steps_since_mineral > 100:
347
+ # Rotate to next direction
348
+ dir_idx = (directions.index(base_direction) + (steps_since_mineral // 100)) % len(directions)
349
+ base_direction = directions[dir_idx]
350
+ if DEBUG:
351
+ print(
352
+ f"[A{state.agent_id}] MINER: No minerals for {steps_since_mineral} steps, "
353
+ f"exploring {base_direction}"
354
+ )
355
+
356
+ state.debug_info = DebugInfo(
357
+ mode="explore",
358
+ goal=base_direction,
359
+ target_object="-",
360
+ )
361
+
362
+ # Use navigator's explore which handles pathfinding around obstacles
363
+ return services.navigator.explore(state, direction_bias=base_direction)
364
+
365
+ def _get_gear(self, state: AgentState, services: Services) -> Optional[Action]:
366
+ """Go get miner gear from station."""
367
+ station_name = ROLE_TO_STATION[Role.MINER]
368
+ station_pos = state.map.stations.get(station_name)
369
+
370
+ if station_pos is None:
371
+ # Station not found yet
372
+ return None
373
+
374
+ dist = manhattan_distance(state.pos, station_pos)
375
+
376
+ # If ON the station (dist=0), we should have received gear from walking in.
377
+ # If gear not received yet, record this attempt and let miner continue mining.
378
+ # After GEAR_RETRY_INTERVAL ticks, they'll try again.
379
+ if state.pos == station_pos:
380
+ if DEBUG:
381
+ print(f"[A{state.agent_id}] MINER: ON station at {station_pos}, no gear - will mine and retry later")
382
+ state.debug_info = DebugInfo(
383
+ mode="get_gear", goal="on_station_no_gear", target_object="miner_station", target_pos=station_pos
384
+ )
385
+ # Record this attempt - miner will mine for GEAR_RETRY_INTERVAL ticks then retry
386
+ state.last_gear_attempt_step = state.step
387
+ # Return None to let the miner continue with mining
388
+ return None
389
+
390
+ if is_adjacent(state.pos, station_pos):
391
+ if DEBUG:
392
+ print(f"[A{state.agent_id}] MINER: Getting gear from {station_pos}")
393
+ state.debug_info = DebugInfo(
394
+ mode="get_gear", goal="use_station", target_object="miner_station", target_pos=station_pos
395
+ )
396
+ return services.navigator.use_object_at(state, station_pos)
397
+
398
+ if DEBUG and state.step % 10 == 0:
399
+ print(f"[A{state.agent_id}] MINER: Moving to station at {station_pos} (dist={dist})")
400
+ state.debug_info = DebugInfo(
401
+ mode="get_gear", goal=f"move_to_station(dist={dist})", target_object="miner_station", target_pos=station_pos
402
+ )
403
+ return services.navigator.move_to(state, station_pos, reach_adjacent=True)
404
+
405
+ def _deposit_resources(self, state: AgentState, services: Services) -> Action:
406
+ """Deposit resources at nearest COGS-ALIGNED hub or junction only.
407
+
408
+ Uses map knowledge (which tracks alignment) to find cogs-aligned depots.
409
+ Observation-based nav doesn't check alignment, so we rely on map knowledge.
410
+ Only considers cogs-aligned structures (hub or cogs junctions), NOT neutral.
411
+ """
412
+ # Use map knowledge - find nearest COGS-ALIGNED depot only
413
+ # Map knowledge correctly tracks alignment changes from observations
414
+ candidates: list[tuple[int, tuple[int, int]]] = []
415
+
416
+ # Only cogs-aligned junctions (NOT neutral, NOT clips)
417
+ for junction in state.map.get_cogs_junctions():
418
+ dist = manhattan_distance(state.pos, junction.position)
419
+ candidates.append((dist, junction.position))
420
+
421
+ # Hub is always cogs-aligned
422
+ hub_pos = state.map.stations.get("hub")
423
+ if hub_pos:
424
+ dist = manhattan_distance(state.pos, hub_pos)
425
+ candidates.append((dist, hub_pos))
426
+
427
+ if not candidates:
428
+ if DEBUG:
429
+ # Show what structures we DO know about
430
+ struct_types: dict[str, int] = {}
431
+ for s in state.map.structures.values():
432
+ t = s.structure_type.name
433
+ struct_types[t] = struct_types.get(t, 0) + 1
434
+ cogs_junctions = len(state.map.get_cogs_junctions())
435
+ all_junctions = len(state.map.get_junctions())
436
+ # Also show junction alignments
437
+ junction_alignments = [(j.position, j.alignment) for j in state.map.get_junctions()]
438
+ print(
439
+ f"[A{state.agent_id}] MINER: No COGS depot found "
440
+ f"(cogs_junctions={cogs_junctions}, all_junctions={all_junctions}), "
441
+ f"structures={struct_types}, junction_alignments={junction_alignments}, exploring"
442
+ )
443
+ state.debug_info = DebugInfo(mode="deposit", goal="explore_for_cogs_depot", target_object="-")
444
+ return services.navigator.explore(state)
445
+
446
+ candidates.sort(key=lambda x: x[0])
447
+ depot_pos = candidates[0][1]
448
+ dist = candidates[0][0]
449
+
450
+ # If we're ON or adjacent to the depot, try to interact with it
451
+ if state.pos == depot_pos or is_adjacent(state.pos, depot_pos):
452
+ # Verify depot is still cogs-aligned (alignment may have changed)
453
+ struct = state.map.get_structure_at(depot_pos)
454
+ if struct is not None and struct.structure_type == StructureType.JUNCTION:
455
+ if not struct.is_cogs_aligned():
456
+ if DEBUG:
457
+ print(
458
+ f"[A{state.agent_id}] MINER: Depot at {depot_pos} is no longer cogs-aligned "
459
+ f"(alignment={struct.alignment}), finding new depot"
460
+ )
461
+ # Depot is no longer cogs-aligned - recurse to find a new one
462
+ # Remove from candidates and try again
463
+ state.debug_info = DebugInfo(
464
+ mode="deposit", goal="depot_lost_alignment", target_object="-", signal="alignment_changed"
465
+ )
466
+ return services.navigator.explore(state)
467
+
468
+ if DEBUG:
469
+ print(f"[A{state.agent_id}] MINER: At cogs depot {depot_pos}, depositing cargo={state.total_cargo}")
470
+ state.debug_info = DebugInfo(
471
+ mode="deposit", goal="use_cogs_depot", target_object="cogs_depot", target_pos=depot_pos
472
+ )
473
+ return services.navigator.use_object_at(state, depot_pos)
474
+
475
+ if DEBUG and state.step % 10 == 0:
476
+ print(
477
+ f"[A{state.agent_id}] MINER: Moving to cogs depot at {depot_pos}, "
478
+ f"dist={dist}, cargo={state.total_cargo}, agent_pos={state.pos}"
479
+ )
480
+
481
+ state.debug_info = DebugInfo(
482
+ mode="deposit", goal=f"move_to_cogs_depot(dist={dist})", target_object="cogs_depot", target_pos=depot_pos
483
+ )
484
+ return services.navigator.move_to(state, depot_pos, reach_adjacent=True)
485
+
486
+ def _move_toward_extractor_from_obs(self, state: AgentState) -> Action:
487
+ """Move toward nearest extractor visible in current observation.
488
+
489
+ Uses relative observation positions, not world coordinates.
490
+ Center is at (obs_hr, obs_wr) = (5, 5) typically.
491
+ """
492
+ import random
493
+
494
+ # Find closest extractor in state.map.structures by iterating all
495
+ # and using relative position calculation from recent observation
496
+ # But since coordinates are broken, use random walk with exploration
497
+
498
+ directions = ["north", "south", "east", "west"]
499
+ random.shuffle(directions)
500
+ return Action(name=f"move_{directions[0]}")
501
+
502
+ def _get_lowest_communal_resource(self, state: AgentState) -> Optional[str]:
503
+ """Get the resource type with the lowest communal amount.
504
+
505
+ Returns:
506
+ Resource type name (carbon, oxygen, germanium, silicon) or None if all are 0.
507
+ """
508
+ resources = {
509
+ "carbon": state.collective_carbon,
510
+ "oxygen": state.collective_oxygen,
511
+ "germanium": state.collective_germanium,
512
+ "silicon": state.collective_silicon,
513
+ }
514
+ # Find the minimum (break ties alphabetically for consistency)
515
+ min_amount = min(resources.values())
516
+ for name in sorted(resources.keys()):
517
+ if resources[name] == min_amount:
518
+ return name
519
+ return None
520
+
521
+ def _find_nearest_extractor(
522
+ self,
523
+ state: AgentState,
524
+ services: Services,
525
+ exclude_positions: Optional[set[tuple[int, int]]] = None,
526
+ ) -> Optional[StructureInfo]:
527
+ """Find nearest usable extractor, preferring the resource with lowest communal amount.
528
+
529
+ Resource prioritization logic:
530
+ - Check communal resource levels and prefer the resource type with the lowest amount
531
+ - Among preferred types, pick the nearest one
532
+ - Fall back to nearest of any type if no preferred available
533
+
534
+ Args:
535
+ exclude_positions: Set of extractor positions to exclude (empty/failed ones)
536
+ """
537
+ extractors = state.map.get_usable_extractors()
538
+
539
+ if not extractors:
540
+ return None
541
+
542
+ # Combine with failed extractors to exclude
543
+ excluded = exclude_positions or set()
544
+ excluded = excluded | state.nav.failed_extractors
545
+
546
+ # Use step-based range limit (encourages gradual expansion)
547
+ # Also cap by HP to avoid stranding
548
+ step_limit = services.safety.step_based_range_limit(state.step)
549
+ hp_limit = max(50, state.hp // 2)
550
+ max_dist = min(step_limit, hp_limit)
551
+
552
+ # Get the resource type with the lowest communal amount
553
+ lowest_resource = self._get_lowest_communal_resource(state)
554
+
555
+ if DEBUG and state.step % 50 == 0:
556
+ struct_types = {}
557
+ for s in state.map.structures.values():
558
+ t = s.structure_type.name
559
+ struct_types[t] = struct_types.get(t, 0) + 1
560
+ print(
561
+ f"[A{state.agent_id}] MINER: _find_nearest_extractor: "
562
+ f"max_dist={max_dist} (step_limit={step_limit}, hp_limit={hp_limit}), "
563
+ f"extractors={len(extractors)}, hp={state.hp}, "
564
+ f"lowest_communal={lowest_resource} "
565
+ f"(C={state.collective_carbon}, O={state.collective_oxygen}, "
566
+ f"G={state.collective_germanium}, S={state.collective_silicon})"
567
+ )
568
+
569
+ # Filter extractors by distance and categorize by preference
570
+ # Prefer extractors that produce the lowest communal resource
571
+ preferred_candidates: list[tuple[int, StructureInfo]] = [] # Lowest communal resource type
572
+ fallback_candidates: list[tuple[int, StructureInfo]] = [] # Other resource types
573
+
574
+ for ext in extractors:
575
+ if ext.position in excluded:
576
+ continue
577
+ dist = manhattan_distance(state.pos, ext.position)
578
+ if dist > max_dist:
579
+ continue
580
+
581
+ res_type = ext.resource_type
582
+ if res_type and res_type == lowest_resource:
583
+ preferred_candidates.append((dist, ext))
584
+ else:
585
+ fallback_candidates.append((dist, ext))
586
+
587
+ # Return nearest preferred, or nearest fallback
588
+ if preferred_candidates:
589
+ preferred_candidates.sort(key=lambda x: x[0])
590
+ return preferred_candidates[0][1]
591
+
592
+ if fallback_candidates:
593
+ fallback_candidates.sort(key=lambda x: x[0])
594
+ return fallback_candidates[0][1]
595
+
596
+ # No extractors in range - return the closest one anyway (any type)
597
+ all_candidates = []
598
+ for ext in extractors:
599
+ if ext.position in excluded:
600
+ continue
601
+ dist = manhattan_distance(state.pos, ext.position)
602
+ res_type = ext.resource_type
603
+ # Still prefer lowest communal resource type even when out of range
604
+ priority = 0 if (res_type and res_type == lowest_resource) else 1
605
+ all_candidates.append((priority, dist, ext))
606
+
607
+ if all_candidates:
608
+ all_candidates.sort(key=lambda x: (x[0], x[1])) # Sort by priority, then distance
609
+ return all_candidates[0][2]
610
+
611
+ return None
612
+
613
+ def _get_nearest_depot(self, state: AgentState, services: Services) -> Optional[tuple[int, int]]:
614
+ """Get nearest COGS-ALIGNED hub/junction for deposit."""
615
+ candidates: list[tuple[int, tuple[int, int]]] = []
616
+
617
+ # Hub (always cogs-aligned)
618
+ hub_pos = state.map.stations.get("hub")
619
+ if hub_pos:
620
+ dist = manhattan_distance(state.pos, hub_pos)
621
+ candidates.append((dist, hub_pos))
622
+
623
+ # Only cogs-aligned junctions (NOT neutral, NOT clips)
624
+ for junction in state.map.get_cogs_junctions():
625
+ dist = manhattan_distance(state.pos, junction.position)
626
+ candidates.append((dist, junction.position))
627
+
628
+ if not candidates:
629
+ return None
630
+
631
+ candidates.sort(key=lambda x: x[0])
632
+ return candidates[0][1]