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,589 @@
1
+ """
2
+ Miner role for CoGsGuard.
3
+
4
+ Miners gather resources from extractors and deposit at aligned buildings.
5
+ With miner gear, they get +40 cargo capacity and extract 10 resources at a time.
6
+
7
+ Strategy:
8
+ - Extractors are in map corners, aligned buildings provide deposit points
9
+ - Miners should quickly head to corners to find extractors
10
+ - Once extractors are known, alternate between mining and depositing
11
+ - Deposit to nearest aligned building (hub or cogs-aligned junctions)
12
+ - If gear is lost, collect resources without gear and check for gear on each dropoff
13
+ - Retry failed mine actions up to MAX_RETRIES times
14
+ - HP-aware: Never venture further than can safely return to healing territory
15
+ (HP drains at -1/step outside aligned building AOE, losing all HP = lose gear)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from cogames_agents.policy.scripted_agent.utils import is_adjacent
21
+ from mettagrid.simulator import Action
22
+
23
+ from .policy import DEBUG, CogsguardAgentPolicyImpl
24
+ from .types import CogsguardAgentState, Role, StructureInfo, StructureType
25
+
26
+ # Extractors are typically in map corners - explore these areas first
27
+ # Map is 200x200, center is ~100,100
28
+ CORNER_OFFSETS = [
29
+ (-10, -10), # NW corner direction
30
+ (-10, 10), # NE corner direction
31
+ (10, -10), # SW corner direction
32
+ (10, 10), # SE corner direction
33
+ ]
34
+
35
+ # Maximum number of times to retry a failed mine action
36
+ MAX_RETRIES = 3
37
+
38
+ # HP management constants
39
+ # Hub AOE range that provides healing (+100 HP to aligned agents)
40
+ HEALING_AOE_RANGE = 10
41
+ # Base HP drain per step outside healing AOE (from regen_amounts)
42
+ HP_DRAIN_BASE = 1
43
+ # Additional HP drain per step when near enemy buildings (from attack_aoe)
44
+ HP_DRAIN_ENEMY_AOE = 1
45
+ # Enemy AOE range
46
+ ENEMY_AOE_RANGE = 10
47
+ # Safety margin - return home with this much HP buffer
48
+ HP_SAFETY_MARGIN = 5
49
+
50
+
51
+ class MinerAgentPolicyImpl(CogsguardAgentPolicyImpl):
52
+ """Miner agent: gather resources and deposit at nearest aligned building."""
53
+
54
+ ROLE = Role.MINER
55
+ RESOURCE_ORDER = ["carbon", "oxygen", "germanium", "silicon"]
56
+
57
+ def __init__(self, *args, **kwargs) -> None:
58
+ super().__init__(*args, **kwargs)
59
+ self._preferred_resource = self.RESOURCE_ORDER[self._agent_id % len(self.RESOURCE_ORDER)]
60
+
61
+ def _get_nearest_aligned_depot(self, s: CogsguardAgentState) -> tuple[int, int] | None:
62
+ """Find the nearest aligned building that accepts deposits.
63
+
64
+ Returns position of nearest cogs-aligned building (hub or junction).
65
+ These buildings have healing AOE and accept resource deposits.
66
+ """
67
+ candidates: list[tuple[int, tuple[int, int]]] = []
68
+
69
+ # Hub is always aligned to cogs
70
+ hub_pos = s.get_structure_position(StructureType.HUB)
71
+ if hub_pos:
72
+ dist = abs(hub_pos[0] - s.row) + abs(hub_pos[1] - s.col)
73
+ candidates.append((dist, hub_pos))
74
+
75
+ # Check for cogs-aligned junctions
76
+ junctions = s.get_structures_by_type(StructureType.CHARGER)
77
+ for junction in junctions:
78
+ if junction.alignment == "cogs":
79
+ dist = abs(junction.position[0] - s.row) + abs(junction.position[1] - s.col)
80
+ candidates.append((dist, junction.position))
81
+
82
+ if not candidates:
83
+ return None
84
+
85
+ # Return nearest
86
+ candidates.sort(key=lambda x: x[0])
87
+ return candidates[0][1]
88
+
89
+ def execute_role(self, s: CogsguardAgentState) -> Action:
90
+ """Execute miner behavior: continuously gather resources and deposit at hub.
91
+
92
+ If gear is lost, collect resources without gear and check for gear on each dropoff.
93
+ Retry failed mine actions up to MAX_RETRIES times.
94
+ HP-aware: Return to healing territory before HP gets too low to avoid losing gear.
95
+ """
96
+ # Use dynamic properties - cargo_capacity updates if gear is lost
97
+ total_cargo = s.total_cargo
98
+ cargo_capacity = s.cargo_capacity
99
+ has_gear = s.miner > 0
100
+
101
+ if DEBUG and s.step_count <= 50:
102
+ num_extractors = len(s.get_structures_by_type(StructureType.EXTRACTOR))
103
+ mode = "deposit" if total_cargo >= cargo_capacity - 2 else "gather"
104
+ gear_status = "GEAR" if has_gear else "NO_GEAR"
105
+ steps_to_heal = self._get_steps_to_healing(s)
106
+ drain_rate = self._get_hp_drain_rate(s)
107
+ nearest_depot = self._get_nearest_aligned_depot(s)
108
+ print(
109
+ f"[A{s.agent_id}] MINER step={s.step_count}: pos=({s.row},{s.col}) "
110
+ f"cargo={total_cargo}/{cargo_capacity} mode={mode} ext={num_extractors} "
111
+ f"{gear_status} energy={s.energy} hp={s.hp} steps_to_heal={steps_to_heal} "
112
+ f"drain={drain_rate}/step depot={nearest_depot}"
113
+ )
114
+
115
+ # === HP check - highest priority ===
116
+ # If HP is getting low, head back to healing territory immediately
117
+ if self._should_return_for_healing(s):
118
+ if DEBUG:
119
+ steps = self._get_steps_to_healing(s)
120
+ print(f"[A{s.agent_id}] MINER: HP LOW ({s.hp}), returning to heal! Steps to safety: {steps}")
121
+ return self._return_to_healing(s)
122
+
123
+ # Check if last action succeeded (for retry logic)
124
+ # Actions can fail due to insufficient energy - agents auto-regen so just retry
125
+ if s._pending_action_type == "mine":
126
+ if s.check_action_success():
127
+ if DEBUG:
128
+ print(f"[A{s.agent_id}] MINER: Previous mine succeeded!")
129
+ elif s.should_retry_action(MAX_RETRIES):
130
+ retry_count = s.increment_retry()
131
+ if DEBUG:
132
+ print(
133
+ f"[A{s.agent_id}] MINER: Mine failed, retrying ({retry_count}/{MAX_RETRIES}) "
134
+ f"at {s._pending_action_target}"
135
+ )
136
+ # Retry the same action - agent will have auto-regenerated some energy
137
+ if s._pending_action_target and is_adjacent((s.row, s.col), s._pending_action_target):
138
+ return self._use_object_at(s, s._pending_action_target)
139
+ else:
140
+ if DEBUG:
141
+ print(f"[A{s.agent_id}] MINER: Mine failed after {MAX_RETRIES} retries, moving on")
142
+ s.clear_pending_action()
143
+
144
+ # === Gear re-acquisition logic ===
145
+ if not has_gear:
146
+ return self._handle_no_gear(s, total_cargo, cargo_capacity)
147
+
148
+ # === Normal mining loop (has gear) ===
149
+ # Simple loop: gather until full, deposit, repeat
150
+ if total_cargo >= cargo_capacity - 2:
151
+ return self._do_deposit(s)
152
+
153
+ # Otherwise gather resources
154
+ return self._do_gather(s)
155
+
156
+ def _get_steps_to_healing(self, s: CogsguardAgentState) -> int:
157
+ """Calculate steps needed to reach healing territory (nearest aligned building AOE).
158
+
159
+ Returns the number of steps to get within any aligned building's healing AOE.
160
+ If no aligned buildings known, returns a large number to be conservative.
161
+ """
162
+ depot_pos = self._get_nearest_aligned_depot(s)
163
+ if depot_pos is None:
164
+ return 100 # Unknown - be conservative
165
+
166
+ dist = abs(depot_pos[0] - s.row) + abs(depot_pos[1] - s.col)
167
+ return max(0, dist - HEALING_AOE_RANGE)
168
+
169
+ def _should_return_for_healing(self, s: CogsguardAgentState) -> bool:
170
+ """Check if miner should return to healing territory based on HP.
171
+
172
+ Returns True if HP is low enough that we need to head back now to survive.
173
+ Accounts for faster HP drain when near enemy buildings.
174
+ """
175
+ steps_to_healing = self._get_steps_to_healing(s)
176
+ current_drain = self._get_hp_drain_rate(s)
177
+
178
+ # Use current drain rate (which may be elevated if near enemies)
179
+ # to calculate HP needed to survive the trip
180
+ hp_needed = (steps_to_healing * current_drain) + HP_SAFETY_MARGIN
181
+
182
+ return s.hp <= hp_needed
183
+
184
+ def _get_hp_drain_rate(self, s: CogsguardAgentState) -> int:
185
+ """Calculate current HP drain rate based on proximity to enemy buildings.
186
+
187
+ Base drain is HP_DRAIN_BASE per step outside healing AOE.
188
+ Additional HP_DRAIN_ENEMY_AOE per step when near enemy junctions.
189
+ """
190
+ drain_rate = HP_DRAIN_BASE
191
+
192
+ # Check if near any enemy junctions
193
+ junctions = s.get_structures_by_type(StructureType.CHARGER)
194
+ for junction in junctions:
195
+ if junction.alignment == "cogs":
196
+ continue # Friendly junction
197
+ dist = abs(junction.position[0] - s.row) + abs(junction.position[1] - s.col)
198
+ if dist <= ENEMY_AOE_RANGE:
199
+ drain_rate += HP_DRAIN_ENEMY_AOE
200
+ if DEBUG:
201
+ print(f"[A{s.agent_id}] MINER: Near enemy junction at {junction.position}, drain_rate={drain_rate}")
202
+ break # Only count once even if near multiple enemies
203
+
204
+ return drain_rate
205
+
206
+ def _return_to_healing(self, s: CogsguardAgentState) -> Action:
207
+ """Return to nearest aligned building to heal. Deposit any cargo while there."""
208
+ depot_pos = self._get_nearest_aligned_depot(s)
209
+
210
+ if depot_pos is None:
211
+ # Don't know where any aligned building is - explore to find one
212
+ if DEBUG:
213
+ print(f"[A{s.agent_id}] MINER_HEAL: No aligned building known, exploring!")
214
+ return self._explore(s)
215
+
216
+ # Check if we're already in healing range
217
+ dist_to_depot = abs(depot_pos[0] - s.row) + abs(depot_pos[1] - s.col)
218
+ if dist_to_depot <= HEALING_AOE_RANGE:
219
+ # We're in healing range - if we have cargo, deposit it
220
+ if s.total_cargo > 0 and is_adjacent((s.row, s.col), depot_pos):
221
+ if DEBUG:
222
+ print(f"[A{s.agent_id}] MINER_HEAL: In range, depositing cargo={s.total_cargo}")
223
+ return self._use_object_at(s, depot_pos)
224
+ # Otherwise just wait here to heal (or move closer to deposit)
225
+ if s.total_cargo > 0:
226
+ return self._move_towards(s, depot_pos, reach_adjacent=True)
227
+ # No cargo, just noop to heal
228
+ if DEBUG and s.step_count % 10 == 0:
229
+ print(f"[A{s.agent_id}] MINER_HEAL: Healing at HP={s.hp}")
230
+ return self._noop()
231
+
232
+ # Move towards nearest aligned building
233
+ if DEBUG and s.step_count % 10 == 0:
234
+ print(f"[A{s.agent_id}] MINER_HEAL: Moving to aligned building at {depot_pos}, dist={dist_to_depot}")
235
+ return self._move_towards(s, depot_pos, reach_adjacent=True)
236
+
237
+ def _handle_no_gear(self, s: CogsguardAgentState, total_cargo: int, cargo_capacity: int) -> Action:
238
+ """Handle behavior when miner doesn't have gear.
239
+
240
+ Strategy: Collect resources even without gear (reduced capacity/extraction rate).
241
+ Check for gear availability on each dropoff at the hub.
242
+ """
243
+ # If cargo is full (even with small capacity), deposit and check for gear
244
+ if total_cargo >= cargo_capacity - 1:
245
+ return self._do_deposit_and_check_gear(s)
246
+
247
+ # If we just deposited, check for gear before gathering again
248
+ if total_cargo == 0 and s._resources_deposited_since_gear_attempt > 0:
249
+ return self._do_deposit_and_check_gear(s)
250
+
251
+ # Otherwise, continue gathering resources (at reduced rate without gear)
252
+ if DEBUG and s.step_count % 20 == 0:
253
+ print(f"[A{s.agent_id}] MINER_NO_GEAR: Gathering without gear, cargo={total_cargo}/{cargo_capacity}")
254
+ return self._do_gather(s)
255
+
256
+ def _do_deposit_and_check_gear(self, s: CogsguardAgentState) -> Action:
257
+ """Deposit resources at nearest aligned building, then check gear station.
258
+
259
+ After depositing, immediately try to get gear before continuing to mine.
260
+ """
261
+ depot_pos = self._get_nearest_aligned_depot(s)
262
+ station_pos = s.get_structure_position(StructureType.MINER_STATION)
263
+
264
+ # If we still have cargo, deposit first at nearest aligned building
265
+ if s.total_cargo > 0:
266
+ if depot_pos is None:
267
+ if DEBUG:
268
+ print(f"[A{s.agent_id}] MINER_NO_GEAR: No aligned building known, exploring")
269
+ return self._explore(s)
270
+
271
+ if not is_adjacent((s.row, s.col), depot_pos):
272
+ if DEBUG and s.step_count % 20 == 0:
273
+ print(f"[A{s.agent_id}] MINER_NO_GEAR: Moving to deposit at {depot_pos}")
274
+ return self._move_towards(s, depot_pos, reach_adjacent=True)
275
+
276
+ # At depot - deposit
277
+ cargo_to_deposit = s.total_cargo
278
+ s._resources_deposited_since_gear_attempt += cargo_to_deposit
279
+ if DEBUG:
280
+ print(
281
+ f"[A{s.agent_id}] MINER_NO_GEAR: Depositing cargo={cargo_to_deposit} at {depot_pos}, "
282
+ f"total_deposited={s._resources_deposited_since_gear_attempt}"
283
+ )
284
+ return self._use_object_at(s, depot_pos)
285
+
286
+ # Cargo deposited, now try to get gear
287
+ if station_pos is None:
288
+ if DEBUG:
289
+ print(f"[A{s.agent_id}] MINER_NO_GEAR: Station unknown, exploring")
290
+ return self._explore(s)
291
+
292
+ if not is_adjacent((s.row, s.col), station_pos):
293
+ if DEBUG and s.step_count % 20 == 0:
294
+ print(f"[A{s.agent_id}] MINER_NO_GEAR: Checking gear station at {station_pos}")
295
+ return self._move_towards(s, station_pos, reach_adjacent=True)
296
+
297
+ # At station - try to get gear (will fail if commons lacks resources, that's ok)
298
+ if DEBUG:
299
+ print(f"[A{s.agent_id}] MINER_NO_GEAR: Attempting to get gear")
300
+ s._resources_deposited_since_gear_attempt = 0
301
+ return self._use_object_at(s, station_pos)
302
+
303
+ def _do_gather(self, s: CogsguardAgentState) -> Action:
304
+ """Gather resources from nearest extractor.
305
+
306
+ Tracks mining attempts for retry logic.
307
+ Note: moves require energy. If move fails due to low energy,
308
+ action failure detection will catch it and we'll retry next step
309
+ (agents auto-regen energy every step, and regen full near aligned buildings)
310
+ """
311
+ # Use structures map for most up-to-date extractor info
312
+ # Prefer extractors that are safe (not near clips junctions)
313
+ extractor = self._get_safe_extractor(s, preferred_resource=self._preferred_resource)
314
+
315
+ if extractor is None:
316
+ # No usable extractors known - explore to find more
317
+ all_extractors = s.get_structures_by_type(StructureType.EXTRACTOR)
318
+ usable_extractors = s.get_usable_extractors()
319
+ total_extractors = len(all_extractors)
320
+ total_usable = len(usable_extractors)
321
+ if total_extractors > 0 and DEBUG:
322
+ # Log why extractors are not usable (empty, clipped, etc.)
323
+ empty_count = sum(1 for e in all_extractors if e.inventory_amount <= 0)
324
+ clipped_count = sum(1 for e in all_extractors if e.clipped)
325
+ print(
326
+ f"[A{s.agent_id}] GATHER: {total_extractors} extractors known, "
327
+ f"{total_usable} usable (empty={empty_count}, clipped={clipped_count}), "
328
+ f"exploring for more"
329
+ )
330
+ return self._explore_for_extractors(s)
331
+
332
+ # Navigate to extractor
333
+ ext_pos = extractor.position
334
+ agent_pos = (s.row, s.col)
335
+ adjacent = is_adjacent(agent_pos, ext_pos)
336
+ if DEBUG and s.step_count <= 60:
337
+ print(f"[A{s.agent_id}] GATHER: agent@{agent_pos} ext@{ext_pos} adjacent={adjacent}")
338
+ if not adjacent:
339
+ if DEBUG and s.step_count <= 50:
340
+ print(f"[A{s.agent_id}] GATHER: Moving to extractor at {extractor.position}")
341
+ return self._move_towards(s, extractor.position, reach_adjacent=True)
342
+
343
+ # At extractor - get CURRENT state from structures map (updated from observation)
344
+ current_extractor = s.get_structure_at(extractor.position)
345
+
346
+ # Check if extractor is still usable (might have been depleted since we started moving)
347
+ if current_extractor is None or not current_extractor.is_usable_extractor():
348
+ # Log why extractor is not usable
349
+ if DEBUG and current_extractor:
350
+ reason = []
351
+ if current_extractor.inventory_amount <= 0:
352
+ reason.append(f"empty(inv={current_extractor.inventory_amount})")
353
+ if current_extractor.clipped:
354
+ reason.append("clipped")
355
+ if current_extractor.remaining_uses <= 0:
356
+ reason.append(f"depleted(uses={current_extractor.remaining_uses})")
357
+ print(
358
+ f"[A{s.agent_id}] GATHER: Extractor at {extractor.position} not usable: "
359
+ f"{', '.join(reason) if reason else 'unknown'}. Switching."
360
+ )
361
+ # Find another extractor
362
+ other = s.get_nearest_usable_extractor(exclude=extractor.position)
363
+ if other is not None:
364
+ if DEBUG:
365
+ print(f"[A{s.agent_id}] GATHER: Switching to extractor at {other.position}")
366
+ return self._move_towards(s, other.position, reach_adjacent=True)
367
+ # No usable extractors - explore to find more
368
+ if DEBUG:
369
+ total_ext = len(s.get_structures_by_type(StructureType.EXTRACTOR))
370
+ usable_ext = len(s.get_usable_extractors())
371
+ print(f"[A{s.agent_id}] GATHER: No usable extractors! total={total_ext}, usable={usable_ext}")
372
+ return self._explore_for_extractors(s)
373
+
374
+ # Check if another agent is blocking the extractor
375
+ if extractor.position in s.agent_occupancy:
376
+ if DEBUG:
377
+ print(f"[A{s.agent_id}] GATHER: Extractor at {extractor.position} blocked by another agent")
378
+ # Try another extractor
379
+ other = s.get_nearest_usable_extractor(exclude=extractor.position)
380
+ if other is not None:
381
+ if DEBUG:
382
+ print(f"[A{s.agent_id}] GATHER: Switching to extractor at {other.position}")
383
+ return self._move_towards(s, other.position, reach_adjacent=True)
384
+ # Wait a bit - other agent should move soon
385
+ return self._noop()
386
+
387
+ # At extractor - check cooldown
388
+ if current_extractor.cooldown_remaining > 0:
389
+ if DEBUG and s.step_count <= 50:
390
+ print(f"[A{s.agent_id}] GATHER: Extractor on cooldown={current_extractor.cooldown_remaining}")
391
+ # Try another extractor while this one cools down
392
+ other = s.get_nearest_usable_extractor(exclude=extractor.position)
393
+ if other is not None and other.cooldown_remaining == 0:
394
+ return self._move_towards(s, other.position, reach_adjacent=True)
395
+ # Wait for cooldown - noop
396
+ return self._noop()
397
+
398
+ # Start tracking this mine attempt
399
+ s.start_action_attempt("mine", current_extractor.position)
400
+
401
+ # Extract!
402
+ if DEBUG and s.step_count <= 60:
403
+ print(f"[A{s.agent_id}] GATHER: MINING at {current_extractor.position} (energy={s.energy})!")
404
+ return self._use_object_at(s, current_extractor.position)
405
+
406
+ def _explore_for_extractors(self, s: CogsguardAgentState) -> Action:
407
+ """Explore towards map corners where extractors are located."""
408
+ # Track exploration progress - rotate through corners quickly
409
+ # Each miner starts at a different corner, rotates every 50 steps
410
+ miner_idx = s.agent_id % 4 # Spread across all 4 corners
411
+ # Rotate corner based on step count - faster rotation (every 50 steps) to find all resources
412
+ corner_rotation = s.step_count // 50
413
+ corner_idx = (miner_idx + corner_rotation) % len(CORNER_OFFSETS)
414
+ dr, dc = CORNER_OFFSETS[corner_idx]
415
+
416
+ # Target a point away from center in the corner direction
417
+ target_r = max(10, min(s.map_height - 10, s.row + dr))
418
+ target_c = max(10, min(s.map_width - 10, s.col + dc))
419
+
420
+ # If we've reached our corner area, switch to regular exploration
421
+ at_corner = ((dr < 0 and s.row < 95) or (dr > 0 and s.row > 105)) and (
422
+ (dc < 0 and s.col < 95) or (dc > 0 and s.col > 105)
423
+ )
424
+
425
+ if at_corner:
426
+ # In corner area, explore locally to find extractors
427
+ return self._explore(s)
428
+
429
+ # Move towards corner
430
+ if DEBUG and s.step_count <= 20:
431
+ print(f"[A{s.agent_id}] MINER: exploring towards corner {corner_idx}, target=({target_r},{target_c})")
432
+ return self._move_towards(s, (target_r, target_c), reach_adjacent=False)
433
+
434
+ def _get_safe_extractor(
435
+ self,
436
+ s: CogsguardAgentState,
437
+ preferred_resource: str | None = None,
438
+ ) -> "StructureInfo | None":
439
+ """Get extractor prioritized by distance to aligned stations.
440
+
441
+ Prioritizes extractors nearest to aligned buildings (hub/cogs junctions)
442
+ for shorter, safer mining routes.
443
+
444
+ Considers:
445
+ 1. Distance from extractor to nearest aligned station (primary sort)
446
+ 2. Distance from clips junctions (enemy AOE damage)
447
+ 3. HP-based range limit - only select extractors we can reach and return from safely
448
+ """
449
+ # Avoid extractors within enemy AOE so "safe" picks never drain faster than expected.
450
+ danger_range = ENEMY_AOE_RANGE
451
+
452
+ # Get all clips junctions
453
+ junctions = s.get_structures_by_type(StructureType.CHARGER)
454
+ danger_zones = [c.position for c in junctions if c.alignment != "cogs"]
455
+
456
+ # Calculate max safe operating distance based on HP
457
+ # We need HP to get to extractor AND back to healing zone
458
+ max_safe_dist = self._get_max_safe_distance(s)
459
+
460
+ # Get usable extractors
461
+ extractors = s.get_usable_extractors()
462
+ if preferred_resource:
463
+ preferred = [ext for ext in extractors if ext.resource_type == preferred_resource]
464
+ if preferred:
465
+ extractors = preferred
466
+
467
+ # Sort by distance to aligned station and prefer safe ones within HP range
468
+ safe_extractors: list[tuple[int, int, StructureInfo]] = [] # (dist_to_depot, dist_to_ext, ext)
469
+ risky_extractors: list[tuple[int, int, StructureInfo]] = []
470
+ fallback_preferred: list[tuple[int, int, StructureInfo]] = []
471
+
472
+ # Get nearest aligned depot for round-trip calculations
473
+ nearest_depot = self._get_nearest_aligned_depot(s)
474
+ steps_currently_to_healing = self._get_steps_to_healing(s)
475
+
476
+ for ext in extractors:
477
+ dist_to_ext = abs(ext.position[0] - s.row) + abs(ext.position[1] - s.col)
478
+
479
+ # Calculate distance from extractor to nearest aligned building
480
+ if nearest_depot:
481
+ dist_ext_to_depot = abs(ext.position[0] - nearest_depot[0]) + abs(ext.position[1] - nearest_depot[1])
482
+ steps_from_ext_to_healing = max(0, dist_ext_to_depot - HEALING_AOE_RANGE)
483
+ if steps_currently_to_healing == 0:
484
+ # Already in healing AOE; only count steps outside AOE both ways.
485
+ round_trip = 2 * steps_from_ext_to_healing
486
+ else:
487
+ round_trip = dist_to_ext + steps_from_ext_to_healing
488
+ else:
489
+ # Unknown depot - use conservative estimate
490
+ dist_ext_to_depot = 100 # Large number to deprioritize
491
+ round_trip = dist_to_ext * 2
492
+
493
+ # Skip extractors that are too far for our current HP
494
+ if round_trip > max_safe_dist:
495
+ if preferred_resource and ext.resource_type == preferred_resource:
496
+ fallback_preferred.append((dist_ext_to_depot, dist_to_ext, ext))
497
+ if DEBUG:
498
+ print(
499
+ f"[A{s.agent_id}] MINER: Skipping extractor at {ext.position}, "
500
+ f"round_trip={round_trip} > max_safe={max_safe_dist}"
501
+ )
502
+ continue
503
+
504
+ # Check if extractor is in danger zone (enemy AOE)
505
+ is_safe = True
506
+ for danger_pos in danger_zones:
507
+ danger_dist = abs(ext.position[0] - danger_pos[0]) + abs(ext.position[1] - danger_pos[1])
508
+ if danger_dist < danger_range:
509
+ is_safe = False
510
+ break
511
+
512
+ # Store with dist_to_depot as primary sort key, dist_to_ext as tiebreaker
513
+ if is_safe:
514
+ safe_extractors.append((dist_ext_to_depot, dist_to_ext, ext))
515
+ else:
516
+ risky_extractors.append((dist_ext_to_depot, dist_to_ext, ext))
517
+
518
+ # Prefer safe extractors, sorted by distance to aligned station
519
+ if safe_extractors:
520
+ safe_extractors.sort(key=lambda x: (x[0], x[1])) # Primary: depot dist, Secondary: agent dist
521
+ return safe_extractors[0][2]
522
+
523
+ # Fall back to risky extractors if no safe ones
524
+ if risky_extractors:
525
+ risky_extractors.sort(key=lambda x: (x[0], x[1]))
526
+ return risky_extractors[0][2]
527
+
528
+ if fallback_preferred:
529
+ fallback_preferred.sort(key=lambda x: (x[0], x[1]))
530
+ return fallback_preferred[0][2]
531
+
532
+ return None
533
+
534
+ def _get_max_safe_distance(self, s: CogsguardAgentState) -> int:
535
+ """Calculate max round-trip distance based on current HP.
536
+
537
+ Returns the maximum total distance (to target + back to healing) that's safe.
538
+ Uses current drain rate which may be elevated if near enemy buildings.
539
+ """
540
+ # Reserve HP_SAFETY_MARGIN for emergencies
541
+ available_hp = max(0, s.hp - HP_SAFETY_MARGIN)
542
+
543
+ # Use current drain rate (may be faster near enemies)
544
+ drain_rate = self._get_hp_drain_rate(s)
545
+
546
+ # Max steps we can take before HP runs out
547
+ max_steps = available_hp // drain_rate if drain_rate > 0 else available_hp
548
+
549
+ return max_steps
550
+
551
+ def _do_deposit(self, s: CogsguardAgentState) -> Action:
552
+ """Deposit resources at the nearest aligned building.
553
+
554
+ Energy-aware: checks if we have enough energy to reach the depot.
555
+ """
556
+ depot_pos = self._get_nearest_aligned_depot(s)
557
+
558
+ if depot_pos is None:
559
+ if DEBUG:
560
+ print(f"[A{s.agent_id}] DEPOSIT: No aligned building found, exploring")
561
+ return self._explore(s)
562
+
563
+ # Check if we have enough energy to reach the depot
564
+ dist = abs(depot_pos[0] - s.row) + abs(depot_pos[1] - s.col)
565
+ if not s.has_enough_energy_for_moves(dist + 2): # +2 for safety margin
566
+ if DEBUG:
567
+ print(
568
+ f"[A{s.agent_id}] DEPOSIT: Not enough energy ({s.energy}) to reach "
569
+ f"depot at dist={dist}, but going anyway (AOE will recharge)"
570
+ )
571
+ # Note: we need to recharge BUT we're carrying cargo, so go to depot anyway
572
+ # since aligned buildings provide energy AOE - recharging and depositing are the same trip!
573
+ pass # Continue to move to depot - we'll recharge there
574
+
575
+ if not is_adjacent((s.row, s.col), depot_pos):
576
+ if DEBUG and s.step_count % 20 == 0:
577
+ print(f"[A{s.agent_id}] DEPOSIT: Moving to depot at {depot_pos}")
578
+ return self._move_towards(s, depot_pos, reach_adjacent=True)
579
+
580
+ # Track resources deposited (for gear re-acquisition logic)
581
+ cargo_to_deposit = s.total_cargo
582
+ s._resources_deposited_since_gear_attempt += cargo_to_deposit
583
+ if DEBUG:
584
+ print(
585
+ f"[A{s.agent_id}] DEPOSIT: At depot {depot_pos}, cargo={cargo_to_deposit}, "
586
+ f"total_deposited={s._resources_deposited_since_gear_attempt}"
587
+ )
588
+
589
+ return self._use_object_at(s, depot_pos)
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable
5
+
6
+ from mettagrid.simulator import Action
7
+
8
+ from .types import CogsguardAgentState
9
+
10
+ OptionPredicate = Callable[[CogsguardAgentState], bool]
11
+ OptionAction = Callable[[CogsguardAgentState], Action]
12
+
13
+
14
+ @dataclass
15
+ class OptionDef:
16
+ name: str
17
+ can_start: OptionPredicate
18
+ act: OptionAction
19
+ should_terminate: OptionPredicate
20
+ interruptible: bool = True
21
+
22
+
23
+ def options_always_can_start(_: CogsguardAgentState) -> bool:
24
+ return True
25
+
26
+
27
+ def options_always_terminate(_: CogsguardAgentState) -> bool:
28
+ return True
29
+
30
+
31
+ def run_options(s: CogsguardAgentState, options: list[OptionDef]) -> Action:
32
+ if not options:
33
+ return Action(name="noop")
34
+
35
+ def reset_active() -> None:
36
+ s.active_option_id = -1
37
+ s.active_option_ticks = 0
38
+
39
+ option_count = len(options)
40
+ if 0 <= s.active_option_id < option_count:
41
+ active_idx = s.active_option_id
42
+ active = options[active_idx]
43
+ if active.interruptible:
44
+ for idx in range(active_idx):
45
+ if options[idx].can_start(s):
46
+ s.active_option_id = idx
47
+ s.active_option_ticks = 0
48
+ active_idx = idx
49
+ active = options[idx]
50
+ break
51
+ s.active_option_ticks += 1
52
+ action = active.act(s)
53
+ if active.should_terminate(s):
54
+ reset_active()
55
+ return action
56
+
57
+ for idx, opt in enumerate(options):
58
+ if not opt.can_start(s):
59
+ continue
60
+ s.active_option_id = idx
61
+ s.active_option_ticks = 1
62
+ action = opt.act(s)
63
+ if opt.should_terminate(s):
64
+ reset_active()
65
+ return action
66
+
67
+ return Action(name="noop")