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,189 @@
1
+ """
2
+ SafetyManager service for Pinky policy.
3
+
4
+ Manages HP/energy awareness and risk assessment.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Optional
10
+
11
+ from cogames_agents.policy.scripted_agent.pinky.types import (
12
+ HP_DRAIN_NEAR_ENEMY,
13
+ HP_DRAIN_OUTSIDE_SAFE_ZONE,
14
+ HP_SAFETY_MARGIN,
15
+ JUNCTION_AOE_RANGE,
16
+ RiskTolerance,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from cogames_agents.policy.scripted_agent.pinky.state import AgentState
21
+
22
+
23
+ class SafetyManager:
24
+ """Manages risk based on HP, energy, and territory."""
25
+
26
+ # HP thresholds for retreat by risk tolerance
27
+ # These define the HP buffer to keep when calculating safe range
28
+ RETREAT_THRESHOLDS = {
29
+ RiskTolerance.CONSERVATIVE: 40, # Miners keep 40 HP buffer for safety
30
+ RiskTolerance.MODERATE: 30, # Aligners keep 30 HP buffer
31
+ RiskTolerance.AGGRESSIVE: 20, # Scouts/Scramblers are bold, keep 20 HP
32
+ }
33
+
34
+ def should_retreat(self, state: AgentState, risk: RiskTolerance) -> bool:
35
+ """Check if agent should retreat to safety based on HP and risk tolerance.
36
+
37
+ Returns True if HP is low enough that we need to head back now.
38
+ """
39
+ # If we're already in a safe zone, no need to retreat
40
+ if self.is_in_safe_zone(state):
41
+ return False
42
+
43
+ # If no safe zones discovered yet, don't retreat - we need to explore first
44
+ # (unless HP is critically low)
45
+ safe_pos = self.nearest_safe_zone(state)
46
+ if safe_pos is None:
47
+ # Critical HP threshold - retreat even without known safe zone
48
+ return state.hp <= 20
49
+
50
+ # Calculate steps to nearest safe zone
51
+ steps_to_safety = self._steps_to_nearest_safe_zone(state)
52
+ drain_rate = self._get_hp_drain_rate(state)
53
+
54
+ # HP needed to survive the trip
55
+ hp_needed = (steps_to_safety * drain_rate) + HP_SAFETY_MARGIN
56
+
57
+ return state.hp <= hp_needed
58
+
59
+ def is_in_safe_zone(self, state: AgentState) -> bool:
60
+ """Check if agent is within AOE of any cogs-aligned junction or hub."""
61
+ # Check hub
62
+ hub_pos = state.map.stations.get("hub")
63
+ if hub_pos:
64
+ dist = abs(state.row - hub_pos[0]) + abs(state.col - hub_pos[1])
65
+ if dist <= JUNCTION_AOE_RANGE:
66
+ return True
67
+
68
+ # Check cogs junctions
69
+ for junction in state.map.get_cogs_junctions():
70
+ dist = abs(state.row - junction.position[0]) + abs(state.col - junction.position[1])
71
+ if dist <= JUNCTION_AOE_RANGE:
72
+ return True
73
+
74
+ return False
75
+
76
+ def is_in_danger_zone(self, state: AgentState) -> bool:
77
+ """Check if agent is within AOE of any clips-aligned junction."""
78
+ for junction in state.map.get_clips_junctions():
79
+ dist = abs(state.row - junction.position[0]) + abs(state.col - junction.position[1])
80
+ if dist <= JUNCTION_AOE_RANGE:
81
+ return True
82
+ return False
83
+
84
+ def nearest_safe_zone(self, state: AgentState) -> Optional[tuple[int, int]]:
85
+ """Find nearest cogs-aligned building (hub or junction)."""
86
+ candidates: list[tuple[int, tuple[int, int]]] = []
87
+
88
+ # Hub is always cogs-aligned
89
+ hub_pos = state.map.stations.get("hub")
90
+ if hub_pos:
91
+ dist = abs(hub_pos[0] - state.row) + abs(hub_pos[1] - state.col)
92
+ candidates.append((dist, hub_pos))
93
+
94
+ # Cogs junctions
95
+ for junction in state.map.get_cogs_junctions():
96
+ dist = abs(junction.position[0] - state.row) + abs(junction.position[1] - state.col)
97
+ candidates.append((dist, junction.position))
98
+
99
+ if not candidates:
100
+ return None
101
+
102
+ candidates.sort(key=lambda x: x[0])
103
+ return candidates[0][1]
104
+
105
+ def step_based_range_limit(self, step: int) -> int:
106
+ """Calculate exploration range limit based on current step.
107
+
108
+ - First 1000 ticks: limit range to 50
109
+ - After 1000 ticks: start at 100, increase by 10 every 100 ticks
110
+ """
111
+ if step < 1000:
112
+ return 50
113
+ else:
114
+ # Start at 100 at step 1000, increase by 10 every 100 steps
115
+ extra_hundreds = (step - 1000) // 100
116
+ return 100 + extra_hundreds * 10
117
+
118
+ def max_safe_distance(self, state: AgentState, risk: RiskTolerance) -> int:
119
+ """Calculate max round-trip distance based on HP, risk tolerance, and step.
120
+
121
+ Returns the maximum total distance (to target + back to healing) that's safe.
122
+ Also applies step-based range limits to encourage gradual expansion.
123
+ """
124
+ # Reserve HP based on risk tolerance
125
+ threshold = self.RETREAT_THRESHOLDS[risk]
126
+ available_hp = max(0, state.hp - threshold)
127
+
128
+ # Calculate drain rate
129
+ drain_rate = self._get_hp_drain_rate(state)
130
+ if drain_rate <= 0:
131
+ hp_based_dist = 999 # No drain, unlimited HP-based range
132
+ else:
133
+ # Max steps we can take before HP runs out
134
+ max_steps = available_hp // drain_rate
135
+ # Round trip, so divide by 2
136
+ hp_based_dist = max_steps // 2
137
+
138
+ # Apply step-based range limit
139
+ step_limit = self.step_based_range_limit(state.step)
140
+
141
+ return min(hp_based_dist, step_limit)
142
+
143
+ def can_reach_safely(self, state: AgentState, target: tuple[int, int], risk: RiskTolerance) -> bool:
144
+ """Check if agent can reach target AND return to safety."""
145
+ dist_to_target = abs(target[0] - state.row) + abs(target[1] - state.col)
146
+
147
+ # Find distance from target back to nearest safe zone
148
+ safe_pos = self.nearest_safe_zone(state)
149
+ if safe_pos is None:
150
+ # No known safe zone, be conservative
151
+ dist_back = dist_to_target
152
+ else:
153
+ dist_back = abs(target[0] - safe_pos[0]) + abs(target[1] - safe_pos[1])
154
+
155
+ total_dist = dist_to_target + max(0, dist_back - JUNCTION_AOE_RANGE)
156
+ max_dist = self.max_safe_distance(state, risk)
157
+
158
+ return total_dist <= max_dist
159
+
160
+ def is_position_in_enemy_aoe(self, state: AgentState, pos: tuple[int, int]) -> bool:
161
+ """Check if a position is within AOE of any enemy junction."""
162
+ for junction in state.map.get_clips_junctions():
163
+ dist = abs(pos[0] - junction.position[0]) + abs(pos[1] - junction.position[1])
164
+ if dist <= JUNCTION_AOE_RANGE:
165
+ return True
166
+ return False
167
+
168
+ def _steps_to_nearest_safe_zone(self, state: AgentState) -> int:
169
+ """Calculate steps to reach nearest safe zone's AOE."""
170
+ safe_pos = self.nearest_safe_zone(state)
171
+ if safe_pos is None:
172
+ return 100 # Unknown, be conservative
173
+
174
+ dist = abs(safe_pos[0] - state.row) + abs(safe_pos[1] - state.col)
175
+ return max(0, dist - JUNCTION_AOE_RANGE)
176
+
177
+ def _get_hp_drain_rate(self, state: AgentState) -> int:
178
+ """Calculate current HP drain rate based on position."""
179
+ # If in safe zone, no drain
180
+ if self.is_in_safe_zone(state):
181
+ return 0
182
+
183
+ drain = HP_DRAIN_OUTSIDE_SAFE_ZONE
184
+
185
+ # Additional drain if near enemy
186
+ if self.is_in_danger_zone(state):
187
+ drain += HP_DRAIN_NEAR_ENEMY
188
+
189
+ return drain
@@ -0,0 +1,299 @@
1
+ """
2
+ State classes for Pinky policy.
3
+
4
+ AgentState, MapKnowledge, and NavigationState dataclasses.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import TYPE_CHECKING, Optional
11
+
12
+ from mettagrid.simulator import Action
13
+
14
+ if TYPE_CHECKING:
15
+ from mettagrid.simulator.interface import AgentObservation
16
+
17
+ from .types import CellType, DebugInfo, Role, StructureInfo, StructureType
18
+
19
+
20
+ @dataclass
21
+ class NavigationState:
22
+ """Navigation-related state managed by Navigator service."""
23
+
24
+ # Path caching
25
+ cached_path: Optional[list[tuple[int, int]]] = None
26
+ cached_path_target: Optional[tuple[int, int]] = None
27
+ cached_path_reach_adjacent: bool = False
28
+
29
+ # Exploration state (expanding box pattern)
30
+ explore_origin: Optional[tuple[int, int]] = None
31
+ explore_start_step: int = 0
32
+ explore_radius: int = 15 # Initial exploration radius, grows by 10% when area exhausted
33
+ explore_last_mineral_step: int = 0 # Last step we saw a mineral
34
+ # Resource rotation for miners - track recently gathered resources
35
+ last_resource_types: list[str] = field(default_factory=list) # Most recent first, max 4
36
+ # Current extractor target - track to detect stuck/empty situations
37
+ current_extractor_target: Optional[tuple[int, int]] = None
38
+ steps_at_current_extractor: int = 0 # Steps spent at/near current target without cargo gain
39
+ failed_extractors: set[tuple[int, int]] = field(default_factory=set) # Extractors that gave nothing
40
+ # Legacy fields (kept for compatibility)
41
+ exploration_direction: Optional[str] = None
42
+ exploration_direction_step: int = 0
43
+
44
+ # Stuck detection
45
+ position_history: list[tuple[int, int]] = field(default_factory=list)
46
+
47
+ # Track last action for position updates
48
+ last_action: Action = field(default_factory=lambda: Action(name="noop"))
49
+ last_action_executed: Optional[str] = None
50
+ using_object_this_step: bool = False
51
+
52
+
53
+ @dataclass
54
+ class AgentSighting:
55
+ """Information about a sighted agent."""
56
+
57
+ position: tuple[int, int]
58
+ last_seen_step: int
59
+
60
+
61
+ @dataclass
62
+ class MapKnowledge:
63
+ """What the agent has discovered about the world."""
64
+
65
+ grid_size: int = 200
66
+
67
+ # Occupancy grid: CellType.FREE or CellType.OBSTACLE
68
+ occupancy: list[list[int]] = field(default_factory=list)
69
+
70
+ # Which cells have been observed
71
+ explored: list[list[bool]] = field(default_factory=list)
72
+
73
+ # All discovered structures: position -> StructureInfo
74
+ structures: dict[tuple[int, int], StructureInfo] = field(default_factory=dict)
75
+
76
+ # Quick lookups (station_name -> position)
77
+ stations: dict[str, tuple[int, int]] = field(default_factory=dict)
78
+
79
+ # Other agents' positions (for collision avoidance)
80
+ # Current observation: positions of agents seen this step
81
+ agent_occupancy: set[tuple[int, int]] = field(default_factory=set)
82
+
83
+ # Recently seen agents with last-known positions
84
+ # Cleared when observation window passes over their position without seeing them
85
+ recent_agents: dict[tuple[int, int], AgentSighting] = field(default_factory=dict)
86
+
87
+ def __post_init__(self) -> None:
88
+ """Initialize grids if empty."""
89
+ if not self.occupancy:
90
+ # Initialize to UNKNOWN - cells become FREE/OBSTACLE when observed
91
+ self.occupancy = [[CellType.UNKNOWN.value] * self.grid_size for _ in range(self.grid_size)]
92
+ if not self.explored:
93
+ self.explored = [[False] * self.grid_size for _ in range(self.grid_size)]
94
+
95
+ # === Structure query methods ===
96
+
97
+ def get_structures_by_type(self, structure_type: StructureType) -> list[StructureInfo]:
98
+ """Get all structures of a given type."""
99
+ return [s for s in self.structures.values() if s.structure_type == structure_type]
100
+
101
+ def get_junctions(self) -> list[StructureInfo]:
102
+ """Get all known junctions."""
103
+ return self.get_structures_by_type(StructureType.JUNCTION)
104
+
105
+ def get_extractors(self) -> list[StructureInfo]:
106
+ """Get all known extractors."""
107
+ return self.get_structures_by_type(StructureType.EXTRACTOR)
108
+
109
+ def get_usable_extractors(self) -> list[StructureInfo]:
110
+ """Get all usable extractors (not depleted)."""
111
+ return [s for s in self.structures.values() if s.is_usable_extractor()]
112
+
113
+ def get_cogs_junctions(self) -> list[StructureInfo]:
114
+ """Get all cogs-aligned junctions (safe zones)."""
115
+ return [j for j in self.get_junctions() if j.is_cogs_aligned()]
116
+
117
+ def get_clips_junctions(self) -> list[StructureInfo]:
118
+ """Get all clips-aligned junctions (enemy zones)."""
119
+ return [j for j in self.get_junctions() if j.is_clips_aligned()]
120
+
121
+ def get_neutral_junctions(self) -> list[StructureInfo]:
122
+ """Get all neutral junctions."""
123
+ return [j for j in self.get_junctions() if j.is_neutral()]
124
+
125
+ def get_structure_at(self, pos: tuple[int, int]) -> Optional[StructureInfo]:
126
+ """Get structure at a specific position."""
127
+ return self.structures.get(pos)
128
+
129
+ def find_nearest_unexplored(
130
+ self, from_pos: tuple[int, int], max_dist: int = 50, direction_bias: Optional[str] = None
131
+ ) -> Optional[tuple[int, int]]:
132
+ """Find the nearest unexplored frontier cell.
133
+
134
+ A frontier cell is an unexplored cell adjacent to an explored FREE cell.
135
+ This guides exploration toward the edges of known territory.
136
+
137
+ Args:
138
+ from_pos: Starting position (row, col)
139
+ max_dist: Maximum search distance
140
+ direction_bias: Optional bias direction ('north', 'south', 'east', 'west')
141
+ to spread agents across the map
142
+
143
+ Returns:
144
+ Position of nearest frontier cell, or None if none found
145
+ """
146
+ from collections import deque
147
+
148
+ r, c = from_pos
149
+ visited = set()
150
+ queue: deque[tuple[int, int, int]] = deque([(r, c, 0)]) # (row, col, distance)
151
+ visited.add((r, c))
152
+
153
+ # Direction deltas with bias ordering
154
+ if direction_bias == "north":
155
+ deltas = [(-1, 0), (0, -1), (0, 1), (1, 0)]
156
+ elif direction_bias == "south":
157
+ deltas = [(1, 0), (0, -1), (0, 1), (-1, 0)]
158
+ elif direction_bias == "east":
159
+ deltas = [(0, 1), (-1, 0), (1, 0), (0, -1)]
160
+ elif direction_bias == "west":
161
+ deltas = [(0, -1), (-1, 0), (1, 0), (0, 1)]
162
+ else:
163
+ deltas = [(-1, 0), (1, 0), (0, -1), (0, 1)]
164
+
165
+ while queue:
166
+ cr, cc, dist = queue.popleft()
167
+
168
+ if dist > max_dist:
169
+ continue
170
+
171
+ for dr, dc in deltas:
172
+ nr, nc = cr + dr, cc + dc
173
+
174
+ if (nr, nc) in visited:
175
+ continue
176
+ if not (0 <= nr < self.grid_size and 0 <= nc < self.grid_size):
177
+ continue
178
+
179
+ visited.add((nr, nc))
180
+
181
+ # Check if this is a frontier cell (unexplored, adjacent to explored FREE)
182
+ if not self.explored[nr][nc]:
183
+ # Check if any neighbor is explored and FREE
184
+ for dr2, dc2 in deltas:
185
+ nnr, nnc = nr + dr2, nc + dc2
186
+ if 0 <= nnr < self.grid_size and 0 <= nnc < self.grid_size:
187
+ if self.explored[nnr][nnc] and self.occupancy[nnr][nnc] == CellType.FREE.value:
188
+ return (nr, nc)
189
+
190
+ # Only expand through explored FREE cells
191
+ if self.explored[cr][cc] and self.occupancy[cr][cc] == CellType.FREE.value:
192
+ queue.append((nr, nc, dist + 1))
193
+
194
+ return None
195
+
196
+
197
+ @dataclass
198
+ class AgentState:
199
+ """Complete state for a Pinky agent."""
200
+
201
+ agent_id: int
202
+ role: Role = Role.MINER
203
+
204
+ # Current vibe (read from observation)
205
+ vibe: str = "default"
206
+
207
+ # Step counter
208
+ step: int = 0
209
+
210
+ # Position (relative to spawn, stored at grid center)
211
+ row: int = 100
212
+ col: int = 100
213
+
214
+ # Inventory
215
+ energy: int = 100
216
+ hp: int = 100
217
+ carbon: int = 0
218
+ oxygen: int = 0
219
+ germanium: int = 0
220
+ silicon: int = 0
221
+ heart: int = 0
222
+ influence: int = 0
223
+
224
+ # Collective inventory (observed from stats tokens)
225
+ collective_carbon: int = 0
226
+ collective_oxygen: int = 0
227
+ collective_germanium: int = 0
228
+ collective_silicon: int = 0
229
+
230
+ # Gear (presence = equipped)
231
+ miner_gear: bool = False
232
+ scout_gear: bool = False
233
+ aligner_gear: bool = False
234
+ scrambler_gear: bool = False
235
+
236
+ # Map knowledge
237
+ map: MapKnowledge = field(default_factory=MapKnowledge)
238
+
239
+ # Navigation state
240
+ nav: NavigationState = field(default_factory=NavigationState)
241
+
242
+ # Recently visited extractor positions (for cooldown avoidance)
243
+ recently_mined: list[tuple[int, int]] = field(default_factory=list)
244
+
245
+ # Track cargo changes to detect extraction failure (inventory full)
246
+ prev_total_cargo: int = 0
247
+ steps_without_cargo_gain: int = 0 # Consecutive steps where cargo didn't increase
248
+
249
+ # Gear retry tracking: when gear is lost, track retry timing
250
+ # If station doesn't give gear, explore/mine for 200 ticks then retry
251
+ last_gear_attempt_step: int = 0 # Step when we last tried to get gear from station
252
+ had_gear_last_step: bool = False # Whether we had gear last step (detect gear loss)
253
+
254
+ # Stuck detection: track consecutive steps at same position
255
+ last_position: tuple[int, int] = (100, 100)
256
+ steps_at_same_position: int = 0
257
+
258
+ # Escape mode: when stuck, commit to escaping for several steps
259
+ escape_direction: Optional[str] = None # Direction to escape (north/south/east/west)
260
+ escape_until_step: int = 0 # Keep escaping until this step
261
+
262
+ # Aligner target tracking: current junction being targeted
263
+ aligner_target: Optional[tuple[int, int]] = None
264
+
265
+ # Last observation (for relative direction calculations)
266
+ last_obs: Optional["AgentObservation"] = None
267
+
268
+ # Debug info (populated by behaviors when debug is enabled)
269
+ debug_info: DebugInfo = field(default_factory=DebugInfo)
270
+
271
+ # === Computed properties ===
272
+
273
+ @property
274
+ def pos(self) -> tuple[int, int]:
275
+ """Current position as tuple."""
276
+ return (self.row, self.col)
277
+
278
+ @property
279
+ def total_cargo(self) -> int:
280
+ """Total resources currently carried."""
281
+ return self.carbon + self.oxygen + self.germanium + self.silicon
282
+
283
+ @property
284
+ def cargo_capacity(self) -> int:
285
+ """Cargo capacity (base 4, +40 with miner gear)."""
286
+ return 4 + (40 if self.miner_gear else 0)
287
+
288
+ def has_gear(self, role: Optional[Role] = None) -> bool:
289
+ """Check if agent has gear for the specified role (or their own role)."""
290
+ check_role = role or self.role
291
+ if check_role == Role.MINER:
292
+ return self.miner_gear
293
+ elif check_role == Role.SCOUT:
294
+ return self.scout_gear
295
+ elif check_role == Role.ALIGNER:
296
+ return self.aligner_gear
297
+ elif check_role == Role.SCRAMBLER:
298
+ return self.scrambler_gear
299
+ return False
@@ -0,0 +1,138 @@
1
+ """
2
+ Types and constants for Pinky policy.
3
+
4
+ Enums, StructureInfo, and role-related definitions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ from typing import Optional
12
+
13
+ from cogames_agents.policy.scripted_agent.common import roles as common_roles
14
+
15
+ Role = common_roles.Role
16
+ ROLE_TO_STATION = common_roles.ROLE_TO_STATION
17
+ VIBE_TO_ROLE = common_roles.VIBE_TO_ROLE
18
+
19
+
20
+ class RiskTolerance(Enum):
21
+ """Risk tolerance levels for different roles."""
22
+
23
+ CONSERVATIVE = "conservative" # Miners - stay in safe zones
24
+ MODERATE = "moderate" # Aligners - venture out carefully
25
+ AGGRESSIVE = "aggressive" # Scouts, Scramblers - deep territory
26
+
27
+
28
+ # Map roles to their risk tolerance
29
+ ROLE_RISK_TOLERANCE: dict[Role, RiskTolerance] = {
30
+ Role.MINER: RiskTolerance.CONSERVATIVE,
31
+ Role.SCOUT: RiskTolerance.AGGRESSIVE,
32
+ Role.ALIGNER: RiskTolerance.MODERATE,
33
+ Role.SCRAMBLER: RiskTolerance.AGGRESSIVE,
34
+ }
35
+
36
+
37
+ class CellType(Enum):
38
+ """Occupancy map cell states."""
39
+
40
+ UNKNOWN = 0 # Not yet explored
41
+ FREE = 1 # Passable (can walk through)
42
+ OBSTACLE = 2 # Impassable (walls, stations, extractors)
43
+
44
+
45
+ class StructureType(Enum):
46
+ """Types of structures in the game."""
47
+
48
+ HUB = "hub" # Main hub / resource deposit point (cogs nexus)
49
+ JUNCTION = "junction" # Territory control point (junction/supply depot)
50
+ MINER_STATION = "miner_station"
51
+ SCOUT_STATION = "scout_station"
52
+ ALIGNER_STATION = "aligner_station"
53
+ SCRAMBLER_STATION = "scrambler_station"
54
+ EXTRACTOR = "extractor" # Resource source
55
+ CHEST = "chest" # Heart source
56
+ WALL = "wall"
57
+ UNKNOWN = "unknown"
58
+
59
+
60
+ @dataclass
61
+ class StructureInfo:
62
+ """Information about a discovered structure."""
63
+
64
+ position: tuple[int, int]
65
+ structure_type: StructureType
66
+ name: str # Original object name
67
+
68
+ # When we last saw this structure
69
+ last_seen_step: int = 0
70
+
71
+ # Alignment: "cogs", "clips", or None (neutral)
72
+ alignment: Optional[str] = None
73
+
74
+ # Extractor-specific attributes
75
+ resource_type: Optional[str] = None # carbon, oxygen, germanium, silicon
76
+ remaining_uses: int = 999
77
+ cooldown_remaining: int = 0
78
+ inventory_amount: int = -1 # -1 = unknown (protocol-based), 0+ = chest-based with that amount
79
+ has_inventory: bool = False # True once we've seen inv: tokens for this extractor
80
+
81
+ def is_usable_extractor(self) -> bool:
82
+ """Check if this is a usable extractor (not depleted, has resources).
83
+
84
+ Protocol-based extractors: Only check remaining_uses > 0
85
+ Chest-based extractors: Also check inventory_amount > 0
86
+ """
87
+ if self.structure_type != StructureType.EXTRACTOR:
88
+ return False
89
+ if self.remaining_uses <= 0:
90
+ return False
91
+ # If we've never seen inventory tokens, assume protocol-based (usable if remaining_uses > 0)
92
+ if not self.has_inventory:
93
+ return True
94
+ # Chest-based: must have inventory > 0
95
+ return self.inventory_amount > 0
96
+
97
+ def is_cogs_aligned(self) -> bool:
98
+ """Check if this structure is aligned to cogs."""
99
+ return self.alignment == "cogs"
100
+
101
+ def is_clips_aligned(self) -> bool:
102
+ """Check if this structure is aligned to clips."""
103
+ return self.alignment == "clips"
104
+
105
+ def is_neutral(self) -> bool:
106
+ """Check if this structure is neutral (unaligned)."""
107
+ return self.alignment is None
108
+
109
+
110
+ # Game constants
111
+ JUNCTION_AOE_RANGE = 10 # AOE range of junctions
112
+ HP_DRAIN_OUTSIDE_SAFE_ZONE = 1 # HP lost per step outside safe zone
113
+ HP_DRAIN_NEAR_ENEMY = 1 # Additional HP lost near enemy junctions
114
+ ENERGY_MOVE_COST = 2 # Energy cost per move
115
+ HP_SAFETY_MARGIN = 10 # Buffer HP to keep before retreating
116
+
117
+
118
+ # Debug flag (legacy, now controlled via URI param)
119
+ DEBUG = False
120
+
121
+
122
+ @dataclass
123
+ class DebugInfo:
124
+ """Structured debug info about agent's current intent."""
125
+
126
+ mode: str = "idle" # Current behavior mode (e.g., "mine", "deposit", "retreat")
127
+ goal: str = "" # Current goal description
128
+ target_object: str = "" # Object being targeted
129
+ target_pos: Optional[tuple[int, int]] = None # Target position
130
+ signal: str = "" # Event signal (e.g., "extract_failed_cargo_full", "hp_too_low")
131
+
132
+ def format(self, role: str, action_name: str) -> str:
133
+ """Format as role:mode:goal:target:action[:signal]."""
134
+ target = self.target_object or (str(self.target_pos) if self.target_pos else "-")
135
+ base = f"{role}:{self.mode}:{self.goal or '-'}:{target}:{action_name}"
136
+ if self.signal:
137
+ return f"{base}:{self.signal}"
138
+ return base