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.
- cogames_agents/__init__.py +0 -0
- cogames_agents/evals/__init__.py +5 -0
- cogames_agents/evals/planky_evals.py +415 -0
- cogames_agents/policy/__init__.py +0 -0
- cogames_agents/policy/evolution/__init__.py +0 -0
- cogames_agents/policy/evolution/cogsguard/__init__.py +0 -0
- cogames_agents/policy/evolution/cogsguard/evolution.py +695 -0
- cogames_agents/policy/evolution/cogsguard/evolutionary_coordinator.py +540 -0
- cogames_agents/policy/nim_agents/__init__.py +20 -0
- cogames_agents/policy/nim_agents/agents.py +98 -0
- cogames_agents/policy/nim_agents/bindings/generated/libnim_agents.dylib +0 -0
- cogames_agents/policy/nim_agents/bindings/generated/nim_agents.py +215 -0
- cogames_agents/policy/nim_agents/cogsguard_agents.nim +555 -0
- cogames_agents/policy/nim_agents/cogsguard_align_all_agents.nim +569 -0
- cogames_agents/policy/nim_agents/common.nim +1054 -0
- cogames_agents/policy/nim_agents/install.sh +1 -0
- cogames_agents/policy/nim_agents/ladybug_agent.nim +954 -0
- cogames_agents/policy/nim_agents/nim_agents.nim +68 -0
- cogames_agents/policy/nim_agents/nim_agents.nims +14 -0
- cogames_agents/policy/nim_agents/nimby.lock +3 -0
- cogames_agents/policy/nim_agents/racecar_agents.nim +844 -0
- cogames_agents/policy/nim_agents/random_agents.nim +68 -0
- cogames_agents/policy/nim_agents/test_agents.py +53 -0
- cogames_agents/policy/nim_agents/thinky_agents.nim +677 -0
- cogames_agents/policy/nim_agents/thinky_eval.py +230 -0
- cogames_agents/policy/scripted_agent/README.md +360 -0
- cogames_agents/policy/scripted_agent/__init__.py +0 -0
- cogames_agents/policy/scripted_agent/baseline_agent.py +1031 -0
- cogames_agents/policy/scripted_agent/cogas/__init__.py +5 -0
- cogames_agents/policy/scripted_agent/cogas/context.py +68 -0
- cogames_agents/policy/scripted_agent/cogas/entity_map.py +152 -0
- cogames_agents/policy/scripted_agent/cogas/goal.py +115 -0
- cogames_agents/policy/scripted_agent/cogas/goals/__init__.py +27 -0
- cogames_agents/policy/scripted_agent/cogas/goals/aligner.py +160 -0
- cogames_agents/policy/scripted_agent/cogas/goals/gear.py +197 -0
- cogames_agents/policy/scripted_agent/cogas/goals/miner.py +441 -0
- cogames_agents/policy/scripted_agent/cogas/goals/scout.py +40 -0
- cogames_agents/policy/scripted_agent/cogas/goals/scrambler.py +174 -0
- cogames_agents/policy/scripted_agent/cogas/goals/shared.py +160 -0
- cogames_agents/policy/scripted_agent/cogas/goals/stem.py +60 -0
- cogames_agents/policy/scripted_agent/cogas/goals/survive.py +100 -0
- cogames_agents/policy/scripted_agent/cogas/navigator.py +401 -0
- cogames_agents/policy/scripted_agent/cogas/obs_parser.py +238 -0
- cogames_agents/policy/scripted_agent/cogas/policy.py +525 -0
- cogames_agents/policy/scripted_agent/cogas/trace.py +69 -0
- cogames_agents/policy/scripted_agent/cogsguard/CLAUDE.md +517 -0
- cogames_agents/policy/scripted_agent/cogsguard/README.md +252 -0
- cogames_agents/policy/scripted_agent/cogsguard/__init__.py +74 -0
- cogames_agents/policy/scripted_agent/cogsguard/aligned_junction_held_investigation.md +152 -0
- cogames_agents/policy/scripted_agent/cogsguard/aligner.py +333 -0
- cogames_agents/policy/scripted_agent/cogsguard/behavior_hooks.py +44 -0
- cogames_agents/policy/scripted_agent/cogsguard/control_agent.py +323 -0
- cogames_agents/policy/scripted_agent/cogsguard/debug_agent.py +533 -0
- cogames_agents/policy/scripted_agent/cogsguard/miner.py +589 -0
- cogames_agents/policy/scripted_agent/cogsguard/options.py +67 -0
- cogames_agents/policy/scripted_agent/cogsguard/parity_metrics.py +36 -0
- cogames_agents/policy/scripted_agent/cogsguard/policy.py +1967 -0
- cogames_agents/policy/scripted_agent/cogsguard/prereq_trace.py +33 -0
- cogames_agents/policy/scripted_agent/cogsguard/role_trace.py +50 -0
- cogames_agents/policy/scripted_agent/cogsguard/roles.py +31 -0
- cogames_agents/policy/scripted_agent/cogsguard/rollout_trace.py +40 -0
- cogames_agents/policy/scripted_agent/cogsguard/scout.py +69 -0
- cogames_agents/policy/scripted_agent/cogsguard/scrambler.py +350 -0
- cogames_agents/policy/scripted_agent/cogsguard/targeted_agent.py +418 -0
- cogames_agents/policy/scripted_agent/cogsguard/teacher.py +224 -0
- cogames_agents/policy/scripted_agent/cogsguard/types.py +381 -0
- cogames_agents/policy/scripted_agent/cogsguard/v2_agent.py +49 -0
- cogames_agents/policy/scripted_agent/common/__init__.py +0 -0
- cogames_agents/policy/scripted_agent/common/geometry.py +24 -0
- cogames_agents/policy/scripted_agent/common/roles.py +34 -0
- cogames_agents/policy/scripted_agent/common/tag_utils.py +48 -0
- cogames_agents/policy/scripted_agent/demo_policy.py +242 -0
- cogames_agents/policy/scripted_agent/pathfinding.py +126 -0
- cogames_agents/policy/scripted_agent/pinky/DESIGN.md +317 -0
- cogames_agents/policy/scripted_agent/pinky/__init__.py +5 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/__init__.py +17 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/aligner.py +400 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/base.py +119 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/miner.py +632 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/scout.py +138 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/scrambler.py +433 -0
- cogames_agents/policy/scripted_agent/pinky/policy.py +570 -0
- cogames_agents/policy/scripted_agent/pinky/services/__init__.py +7 -0
- cogames_agents/policy/scripted_agent/pinky/services/map_tracker.py +808 -0
- cogames_agents/policy/scripted_agent/pinky/services/navigator.py +864 -0
- cogames_agents/policy/scripted_agent/pinky/services/safety.py +189 -0
- cogames_agents/policy/scripted_agent/pinky/state.py +299 -0
- cogames_agents/policy/scripted_agent/pinky/types.py +138 -0
- cogames_agents/policy/scripted_agent/planky/CLAUDE.md +124 -0
- cogames_agents/policy/scripted_agent/planky/IMPROVEMENTS.md +160 -0
- cogames_agents/policy/scripted_agent/planky/NOTES.md +153 -0
- cogames_agents/policy/scripted_agent/planky/PLAN.md +254 -0
- cogames_agents/policy/scripted_agent/planky/README.md +214 -0
- cogames_agents/policy/scripted_agent/planky/STRATEGY.md +100 -0
- cogames_agents/policy/scripted_agent/planky/__init__.py +5 -0
- cogames_agents/policy/scripted_agent/planky/context.py +68 -0
- cogames_agents/policy/scripted_agent/planky/entity_map.py +152 -0
- cogames_agents/policy/scripted_agent/planky/goal.py +107 -0
- cogames_agents/policy/scripted_agent/planky/goals/__init__.py +27 -0
- cogames_agents/policy/scripted_agent/planky/goals/aligner.py +168 -0
- cogames_agents/policy/scripted_agent/planky/goals/gear.py +179 -0
- cogames_agents/policy/scripted_agent/planky/goals/miner.py +416 -0
- cogames_agents/policy/scripted_agent/planky/goals/scout.py +40 -0
- cogames_agents/policy/scripted_agent/planky/goals/scrambler.py +174 -0
- cogames_agents/policy/scripted_agent/planky/goals/shared.py +160 -0
- cogames_agents/policy/scripted_agent/planky/goals/stem.py +49 -0
- cogames_agents/policy/scripted_agent/planky/goals/survive.py +96 -0
- cogames_agents/policy/scripted_agent/planky/navigator.py +388 -0
- cogames_agents/policy/scripted_agent/planky/obs_parser.py +238 -0
- cogames_agents/policy/scripted_agent/planky/policy.py +485 -0
- cogames_agents/policy/scripted_agent/planky/tests/__init__.py +0 -0
- cogames_agents/policy/scripted_agent/planky/tests/conftest.py +66 -0
- cogames_agents/policy/scripted_agent/planky/tests/helpers.py +152 -0
- cogames_agents/policy/scripted_agent/planky/tests/test_aligner.py +24 -0
- cogames_agents/policy/scripted_agent/planky/tests/test_miner.py +30 -0
- cogames_agents/policy/scripted_agent/planky/tests/test_scout.py +15 -0
- cogames_agents/policy/scripted_agent/planky/tests/test_scrambler.py +29 -0
- cogames_agents/policy/scripted_agent/planky/tests/test_stem.py +36 -0
- cogames_agents/policy/scripted_agent/planky/trace.py +69 -0
- cogames_agents/policy/scripted_agent/types.py +239 -0
- cogames_agents/policy/scripted_agent/unclipping_agent.py +461 -0
- cogames_agents/policy/scripted_agent/utils.py +381 -0
- cogames_agents/policy/scripted_registry.py +80 -0
- cogames_agents/py.typed +0 -0
- cogames_agents-0.0.0.7.dist-info/METADATA +98 -0
- cogames_agents-0.0.0.7.dist-info/RECORD +128 -0
- cogames_agents-0.0.0.7.dist-info/WHEEL +6 -0
- 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
|