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