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,416 @@
|
|
|
1
|
+
"""Miner goals — pick resource, mine, deposit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
from cogames_agents.policy.scripted_agent.planky.goal import Goal
|
|
8
|
+
from cogames_agents.policy.scripted_agent.planky.navigator import _manhattan
|
|
9
|
+
from mettagrid.simulator import Action
|
|
10
|
+
|
|
11
|
+
from .gear import GetGearGoal
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from cogames_agents.policy.scripted_agent.planky.context import PlankyContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GetMinerGearGoal(GetGearGoal):
|
|
18
|
+
"""Get miner gear (costs C1 O1 G3 S1 from collective).
|
|
19
|
+
|
|
20
|
+
Miners always get gear regardless of reserves — they produce resources.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
super().__init__(
|
|
25
|
+
gear_attr="miner_gear",
|
|
26
|
+
station_type="miner_station",
|
|
27
|
+
goal_name="GetMinerGear",
|
|
28
|
+
gear_cost={"carbon": 1, "oxygen": 1, "germanium": 3, "silicon": 1},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def _collective_can_afford(self, ctx: "PlankyContext") -> bool:
|
|
32
|
+
"""Miners always get gear — they're the resource producers.
|
|
33
|
+
|
|
34
|
+
But skip if collective is already well-stocked (no need to mine).
|
|
35
|
+
"""
|
|
36
|
+
if _collective_resources_sufficient(ctx):
|
|
37
|
+
return False
|
|
38
|
+
if not self._gear_cost:
|
|
39
|
+
return True
|
|
40
|
+
s = ctx.state
|
|
41
|
+
collective = {
|
|
42
|
+
"carbon": s.collective_carbon,
|
|
43
|
+
"oxygen": s.collective_oxygen,
|
|
44
|
+
"germanium": s.collective_germanium,
|
|
45
|
+
"silicon": s.collective_silicon,
|
|
46
|
+
}
|
|
47
|
+
# No reserve requirement for miners — just need the cost
|
|
48
|
+
return all(collective.get(res, 0) >= amt for res, amt in self._gear_cost.items())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Resource types that can be mined
|
|
52
|
+
RESOURCE_TYPES = ["carbon", "oxygen", "germanium", "silicon"]
|
|
53
|
+
|
|
54
|
+
# When the collective has more than this amount of every resource, stop mining.
|
|
55
|
+
COLLECTIVE_SUFFICIENT_THRESHOLD = 100
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _collective_resources_sufficient(ctx: "PlankyContext") -> bool:
|
|
59
|
+
"""Return True when the collective has >COLLECTIVE_SUFFICIENT_THRESHOLD of every resource."""
|
|
60
|
+
s = ctx.state
|
|
61
|
+
return (
|
|
62
|
+
s.collective_carbon > COLLECTIVE_SUFFICIENT_THRESHOLD
|
|
63
|
+
and s.collective_oxygen > COLLECTIVE_SUFFICIENT_THRESHOLD
|
|
64
|
+
and s.collective_germanium > COLLECTIVE_SUFFICIENT_THRESHOLD
|
|
65
|
+
and s.collective_silicon > COLLECTIVE_SUFFICIENT_THRESHOLD
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ExploreHubGoal(Goal):
|
|
70
|
+
"""Explore the hub to discover all 4 extractors before mining.
|
|
71
|
+
|
|
72
|
+
Extractors are at hub corners: (±5, ±5) from center.
|
|
73
|
+
Each miner visits corners in a rotated order based on agent_id.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
name = "ExploreHub"
|
|
77
|
+
# Hub corner offsets from SPAWN_POS — extractors at these positions
|
|
78
|
+
HUB_OFFSETS = [(-5, -5), (-5, 5), (5, 5), (5, -5)]
|
|
79
|
+
|
|
80
|
+
def is_satisfied(self, ctx: PlankyContext) -> bool:
|
|
81
|
+
found = sum(1 for r in RESOURCE_TYPES if ctx.map.find(type=f"{r}_extractor"))
|
|
82
|
+
if found >= 4:
|
|
83
|
+
return True
|
|
84
|
+
# Time limit: don't explore forever
|
|
85
|
+
if ctx.step > 15:
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def execute(self, ctx: PlankyContext) -> Optional[Action]:
|
|
90
|
+
from cogames_agents.policy.scripted_agent.planky.policy import SPAWN_POS
|
|
91
|
+
|
|
92
|
+
corner_idx = ctx.blackboard.get("_hub_corner_idx", ctx.agent_id % 4)
|
|
93
|
+
offsets = self.HUB_OFFSETS
|
|
94
|
+
target = (SPAWN_POS[0] + offsets[corner_idx][0], SPAWN_POS[1] + offsets[corner_idx][1])
|
|
95
|
+
|
|
96
|
+
dist = _manhattan(ctx.state.position, target)
|
|
97
|
+
if dist <= 2:
|
|
98
|
+
corner_idx = (corner_idx + 1) % 4
|
|
99
|
+
ctx.blackboard["_hub_corner_idx"] = corner_idx
|
|
100
|
+
target = (SPAWN_POS[0] + offsets[corner_idx][0], SPAWN_POS[1] + offsets[corner_idx][1])
|
|
101
|
+
|
|
102
|
+
if ctx.trace:
|
|
103
|
+
ctx.trace.nav_target = target
|
|
104
|
+
found = sum(1 for r in RESOURCE_TYPES if ctx.map.find(type=f"{r}_extractor"))
|
|
105
|
+
ctx.trace.activate(self.name, f"corner={corner_idx} found={found}/4")
|
|
106
|
+
|
|
107
|
+
return ctx.navigator.get_action(ctx.state.position, target, ctx.map, reach_adjacent=True)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class PickResourceGoal(Goal):
|
|
111
|
+
"""Select a target resource based on collective needs.
|
|
112
|
+
|
|
113
|
+
Prioritizes the resource that the collective has the least of,
|
|
114
|
+
ensuring balanced gathering for heart production.
|
|
115
|
+
Re-evaluates every 50 steps to adapt to changing needs.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
name = "PickResource"
|
|
119
|
+
REEVALUATE_INTERVAL = 50
|
|
120
|
+
|
|
121
|
+
def is_satisfied(self, ctx: PlankyContext) -> bool:
|
|
122
|
+
# Don't bother picking a resource if collective is well-stocked
|
|
123
|
+
if _collective_resources_sufficient(ctx):
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
if "target_resource" not in ctx.blackboard:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
# Re-evaluate periodically to ensure we're mining what's needed
|
|
130
|
+
last_pick = ctx.blackboard.get("_target_resource_step", 0)
|
|
131
|
+
if ctx.step - last_pick >= self.REEVALUATE_INTERVAL:
|
|
132
|
+
# Clear to force re-evaluation
|
|
133
|
+
ctx.blackboard.pop("target_resource", None)
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def execute(self, ctx: PlankyContext) -> Optional[Action]:
|
|
139
|
+
# Get collective resource levels
|
|
140
|
+
collective = {
|
|
141
|
+
"carbon": ctx.state.collective_carbon,
|
|
142
|
+
"oxygen": ctx.state.collective_oxygen,
|
|
143
|
+
"germanium": ctx.state.collective_germanium,
|
|
144
|
+
"silicon": ctx.state.collective_silicon,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Find resources with available extractors
|
|
148
|
+
available_resources: list[tuple[int, str]] = []
|
|
149
|
+
for resource in RESOURCE_TYPES:
|
|
150
|
+
extractors = ctx.map.find(type=f"{resource}_extractor")
|
|
151
|
+
usable = [
|
|
152
|
+
(pos, e)
|
|
153
|
+
for pos, e in extractors
|
|
154
|
+
if e.properties.get("remaining_uses", 999) > 0
|
|
155
|
+
and e.properties.get("inventory_amount", -1) != 0
|
|
156
|
+
and not _extractor_recently_failed(ctx, pos)
|
|
157
|
+
]
|
|
158
|
+
if usable:
|
|
159
|
+
# Score by collective amount (lower = higher priority)
|
|
160
|
+
available_resources.append((collective.get(resource, 0), resource))
|
|
161
|
+
|
|
162
|
+
if not available_resources:
|
|
163
|
+
# No extractors known — pick carbon as default, MineResource will explore
|
|
164
|
+
ctx.blackboard["target_resource"] = "carbon"
|
|
165
|
+
ctx.blackboard["_target_resource_step"] = ctx.step
|
|
166
|
+
if ctx.trace:
|
|
167
|
+
ctx.trace.activate(self.name, "no extractors known, defaulting to carbon")
|
|
168
|
+
return Action(name="noop")
|
|
169
|
+
|
|
170
|
+
# Pick the resource the collective has least of (that we can mine)
|
|
171
|
+
available_resources.sort()
|
|
172
|
+
best_resource = available_resources[0][1]
|
|
173
|
+
|
|
174
|
+
if ctx.trace:
|
|
175
|
+
ctx.trace.activate(self.name, f"need={best_resource} coll={collective}")
|
|
176
|
+
|
|
177
|
+
ctx.blackboard["target_resource"] = best_resource
|
|
178
|
+
ctx.blackboard["_target_resource_step"] = ctx.step
|
|
179
|
+
return Action(name="noop")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _extractor_recently_failed(ctx: PlankyContext, pos: tuple[int, int]) -> bool:
|
|
183
|
+
"""Check if we recently failed to mine from this extractor."""
|
|
184
|
+
failed_step = ctx.blackboard.get(f"mine_failed_{pos}", -9999)
|
|
185
|
+
return ctx.step - failed_step < 100 # 100 step cooldown - extractors may refill
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class DepositCargoGoal(Goal):
|
|
189
|
+
"""Deposit resources at nearest cogs-aligned building when cargo is reasonably full.
|
|
190
|
+
|
|
191
|
+
Triggers when cargo is >= 50% full (or >= 10 resources for small capacity).
|
|
192
|
+
Once triggered, keeps depositing until cargo is EMPTY.
|
|
193
|
+
Tracks attempts and marks depots as failed if cargo doesn't decrease.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
name = "DepositCargo"
|
|
197
|
+
MAX_ATTEMPTS_PER_DEPOT = 5
|
|
198
|
+
|
|
199
|
+
def is_satisfied(self, ctx: PlankyContext) -> bool:
|
|
200
|
+
cargo = ctx.state.cargo_total
|
|
201
|
+
|
|
202
|
+
# If we're currently depositing (flag set), keep going until empty
|
|
203
|
+
if ctx.blackboard.get("_depositing", False):
|
|
204
|
+
if cargo == 0:
|
|
205
|
+
ctx.blackboard["_depositing"] = False
|
|
206
|
+
return True
|
|
207
|
+
return False # Keep depositing until empty
|
|
208
|
+
|
|
209
|
+
# Not currently depositing - check if we should start
|
|
210
|
+
# Deposit when at least 50% full (but always deposit if cargo == capacity)
|
|
211
|
+
capacity = ctx.state.cargo_capacity
|
|
212
|
+
threshold = max(2, capacity // 2)
|
|
213
|
+
|
|
214
|
+
if cargo >= threshold:
|
|
215
|
+
ctx.blackboard["_depositing"] = True
|
|
216
|
+
return False # Start depositing
|
|
217
|
+
|
|
218
|
+
return True # Don't need to deposit yet
|
|
219
|
+
|
|
220
|
+
def execute(self, ctx: PlankyContext) -> Optional[Action]:
|
|
221
|
+
# Track cargo to detect successful deposit
|
|
222
|
+
prev_cargo = ctx.blackboard.get("prev_deposit_cargo", ctx.state.cargo_total)
|
|
223
|
+
current_cargo = ctx.state.cargo_total
|
|
224
|
+
ctx.blackboard["prev_deposit_cargo"] = current_cargo
|
|
225
|
+
|
|
226
|
+
# Find nearest cogs depot
|
|
227
|
+
depot_pos = _find_cogs_depot(ctx)
|
|
228
|
+
if depot_pos is None:
|
|
229
|
+
return ctx.navigator.explore(ctx.state.position, ctx.map)
|
|
230
|
+
|
|
231
|
+
if ctx.trace:
|
|
232
|
+
ctx.trace.nav_target = depot_pos
|
|
233
|
+
|
|
234
|
+
dist = _manhattan(ctx.state.position, depot_pos)
|
|
235
|
+
if dist <= 1:
|
|
236
|
+
if ctx.trace:
|
|
237
|
+
hub_dbg_filter = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
|
|
238
|
+
hubs = ctx.map.find(type_contains="hub", property_filter=hub_dbg_filter)
|
|
239
|
+
depot_entity = ctx.map.entities.get(depot_pos)
|
|
240
|
+
print(
|
|
241
|
+
f"[deposit-debug] agent={ctx.agent_id} t={ctx.step} pos={ctx.state.position}"
|
|
242
|
+
f" depot={depot_pos} depot_type={depot_entity.type if depot_entity else 'NONE'}"
|
|
243
|
+
f" depot_align={depot_entity.properties.get('alignment') if depot_entity else 'N/A'}"
|
|
244
|
+
f" cargo={current_cargo} prev={prev_cargo}"
|
|
245
|
+
f" hubs={[(p, e.properties.get('alignment')) for p, e in hubs]}"
|
|
246
|
+
)
|
|
247
|
+
# Adjacent to depot - track attempts
|
|
248
|
+
attempts_key = f"deposit_attempts_{depot_pos}"
|
|
249
|
+
attempts = ctx.blackboard.get(attempts_key, 0) + 1
|
|
250
|
+
|
|
251
|
+
# Reset if cargo decreased (deposit succeeded)
|
|
252
|
+
if current_cargo < prev_cargo:
|
|
253
|
+
ctx.blackboard[attempts_key] = 0
|
|
254
|
+
else:
|
|
255
|
+
ctx.blackboard[attempts_key] = attempts
|
|
256
|
+
|
|
257
|
+
if attempts > self.MAX_ATTEMPTS_PER_DEPOT:
|
|
258
|
+
# Mark as failed temporarily
|
|
259
|
+
ctx.blackboard[f"deposit_failed_{depot_pos}"] = ctx.step
|
|
260
|
+
ctx.blackboard[attempts_key] = 0
|
|
261
|
+
if ctx.trace:
|
|
262
|
+
ctx.trace.activate(self.name, f"giving up on {depot_pos}")
|
|
263
|
+
return ctx.navigator.explore(ctx.state.position, ctx.map)
|
|
264
|
+
|
|
265
|
+
return _move_toward(ctx.state.position, depot_pos)
|
|
266
|
+
|
|
267
|
+
# Not adjacent - reset attempts
|
|
268
|
+
ctx.blackboard[f"deposit_attempts_{depot_pos}"] = 0
|
|
269
|
+
return ctx.navigator.get_action(ctx.state.position, depot_pos, ctx.map, reach_adjacent=True)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class MineResourceGoal(Goal):
|
|
273
|
+
"""Navigate to extractor for target_resource and bump it.
|
|
274
|
+
|
|
275
|
+
Tracks attempts at each extractor and marks them as failed if
|
|
276
|
+
cargo doesn't increase after several bumps (extractor empty/broken).
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
name = "MineResource"
|
|
280
|
+
MAX_ATTEMPTS_PER_EXTRACTOR = 3 # Reduced from 5 - fail faster
|
|
281
|
+
|
|
282
|
+
def is_satisfied(self, ctx: PlankyContext) -> bool:
|
|
283
|
+
# Stop mining when the collective is well-stocked
|
|
284
|
+
if _collective_resources_sufficient(ctx) and ctx.state.cargo_total == 0:
|
|
285
|
+
if ctx.trace:
|
|
286
|
+
ctx.trace.skip(self.name, "collective resources sufficient, idling")
|
|
287
|
+
return True
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
def execute(self, ctx: PlankyContext) -> Optional[Action]:
|
|
291
|
+
target_resource = ctx.blackboard.get("target_resource", "carbon")
|
|
292
|
+
|
|
293
|
+
# Track cargo to detect successful mining
|
|
294
|
+
prev_cargo = ctx.blackboard.get("prev_cargo", 0)
|
|
295
|
+
current_cargo = ctx.state.cargo_total
|
|
296
|
+
ctx.blackboard["prev_cargo"] = current_cargo
|
|
297
|
+
|
|
298
|
+
# Find nearest usable extractor for this resource
|
|
299
|
+
target_pos = self._find_extractor(ctx, target_resource)
|
|
300
|
+
|
|
301
|
+
if target_pos is None:
|
|
302
|
+
# Try any resource type
|
|
303
|
+
for resource in RESOURCE_TYPES:
|
|
304
|
+
if resource == target_resource:
|
|
305
|
+
continue
|
|
306
|
+
target_pos = self._find_extractor(ctx, resource)
|
|
307
|
+
if target_pos:
|
|
308
|
+
ctx.blackboard["target_resource"] = resource
|
|
309
|
+
ctx.blackboard["_target_resource_step"] = ctx.step
|
|
310
|
+
break
|
|
311
|
+
|
|
312
|
+
if target_pos is None:
|
|
313
|
+
# No extractors found — explore in agent-specific direction to discover them
|
|
314
|
+
ctx.blackboard.pop("target_resource", None)
|
|
315
|
+
directions = ["north", "east", "south", "west"]
|
|
316
|
+
return ctx.navigator.explore(
|
|
317
|
+
ctx.state.position,
|
|
318
|
+
ctx.map,
|
|
319
|
+
direction_bias=directions[ctx.agent_id % 4],
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if ctx.trace:
|
|
323
|
+
ctx.trace.nav_target = target_pos
|
|
324
|
+
|
|
325
|
+
dist = _manhattan(ctx.state.position, target_pos)
|
|
326
|
+
if dist <= 1:
|
|
327
|
+
# Adjacent to extractor — track attempts
|
|
328
|
+
attempts_key = f"mine_attempts_{target_pos}"
|
|
329
|
+
attempts = ctx.blackboard.get(attempts_key, 0) + 1
|
|
330
|
+
|
|
331
|
+
# Reset attempts if cargo increased (mining succeeded)
|
|
332
|
+
if current_cargo > prev_cargo:
|
|
333
|
+
ctx.blackboard[attempts_key] = 0
|
|
334
|
+
else:
|
|
335
|
+
ctx.blackboard[attempts_key] = attempts
|
|
336
|
+
|
|
337
|
+
if attempts > self.MAX_ATTEMPTS_PER_EXTRACTOR:
|
|
338
|
+
# Mark as failed permanently for this episode
|
|
339
|
+
ctx.blackboard[f"mine_failed_{target_pos}"] = ctx.step
|
|
340
|
+
ctx.blackboard[attempts_key] = 0
|
|
341
|
+
# Also clear target resource to force re-evaluation
|
|
342
|
+
ctx.blackboard.pop("target_resource", None)
|
|
343
|
+
if ctx.trace:
|
|
344
|
+
ctx.trace.activate(self.name, f"giving up on {target_pos}")
|
|
345
|
+
return ctx.navigator.explore(
|
|
346
|
+
ctx.state.position,
|
|
347
|
+
ctx.map,
|
|
348
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return _move_toward(ctx.state.position, target_pos)
|
|
352
|
+
|
|
353
|
+
# Don't reset attempts when moving away - only reset on successful mine
|
|
354
|
+
return ctx.navigator.get_action(ctx.state.position, target_pos, ctx.map, reach_adjacent=True)
|
|
355
|
+
|
|
356
|
+
def _find_extractor(self, ctx: PlankyContext, resource: str) -> Optional[tuple[int, int]]:
|
|
357
|
+
"""Find nearest usable extractor."""
|
|
358
|
+
extractors = ctx.map.find(type=f"{resource}_extractor")
|
|
359
|
+
usable = [
|
|
360
|
+
(pos, e)
|
|
361
|
+
for pos, e in extractors
|
|
362
|
+
if e.properties.get("remaining_uses", 999) > 0
|
|
363
|
+
and e.properties.get("inventory_amount", -1) != 0
|
|
364
|
+
and not _extractor_recently_failed(ctx, pos)
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
if not usable:
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
# Sort by distance to agent
|
|
371
|
+
usable.sort(key=lambda x: _manhattan(ctx.state.position, x[0]))
|
|
372
|
+
return usable[0][0]
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _find_cogs_depot(ctx: PlankyContext) -> tuple[int, int] | None:
|
|
376
|
+
"""Find nearest cogs-aligned depot, prioritizing hub."""
|
|
377
|
+
from cogames_agents.policy.scripted_agent.planky.policy import SPAWN_POS
|
|
378
|
+
|
|
379
|
+
pos = ctx.state.position
|
|
380
|
+
|
|
381
|
+
def recently_failed(p: tuple[int, int]) -> bool:
|
|
382
|
+
failed_step = ctx.blackboard.get(f"deposit_failed_{p}", -9999)
|
|
383
|
+
return ctx.step - failed_step < 100
|
|
384
|
+
|
|
385
|
+
# Prioritize own team's hub
|
|
386
|
+
hub_filter = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
|
|
387
|
+
for apos, _ in ctx.map.find(type_contains="hub", property_filter=hub_filter):
|
|
388
|
+
if not recently_failed(apos):
|
|
389
|
+
return apos
|
|
390
|
+
|
|
391
|
+
# Fallback: nearest cogs junction near hub
|
|
392
|
+
candidates: list[tuple[int, tuple[int, int]]] = []
|
|
393
|
+
for jpos, _ in ctx.map.find(type_contains="junction", property_filter={"alignment": "cogs"}):
|
|
394
|
+
if not recently_failed(jpos) and _manhattan(jpos, SPAWN_POS) <= 15:
|
|
395
|
+
candidates.append((_manhattan(pos, jpos), jpos))
|
|
396
|
+
|
|
397
|
+
if not candidates:
|
|
398
|
+
# Last resort: navigate to hub area
|
|
399
|
+
return SPAWN_POS
|
|
400
|
+
candidates.sort()
|
|
401
|
+
return candidates[0][1]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _move_toward(current: tuple[int, int], target: tuple[int, int]) -> Action:
|
|
405
|
+
dr = target[0] - current[0]
|
|
406
|
+
dc = target[1] - current[1]
|
|
407
|
+
if abs(dr) >= abs(dc):
|
|
408
|
+
if dr > 0:
|
|
409
|
+
return Action(name="move_south")
|
|
410
|
+
elif dr < 0:
|
|
411
|
+
return Action(name="move_north")
|
|
412
|
+
if dc > 0:
|
|
413
|
+
return Action(name="move_east")
|
|
414
|
+
elif dc < 0:
|
|
415
|
+
return Action(name="move_west")
|
|
416
|
+
return Action(name="move_north")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Scout goals — explore the map."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from cogames_agents.policy.scripted_agent.planky.goal import Goal
|
|
8
|
+
from mettagrid.simulator import Action
|
|
9
|
+
|
|
10
|
+
from .gear import GetGearGoal
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from cogames_agents.policy.scripted_agent.planky.context import PlankyContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GetScoutGearGoal(GetGearGoal):
|
|
17
|
+
"""Get scout gear (costs C1 O1 G1 S3 from collective)."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
super().__init__(
|
|
21
|
+
gear_attr="scout_gear",
|
|
22
|
+
station_type="scout_station",
|
|
23
|
+
goal_name="GetScoutGear",
|
|
24
|
+
gear_cost={"carbon": 1, "oxygen": 1, "germanium": 1, "silicon": 3},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExploreGoal(Goal):
|
|
29
|
+
"""Explore the map by navigating to frontier cells."""
|
|
30
|
+
|
|
31
|
+
name = "Explore"
|
|
32
|
+
|
|
33
|
+
def is_satisfied(self, ctx: PlankyContext) -> bool:
|
|
34
|
+
# Never satisfied — always explore
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def execute(self, ctx: PlankyContext) -> Action:
|
|
38
|
+
directions = ["north", "east", "south", "west"]
|
|
39
|
+
direction_bias = directions[ctx.agent_id % 4]
|
|
40
|
+
return ctx.navigator.explore(ctx.state.position, ctx.map, direction_bias=direction_bias)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Scrambler goals — neutralize enemy junctions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
from cogames_agents.policy.scripted_agent.planky.goal import Goal
|
|
8
|
+
from cogames_agents.policy.scripted_agent.planky.navigator import _manhattan
|
|
9
|
+
from mettagrid.simulator import Action
|
|
10
|
+
|
|
11
|
+
from .gear import GetGearGoal
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from cogames_agents.policy.scripted_agent.planky.context import PlankyContext
|
|
15
|
+
|
|
16
|
+
JUNCTION_AOE_RANGE = 10
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GetScramblerGearGoal(GetGearGoal):
|
|
20
|
+
"""Get scrambler gear (costs C1 O3 G1 S1 from collective)."""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
super().__init__(
|
|
24
|
+
gear_attr="scrambler_gear",
|
|
25
|
+
station_type="scrambler_station",
|
|
26
|
+
goal_name="GetScramblerGear",
|
|
27
|
+
gear_cost={"carbon": 1, "oxygen": 3, "germanium": 1, "silicon": 1},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ScrambleJunctionGoal(Goal):
|
|
32
|
+
"""Find and scramble enemy (clips) junctions.
|
|
33
|
+
|
|
34
|
+
Tracks attempts per junction to avoid getting stuck.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name = "ScrambleJunction"
|
|
38
|
+
MAX_ATTEMPTS_PER_TARGET = 5
|
|
39
|
+
MAX_NAV_STEPS_PER_TARGET = 40 # Give up navigating to a target after this many steps
|
|
40
|
+
COOLDOWN_STEPS = 50
|
|
41
|
+
|
|
42
|
+
def is_satisfied(self, ctx: PlankyContext) -> bool:
|
|
43
|
+
# Can't scramble without gear and a heart
|
|
44
|
+
if not ctx.state.scrambler_gear:
|
|
45
|
+
if ctx.trace:
|
|
46
|
+
ctx.trace.skip(self.name, "no gear")
|
|
47
|
+
return True
|
|
48
|
+
if ctx.state.heart < 1:
|
|
49
|
+
if ctx.trace:
|
|
50
|
+
ctx.trace.skip(self.name, "no heart")
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def execute(self, ctx: PlankyContext) -> Optional[Action]:
|
|
55
|
+
# Track navigation steps toward current target to detect stuck
|
|
56
|
+
nav_key = "_scramble_nav_steps"
|
|
57
|
+
nav_target_key = "_scramble_nav_target"
|
|
58
|
+
nav_steps = ctx.blackboard.get(nav_key, 0) + 1
|
|
59
|
+
ctx.blackboard[nav_key] = nav_steps
|
|
60
|
+
|
|
61
|
+
target = self._find_best_target(ctx)
|
|
62
|
+
if target is None:
|
|
63
|
+
ctx.blackboard[nav_key] = 0
|
|
64
|
+
return ctx.navigator.explore(
|
|
65
|
+
ctx.state.position,
|
|
66
|
+
ctx.map,
|
|
67
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Reset nav counter if target changed
|
|
71
|
+
prev_target = ctx.blackboard.get(nav_target_key)
|
|
72
|
+
if prev_target != target:
|
|
73
|
+
ctx.blackboard[nav_key] = 0
|
|
74
|
+
nav_steps = 0
|
|
75
|
+
ctx.blackboard[nav_target_key] = target
|
|
76
|
+
|
|
77
|
+
# If we've been navigating too long, mark target as failed
|
|
78
|
+
if nav_steps > self.MAX_NAV_STEPS_PER_TARGET:
|
|
79
|
+
failed_key = f"scramble_failed_{target}"
|
|
80
|
+
ctx.blackboard[failed_key] = ctx.step
|
|
81
|
+
ctx.blackboard[nav_key] = 0
|
|
82
|
+
if ctx.trace:
|
|
83
|
+
ctx.trace.activate(self.name, f"nav timeout on {target}")
|
|
84
|
+
return ctx.navigator.explore(
|
|
85
|
+
ctx.state.position,
|
|
86
|
+
ctx.map,
|
|
87
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if ctx.trace:
|
|
91
|
+
ctx.trace.nav_target = target
|
|
92
|
+
|
|
93
|
+
dist = _manhattan(ctx.state.position, target)
|
|
94
|
+
if dist <= 1:
|
|
95
|
+
# Track attempts on this specific junction
|
|
96
|
+
attempts_key = f"scramble_attempts_{target}"
|
|
97
|
+
attempts = ctx.blackboard.get(attempts_key, 0) + 1
|
|
98
|
+
ctx.blackboard[attempts_key] = attempts
|
|
99
|
+
|
|
100
|
+
if attempts > self.MAX_ATTEMPTS_PER_TARGET:
|
|
101
|
+
# Mark this junction as failed temporarily
|
|
102
|
+
failed_key = f"scramble_failed_{target}"
|
|
103
|
+
ctx.blackboard[failed_key] = ctx.step
|
|
104
|
+
ctx.blackboard[attempts_key] = 0
|
|
105
|
+
if ctx.trace:
|
|
106
|
+
ctx.trace.activate(self.name, f"giving up on {target}")
|
|
107
|
+
return ctx.navigator.explore(
|
|
108
|
+
ctx.state.position,
|
|
109
|
+
ctx.map,
|
|
110
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if ctx.trace:
|
|
114
|
+
ctx.trace.activate(self.name, f"bump {attempts}/{self.MAX_ATTEMPTS_PER_TARGET}")
|
|
115
|
+
return _move_toward(ctx.state.position, target)
|
|
116
|
+
|
|
117
|
+
# Not adjacent - reset attempts for this target
|
|
118
|
+
attempts_key = f"scramble_attempts_{target}"
|
|
119
|
+
ctx.blackboard[attempts_key] = 0
|
|
120
|
+
return ctx.navigator.get_action(ctx.state.position, target, ctx.map, reach_adjacent=True)
|
|
121
|
+
|
|
122
|
+
def _find_best_target(self, ctx: PlankyContext) -> tuple[int, int] | None:
|
|
123
|
+
"""Find enemy junction to scramble, prioritized by blocking count."""
|
|
124
|
+
pos = ctx.state.position
|
|
125
|
+
|
|
126
|
+
def recently_failed(p: tuple[int, int]) -> bool:
|
|
127
|
+
failed_step = ctx.blackboard.get(f"scramble_failed_{p}", -9999)
|
|
128
|
+
return ctx.step - failed_step < self.COOLDOWN_STEPS
|
|
129
|
+
|
|
130
|
+
# Get clips junctions
|
|
131
|
+
enemy: list[tuple[tuple[int, int], dict]] = []
|
|
132
|
+
for jpos, e in ctx.map.find(type_contains="junction", property_filter={"alignment": "clips"}):
|
|
133
|
+
if not recently_failed(jpos):
|
|
134
|
+
enemy.append((jpos, e.properties))
|
|
135
|
+
for cpos, e in ctx.map.find(type_contains="junction", property_filter={"alignment": "clips"}):
|
|
136
|
+
if not recently_failed(cpos):
|
|
137
|
+
enemy.append((cpos, e.properties))
|
|
138
|
+
|
|
139
|
+
if not enemy:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
# Get neutral junctions for scoring
|
|
143
|
+
neutral_positions: list[tuple[int, int]] = []
|
|
144
|
+
for jpos, e in ctx.map.find(type_contains="junction"):
|
|
145
|
+
if e.properties.get("alignment") is None:
|
|
146
|
+
neutral_positions.append(jpos)
|
|
147
|
+
for cpos, e in ctx.map.find(type_contains="junction"):
|
|
148
|
+
if e.properties.get("alignment") is None:
|
|
149
|
+
neutral_positions.append(cpos)
|
|
150
|
+
|
|
151
|
+
# Score by: how many neutrals this enemy blocks, then by distance
|
|
152
|
+
scored: list[tuple[int, int, tuple[int, int]]] = []
|
|
153
|
+
for epos, _ in enemy:
|
|
154
|
+
blocked = sum(1 for np in neutral_positions if _manhattan(epos, np) <= JUNCTION_AOE_RANGE)
|
|
155
|
+
dist = _manhattan(pos, epos)
|
|
156
|
+
scored.append((-blocked, dist, epos)) # Negative blocked for descending sort
|
|
157
|
+
|
|
158
|
+
scored.sort()
|
|
159
|
+
return scored[0][2]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _move_toward(current: tuple[int, int], target: tuple[int, int]) -> Action:
|
|
163
|
+
dr = target[0] - current[0]
|
|
164
|
+
dc = target[1] - current[1]
|
|
165
|
+
if abs(dr) >= abs(dc):
|
|
166
|
+
if dr > 0:
|
|
167
|
+
return Action(name="move_south")
|
|
168
|
+
elif dr < 0:
|
|
169
|
+
return Action(name="move_north")
|
|
170
|
+
if dc > 0:
|
|
171
|
+
return Action(name="move_east")
|
|
172
|
+
elif dc < 0:
|
|
173
|
+
return Action(name="move_west")
|
|
174
|
+
return Action(name="move_north")
|