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,17 @@
1
+ """Behaviors for Pinky policy."""
2
+
3
+ from .aligner import AlignerBehavior
4
+ from .base import RoleBehavior, Services, change_vibe_action
5
+ from .miner import MinerBehavior
6
+ from .scout import ScoutBehavior
7
+ from .scrambler import ScramblerBehavior
8
+
9
+ __all__ = [
10
+ "RoleBehavior",
11
+ "Services",
12
+ "MinerBehavior",
13
+ "ScoutBehavior",
14
+ "AlignerBehavior",
15
+ "ScramblerBehavior",
16
+ "change_vibe_action",
17
+ ]
@@ -0,0 +1,400 @@
1
+ """
2
+ Aligner behavior for Pinky policy.
3
+
4
+ Aligners convert neutral junctions to expand cogs territory.
5
+ Strategy: Find viable target first, then get gear + hearts, then align.
6
+ A viable target is a neutral junction that is 10+ tiles away from any clips junction.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Optional
12
+
13
+ from cogames_agents.policy.scripted_agent.pinky.behaviors.base import (
14
+ Services,
15
+ explore_for_station,
16
+ get_explore_direction_for_agent,
17
+ is_adjacent,
18
+ manhattan_distance,
19
+ )
20
+ from cogames_agents.policy.scripted_agent.pinky.types import (
21
+ DEBUG,
22
+ ROLE_TO_STATION,
23
+ DebugInfo,
24
+ RiskTolerance,
25
+ Role,
26
+ StructureInfo,
27
+ )
28
+ from mettagrid.simulator import Action
29
+
30
+ if TYPE_CHECKING:
31
+ from cogames_agents.policy.scripted_agent.pinky.state import AgentState
32
+
33
+ # Minimum distance from clips junctions for a valid aligner target
34
+ MIN_DISTANCE_FROM_CLIPS = 8
35
+
36
+
37
+ class AlignerBehavior:
38
+ """Aligner agent: convert neutral junctions to cogs."""
39
+
40
+ role = Role.ALIGNER
41
+ risk_tolerance = RiskTolerance.MODERATE
42
+
43
+ # How many ticks to explore before retrying gear station
44
+ GEAR_RETRY_INTERVAL = 100
45
+
46
+ def act(self, state: AgentState, services: Services) -> Action:
47
+ """Execute aligner behavior.
48
+
49
+ Flow:
50
+ 1. Handle stuck/escape
51
+ 2. Retreat if low HP
52
+ 3. Find viable target (neutral junction 10+ from clips junctions)
53
+ 4. If no target -> explore
54
+ 5. If has target -> get gear -> get hearts -> align
55
+ """
56
+ # Track gear state for detecting gear loss
57
+ has_gear_now = state.aligner_gear
58
+ just_lost_gear = state.had_gear_last_step and not has_gear_now
59
+ state.had_gear_last_step = has_gear_now
60
+
61
+ # Priority 0: Check for stuck patterns and handle escape mode
62
+ escape_action = services.navigator.check_and_handle_escape(state)
63
+ if escape_action:
64
+ state.aligner_target = None # Clear target when escaping
65
+ debug_info = services.navigator.get_escape_debug_info(state)
66
+ state.debug_info = DebugInfo(**debug_info)
67
+ return escape_action
68
+
69
+ # Priority 1: Retreat if HP is getting low (be more conservative to avoid dying)
70
+ # Aligners are valuable - retreat at 50 HP to stay alive
71
+ if state.hp <= 50:
72
+ if DEBUG:
73
+ print(f"[A{state.agent_id}] ALIGNER: Retreating! HP={state.hp}")
74
+ state.debug_info = DebugInfo(mode="retreat", goal="safety", target_object="safe_zone", signal="hp_low")
75
+ return self._retreat_to_safety(state, services)
76
+
77
+ # Priority 2: Find or validate a viable target
78
+ # Re-validate current target each step (it may have been aligned by someone else or become invalid)
79
+ target = self._get_or_find_target(state, services)
80
+
81
+ # If no viable target, explore to find junctions
82
+ if target is None:
83
+ state.aligner_target = None
84
+ if DEBUG:
85
+ print(f"[A{state.agent_id}] ALIGNER: No viable target, exploring")
86
+ state.debug_info = DebugInfo(mode="explore", goal="find_junction", target_object="junction")
87
+ return self._explore_for_junctions(state, services)
88
+
89
+ # Store the target
90
+ state.aligner_target = target.position
91
+
92
+ # Priority 3: Get gear if missing (required for aligning)
93
+ if self.needs_gear(state):
94
+ # Check if we should try to get gear now
95
+ ticks_since_last_attempt = state.step - state.last_gear_attempt_step
96
+
97
+ # Try to get gear if:
98
+ # 1. Just lost gear (immediately go home)
99
+ # 2. First 30 steps (initial period)
100
+ # 3. 100 ticks have passed since last attempt
101
+ should_try_gear = just_lost_gear or state.step < 30 or ticks_since_last_attempt >= self.GEAR_RETRY_INTERVAL
102
+
103
+ if should_try_gear:
104
+ return self._get_gear(state, services)
105
+ else:
106
+ # Between retries, get hearts so we're ready when we get gear
107
+ if not self.has_resources_to_act(state):
108
+ return self._get_hearts(state, services)
109
+ # Otherwise explore toward target
110
+ state.debug_info = DebugInfo(
111
+ mode="explore", goal="toward_target", target_object="junction", target_pos=target.position
112
+ )
113
+ return self._move_toward_target(state, target.position)
114
+
115
+ # Priority 4: Get hearts if empty (needed to align)
116
+ if not self.has_resources_to_act(state):
117
+ return self._get_hearts(state, services)
118
+
119
+ # Priority 5: Move to and align the target junction
120
+ return self._align_junction(state, services, target)
121
+
122
+ def needs_gear(self, state: AgentState) -> bool:
123
+ """Aligners need aligner gear for +20 influence."""
124
+ return not state.aligner_gear
125
+
126
+ def has_resources_to_act(self, state: AgentState) -> bool:
127
+ """Aligners need hearts to align junctions."""
128
+ return state.heart >= 1
129
+
130
+ def _retreat_to_safety(self, state: AgentState, services: Services) -> Action:
131
+ """Return to nearest safe zone."""
132
+ safe_pos = services.safety.nearest_safe_zone(state)
133
+ if safe_pos is None:
134
+ return services.navigator.explore(state)
135
+ return services.navigator.move_to(state, safe_pos, reach_adjacent=True)
136
+
137
+ def _is_valid_target(self, pos: tuple[int, int], state: AgentState) -> bool:
138
+ """Check if a position is a valid aligner target.
139
+
140
+ Valid target = neutral junction that is 10+ tiles from any clips junction.
141
+ """
142
+ struct = state.map.get_structure_at(pos)
143
+ if struct is None:
144
+ return False
145
+
146
+ # Must be neutral (not already aligned)
147
+ if not struct.is_neutral():
148
+ return False
149
+
150
+ # Must be 10+ tiles from any clips junction
151
+ clips_junctions = state.map.get_clips_junctions()
152
+ for clips_j in clips_junctions:
153
+ if manhattan_distance(pos, clips_j.position) < MIN_DISTANCE_FROM_CLIPS:
154
+ return False
155
+
156
+ return True
157
+
158
+ def _get_or_find_target(self, state: AgentState, services: Services) -> Optional[StructureInfo]:
159
+ """Get current target if still valid, otherwise find a new one.
160
+
161
+ Returns None if no valid targets exist.
162
+ """
163
+ # Check if current target is still valid
164
+ current_target = getattr(state, "aligner_target", None)
165
+ if current_target is not None and self._is_valid_target(current_target, state):
166
+ struct = state.map.get_structure_at(current_target)
167
+ if struct is not None:
168
+ if DEBUG:
169
+ print(f"[A{state.agent_id}] ALIGNER: Keeping target at {current_target}")
170
+ return struct
171
+
172
+ # Current target invalid or missing, find a new one
173
+ return self._find_best_target(state, services)
174
+
175
+ def _get_gear(self, state: AgentState, services: Services) -> Action:
176
+ """Get aligner gear from station."""
177
+ station_name = ROLE_TO_STATION[Role.ALIGNER]
178
+
179
+ # First try visible aligner_station in current observation (most reliable)
180
+ if state.last_obs is not None:
181
+ result = services.map_tracker.get_direction_to_nearest(state, state.last_obs, frozenset({station_name}))
182
+ if result:
183
+ direction, target_pos = result
184
+ if DEBUG:
185
+ print(f"[A{state.agent_id}] ALIGNER: Station visible at {target_pos}, moving {direction}")
186
+ state.debug_info = DebugInfo(
187
+ mode="get_gear", goal="aligner_station", target_object=station_name, target_pos=target_pos
188
+ )
189
+ return Action(name=f"move_{direction}")
190
+
191
+ # Use accumulated map knowledge if station was found
192
+ station_pos = state.map.stations.get(station_name)
193
+
194
+ if station_pos is not None:
195
+ dist = manhattan_distance(state.pos, station_pos)
196
+
197
+ # If ON the station, we should have received gear from walking in
198
+ # If gear not received yet, record this attempt and try to be useful while waiting
199
+ if state.pos == station_pos:
200
+ if DEBUG:
201
+ print(
202
+ f"[A{state.agent_id}] ALIGNER: ON station at {station_pos}, no gear - "
203
+ f"will try to be useful and retry in {self.GEAR_RETRY_INTERVAL} ticks"
204
+ )
205
+ state.debug_info = DebugInfo(
206
+ mode="get_gear", goal="on_station_no_gear", target_object=station_name, target_pos=station_pos
207
+ )
208
+ # Record this attempt - aligner will try other things for GEAR_RETRY_INTERVAL ticks then retry
209
+ state.last_gear_attempt_step = state.step
210
+ # Step off the station, then fall through to try other behaviors
211
+ return Action(name="move_east")
212
+
213
+ if is_adjacent(state.pos, station_pos):
214
+ if DEBUG:
215
+ print(f"[A{state.agent_id}] ALIGNER: Getting gear from {station_pos}")
216
+ state.debug_info = DebugInfo(
217
+ mode="get_gear", goal="use_station", target_object=station_name, target_pos=station_pos
218
+ )
219
+ return services.navigator.use_object_at(state, station_pos)
220
+
221
+ if DEBUG and state.step % 10 == 0:
222
+ print(f"[A{state.agent_id}] ALIGNER: Moving to station at {station_pos} (dist={dist})")
223
+ state.debug_info = DebugInfo(
224
+ mode="get_gear",
225
+ goal=f"move_to_station(dist={dist})",
226
+ target_object=station_name,
227
+ target_pos=station_pos,
228
+ )
229
+ # Use simple directional movement - more reliable for aligners
230
+ return self._move_toward_target(state, station_pos)
231
+
232
+ # Station not found yet - keep exploring until we find it
233
+ # Don't give up - gear is required before getting hearts
234
+ if DEBUG and state.step % 10 == 0:
235
+ print(f"[A{state.agent_id}] ALIGNER: Exploring for station (step {state.step})")
236
+ state.debug_info = DebugInfo(mode="explore", goal="find_station", target_object=station_name)
237
+ return self._explore_for_station(state, services)
238
+
239
+ def _explore_for_station(self, state: AgentState, services: Services) -> Action:
240
+ """Explore to find the aligner station."""
241
+ # Spread aligners out by giving each agent a different primary direction
242
+ direction = get_explore_direction_for_agent(state.agent_id)
243
+ return explore_for_station(state, services, primary_direction=direction)
244
+
245
+ def _get_hearts(self, state: AgentState, services: Services) -> Action:
246
+ """Get hearts from chest."""
247
+ # First try visible chest in current observation
248
+ if state.last_obs is not None:
249
+ result = services.map_tracker.get_direction_to_nearest(state, state.last_obs, frozenset({"chest"}))
250
+ if result:
251
+ direction, target_pos = result
252
+ if DEBUG:
253
+ print(f"[A{state.agent_id}] ALIGNER: Chest visible at {target_pos}, moving {direction}")
254
+ state.debug_info = DebugInfo(
255
+ mode="get_hearts", goal="chest", target_object="chest", target_pos=target_pos
256
+ )
257
+ return Action(name=f"move_{direction}")
258
+
259
+ # Use accumulated map knowledge
260
+ chest_pos = state.map.stations.get("chest")
261
+
262
+ if chest_pos is None:
263
+ # Try hub as fallback (can also give hearts)
264
+ hub_pos = state.map.stations.get("hub")
265
+ if hub_pos is not None:
266
+ chest_pos = hub_pos
267
+ else:
268
+ if DEBUG and state.step % 20 == 0:
269
+ print(f"[A{state.agent_id}] ALIGNER: No chest/hub found, exploring")
270
+ state.debug_info = DebugInfo(mode="explore", goal="find_chest", target_object="chest")
271
+ return services.navigator.explore(state)
272
+
273
+ dist = manhattan_distance(state.pos, chest_pos)
274
+
275
+ if is_adjacent(state.pos, chest_pos):
276
+ if DEBUG:
277
+ print(f"[A{state.agent_id}] ALIGNER: Getting hearts from {chest_pos}")
278
+ state.debug_info = DebugInfo(
279
+ mode="get_hearts", goal="use_chest", target_object="chest", target_pos=chest_pos
280
+ )
281
+ return services.navigator.use_object_at(state, chest_pos)
282
+
283
+ if DEBUG and state.step % 10 == 0:
284
+ print(f"[A{state.agent_id}] ALIGNER: Moving to chest at {chest_pos} (dist={dist})")
285
+ state.debug_info = DebugInfo(
286
+ mode="get_hearts", goal=f"move_to_chest(dist={dist})", target_object="chest", target_pos=chest_pos
287
+ )
288
+ # Use simple directional movement toward target - more reliable than pathfinding
289
+ # when the internal map hasn't been fully explored
290
+ return self._move_toward_target(state, chest_pos)
291
+
292
+ def _align_junction(self, state: AgentState, services: Services, target: StructureInfo) -> Action:
293
+ """Move to and align the target junction."""
294
+ dist = manhattan_distance(state.pos, target.position)
295
+
296
+ if is_adjacent(state.pos, target.position):
297
+ if DEBUG:
298
+ print(f"[A{state.agent_id}] ALIGNER: Aligning junction at {target.position}")
299
+ state.debug_info = DebugInfo(
300
+ mode="align", goal="use_junction", target_object="junction", target_pos=target.position
301
+ )
302
+ return services.navigator.use_object_at(state, target.position)
303
+
304
+ if DEBUG and state.step % 10 == 0:
305
+ print(f"[A{state.agent_id}] ALIGNER: Moving to junction at {target.position} (dist={dist})")
306
+ state.debug_info = DebugInfo(
307
+ mode="align", goal=f"move_to_junction(dist={dist})", target_object="junction", target_pos=target.position
308
+ )
309
+ # Use simple directional movement - more reliable for aligners
310
+ return self._move_toward_target(state, target.position)
311
+
312
+ def _move_toward_target(self, state: AgentState, target: tuple[int, int]) -> Action:
313
+ """Move one step toward target using simple directional movement.
314
+
315
+ More reliable than pathfinding when internal map hasn't been fully explored.
316
+ Prioritizes the axis with the larger delta.
317
+ """
318
+ dr = target[0] - state.pos[0] # row delta (positive = south)
319
+ dc = target[1] - state.pos[1] # col delta (positive = east)
320
+
321
+ # Prioritize the axis with larger delta
322
+ if abs(dr) >= abs(dc):
323
+ # Try vertical first, then horizontal
324
+ if dr > 0:
325
+ return Action(name="move_south")
326
+ elif dr < 0:
327
+ return Action(name="move_north")
328
+ elif dc > 0:
329
+ return Action(name="move_east")
330
+ elif dc < 0:
331
+ return Action(name="move_west")
332
+ else:
333
+ # Try horizontal first, then vertical
334
+ if dc > 0:
335
+ return Action(name="move_east")
336
+ elif dc < 0:
337
+ return Action(name="move_west")
338
+ elif dr > 0:
339
+ return Action(name="move_south")
340
+ elif dr < 0:
341
+ return Action(name="move_north")
342
+
343
+ return Action(name="noop") # Already at target
344
+
345
+ def _find_best_target(self, state: AgentState, services: Services) -> Optional[StructureInfo]:
346
+ """Find alignable junction - must be neutral AND 10+ tiles from clips junctions.
347
+
348
+ Prioritizes junctions near the hub (strategic value).
349
+ """
350
+ max_dist = services.safety.max_safe_distance(state, self.risk_tolerance)
351
+
352
+ # Get hub position for prioritization
353
+ hub_pos = state.map.stations.get("hub")
354
+
355
+ # Get all clips junction positions for distance check
356
+ clips_junctions = state.map.get_clips_junctions()
357
+
358
+ def is_too_close_to_clips(pos: tuple[int, int]) -> bool:
359
+ """Target must be 10+ tiles from any clips junction."""
360
+ return any(manhattan_distance(pos, ej.position) < MIN_DISTANCE_FROM_CLIPS for ej in clips_junctions)
361
+
362
+ # Find alignable junctions: neutral AND 10+ from clips junctions
363
+ alignable: list[tuple[int, int, StructureInfo]] = [] # (hub_dist, agent_dist, junction)
364
+ for junction in state.map.get_neutral_junctions():
365
+ if is_too_close_to_clips(junction.position):
366
+ continue # Can't align - too close to clips junction
367
+
368
+ agent_dist = manhattan_distance(state.pos, junction.position)
369
+ if agent_dist > max_dist:
370
+ continue
371
+
372
+ # Prioritize junctions near hub
373
+ hub_dist = manhattan_distance(junction.position, hub_pos) if hub_pos else 999
374
+ alignable.append((hub_dist, agent_dist, junction))
375
+
376
+ if not alignable:
377
+ if DEBUG:
378
+ neutral_count = len(state.map.get_neutral_junctions())
379
+ clips_count = len(clips_junctions)
380
+ print(
381
+ f"[A{state.agent_id}] ALIGNER: No valid targets. "
382
+ f"Neutral junctions: {neutral_count}, Clips junctions: {clips_count}"
383
+ )
384
+ return None
385
+
386
+ # Sort by: 1) distance to hub (closer is better), 2) distance to agent
387
+ alignable.sort(key=lambda x: (x[0], x[1]))
388
+ return alignable[0][2]
389
+
390
+ def _explore_for_junctions(self, state: AgentState, services: Services) -> Action:
391
+ """Explore to find new junctions by filling out the map.
392
+
393
+ Uses the navigator's frontier-based exploration with a direction bias
394
+ based on agent ID to spread coverage across the map.
395
+ """
396
+ # Each aligner explores toward a different direction to spread out
397
+ directions = ["north", "east", "south", "west"]
398
+ direction_bias = directions[state.agent_id % 4]
399
+
400
+ return services.navigator.explore(state, direction_bias=direction_bias)
@@ -0,0 +1,119 @@
1
+ """
2
+ Base behavior protocol and Services dataclass for Pinky policy.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Protocol
9
+
10
+ from cogames_agents.policy.scripted_agent.common.geometry import (
11
+ is_adjacent as geometry_is_adjacent,
12
+ )
13
+ from cogames_agents.policy.scripted_agent.common.geometry import (
14
+ manhattan_distance as geometry_manhattan_distance,
15
+ )
16
+ from cogames_agents.policy.scripted_agent.pinky.types import RiskTolerance, Role
17
+ from cogames_agents.policy.scripted_agent.utils import change_vibe_action as utils_change_vibe_action
18
+ from mettagrid.simulator import Action
19
+
20
+ if TYPE_CHECKING:
21
+ from cogames_agents.policy.scripted_agent.pinky.services import MapTracker, Navigator, SafetyManager
22
+ from cogames_agents.policy.scripted_agent.pinky.state import AgentState
23
+
24
+
25
+ @dataclass
26
+ class Services:
27
+ """Bundle of shared services passed to behaviors."""
28
+
29
+ navigator: Navigator
30
+ map_tracker: MapTracker
31
+ safety: SafetyManager
32
+ action_names: list[str] # List of action names for vibe changes
33
+
34
+
35
+ class RoleBehavior(Protocol):
36
+ """Interface for role-specific decision making."""
37
+
38
+ role: Role
39
+ risk_tolerance: RiskTolerance
40
+
41
+ def act(self, state: AgentState, services: Services) -> Action:
42
+ """Decide what action to take this step."""
43
+ ...
44
+
45
+ def needs_gear(self, state: AgentState) -> bool:
46
+ """Does this role need to acquire gear?"""
47
+ ...
48
+
49
+ def has_resources_to_act(self, state: AgentState) -> bool:
50
+ """Does agent have resources needed for role actions?"""
51
+ ...
52
+
53
+
54
+ def is_adjacent(pos1: tuple[int, int], pos2: tuple[int, int]) -> bool:
55
+ """Check if two positions are adjacent (4-way)."""
56
+ return geometry_is_adjacent(pos1, pos2)
57
+
58
+
59
+ def manhattan_distance(pos1: tuple[int, int], pos2: tuple[int, int]) -> int:
60
+ """Calculate Manhattan distance."""
61
+ return geometry_manhattan_distance(pos1, pos2)
62
+
63
+
64
+ def change_vibe_action(vibe_name: str, services: Services) -> Action:
65
+ """Return action to change vibe."""
66
+ return utils_change_vibe_action(vibe_name, action_names=services.action_names)
67
+
68
+
69
+ # Steps to aggressively explore before falling back to navigator
70
+ AGGRESSIVE_EXPLORE_STEPS = 50
71
+
72
+
73
+ def explore_for_station(state: AgentState, services: Services, primary_direction: str = "south") -> Action:
74
+ """Explore to find a gear station using proper pathfinding.
75
+
76
+ Shared exploration logic for all behaviors:
77
+ - First N steps: aggressively move in primary direction (stations are typically south of spawn)
78
+ - Uses traversability checks to avoid getting stuck on walls
79
+ - Falls back to navigator.explore with direction bias
80
+
81
+ Args:
82
+ state: Agent state
83
+ services: Shared services
84
+ primary_direction: Direction to explore first (default "south" since stations are south of spawn)
85
+
86
+ Returns:
87
+ Action to explore
88
+ """
89
+ directions = ["south", "east", "west", "north"]
90
+
91
+ # First N steps: aggressively move in primary direction, checking traversability
92
+ if state.step < AGGRESSIVE_EXPLORE_STEPS:
93
+ dr, dc = services.navigator.MOVE_DELTAS[primary_direction]
94
+ target_r, target_c = state.row + dr, state.col + dc
95
+ if services.navigator._is_traversable(state, target_r, target_c, allow_unknown=True, check_agents=True):
96
+ return Action(name=f"move_{primary_direction}")
97
+ # Primary direction blocked - try alternatives
98
+ for alt_dir in directions:
99
+ if alt_dir == primary_direction:
100
+ continue
101
+ dr, dc = services.navigator.MOVE_DELTAS[alt_dir]
102
+ if services.navigator._is_traversable(
103
+ state, state.row + dr, state.col + dc, allow_unknown=True, check_agents=True
104
+ ):
105
+ return Action(name=f"move_{alt_dir}")
106
+ # All blocked - use navigator explore
107
+ return services.navigator.explore(state, direction_bias=primary_direction)
108
+
109
+ # After aggressive phase, use navigator's explore with direction bias
110
+ return services.navigator.explore(state, direction_bias=primary_direction)
111
+
112
+
113
+ def get_explore_direction_for_agent(agent_id: int) -> str:
114
+ """Get a direction bias for exploration based on agent_id.
115
+
116
+ Spreads agents out by giving each one a different primary direction.
117
+ """
118
+ directions = ["south", "east", "west", "north"]
119
+ return directions[agent_id % len(directions)]