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,416 @@
1
+ """Miner goals — pick resource, mine, deposit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ from cogames_agents.policy.scripted_agent.planky.goal import Goal
8
+ from cogames_agents.policy.scripted_agent.planky.navigator import _manhattan
9
+ from mettagrid.simulator import Action
10
+
11
+ from .gear import GetGearGoal
12
+
13
+ if TYPE_CHECKING:
14
+ from cogames_agents.policy.scripted_agent.planky.context import PlankyContext
15
+
16
+
17
+ class GetMinerGearGoal(GetGearGoal):
18
+ """Get miner gear (costs C1 O1 G3 S1 from collective).
19
+
20
+ Miners always get gear regardless of reserves — they produce resources.
21
+ """
22
+
23
+ def __init__(self) -> None:
24
+ super().__init__(
25
+ gear_attr="miner_gear",
26
+ station_type="miner_station",
27
+ goal_name="GetMinerGear",
28
+ gear_cost={"carbon": 1, "oxygen": 1, "germanium": 3, "silicon": 1},
29
+ )
30
+
31
+ def _collective_can_afford(self, ctx: "PlankyContext") -> bool:
32
+ """Miners always get gear — they're the resource producers.
33
+
34
+ But skip if collective is already well-stocked (no need to mine).
35
+ """
36
+ if _collective_resources_sufficient(ctx):
37
+ return False
38
+ if not self._gear_cost:
39
+ return True
40
+ s = ctx.state
41
+ collective = {
42
+ "carbon": s.collective_carbon,
43
+ "oxygen": s.collective_oxygen,
44
+ "germanium": s.collective_germanium,
45
+ "silicon": s.collective_silicon,
46
+ }
47
+ # No reserve requirement for miners — just need the cost
48
+ return all(collective.get(res, 0) >= amt for res, amt in self._gear_cost.items())
49
+
50
+
51
+ # Resource types that can be mined
52
+ RESOURCE_TYPES = ["carbon", "oxygen", "germanium", "silicon"]
53
+
54
+ # When the collective has more than this amount of every resource, stop mining.
55
+ COLLECTIVE_SUFFICIENT_THRESHOLD = 100
56
+
57
+
58
+ def _collective_resources_sufficient(ctx: "PlankyContext") -> bool:
59
+ """Return True when the collective has >COLLECTIVE_SUFFICIENT_THRESHOLD of every resource."""
60
+ s = ctx.state
61
+ return (
62
+ s.collective_carbon > COLLECTIVE_SUFFICIENT_THRESHOLD
63
+ and s.collective_oxygen > COLLECTIVE_SUFFICIENT_THRESHOLD
64
+ and s.collective_germanium > COLLECTIVE_SUFFICIENT_THRESHOLD
65
+ and s.collective_silicon > COLLECTIVE_SUFFICIENT_THRESHOLD
66
+ )
67
+
68
+
69
+ class ExploreHubGoal(Goal):
70
+ """Explore the hub to discover all 4 extractors before mining.
71
+
72
+ Extractors are at hub corners: (±5, ±5) from center.
73
+ Each miner visits corners in a rotated order based on agent_id.
74
+ """
75
+
76
+ name = "ExploreHub"
77
+ # Hub corner offsets from SPAWN_POS — extractors at these positions
78
+ HUB_OFFSETS = [(-5, -5), (-5, 5), (5, 5), (5, -5)]
79
+
80
+ def is_satisfied(self, ctx: PlankyContext) -> bool:
81
+ found = sum(1 for r in RESOURCE_TYPES if ctx.map.find(type=f"{r}_extractor"))
82
+ if found >= 4:
83
+ return True
84
+ # Time limit: don't explore forever
85
+ if ctx.step > 15:
86
+ return True
87
+ return False
88
+
89
+ def execute(self, ctx: PlankyContext) -> Optional[Action]:
90
+ from cogames_agents.policy.scripted_agent.planky.policy import SPAWN_POS
91
+
92
+ corner_idx = ctx.blackboard.get("_hub_corner_idx", ctx.agent_id % 4)
93
+ offsets = self.HUB_OFFSETS
94
+ target = (SPAWN_POS[0] + offsets[corner_idx][0], SPAWN_POS[1] + offsets[corner_idx][1])
95
+
96
+ dist = _manhattan(ctx.state.position, target)
97
+ if dist <= 2:
98
+ corner_idx = (corner_idx + 1) % 4
99
+ ctx.blackboard["_hub_corner_idx"] = corner_idx
100
+ target = (SPAWN_POS[0] + offsets[corner_idx][0], SPAWN_POS[1] + offsets[corner_idx][1])
101
+
102
+ if ctx.trace:
103
+ ctx.trace.nav_target = target
104
+ found = sum(1 for r in RESOURCE_TYPES if ctx.map.find(type=f"{r}_extractor"))
105
+ ctx.trace.activate(self.name, f"corner={corner_idx} found={found}/4")
106
+
107
+ return ctx.navigator.get_action(ctx.state.position, target, ctx.map, reach_adjacent=True)
108
+
109
+
110
+ class PickResourceGoal(Goal):
111
+ """Select a target resource based on collective needs.
112
+
113
+ Prioritizes the resource that the collective has the least of,
114
+ ensuring balanced gathering for heart production.
115
+ Re-evaluates every 50 steps to adapt to changing needs.
116
+ """
117
+
118
+ name = "PickResource"
119
+ REEVALUATE_INTERVAL = 50
120
+
121
+ def is_satisfied(self, ctx: PlankyContext) -> bool:
122
+ # Don't bother picking a resource if collective is well-stocked
123
+ if _collective_resources_sufficient(ctx):
124
+ return True
125
+
126
+ if "target_resource" not in ctx.blackboard:
127
+ return False
128
+
129
+ # Re-evaluate periodically to ensure we're mining what's needed
130
+ last_pick = ctx.blackboard.get("_target_resource_step", 0)
131
+ if ctx.step - last_pick >= self.REEVALUATE_INTERVAL:
132
+ # Clear to force re-evaluation
133
+ ctx.blackboard.pop("target_resource", None)
134
+ return False
135
+
136
+ return True
137
+
138
+ def execute(self, ctx: PlankyContext) -> Optional[Action]:
139
+ # Get collective resource levels
140
+ collective = {
141
+ "carbon": ctx.state.collective_carbon,
142
+ "oxygen": ctx.state.collective_oxygen,
143
+ "germanium": ctx.state.collective_germanium,
144
+ "silicon": ctx.state.collective_silicon,
145
+ }
146
+
147
+ # Find resources with available extractors
148
+ available_resources: list[tuple[int, str]] = []
149
+ for resource in RESOURCE_TYPES:
150
+ extractors = ctx.map.find(type=f"{resource}_extractor")
151
+ usable = [
152
+ (pos, e)
153
+ for pos, e in extractors
154
+ if e.properties.get("remaining_uses", 999) > 0
155
+ and e.properties.get("inventory_amount", -1) != 0
156
+ and not _extractor_recently_failed(ctx, pos)
157
+ ]
158
+ if usable:
159
+ # Score by collective amount (lower = higher priority)
160
+ available_resources.append((collective.get(resource, 0), resource))
161
+
162
+ if not available_resources:
163
+ # No extractors known — pick carbon as default, MineResource will explore
164
+ ctx.blackboard["target_resource"] = "carbon"
165
+ ctx.blackboard["_target_resource_step"] = ctx.step
166
+ if ctx.trace:
167
+ ctx.trace.activate(self.name, "no extractors known, defaulting to carbon")
168
+ return Action(name="noop")
169
+
170
+ # Pick the resource the collective has least of (that we can mine)
171
+ available_resources.sort()
172
+ best_resource = available_resources[0][1]
173
+
174
+ if ctx.trace:
175
+ ctx.trace.activate(self.name, f"need={best_resource} coll={collective}")
176
+
177
+ ctx.blackboard["target_resource"] = best_resource
178
+ ctx.blackboard["_target_resource_step"] = ctx.step
179
+ return Action(name="noop")
180
+
181
+
182
+ def _extractor_recently_failed(ctx: PlankyContext, pos: tuple[int, int]) -> bool:
183
+ """Check if we recently failed to mine from this extractor."""
184
+ failed_step = ctx.blackboard.get(f"mine_failed_{pos}", -9999)
185
+ return ctx.step - failed_step < 100 # 100 step cooldown - extractors may refill
186
+
187
+
188
+ class DepositCargoGoal(Goal):
189
+ """Deposit resources at nearest cogs-aligned building when cargo is reasonably full.
190
+
191
+ Triggers when cargo is >= 50% full (or >= 10 resources for small capacity).
192
+ Once triggered, keeps depositing until cargo is EMPTY.
193
+ Tracks attempts and marks depots as failed if cargo doesn't decrease.
194
+ """
195
+
196
+ name = "DepositCargo"
197
+ MAX_ATTEMPTS_PER_DEPOT = 5
198
+
199
+ def is_satisfied(self, ctx: PlankyContext) -> bool:
200
+ cargo = ctx.state.cargo_total
201
+
202
+ # If we're currently depositing (flag set), keep going until empty
203
+ if ctx.blackboard.get("_depositing", False):
204
+ if cargo == 0:
205
+ ctx.blackboard["_depositing"] = False
206
+ return True
207
+ return False # Keep depositing until empty
208
+
209
+ # Not currently depositing - check if we should start
210
+ # Deposit when at least 50% full (but always deposit if cargo == capacity)
211
+ capacity = ctx.state.cargo_capacity
212
+ threshold = max(2, capacity // 2)
213
+
214
+ if cargo >= threshold:
215
+ ctx.blackboard["_depositing"] = True
216
+ return False # Start depositing
217
+
218
+ return True # Don't need to deposit yet
219
+
220
+ def execute(self, ctx: PlankyContext) -> Optional[Action]:
221
+ # Track cargo to detect successful deposit
222
+ prev_cargo = ctx.blackboard.get("prev_deposit_cargo", ctx.state.cargo_total)
223
+ current_cargo = ctx.state.cargo_total
224
+ ctx.blackboard["prev_deposit_cargo"] = current_cargo
225
+
226
+ # Find nearest cogs depot
227
+ depot_pos = _find_cogs_depot(ctx)
228
+ if depot_pos is None:
229
+ return ctx.navigator.explore(ctx.state.position, ctx.map)
230
+
231
+ if ctx.trace:
232
+ ctx.trace.nav_target = depot_pos
233
+
234
+ dist = _manhattan(ctx.state.position, depot_pos)
235
+ if dist <= 1:
236
+ if ctx.trace:
237
+ hub_dbg_filter = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
238
+ hubs = ctx.map.find(type_contains="hub", property_filter=hub_dbg_filter)
239
+ depot_entity = ctx.map.entities.get(depot_pos)
240
+ print(
241
+ f"[deposit-debug] agent={ctx.agent_id} t={ctx.step} pos={ctx.state.position}"
242
+ f" depot={depot_pos} depot_type={depot_entity.type if depot_entity else 'NONE'}"
243
+ f" depot_align={depot_entity.properties.get('alignment') if depot_entity else 'N/A'}"
244
+ f" cargo={current_cargo} prev={prev_cargo}"
245
+ f" hubs={[(p, e.properties.get('alignment')) for p, e in hubs]}"
246
+ )
247
+ # Adjacent to depot - track attempts
248
+ attempts_key = f"deposit_attempts_{depot_pos}"
249
+ attempts = ctx.blackboard.get(attempts_key, 0) + 1
250
+
251
+ # Reset if cargo decreased (deposit succeeded)
252
+ if current_cargo < prev_cargo:
253
+ ctx.blackboard[attempts_key] = 0
254
+ else:
255
+ ctx.blackboard[attempts_key] = attempts
256
+
257
+ if attempts > self.MAX_ATTEMPTS_PER_DEPOT:
258
+ # Mark as failed temporarily
259
+ ctx.blackboard[f"deposit_failed_{depot_pos}"] = ctx.step
260
+ ctx.blackboard[attempts_key] = 0
261
+ if ctx.trace:
262
+ ctx.trace.activate(self.name, f"giving up on {depot_pos}")
263
+ return ctx.navigator.explore(ctx.state.position, ctx.map)
264
+
265
+ return _move_toward(ctx.state.position, depot_pos)
266
+
267
+ # Not adjacent - reset attempts
268
+ ctx.blackboard[f"deposit_attempts_{depot_pos}"] = 0
269
+ return ctx.navigator.get_action(ctx.state.position, depot_pos, ctx.map, reach_adjacent=True)
270
+
271
+
272
+ class MineResourceGoal(Goal):
273
+ """Navigate to extractor for target_resource and bump it.
274
+
275
+ Tracks attempts at each extractor and marks them as failed if
276
+ cargo doesn't increase after several bumps (extractor empty/broken).
277
+ """
278
+
279
+ name = "MineResource"
280
+ MAX_ATTEMPTS_PER_EXTRACTOR = 3 # Reduced from 5 - fail faster
281
+
282
+ def is_satisfied(self, ctx: PlankyContext) -> bool:
283
+ # Stop mining when the collective is well-stocked
284
+ if _collective_resources_sufficient(ctx) and ctx.state.cargo_total == 0:
285
+ if ctx.trace:
286
+ ctx.trace.skip(self.name, "collective resources sufficient, idling")
287
+ return True
288
+ return False
289
+
290
+ def execute(self, ctx: PlankyContext) -> Optional[Action]:
291
+ target_resource = ctx.blackboard.get("target_resource", "carbon")
292
+
293
+ # Track cargo to detect successful mining
294
+ prev_cargo = ctx.blackboard.get("prev_cargo", 0)
295
+ current_cargo = ctx.state.cargo_total
296
+ ctx.blackboard["prev_cargo"] = current_cargo
297
+
298
+ # Find nearest usable extractor for this resource
299
+ target_pos = self._find_extractor(ctx, target_resource)
300
+
301
+ if target_pos is None:
302
+ # Try any resource type
303
+ for resource in RESOURCE_TYPES:
304
+ if resource == target_resource:
305
+ continue
306
+ target_pos = self._find_extractor(ctx, resource)
307
+ if target_pos:
308
+ ctx.blackboard["target_resource"] = resource
309
+ ctx.blackboard["_target_resource_step"] = ctx.step
310
+ break
311
+
312
+ if target_pos is None:
313
+ # No extractors found — explore in agent-specific direction to discover them
314
+ ctx.blackboard.pop("target_resource", None)
315
+ directions = ["north", "east", "south", "west"]
316
+ return ctx.navigator.explore(
317
+ ctx.state.position,
318
+ ctx.map,
319
+ direction_bias=directions[ctx.agent_id % 4],
320
+ )
321
+
322
+ if ctx.trace:
323
+ ctx.trace.nav_target = target_pos
324
+
325
+ dist = _manhattan(ctx.state.position, target_pos)
326
+ if dist <= 1:
327
+ # Adjacent to extractor — track attempts
328
+ attempts_key = f"mine_attempts_{target_pos}"
329
+ attempts = ctx.blackboard.get(attempts_key, 0) + 1
330
+
331
+ # Reset attempts if cargo increased (mining succeeded)
332
+ if current_cargo > prev_cargo:
333
+ ctx.blackboard[attempts_key] = 0
334
+ else:
335
+ ctx.blackboard[attempts_key] = attempts
336
+
337
+ if attempts > self.MAX_ATTEMPTS_PER_EXTRACTOR:
338
+ # Mark as failed permanently for this episode
339
+ ctx.blackboard[f"mine_failed_{target_pos}"] = ctx.step
340
+ ctx.blackboard[attempts_key] = 0
341
+ # Also clear target resource to force re-evaluation
342
+ ctx.blackboard.pop("target_resource", None)
343
+ if ctx.trace:
344
+ ctx.trace.activate(self.name, f"giving up on {target_pos}")
345
+ return ctx.navigator.explore(
346
+ ctx.state.position,
347
+ ctx.map,
348
+ direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
349
+ )
350
+
351
+ return _move_toward(ctx.state.position, target_pos)
352
+
353
+ # Don't reset attempts when moving away - only reset on successful mine
354
+ return ctx.navigator.get_action(ctx.state.position, target_pos, ctx.map, reach_adjacent=True)
355
+
356
+ def _find_extractor(self, ctx: PlankyContext, resource: str) -> Optional[tuple[int, int]]:
357
+ """Find nearest usable extractor."""
358
+ extractors = ctx.map.find(type=f"{resource}_extractor")
359
+ usable = [
360
+ (pos, e)
361
+ for pos, e in extractors
362
+ if e.properties.get("remaining_uses", 999) > 0
363
+ and e.properties.get("inventory_amount", -1) != 0
364
+ and not _extractor_recently_failed(ctx, pos)
365
+ ]
366
+
367
+ if not usable:
368
+ return None
369
+
370
+ # Sort by distance to agent
371
+ usable.sort(key=lambda x: _manhattan(ctx.state.position, x[0]))
372
+ return usable[0][0]
373
+
374
+
375
+ def _find_cogs_depot(ctx: PlankyContext) -> tuple[int, int] | None:
376
+ """Find nearest cogs-aligned depot, prioritizing hub."""
377
+ from cogames_agents.policy.scripted_agent.planky.policy import SPAWN_POS
378
+
379
+ pos = ctx.state.position
380
+
381
+ def recently_failed(p: tuple[int, int]) -> bool:
382
+ failed_step = ctx.blackboard.get(f"deposit_failed_{p}", -9999)
383
+ return ctx.step - failed_step < 100
384
+
385
+ # Prioritize own team's hub
386
+ hub_filter = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
387
+ for apos, _ in ctx.map.find(type_contains="hub", property_filter=hub_filter):
388
+ if not recently_failed(apos):
389
+ return apos
390
+
391
+ # Fallback: nearest cogs junction near hub
392
+ candidates: list[tuple[int, tuple[int, int]]] = []
393
+ for jpos, _ in ctx.map.find(type_contains="junction", property_filter={"alignment": "cogs"}):
394
+ if not recently_failed(jpos) and _manhattan(jpos, SPAWN_POS) <= 15:
395
+ candidates.append((_manhattan(pos, jpos), jpos))
396
+
397
+ if not candidates:
398
+ # Last resort: navigate to hub area
399
+ return SPAWN_POS
400
+ candidates.sort()
401
+ return candidates[0][1]
402
+
403
+
404
+ def _move_toward(current: tuple[int, int], target: tuple[int, int]) -> Action:
405
+ dr = target[0] - current[0]
406
+ dc = target[1] - current[1]
407
+ if abs(dr) >= abs(dc):
408
+ if dr > 0:
409
+ return Action(name="move_south")
410
+ elif dr < 0:
411
+ return Action(name="move_north")
412
+ if dc > 0:
413
+ return Action(name="move_east")
414
+ elif dc < 0:
415
+ return Action(name="move_west")
416
+ return Action(name="move_north")
@@ -0,0 +1,40 @@
1
+ """Scout goals — explore the map."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from cogames_agents.policy.scripted_agent.planky.goal import Goal
8
+ from mettagrid.simulator import Action
9
+
10
+ from .gear import GetGearGoal
11
+
12
+ if TYPE_CHECKING:
13
+ from cogames_agents.policy.scripted_agent.planky.context import PlankyContext
14
+
15
+
16
+ class GetScoutGearGoal(GetGearGoal):
17
+ """Get scout gear (costs C1 O1 G1 S3 from collective)."""
18
+
19
+ def __init__(self) -> None:
20
+ super().__init__(
21
+ gear_attr="scout_gear",
22
+ station_type="scout_station",
23
+ goal_name="GetScoutGear",
24
+ gear_cost={"carbon": 1, "oxygen": 1, "germanium": 1, "silicon": 3},
25
+ )
26
+
27
+
28
+ class ExploreGoal(Goal):
29
+ """Explore the map by navigating to frontier cells."""
30
+
31
+ name = "Explore"
32
+
33
+ def is_satisfied(self, ctx: PlankyContext) -> bool:
34
+ # Never satisfied — always explore
35
+ return False
36
+
37
+ def execute(self, ctx: PlankyContext) -> Action:
38
+ directions = ["north", "east", "south", "west"]
39
+ direction_bias = directions[ctx.agent_id % 4]
40
+ return ctx.navigator.explore(ctx.state.position, ctx.map, direction_bias=direction_bias)
@@ -0,0 +1,174 @@
1
+ """Scrambler goals — neutralize enemy junctions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ from cogames_agents.policy.scripted_agent.planky.goal import Goal
8
+ from cogames_agents.policy.scripted_agent.planky.navigator import _manhattan
9
+ from mettagrid.simulator import Action
10
+
11
+ from .gear import GetGearGoal
12
+
13
+ if TYPE_CHECKING:
14
+ from cogames_agents.policy.scripted_agent.planky.context import PlankyContext
15
+
16
+ JUNCTION_AOE_RANGE = 10
17
+
18
+
19
+ class GetScramblerGearGoal(GetGearGoal):
20
+ """Get scrambler gear (costs C1 O3 G1 S1 from collective)."""
21
+
22
+ def __init__(self) -> None:
23
+ super().__init__(
24
+ gear_attr="scrambler_gear",
25
+ station_type="scrambler_station",
26
+ goal_name="GetScramblerGear",
27
+ gear_cost={"carbon": 1, "oxygen": 3, "germanium": 1, "silicon": 1},
28
+ )
29
+
30
+
31
+ class ScrambleJunctionGoal(Goal):
32
+ """Find and scramble enemy (clips) junctions.
33
+
34
+ Tracks attempts per junction to avoid getting stuck.
35
+ """
36
+
37
+ name = "ScrambleJunction"
38
+ MAX_ATTEMPTS_PER_TARGET = 5
39
+ MAX_NAV_STEPS_PER_TARGET = 40 # Give up navigating to a target after this many steps
40
+ COOLDOWN_STEPS = 50
41
+
42
+ def is_satisfied(self, ctx: PlankyContext) -> bool:
43
+ # Can't scramble without gear and a heart
44
+ if not ctx.state.scrambler_gear:
45
+ if ctx.trace:
46
+ ctx.trace.skip(self.name, "no gear")
47
+ return True
48
+ if ctx.state.heart < 1:
49
+ if ctx.trace:
50
+ ctx.trace.skip(self.name, "no heart")
51
+ return True
52
+ return False
53
+
54
+ def execute(self, ctx: PlankyContext) -> Optional[Action]:
55
+ # Track navigation steps toward current target to detect stuck
56
+ nav_key = "_scramble_nav_steps"
57
+ nav_target_key = "_scramble_nav_target"
58
+ nav_steps = ctx.blackboard.get(nav_key, 0) + 1
59
+ ctx.blackboard[nav_key] = nav_steps
60
+
61
+ target = self._find_best_target(ctx)
62
+ if target is None:
63
+ ctx.blackboard[nav_key] = 0
64
+ return ctx.navigator.explore(
65
+ ctx.state.position,
66
+ ctx.map,
67
+ direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
68
+ )
69
+
70
+ # Reset nav counter if target changed
71
+ prev_target = ctx.blackboard.get(nav_target_key)
72
+ if prev_target != target:
73
+ ctx.blackboard[nav_key] = 0
74
+ nav_steps = 0
75
+ ctx.blackboard[nav_target_key] = target
76
+
77
+ # If we've been navigating too long, mark target as failed
78
+ if nav_steps > self.MAX_NAV_STEPS_PER_TARGET:
79
+ failed_key = f"scramble_failed_{target}"
80
+ ctx.blackboard[failed_key] = ctx.step
81
+ ctx.blackboard[nav_key] = 0
82
+ if ctx.trace:
83
+ ctx.trace.activate(self.name, f"nav timeout on {target}")
84
+ return ctx.navigator.explore(
85
+ ctx.state.position,
86
+ ctx.map,
87
+ direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
88
+ )
89
+
90
+ if ctx.trace:
91
+ ctx.trace.nav_target = target
92
+
93
+ dist = _manhattan(ctx.state.position, target)
94
+ if dist <= 1:
95
+ # Track attempts on this specific junction
96
+ attempts_key = f"scramble_attempts_{target}"
97
+ attempts = ctx.blackboard.get(attempts_key, 0) + 1
98
+ ctx.blackboard[attempts_key] = attempts
99
+
100
+ if attempts > self.MAX_ATTEMPTS_PER_TARGET:
101
+ # Mark this junction as failed temporarily
102
+ failed_key = f"scramble_failed_{target}"
103
+ ctx.blackboard[failed_key] = ctx.step
104
+ ctx.blackboard[attempts_key] = 0
105
+ if ctx.trace:
106
+ ctx.trace.activate(self.name, f"giving up on {target}")
107
+ return ctx.navigator.explore(
108
+ ctx.state.position,
109
+ ctx.map,
110
+ direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
111
+ )
112
+
113
+ if ctx.trace:
114
+ ctx.trace.activate(self.name, f"bump {attempts}/{self.MAX_ATTEMPTS_PER_TARGET}")
115
+ return _move_toward(ctx.state.position, target)
116
+
117
+ # Not adjacent - reset attempts for this target
118
+ attempts_key = f"scramble_attempts_{target}"
119
+ ctx.blackboard[attempts_key] = 0
120
+ return ctx.navigator.get_action(ctx.state.position, target, ctx.map, reach_adjacent=True)
121
+
122
+ def _find_best_target(self, ctx: PlankyContext) -> tuple[int, int] | None:
123
+ """Find enemy junction to scramble, prioritized by blocking count."""
124
+ pos = ctx.state.position
125
+
126
+ def recently_failed(p: tuple[int, int]) -> bool:
127
+ failed_step = ctx.blackboard.get(f"scramble_failed_{p}", -9999)
128
+ return ctx.step - failed_step < self.COOLDOWN_STEPS
129
+
130
+ # Get clips junctions
131
+ enemy: list[tuple[tuple[int, int], dict]] = []
132
+ for jpos, e in ctx.map.find(type_contains="junction", property_filter={"alignment": "clips"}):
133
+ if not recently_failed(jpos):
134
+ enemy.append((jpos, e.properties))
135
+ for cpos, e in ctx.map.find(type_contains="junction", property_filter={"alignment": "clips"}):
136
+ if not recently_failed(cpos):
137
+ enemy.append((cpos, e.properties))
138
+
139
+ if not enemy:
140
+ return None
141
+
142
+ # Get neutral junctions for scoring
143
+ neutral_positions: list[tuple[int, int]] = []
144
+ for jpos, e in ctx.map.find(type_contains="junction"):
145
+ if e.properties.get("alignment") is None:
146
+ neutral_positions.append(jpos)
147
+ for cpos, e in ctx.map.find(type_contains="junction"):
148
+ if e.properties.get("alignment") is None:
149
+ neutral_positions.append(cpos)
150
+
151
+ # Score by: how many neutrals this enemy blocks, then by distance
152
+ scored: list[tuple[int, int, tuple[int, int]]] = []
153
+ for epos, _ in enemy:
154
+ blocked = sum(1 for np in neutral_positions if _manhattan(epos, np) <= JUNCTION_AOE_RANGE)
155
+ dist = _manhattan(pos, epos)
156
+ scored.append((-blocked, dist, epos)) # Negative blocked for descending sort
157
+
158
+ scored.sort()
159
+ return scored[0][2]
160
+
161
+
162
+ def _move_toward(current: tuple[int, int], target: tuple[int, int]) -> Action:
163
+ dr = target[0] - current[0]
164
+ dc = target[1] - current[1]
165
+ if abs(dr) >= abs(dc):
166
+ if dr > 0:
167
+ return Action(name="move_south")
168
+ elif dr < 0:
169
+ return Action(name="move_north")
170
+ if dc > 0:
171
+ return Action(name="move_east")
172
+ elif dc < 0:
173
+ return Action(name="move_west")
174
+ return Action(name="move_north")