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,632 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Miner behavior for Pinky policy.
|
|
3
|
+
|
|
4
|
+
Miners gather resources from extractors and deposit at aligned buildings.
|
|
5
|
+
Strategy: Mine aggressively, deposit when full, only retreat when critical.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Optional
|
|
11
|
+
|
|
12
|
+
from cogames_agents.policy.scripted_agent.pinky.behaviors.base import (
|
|
13
|
+
Services,
|
|
14
|
+
explore_for_station,
|
|
15
|
+
is_adjacent,
|
|
16
|
+
manhattan_distance,
|
|
17
|
+
)
|
|
18
|
+
from cogames_agents.policy.scripted_agent.pinky.types import (
|
|
19
|
+
DEBUG,
|
|
20
|
+
ROLE_TO_STATION,
|
|
21
|
+
DebugInfo,
|
|
22
|
+
RiskTolerance,
|
|
23
|
+
Role,
|
|
24
|
+
StructureInfo,
|
|
25
|
+
StructureType,
|
|
26
|
+
)
|
|
27
|
+
from mettagrid.simulator import Action
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from cogames_agents.policy.scripted_agent.pinky.state import AgentState
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MinerBehavior:
|
|
34
|
+
"""Miner agent: gather resources and deposit at aligned buildings."""
|
|
35
|
+
|
|
36
|
+
role = Role.MINER
|
|
37
|
+
risk_tolerance = RiskTolerance.CONSERVATIVE
|
|
38
|
+
|
|
39
|
+
# How many steps to explore before mining without gear
|
|
40
|
+
EXPLORATION_STEPS = 100
|
|
41
|
+
|
|
42
|
+
# How many ticks to mine/explore before retrying gear station
|
|
43
|
+
GEAR_RETRY_INTERVAL = 100
|
|
44
|
+
|
|
45
|
+
# How often to clear the failed extractors list (allow retry)
|
|
46
|
+
FAILED_EXTRACTOR_RETRY_INTERVAL = 200
|
|
47
|
+
|
|
48
|
+
def act(self, state: AgentState, services: Services) -> Action:
|
|
49
|
+
"""Execute miner behavior - move toward extractors.
|
|
50
|
+
|
|
51
|
+
Mining happens automatically when walking into extractor cells.
|
|
52
|
+
Uses observation-relative coordinates to avoid position drift issues.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# Periodically clear failed extractors to allow retry (they may have replenished)
|
|
56
|
+
if state.step % self.FAILED_EXTRACTOR_RETRY_INTERVAL == 0:
|
|
57
|
+
state.nav.failed_extractors.clear()
|
|
58
|
+
|
|
59
|
+
# Track cargo changes to detect when extraction stops working (inventory full).
|
|
60
|
+
# If cargo doesn't increase for several steps while we have energy to move,
|
|
61
|
+
# the inventory is likely full. This is more robust than tracking cargo capacity.
|
|
62
|
+
cargo_gained = state.total_cargo > state.prev_total_cargo
|
|
63
|
+
if cargo_gained:
|
|
64
|
+
# Cargo increased - reset counter and clear current target (successfully mined)
|
|
65
|
+
state.steps_without_cargo_gain = 0
|
|
66
|
+
state.nav.steps_at_current_extractor = 0
|
|
67
|
+
# Clear the current target - we'll pick a new one next step
|
|
68
|
+
state.nav.current_extractor_target = None
|
|
69
|
+
elif state.total_cargo == state.prev_total_cargo and state.total_cargo > 0:
|
|
70
|
+
# Cargo unchanged but we have some cargo - increment counter
|
|
71
|
+
state.steps_without_cargo_gain += 1
|
|
72
|
+
elif state.total_cargo < state.prev_total_cargo:
|
|
73
|
+
# Cargo decreased (deposit) - reset counter
|
|
74
|
+
state.steps_without_cargo_gain = 0
|
|
75
|
+
|
|
76
|
+
# Track time at current extractor target to detect empty/stuck extractors
|
|
77
|
+
if state.nav.current_extractor_target is not None:
|
|
78
|
+
target = state.nav.current_extractor_target
|
|
79
|
+
dist = manhattan_distance(state.pos, target)
|
|
80
|
+
if dist <= 1: # At or adjacent to target
|
|
81
|
+
state.nav.steps_at_current_extractor += 1
|
|
82
|
+
# If we've been at this extractor for 5+ steps without cargo gain, mark it as failed
|
|
83
|
+
if state.nav.steps_at_current_extractor >= 5 and not cargo_gained:
|
|
84
|
+
if DEBUG:
|
|
85
|
+
print(
|
|
86
|
+
f"[A{state.agent_id}] MINER: Extractor at {target} appears empty/blocked, "
|
|
87
|
+
f"marking as failed after {state.nav.steps_at_current_extractor} steps"
|
|
88
|
+
)
|
|
89
|
+
state.nav.failed_extractors.add(target)
|
|
90
|
+
state.nav.current_extractor_target = None
|
|
91
|
+
state.nav.steps_at_current_extractor = 0
|
|
92
|
+
|
|
93
|
+
# Update prev_total_cargo for next step
|
|
94
|
+
state.prev_total_cargo = state.total_cargo
|
|
95
|
+
|
|
96
|
+
# Priority 0: Check for stuck patterns and handle escape mode (via navigator)
|
|
97
|
+
escape_action = services.navigator.check_and_handle_escape(state)
|
|
98
|
+
if escape_action:
|
|
99
|
+
debug_info = services.navigator.get_escape_debug_info(state)
|
|
100
|
+
state.debug_info = DebugInfo(**debug_info)
|
|
101
|
+
return escape_action
|
|
102
|
+
|
|
103
|
+
# Priority 1: Critical HP retreat
|
|
104
|
+
if state.hp <= 15:
|
|
105
|
+
if DEBUG:
|
|
106
|
+
print(f"[A{state.agent_id}] MINER: CRITICAL HP={state.hp}, retreating!")
|
|
107
|
+
state.debug_info = DebugInfo(mode="retreat", goal="safety", target_object="safe_zone", signal="hp_critical")
|
|
108
|
+
return self._retreat_to_safety(state, services)
|
|
109
|
+
|
|
110
|
+
# Priority 2: Get miner gear if we don't have it
|
|
111
|
+
# Track gear state for detecting gear loss
|
|
112
|
+
has_gear_now = state.miner_gear
|
|
113
|
+
just_lost_gear = state.had_gear_last_step and not has_gear_now
|
|
114
|
+
state.had_gear_last_step = has_gear_now
|
|
115
|
+
|
|
116
|
+
if not state.miner_gear:
|
|
117
|
+
# Check if we should try to get gear now
|
|
118
|
+
ticks_since_last_attempt = state.step - state.last_gear_attempt_step
|
|
119
|
+
|
|
120
|
+
# Try to get gear if:
|
|
121
|
+
# 1. Just lost gear (immediately go home)
|
|
122
|
+
# 2. Initial exploration period
|
|
123
|
+
# 3. 200 ticks have passed since last attempt
|
|
124
|
+
should_try_gear = (
|
|
125
|
+
just_lost_gear
|
|
126
|
+
or state.step < self.EXPLORATION_STEPS
|
|
127
|
+
or ticks_since_last_attempt >= self.GEAR_RETRY_INTERVAL
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if should_try_gear:
|
|
131
|
+
# First try visible miner_station in current observation
|
|
132
|
+
if state.last_obs is not None:
|
|
133
|
+
result = services.map_tracker.get_direction_to_nearest(
|
|
134
|
+
state, state.last_obs, frozenset({"miner_station"})
|
|
135
|
+
)
|
|
136
|
+
if result:
|
|
137
|
+
direction, target_pos = result
|
|
138
|
+
if DEBUG:
|
|
139
|
+
print(f"[A{state.agent_id}] MINER: Station visible at {target_pos}, moving {direction}")
|
|
140
|
+
state.debug_info = DebugInfo(
|
|
141
|
+
mode="get_gear", goal="miner_station", target_object="miner_station", target_pos=target_pos
|
|
142
|
+
)
|
|
143
|
+
return Action(name=f"move_{direction}")
|
|
144
|
+
|
|
145
|
+
# Use accumulated map knowledge if station was found
|
|
146
|
+
gear_action = self._get_gear(state, services)
|
|
147
|
+
if gear_action:
|
|
148
|
+
return gear_action
|
|
149
|
+
|
|
150
|
+
# Station not found - explore for it
|
|
151
|
+
if DEBUG and state.step % 10 == 0:
|
|
152
|
+
print(f"[A{state.agent_id}] MINER: Exploring for station (step {state.step})")
|
|
153
|
+
state.debug_info = DebugInfo(mode="explore", goal="find_station", target_object="miner_station")
|
|
154
|
+
return self._explore_for_station(state, services)
|
|
155
|
+
|
|
156
|
+
# Priority 3: Deposit when cargo is FULL
|
|
157
|
+
cargo_full_reason = self._cargo_full_reason(state)
|
|
158
|
+
if cargo_full_reason:
|
|
159
|
+
if DEBUG:
|
|
160
|
+
print(
|
|
161
|
+
f"[A{state.agent_id}] MINER: Cargo FULL {state.total_cargo}/{state.cargo_capacity}, "
|
|
162
|
+
"returning to depot"
|
|
163
|
+
)
|
|
164
|
+
state.debug_info = DebugInfo(
|
|
165
|
+
mode="deposit", goal=f"drop_cargo({state.total_cargo})", target_object="depot", signal=cargo_full_reason
|
|
166
|
+
)
|
|
167
|
+
return self._deposit_resources(state, services)
|
|
168
|
+
|
|
169
|
+
# Priority 4: Keep mining - move toward extractors
|
|
170
|
+
# First try to find extractor in current observation (most accurate)
|
|
171
|
+
# Build set of positions to exclude: known-empty + recently failed extractors
|
|
172
|
+
excluded_extractors: set[tuple[int, int]] = set(state.nav.failed_extractors)
|
|
173
|
+
for pos, struct in state.map.structures.items():
|
|
174
|
+
if struct.structure_type == StructureType.EXTRACTOR and not struct.is_usable_extractor():
|
|
175
|
+
excluded_extractors.add(pos)
|
|
176
|
+
|
|
177
|
+
if state.last_obs is not None:
|
|
178
|
+
# Get the resource type with the lowest communal amount
|
|
179
|
+
lowest_resource = self._get_lowest_communal_resource(state)
|
|
180
|
+
all_extractor_types = {"carbon_extractor", "oxygen_extractor", "germanium_extractor", "silicon_extractor"}
|
|
181
|
+
|
|
182
|
+
# Build preferred types (lowest communal resource)
|
|
183
|
+
if lowest_resource:
|
|
184
|
+
preferred_types = {f"{lowest_resource}_extractor"}
|
|
185
|
+
else:
|
|
186
|
+
preferred_types = set()
|
|
187
|
+
|
|
188
|
+
# Try preferred types first (lowest communal resource)
|
|
189
|
+
result = None
|
|
190
|
+
if preferred_types:
|
|
191
|
+
result = services.map_tracker.get_direction_to_nearest(
|
|
192
|
+
state,
|
|
193
|
+
state.last_obs,
|
|
194
|
+
frozenset(preferred_types),
|
|
195
|
+
exclude_positions=excluded_extractors,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Fall back to any extractor type
|
|
199
|
+
if not result:
|
|
200
|
+
result = services.map_tracker.get_direction_to_nearest(
|
|
201
|
+
state,
|
|
202
|
+
state.last_obs,
|
|
203
|
+
frozenset(all_extractor_types),
|
|
204
|
+
exclude_positions=excluded_extractors,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if result:
|
|
208
|
+
direction, target_pos = result
|
|
209
|
+
# Track this as our current target
|
|
210
|
+
if state.nav.current_extractor_target != target_pos:
|
|
211
|
+
state.nav.current_extractor_target = target_pos
|
|
212
|
+
state.nav.steps_at_current_extractor = 0
|
|
213
|
+
# Found a visible mineral - remember this step
|
|
214
|
+
state.nav.explore_last_mineral_step = state.step
|
|
215
|
+
state.debug_info = DebugInfo(
|
|
216
|
+
mode="mine", goal="find_extractor", target_object="extractor", target_pos=target_pos
|
|
217
|
+
)
|
|
218
|
+
return Action(name=f"move_{direction}")
|
|
219
|
+
|
|
220
|
+
# No extractor visible - use internal map knowledge to navigate to known extractors
|
|
221
|
+
known_extractor = self._find_nearest_extractor(state, services, excluded_extractors)
|
|
222
|
+
if known_extractor:
|
|
223
|
+
# Track this as our current target
|
|
224
|
+
if state.nav.current_extractor_target != known_extractor.position:
|
|
225
|
+
state.nav.current_extractor_target = known_extractor.position
|
|
226
|
+
state.nav.steps_at_current_extractor = 0
|
|
227
|
+
# Found a mineral - remember this step
|
|
228
|
+
state.nav.explore_last_mineral_step = state.step
|
|
229
|
+
|
|
230
|
+
# Track this resource type for rotation
|
|
231
|
+
res_type = known_extractor.resource_type
|
|
232
|
+
if res_type:
|
|
233
|
+
self._record_resource_gathered(state, res_type)
|
|
234
|
+
|
|
235
|
+
if DEBUG:
|
|
236
|
+
print(
|
|
237
|
+
f"[A{state.agent_id}] MINER: No extractor visible, navigating to known {known_extractor.name} "
|
|
238
|
+
f"at {known_extractor.position}"
|
|
239
|
+
)
|
|
240
|
+
# Format: "carbon:100" or "extractor" if no resource type
|
|
241
|
+
res_type_name = res_type or "extractor"
|
|
242
|
+
inv_amt = known_extractor.inventory_amount
|
|
243
|
+
target_name = f"{res_type_name}:{inv_amt}" if inv_amt >= 0 else res_type_name
|
|
244
|
+
state.debug_info = DebugInfo(
|
|
245
|
+
mode="mine",
|
|
246
|
+
goal="navigate_to_known_extractor",
|
|
247
|
+
target_object=target_name,
|
|
248
|
+
target_pos=known_extractor.position,
|
|
249
|
+
)
|
|
250
|
+
return services.navigator.move_to(state, known_extractor.position, reach_adjacent=True)
|
|
251
|
+
|
|
252
|
+
# No minerals found - explore with expanding radius
|
|
253
|
+
return self._explore_for_minerals(state, services)
|
|
254
|
+
|
|
255
|
+
def needs_gear(self, state: AgentState) -> bool:
|
|
256
|
+
"""Miners need miner gear for +40 cargo capacity."""
|
|
257
|
+
return not state.miner_gear
|
|
258
|
+
|
|
259
|
+
def has_resources_to_act(self, state: AgentState) -> bool:
|
|
260
|
+
"""Miners don't need resources to mine."""
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
def _record_resource_gathered(self, state: AgentState, resource_type: str) -> None:
|
|
264
|
+
"""Record that we gathered a resource type for rotation tracking.
|
|
265
|
+
|
|
266
|
+
Maintains a list of the last 4 resource types gathered.
|
|
267
|
+
When the miner fills up on one type, it will prefer other types.
|
|
268
|
+
"""
|
|
269
|
+
recent = state.nav.last_resource_types
|
|
270
|
+
# Only add if different from the most recent one (avoid duplicates from same extractor)
|
|
271
|
+
if not recent or recent[0] != resource_type:
|
|
272
|
+
recent.insert(0, resource_type)
|
|
273
|
+
# Keep only the last 4 types
|
|
274
|
+
if len(recent) > 4:
|
|
275
|
+
recent.pop()
|
|
276
|
+
|
|
277
|
+
# How many steps without cargo gain before assuming inventory is full
|
|
278
|
+
STEPS_TO_ASSUME_FULL = 3
|
|
279
|
+
|
|
280
|
+
def _cargo_full_reason(self, state: AgentState) -> str:
|
|
281
|
+
"""Check if cargo is full and return the reason signal.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
- "extract_failed_cargo_full" if no cargo gain for several steps (extraction stopped working)
|
|
285
|
+
- "cargo_at_capacity" if cargo >= computed capacity
|
|
286
|
+
- "" if cargo is not full
|
|
287
|
+
"""
|
|
288
|
+
# If we have cargo and haven't gained any for several steps, assume full
|
|
289
|
+
if state.total_cargo > 0 and state.steps_without_cargo_gain >= self.STEPS_TO_ASSUME_FULL:
|
|
290
|
+
if DEBUG:
|
|
291
|
+
print(
|
|
292
|
+
f"[A{state.agent_id}] MINER: No cargo gain for {state.steps_without_cargo_gain} steps, "
|
|
293
|
+
f"assuming full (cargo={state.total_cargo})"
|
|
294
|
+
)
|
|
295
|
+
return "extract_failed_cargo_full"
|
|
296
|
+
# Fallback to capacity check
|
|
297
|
+
if state.total_cargo >= state.cargo_capacity:
|
|
298
|
+
return "cargo_at_capacity"
|
|
299
|
+
return ""
|
|
300
|
+
|
|
301
|
+
def _retreat_to_safety(self, state: AgentState, services: Services) -> Action:
|
|
302
|
+
"""Return to nearest safe zone."""
|
|
303
|
+
safe_pos = services.safety.nearest_safe_zone(state)
|
|
304
|
+
if safe_pos is None:
|
|
305
|
+
# No known safe zone, just try to find any junction
|
|
306
|
+
for junction in state.map.get_junctions():
|
|
307
|
+
safe_pos = junction.position
|
|
308
|
+
break
|
|
309
|
+
|
|
310
|
+
if safe_pos is None:
|
|
311
|
+
state.debug_info = DebugInfo(mode="retreat", goal="explore_for_safety", target_object="-")
|
|
312
|
+
return services.navigator.explore(state)
|
|
313
|
+
|
|
314
|
+
# If we have cargo and are adjacent to safe building, deposit
|
|
315
|
+
if state.total_cargo > 0 and is_adjacent(state.pos, safe_pos):
|
|
316
|
+
state.debug_info = DebugInfo(
|
|
317
|
+
mode="retreat", goal="deposit_and_heal", target_object="safe_zone", target_pos=safe_pos
|
|
318
|
+
)
|
|
319
|
+
return services.navigator.use_object_at(state, safe_pos)
|
|
320
|
+
|
|
321
|
+
state.debug_info = DebugInfo(
|
|
322
|
+
mode="retreat", goal="reach_safety", target_object="safe_zone", target_pos=safe_pos
|
|
323
|
+
)
|
|
324
|
+
return services.navigator.move_to(state, safe_pos, reach_adjacent=True)
|
|
325
|
+
|
|
326
|
+
def _explore_for_station(self, state: AgentState, services: Services) -> Action:
|
|
327
|
+
"""Explore to find the miner station."""
|
|
328
|
+
# Miners explore south since stations are south of spawn in the hub
|
|
329
|
+
return explore_for_station(state, services, primary_direction="south")
|
|
330
|
+
|
|
331
|
+
def _explore_for_minerals(self, state: AgentState, services: Services) -> Action:
|
|
332
|
+
"""Explore to find new mineral extractors.
|
|
333
|
+
|
|
334
|
+
Uses the navigator's explore method with direction bias to spread out
|
|
335
|
+
from the base and find new resources. Tracks exploration direction to
|
|
336
|
+
avoid circling.
|
|
337
|
+
"""
|
|
338
|
+
# Pick a consistent exploration direction based on agent ID to spread miners out
|
|
339
|
+
# Agent 0 explores south, 1 explores east, 2 explores west, etc.
|
|
340
|
+
directions = ["south", "east", "west", "north"]
|
|
341
|
+
base_direction = directions[state.agent_id % len(directions)]
|
|
342
|
+
|
|
343
|
+
# If we've been exploring the same direction for a while without finding minerals,
|
|
344
|
+
# switch to a different direction
|
|
345
|
+
steps_since_mineral = state.step - state.nav.explore_last_mineral_step
|
|
346
|
+
if steps_since_mineral > 100:
|
|
347
|
+
# Rotate to next direction
|
|
348
|
+
dir_idx = (directions.index(base_direction) + (steps_since_mineral // 100)) % len(directions)
|
|
349
|
+
base_direction = directions[dir_idx]
|
|
350
|
+
if DEBUG:
|
|
351
|
+
print(
|
|
352
|
+
f"[A{state.agent_id}] MINER: No minerals for {steps_since_mineral} steps, "
|
|
353
|
+
f"exploring {base_direction}"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
state.debug_info = DebugInfo(
|
|
357
|
+
mode="explore",
|
|
358
|
+
goal=base_direction,
|
|
359
|
+
target_object="-",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Use navigator's explore which handles pathfinding around obstacles
|
|
363
|
+
return services.navigator.explore(state, direction_bias=base_direction)
|
|
364
|
+
|
|
365
|
+
def _get_gear(self, state: AgentState, services: Services) -> Optional[Action]:
|
|
366
|
+
"""Go get miner gear from station."""
|
|
367
|
+
station_name = ROLE_TO_STATION[Role.MINER]
|
|
368
|
+
station_pos = state.map.stations.get(station_name)
|
|
369
|
+
|
|
370
|
+
if station_pos is None:
|
|
371
|
+
# Station not found yet
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
dist = manhattan_distance(state.pos, station_pos)
|
|
375
|
+
|
|
376
|
+
# If ON the station (dist=0), we should have received gear from walking in.
|
|
377
|
+
# If gear not received yet, record this attempt and let miner continue mining.
|
|
378
|
+
# After GEAR_RETRY_INTERVAL ticks, they'll try again.
|
|
379
|
+
if state.pos == station_pos:
|
|
380
|
+
if DEBUG:
|
|
381
|
+
print(f"[A{state.agent_id}] MINER: ON station at {station_pos}, no gear - will mine and retry later")
|
|
382
|
+
state.debug_info = DebugInfo(
|
|
383
|
+
mode="get_gear", goal="on_station_no_gear", target_object="miner_station", target_pos=station_pos
|
|
384
|
+
)
|
|
385
|
+
# Record this attempt - miner will mine for GEAR_RETRY_INTERVAL ticks then retry
|
|
386
|
+
state.last_gear_attempt_step = state.step
|
|
387
|
+
# Return None to let the miner continue with mining
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
if is_adjacent(state.pos, station_pos):
|
|
391
|
+
if DEBUG:
|
|
392
|
+
print(f"[A{state.agent_id}] MINER: Getting gear from {station_pos}")
|
|
393
|
+
state.debug_info = DebugInfo(
|
|
394
|
+
mode="get_gear", goal="use_station", target_object="miner_station", target_pos=station_pos
|
|
395
|
+
)
|
|
396
|
+
return services.navigator.use_object_at(state, station_pos)
|
|
397
|
+
|
|
398
|
+
if DEBUG and state.step % 10 == 0:
|
|
399
|
+
print(f"[A{state.agent_id}] MINER: Moving to station at {station_pos} (dist={dist})")
|
|
400
|
+
state.debug_info = DebugInfo(
|
|
401
|
+
mode="get_gear", goal=f"move_to_station(dist={dist})", target_object="miner_station", target_pos=station_pos
|
|
402
|
+
)
|
|
403
|
+
return services.navigator.move_to(state, station_pos, reach_adjacent=True)
|
|
404
|
+
|
|
405
|
+
def _deposit_resources(self, state: AgentState, services: Services) -> Action:
|
|
406
|
+
"""Deposit resources at nearest COGS-ALIGNED hub or junction only.
|
|
407
|
+
|
|
408
|
+
Uses map knowledge (which tracks alignment) to find cogs-aligned depots.
|
|
409
|
+
Observation-based nav doesn't check alignment, so we rely on map knowledge.
|
|
410
|
+
Only considers cogs-aligned structures (hub or cogs junctions), NOT neutral.
|
|
411
|
+
"""
|
|
412
|
+
# Use map knowledge - find nearest COGS-ALIGNED depot only
|
|
413
|
+
# Map knowledge correctly tracks alignment changes from observations
|
|
414
|
+
candidates: list[tuple[int, tuple[int, int]]] = []
|
|
415
|
+
|
|
416
|
+
# Only cogs-aligned junctions (NOT neutral, NOT clips)
|
|
417
|
+
for junction in state.map.get_cogs_junctions():
|
|
418
|
+
dist = manhattan_distance(state.pos, junction.position)
|
|
419
|
+
candidates.append((dist, junction.position))
|
|
420
|
+
|
|
421
|
+
# Hub is always cogs-aligned
|
|
422
|
+
hub_pos = state.map.stations.get("hub")
|
|
423
|
+
if hub_pos:
|
|
424
|
+
dist = manhattan_distance(state.pos, hub_pos)
|
|
425
|
+
candidates.append((dist, hub_pos))
|
|
426
|
+
|
|
427
|
+
if not candidates:
|
|
428
|
+
if DEBUG:
|
|
429
|
+
# Show what structures we DO know about
|
|
430
|
+
struct_types: dict[str, int] = {}
|
|
431
|
+
for s in state.map.structures.values():
|
|
432
|
+
t = s.structure_type.name
|
|
433
|
+
struct_types[t] = struct_types.get(t, 0) + 1
|
|
434
|
+
cogs_junctions = len(state.map.get_cogs_junctions())
|
|
435
|
+
all_junctions = len(state.map.get_junctions())
|
|
436
|
+
# Also show junction alignments
|
|
437
|
+
junction_alignments = [(j.position, j.alignment) for j in state.map.get_junctions()]
|
|
438
|
+
print(
|
|
439
|
+
f"[A{state.agent_id}] MINER: No COGS depot found "
|
|
440
|
+
f"(cogs_junctions={cogs_junctions}, all_junctions={all_junctions}), "
|
|
441
|
+
f"structures={struct_types}, junction_alignments={junction_alignments}, exploring"
|
|
442
|
+
)
|
|
443
|
+
state.debug_info = DebugInfo(mode="deposit", goal="explore_for_cogs_depot", target_object="-")
|
|
444
|
+
return services.navigator.explore(state)
|
|
445
|
+
|
|
446
|
+
candidates.sort(key=lambda x: x[0])
|
|
447
|
+
depot_pos = candidates[0][1]
|
|
448
|
+
dist = candidates[0][0]
|
|
449
|
+
|
|
450
|
+
# If we're ON or adjacent to the depot, try to interact with it
|
|
451
|
+
if state.pos == depot_pos or is_adjacent(state.pos, depot_pos):
|
|
452
|
+
# Verify depot is still cogs-aligned (alignment may have changed)
|
|
453
|
+
struct = state.map.get_structure_at(depot_pos)
|
|
454
|
+
if struct is not None and struct.structure_type == StructureType.JUNCTION:
|
|
455
|
+
if not struct.is_cogs_aligned():
|
|
456
|
+
if DEBUG:
|
|
457
|
+
print(
|
|
458
|
+
f"[A{state.agent_id}] MINER: Depot at {depot_pos} is no longer cogs-aligned "
|
|
459
|
+
f"(alignment={struct.alignment}), finding new depot"
|
|
460
|
+
)
|
|
461
|
+
# Depot is no longer cogs-aligned - recurse to find a new one
|
|
462
|
+
# Remove from candidates and try again
|
|
463
|
+
state.debug_info = DebugInfo(
|
|
464
|
+
mode="deposit", goal="depot_lost_alignment", target_object="-", signal="alignment_changed"
|
|
465
|
+
)
|
|
466
|
+
return services.navigator.explore(state)
|
|
467
|
+
|
|
468
|
+
if DEBUG:
|
|
469
|
+
print(f"[A{state.agent_id}] MINER: At cogs depot {depot_pos}, depositing cargo={state.total_cargo}")
|
|
470
|
+
state.debug_info = DebugInfo(
|
|
471
|
+
mode="deposit", goal="use_cogs_depot", target_object="cogs_depot", target_pos=depot_pos
|
|
472
|
+
)
|
|
473
|
+
return services.navigator.use_object_at(state, depot_pos)
|
|
474
|
+
|
|
475
|
+
if DEBUG and state.step % 10 == 0:
|
|
476
|
+
print(
|
|
477
|
+
f"[A{state.agent_id}] MINER: Moving to cogs depot at {depot_pos}, "
|
|
478
|
+
f"dist={dist}, cargo={state.total_cargo}, agent_pos={state.pos}"
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
state.debug_info = DebugInfo(
|
|
482
|
+
mode="deposit", goal=f"move_to_cogs_depot(dist={dist})", target_object="cogs_depot", target_pos=depot_pos
|
|
483
|
+
)
|
|
484
|
+
return services.navigator.move_to(state, depot_pos, reach_adjacent=True)
|
|
485
|
+
|
|
486
|
+
def _move_toward_extractor_from_obs(self, state: AgentState) -> Action:
|
|
487
|
+
"""Move toward nearest extractor visible in current observation.
|
|
488
|
+
|
|
489
|
+
Uses relative observation positions, not world coordinates.
|
|
490
|
+
Center is at (obs_hr, obs_wr) = (5, 5) typically.
|
|
491
|
+
"""
|
|
492
|
+
import random
|
|
493
|
+
|
|
494
|
+
# Find closest extractor in state.map.structures by iterating all
|
|
495
|
+
# and using relative position calculation from recent observation
|
|
496
|
+
# But since coordinates are broken, use random walk with exploration
|
|
497
|
+
|
|
498
|
+
directions = ["north", "south", "east", "west"]
|
|
499
|
+
random.shuffle(directions)
|
|
500
|
+
return Action(name=f"move_{directions[0]}")
|
|
501
|
+
|
|
502
|
+
def _get_lowest_communal_resource(self, state: AgentState) -> Optional[str]:
|
|
503
|
+
"""Get the resource type with the lowest communal amount.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
Resource type name (carbon, oxygen, germanium, silicon) or None if all are 0.
|
|
507
|
+
"""
|
|
508
|
+
resources = {
|
|
509
|
+
"carbon": state.collective_carbon,
|
|
510
|
+
"oxygen": state.collective_oxygen,
|
|
511
|
+
"germanium": state.collective_germanium,
|
|
512
|
+
"silicon": state.collective_silicon,
|
|
513
|
+
}
|
|
514
|
+
# Find the minimum (break ties alphabetically for consistency)
|
|
515
|
+
min_amount = min(resources.values())
|
|
516
|
+
for name in sorted(resources.keys()):
|
|
517
|
+
if resources[name] == min_amount:
|
|
518
|
+
return name
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
def _find_nearest_extractor(
|
|
522
|
+
self,
|
|
523
|
+
state: AgentState,
|
|
524
|
+
services: Services,
|
|
525
|
+
exclude_positions: Optional[set[tuple[int, int]]] = None,
|
|
526
|
+
) -> Optional[StructureInfo]:
|
|
527
|
+
"""Find nearest usable extractor, preferring the resource with lowest communal amount.
|
|
528
|
+
|
|
529
|
+
Resource prioritization logic:
|
|
530
|
+
- Check communal resource levels and prefer the resource type with the lowest amount
|
|
531
|
+
- Among preferred types, pick the nearest one
|
|
532
|
+
- Fall back to nearest of any type if no preferred available
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
exclude_positions: Set of extractor positions to exclude (empty/failed ones)
|
|
536
|
+
"""
|
|
537
|
+
extractors = state.map.get_usable_extractors()
|
|
538
|
+
|
|
539
|
+
if not extractors:
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
# Combine with failed extractors to exclude
|
|
543
|
+
excluded = exclude_positions or set()
|
|
544
|
+
excluded = excluded | state.nav.failed_extractors
|
|
545
|
+
|
|
546
|
+
# Use step-based range limit (encourages gradual expansion)
|
|
547
|
+
# Also cap by HP to avoid stranding
|
|
548
|
+
step_limit = services.safety.step_based_range_limit(state.step)
|
|
549
|
+
hp_limit = max(50, state.hp // 2)
|
|
550
|
+
max_dist = min(step_limit, hp_limit)
|
|
551
|
+
|
|
552
|
+
# Get the resource type with the lowest communal amount
|
|
553
|
+
lowest_resource = self._get_lowest_communal_resource(state)
|
|
554
|
+
|
|
555
|
+
if DEBUG and state.step % 50 == 0:
|
|
556
|
+
struct_types = {}
|
|
557
|
+
for s in state.map.structures.values():
|
|
558
|
+
t = s.structure_type.name
|
|
559
|
+
struct_types[t] = struct_types.get(t, 0) + 1
|
|
560
|
+
print(
|
|
561
|
+
f"[A{state.agent_id}] MINER: _find_nearest_extractor: "
|
|
562
|
+
f"max_dist={max_dist} (step_limit={step_limit}, hp_limit={hp_limit}), "
|
|
563
|
+
f"extractors={len(extractors)}, hp={state.hp}, "
|
|
564
|
+
f"lowest_communal={lowest_resource} "
|
|
565
|
+
f"(C={state.collective_carbon}, O={state.collective_oxygen}, "
|
|
566
|
+
f"G={state.collective_germanium}, S={state.collective_silicon})"
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Filter extractors by distance and categorize by preference
|
|
570
|
+
# Prefer extractors that produce the lowest communal resource
|
|
571
|
+
preferred_candidates: list[tuple[int, StructureInfo]] = [] # Lowest communal resource type
|
|
572
|
+
fallback_candidates: list[tuple[int, StructureInfo]] = [] # Other resource types
|
|
573
|
+
|
|
574
|
+
for ext in extractors:
|
|
575
|
+
if ext.position in excluded:
|
|
576
|
+
continue
|
|
577
|
+
dist = manhattan_distance(state.pos, ext.position)
|
|
578
|
+
if dist > max_dist:
|
|
579
|
+
continue
|
|
580
|
+
|
|
581
|
+
res_type = ext.resource_type
|
|
582
|
+
if res_type and res_type == lowest_resource:
|
|
583
|
+
preferred_candidates.append((dist, ext))
|
|
584
|
+
else:
|
|
585
|
+
fallback_candidates.append((dist, ext))
|
|
586
|
+
|
|
587
|
+
# Return nearest preferred, or nearest fallback
|
|
588
|
+
if preferred_candidates:
|
|
589
|
+
preferred_candidates.sort(key=lambda x: x[0])
|
|
590
|
+
return preferred_candidates[0][1]
|
|
591
|
+
|
|
592
|
+
if fallback_candidates:
|
|
593
|
+
fallback_candidates.sort(key=lambda x: x[0])
|
|
594
|
+
return fallback_candidates[0][1]
|
|
595
|
+
|
|
596
|
+
# No extractors in range - return the closest one anyway (any type)
|
|
597
|
+
all_candidates = []
|
|
598
|
+
for ext in extractors:
|
|
599
|
+
if ext.position in excluded:
|
|
600
|
+
continue
|
|
601
|
+
dist = manhattan_distance(state.pos, ext.position)
|
|
602
|
+
res_type = ext.resource_type
|
|
603
|
+
# Still prefer lowest communal resource type even when out of range
|
|
604
|
+
priority = 0 if (res_type and res_type == lowest_resource) else 1
|
|
605
|
+
all_candidates.append((priority, dist, ext))
|
|
606
|
+
|
|
607
|
+
if all_candidates:
|
|
608
|
+
all_candidates.sort(key=lambda x: (x[0], x[1])) # Sort by priority, then distance
|
|
609
|
+
return all_candidates[0][2]
|
|
610
|
+
|
|
611
|
+
return None
|
|
612
|
+
|
|
613
|
+
def _get_nearest_depot(self, state: AgentState, services: Services) -> Optional[tuple[int, int]]:
|
|
614
|
+
"""Get nearest COGS-ALIGNED hub/junction for deposit."""
|
|
615
|
+
candidates: list[tuple[int, tuple[int, int]]] = []
|
|
616
|
+
|
|
617
|
+
# Hub (always cogs-aligned)
|
|
618
|
+
hub_pos = state.map.stations.get("hub")
|
|
619
|
+
if hub_pos:
|
|
620
|
+
dist = manhattan_distance(state.pos, hub_pos)
|
|
621
|
+
candidates.append((dist, hub_pos))
|
|
622
|
+
|
|
623
|
+
# Only cogs-aligned junctions (NOT neutral, NOT clips)
|
|
624
|
+
for junction in state.map.get_cogs_junctions():
|
|
625
|
+
dist = manhattan_distance(state.pos, junction.position)
|
|
626
|
+
candidates.append((dist, junction.position))
|
|
627
|
+
|
|
628
|
+
if not candidates:
|
|
629
|
+
return None
|
|
630
|
+
|
|
631
|
+
candidates.sort(key=lambda x: x[0])
|
|
632
|
+
return candidates[0][1]
|