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,381 @@
1
+ """
2
+ Data types and structures for CoGsGuard scripted agents.
3
+
4
+ This module contains state, enums, and type definitions for role-based agents.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from typing import TYPE_CHECKING, Optional
12
+
13
+ from cogames_agents.policy.scripted_agent.common.roles import ROLE_TO_GEAR, ROLE_TO_STATION, Role
14
+ from mettagrid.simulator import Action
15
+
16
+ if TYPE_CHECKING:
17
+ from mettagrid.simulator.interface import AgentObservation
18
+
19
+
20
+ class CogsguardPhase(Enum):
21
+ """Phases for CoGsGuard agents."""
22
+
23
+ GET_GEAR = "get_gear" # Find and equip role-specific gear
24
+ EXECUTE_ROLE = "execute_role" # Execute role-specific behavior
25
+ RECHARGE = "recharge" # Recharge energy at junction
26
+
27
+
28
+ class StructureType(Enum):
29
+ """Types of structures in the game."""
30
+
31
+ HUB = "hub" # Main hub / resource deposit point
32
+ CHARGER = "junction" # Supply depot
33
+ MINER_STATION = "miner_station"
34
+ SCOUT_STATION = "scout_station"
35
+ ALIGNER_STATION = "aligner_station"
36
+ SCRAMBLER_STATION = "scrambler_station"
37
+ EXTRACTOR = "extractor" # Resource extractor/chest
38
+ CHEST = "chest" # Heart acquisition point
39
+ WALL = "wall"
40
+ UNKNOWN = "unknown"
41
+
42
+
43
+ @dataclass
44
+ class StructureInfo:
45
+ """Information about a discovered structure."""
46
+
47
+ position: tuple[int, int]
48
+ structure_type: StructureType
49
+ name: str # Original object name
50
+
51
+ # Common attributes
52
+ last_seen_step: int = 0
53
+
54
+ # Alignment (for depots/hubs): "cogs", "clips", or None (neutral)
55
+ alignment: Optional[str] = None
56
+
57
+ # Extractor-specific attributes
58
+ resource_type: Optional[str] = None # carbon, oxygen, germanium, silicon
59
+ remaining_uses: int = 999
60
+ cooldown_remaining: int = 0
61
+ clipped: bool = False # True if owned by clips
62
+ inventory_amount: int = 999 # Current resource amount in extractor inventory
63
+
64
+ def is_usable_extractor(self) -> bool:
65
+ """Check if this is a usable extractor (not depleted, not clipped, has resources).
66
+
67
+ An extractor is usable if:
68
+ - It's an extractor structure type
69
+ - It has remaining uses (not permanently depleted)
70
+ - It has resources to extract (inventory_amount > 0)
71
+ - It's not owned by clips (clipped)
72
+ """
73
+ if self.structure_type != StructureType.EXTRACTOR:
74
+ return False
75
+ if self.remaining_uses <= 0:
76
+ return False
77
+ if self.inventory_amount <= 0:
78
+ return False
79
+ if self.clipped:
80
+ return False
81
+ return True
82
+
83
+
84
+ # Map roles to their gear station structure types
85
+ ROLE_TO_STRUCTURE_TYPE = {
86
+ Role.MINER: StructureType.MINER_STATION,
87
+ Role.SCOUT: StructureType.SCOUT_STATION,
88
+ Role.ALIGNER: StructureType.ALIGNER_STATION,
89
+ Role.SCRAMBLER: StructureType.SCRAMBLER_STATION,
90
+ }
91
+
92
+
93
+ @dataclass
94
+ class CogsguardAgentState:
95
+ """State for a CoGsGuard agent."""
96
+
97
+ agent_id: int
98
+ role: Role
99
+
100
+ # Current phase
101
+ phase: CogsguardPhase = CogsguardPhase.GET_GEAR
102
+
103
+ # Current vibe (read from observation)
104
+ current_vibe: str = "default"
105
+
106
+ # Role switching guardrails (for smart-role logic)
107
+ last_role_switch_step: int = 0
108
+ role_lock_until_step: int = 0
109
+
110
+ step_count: int = 0
111
+
112
+ # Position tracking (origin-relative)
113
+ row: int = 0
114
+ col: int = 0
115
+ energy: int = 100
116
+
117
+ # Map knowledge
118
+ map_height: int = 200
119
+ map_width: int = 200
120
+ occupancy: list[list[int]] = field(default_factory=list)
121
+ # Track which cells have been observed (explored)
122
+ explored: list[list[bool]] = field(default_factory=list)
123
+
124
+ # === Unified structure map ===
125
+ # All discovered structures: position -> StructureInfo
126
+ structures: dict[tuple[int, int], StructureInfo] = field(default_factory=dict)
127
+ stations: dict[str, Optional[tuple[int, int]]] = field(default_factory=dict)
128
+ supply_depots: list[tuple[tuple[int, int], Optional[str]]] = field(default_factory=list)
129
+
130
+ # Alignment overrides from our own actions (pos -> alignment).
131
+ alignment_overrides: dict[tuple[int, int], Optional[str]] = field(default_factory=dict)
132
+
133
+ # Inventory - gear items
134
+ miner: int = 0
135
+ scout: int = 0
136
+ aligner: int = 0
137
+ scrambler: int = 0
138
+
139
+ # Inventory - resources
140
+ carbon: int = 0
141
+ oxygen: int = 0
142
+ germanium: int = 0
143
+ silicon: int = 0
144
+ heart: int = 0
145
+ influence: int = 0
146
+ hp: int = 100
147
+
148
+ @property
149
+ def cargo_capacity(self) -> int:
150
+ """Current cargo capacity based on miner gear.
151
+
152
+ Base capacity is 4. Miner gear sets capacity to 40 per item.
153
+ This matches ResourceLimitsConfig semantics (max(min, sum(modifiers))).
154
+ """
155
+ base = 4
156
+ bonus = 40 * self.miner
157
+ return max(base, bonus)
158
+
159
+ @property
160
+ def total_cargo(self) -> int:
161
+ """Total resources currently carried."""
162
+ return self.carbon + self.oxygen + self.germanium + self.silicon
163
+
164
+ # Track last action for position updates
165
+ last_action: Action = field(default_factory=lambda: Action(name="noop"))
166
+
167
+ # Track what action the simulator actually executed (from observation)
168
+ last_action_executed: Optional[str] = None
169
+
170
+ # Navigation state
171
+ target_position: Optional[tuple[int, int]] = None
172
+ cached_path: Optional[list[tuple[int, int]]] = None
173
+ cached_path_target: Optional[tuple[int, int]] = None
174
+ cached_path_reach_adjacent: bool = False
175
+
176
+ # Exploration state
177
+ exploration_target: Optional[str] = None
178
+ exploration_target_step: int = 0
179
+
180
+ # Agent collision detection
181
+ agent_occupancy: set[tuple[int, int]] = field(default_factory=set)
182
+
183
+ # Position history for stuck detection
184
+ position_history: list[tuple[int, int]] = field(default_factory=list)
185
+
186
+ # Object interaction tracking
187
+ using_object_this_step: bool = False
188
+
189
+ # Current observation reference
190
+ current_obs: Optional[AgentObservation] = None
191
+
192
+ # Track junctions we've worked on (position -> last interaction step)
193
+ # Used by aligners/scramblers to avoid getting stuck on the same junction
194
+ worked_junctions: dict[tuple[int, int], int] = field(default_factory=dict)
195
+
196
+ # Scrambler-specific tracking for heart acquisition timeout
197
+ _heart_wait_start: int = 0
198
+ _last_heart_count: int = 0
199
+
200
+ # Action retry tracking
201
+ _pending_action_type: Optional[str] = None # "scramble", "align", "mine"
202
+ _pending_action_target: Optional[tuple[int, int]] = None
203
+ _action_retry_count: int = 0
204
+ _pre_action_heart: int = 0 # Heart count before action attempt
205
+ _pre_action_cargo: int = 0 # Cargo count before action attempt
206
+ _pending_alignment_target: Optional[tuple[int, int]] = None
207
+
208
+ # Miner gear acquisition tracking
209
+ _gear_attempt_step: int = 0 # Step when we last tried to get gear
210
+ _resources_deposited_since_gear_attempt: int = 0 # Resources deposited since last gear attempt
211
+ _gear_attempts_failed: int = 0 # Count of failed gear acquisition attempts (for scrambler)
212
+
213
+ # Option execution state
214
+ active_option_id: int = -1
215
+ active_option_ticks: int = 0
216
+
217
+ # Energy costs (can be overridden based on game config)
218
+ MOVE_ENERGY_COST: int = 3 # Default move energy cost
219
+
220
+ def has_gear(self) -> bool:
221
+ """Check if agent has their role's gear equipped."""
222
+ gear_name = ROLE_TO_GEAR[self.role]
223
+ return getattr(self, gear_name, 0) > 0
224
+
225
+ def get_gear_station_name(self) -> str:
226
+ """Get the station name for this agent's role."""
227
+ return ROLE_TO_STATION[self.role]
228
+
229
+ def get_gear_station_type(self) -> StructureType:
230
+ """Get the structure type for this agent's gear station."""
231
+ return ROLE_TO_STRUCTURE_TYPE[self.role]
232
+
233
+ def get_structure_position(self, structure_type: StructureType) -> Optional[tuple[int, int]]:
234
+ """Get the nearest structure position for the given type."""
235
+ structure = self.get_nearest_structure(structure_type)
236
+ return structure.position if structure else None
237
+
238
+ # === Structure query methods ===
239
+
240
+ def get_structures_by_type(self, structure_type: StructureType) -> list[StructureInfo]:
241
+ """Get all structures of a given type."""
242
+ return [s for s in self.structures.values() if s.structure_type == structure_type]
243
+
244
+ def get_structure_at(self, pos: tuple[int, int]) -> Optional[StructureInfo]:
245
+ """Get structure at a specific position."""
246
+ return self.structures.get(pos)
247
+
248
+ def get_nearest_structure(
249
+ self,
250
+ structure_type: StructureType,
251
+ exclude: Optional[tuple[int, int]] = None,
252
+ ) -> Optional[StructureInfo]:
253
+ """Find the nearest structure of a given type."""
254
+ best: Optional[StructureInfo] = None
255
+ best_dist = float("inf")
256
+
257
+ for struct in self.structures.values():
258
+ if struct.structure_type != structure_type:
259
+ continue
260
+ if exclude and struct.position == exclude:
261
+ continue
262
+
263
+ dist = abs(struct.position[0] - self.row) + abs(struct.position[1] - self.col)
264
+ if dist < best_dist:
265
+ best = struct
266
+ best_dist = dist
267
+
268
+ return best
269
+
270
+ def get_usable_extractors(self) -> list[StructureInfo]:
271
+ """Get all usable extractors (not depleted, not clipped)."""
272
+ return [s for s in self.structures.values() if s.is_usable_extractor()]
273
+
274
+ def get_nearest_usable_extractor(self, exclude: Optional[tuple[int, int]] = None) -> Optional[StructureInfo]:
275
+ """Find nearest usable extractor."""
276
+ best: Optional[StructureInfo] = None
277
+ best_dist = float("inf")
278
+
279
+ for struct in self.structures.values():
280
+ if not struct.is_usable_extractor():
281
+ continue
282
+ if exclude and struct.position == exclude:
283
+ continue
284
+
285
+ dist = abs(struct.position[0] - self.row) + abs(struct.position[1] - self.col)
286
+ if dist < best_dist:
287
+ best = struct
288
+ best_dist = dist
289
+
290
+ return best
291
+
292
+ # === Action retry helpers ===
293
+
294
+ def has_enough_energy_for_moves(self, num_moves: int) -> bool:
295
+ """Check if agent has enough energy to make num_moves moves."""
296
+ return self.energy >= num_moves * self.MOVE_ENERGY_COST
297
+
298
+ def start_action_attempt(self, action_type: str, target: tuple[int, int]) -> None:
299
+ """Start tracking an action attempt for retry purposes."""
300
+ self._pending_action_type = action_type
301
+ self._pending_action_target = target
302
+ self._action_retry_count = 0
303
+ self._pre_action_heart = self.heart
304
+ self._pre_action_cargo = self.total_cargo
305
+
306
+ def record_alignment_override(self, pos: tuple[int, int], alignment: Optional[str]) -> None:
307
+ """Persist alignment changes from successful actions."""
308
+ self.alignment_overrides[pos] = alignment
309
+ struct = self.structures.get(pos)
310
+ if struct is not None:
311
+ struct.alignment = alignment
312
+
313
+ def check_action_success(self) -> bool:
314
+ """Check if the last action attempt succeeded based on state changes.
315
+
316
+ Returns True if action succeeded or no action was pending.
317
+
318
+ For moves: checks if last_action_executed matches last_action (intended).
319
+ For scramble/align: checks if heart count decreased.
320
+ For mine: checks if cargo increased.
321
+ """
322
+ if self._pending_action_type is None:
323
+ return True
324
+
325
+ action_type = self._pending_action_type
326
+
327
+ # First check: did our intended action actually execute?
328
+ # If we intended to move but executed noop, the action failed
329
+ intended = self.last_action.name if self.last_action else None
330
+ executed = self.last_action_executed
331
+ if intended and executed and intended != executed:
332
+ # Action failed at the move level - don't clear, allow retry
333
+ return False
334
+
335
+ # Check based on action type
336
+ if action_type in ("scramble", "align"):
337
+ # These actions consume 1 heart on success
338
+ if self.heart < self._pre_action_heart:
339
+ if self._pending_action_target:
340
+ target = self._pending_action_target
341
+ new_alignment = "cogs" if action_type == "align" else None
342
+ self.record_alignment_override(target, new_alignment)
343
+ for idx, (pos, _alignment) in enumerate(self.supply_depots):
344
+ if pos == target:
345
+ self.supply_depots[idx] = (pos, new_alignment)
346
+ if action_type == "scramble":
347
+ self._pending_alignment_target = target
348
+ elif action_type == "align" and target == self._pending_alignment_target:
349
+ self._pending_alignment_target = None
350
+ # Heart was consumed - action succeeded
351
+ self.clear_pending_action()
352
+ return True
353
+ return False
354
+
355
+ elif action_type == "mine":
356
+ # Mining increases cargo
357
+ if self.total_cargo > self._pre_action_cargo:
358
+ self.clear_pending_action()
359
+ return True
360
+ return False
361
+
362
+ # Unknown action type - assume success
363
+ self.clear_pending_action()
364
+ return True
365
+
366
+ def increment_retry(self) -> int:
367
+ """Increment retry count and return current count."""
368
+ self._action_retry_count += 1
369
+ return self._action_retry_count
370
+
371
+ def clear_pending_action(self) -> None:
372
+ """Clear pending action tracking."""
373
+ self._pending_action_type = None
374
+ self._pending_action_target = None
375
+ self._action_retry_count = 0
376
+
377
+ def should_retry_action(self, max_retries: int = 3) -> bool:
378
+ """Check if we should retry the pending action."""
379
+ if self._pending_action_type is None:
380
+ return False
381
+ return self._action_retry_count < max_retries
@@ -0,0 +1,49 @@
1
+ """CogsGuard scripted policy with a tuned default role mix."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from mettagrid.policy.policy_env_interface import PolicyEnvInterface
8
+
9
+ from .policy import CogsguardPolicy
10
+
11
+
12
+ def _default_role_counts(num_agents: int) -> dict[str, int]:
13
+ if num_agents <= 1:
14
+ return {"miner": 1}
15
+ if num_agents == 2:
16
+ return {"scrambler": 1, "miner": 1}
17
+ if num_agents == 3:
18
+ return {"scrambler": 1, "miner": 1, "scout": 1}
19
+ if num_agents <= 7:
20
+ scramblers = 1
21
+ aligners = 1
22
+ scouts = 1
23
+ else:
24
+ scramblers = max(2, num_agents // 6)
25
+ aligners = max(2, num_agents // 6)
26
+ scouts = 1
27
+ miners = max(1, num_agents - scramblers - scouts - aligners)
28
+ return {
29
+ "scrambler": scramblers,
30
+ "aligner": aligners,
31
+ "miner": miners,
32
+ "scout": scouts,
33
+ }
34
+
35
+
36
+ class CogsguardV2Agent(CogsguardPolicy):
37
+ """Scripted cogsguard policy with better default role allocation."""
38
+
39
+ short_names = ["cogsguard_v2"]
40
+
41
+ def __init__(
42
+ self,
43
+ policy_env_info: PolicyEnvInterface,
44
+ device: str = "cpu",
45
+ **vibe_counts: Any,
46
+ ):
47
+ if not any(isinstance(v, int) for v in vibe_counts.values()):
48
+ vibe_counts = {**vibe_counts, **_default_role_counts(policy_env_info.num_agents)}
49
+ super().__init__(policy_env_info, device=device, **vibe_counts)
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ MOVE_DELTAS: dict[str, tuple[int, int]] = {
4
+ "north": (-1, 0),
5
+ "south": (1, 0),
6
+ "east": (0, 1),
7
+ "west": (0, -1),
8
+ }
9
+
10
+ DIRECTIONS = ["north", "south", "east", "west"]
11
+
12
+
13
+ def manhattan(a: tuple[int, int], b: tuple[int, int]) -> int:
14
+ return abs(a[0] - b[0]) + abs(a[1] - b[1])
15
+
16
+
17
+ def manhattan_distance(a: tuple[int, int], b: tuple[int, int]) -> int:
18
+ return manhattan(a, b)
19
+
20
+
21
+ def is_adjacent(pos1: tuple[int, int], pos2: tuple[int, int]) -> bool:
22
+ dr = abs(pos1[0] - pos2[0])
23
+ dc = abs(pos1[1] - pos2[1])
24
+ return (dr == 1 and dc == 0) or (dr == 0 and dc == 1)
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class Role(Enum):
7
+ MINER = "miner"
8
+ SCOUT = "scout"
9
+ ALIGNER = "aligner"
10
+ SCRAMBLER = "scrambler"
11
+
12
+
13
+ ROLE_TO_STATION: dict[Role, str] = {
14
+ Role.MINER: "miner_station",
15
+ Role.SCOUT: "scout_station",
16
+ Role.ALIGNER: "aligner_station",
17
+ Role.SCRAMBLER: "scrambler_station",
18
+ }
19
+
20
+ ROLE_TO_GEAR: dict[Role, str] = {
21
+ Role.MINER: "miner",
22
+ Role.SCOUT: "scout",
23
+ Role.ALIGNER: "aligner",
24
+ Role.SCRAMBLER: "scrambler",
25
+ }
26
+
27
+ ROLE_VIBES = ["miner", "scout", "aligner", "scrambler"]
28
+
29
+ VIBE_TO_ROLE: dict[str, Role] = {
30
+ "miner": Role.MINER,
31
+ "scout": Role.SCOUT,
32
+ "aligner": Role.ALIGNER,
33
+ "scrambler": Role.SCRAMBLER,
34
+ }
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+
6
+ def select_primary_tag(tags: list[str], *, priority_objects: Optional[set[str]] = None) -> str:
7
+ if not tags:
8
+ return "unknown"
9
+
10
+ for tag in tags:
11
+ if tag.startswith("type:"):
12
+ return tag.split(":", 1)[1]
13
+
14
+ if priority_objects:
15
+ for tag in tags:
16
+ if tag and not tag.startswith("collective:") and tag in priority_objects:
17
+ return tag
18
+
19
+ for tag in tags:
20
+ if tag and not tag.startswith("collective:"):
21
+ return tag
22
+
23
+ for tag in tags:
24
+ if tag:
25
+ return tag
26
+
27
+ return "unknown"
28
+
29
+
30
+ def derive_alignment(
31
+ obj_name: str,
32
+ clipped: int,
33
+ collective_id: Optional[int],
34
+ *,
35
+ cogs_collective_id: Optional[int],
36
+ clips_collective_id: Optional[int],
37
+ ) -> Optional[str]:
38
+ if collective_id is not None:
39
+ if collective_id == cogs_collective_id:
40
+ return "cogs"
41
+ if collective_id == clips_collective_id:
42
+ return "clips"
43
+
44
+ if "cogs" in obj_name:
45
+ return "cogs"
46
+ if "clips" in obj_name or clipped > 0:
47
+ return "clips"
48
+ return None