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,589 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Miner role for CoGsGuard.
|
|
3
|
+
|
|
4
|
+
Miners gather resources from extractors and deposit at aligned buildings.
|
|
5
|
+
With miner gear, they get +40 cargo capacity and extract 10 resources at a time.
|
|
6
|
+
|
|
7
|
+
Strategy:
|
|
8
|
+
- Extractors are in map corners, aligned buildings provide deposit points
|
|
9
|
+
- Miners should quickly head to corners to find extractors
|
|
10
|
+
- Once extractors are known, alternate between mining and depositing
|
|
11
|
+
- Deposit to nearest aligned building (hub or cogs-aligned junctions)
|
|
12
|
+
- If gear is lost, collect resources without gear and check for gear on each dropoff
|
|
13
|
+
- Retry failed mine actions up to MAX_RETRIES times
|
|
14
|
+
- HP-aware: Never venture further than can safely return to healing territory
|
|
15
|
+
(HP drains at -1/step outside aligned building AOE, losing all HP = lose gear)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from cogames_agents.policy.scripted_agent.utils import is_adjacent
|
|
21
|
+
from mettagrid.simulator import Action
|
|
22
|
+
|
|
23
|
+
from .policy import DEBUG, CogsguardAgentPolicyImpl
|
|
24
|
+
from .types import CogsguardAgentState, Role, StructureInfo, StructureType
|
|
25
|
+
|
|
26
|
+
# Extractors are typically in map corners - explore these areas first
|
|
27
|
+
# Map is 200x200, center is ~100,100
|
|
28
|
+
CORNER_OFFSETS = [
|
|
29
|
+
(-10, -10), # NW corner direction
|
|
30
|
+
(-10, 10), # NE corner direction
|
|
31
|
+
(10, -10), # SW corner direction
|
|
32
|
+
(10, 10), # SE corner direction
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Maximum number of times to retry a failed mine action
|
|
36
|
+
MAX_RETRIES = 3
|
|
37
|
+
|
|
38
|
+
# HP management constants
|
|
39
|
+
# Hub AOE range that provides healing (+100 HP to aligned agents)
|
|
40
|
+
HEALING_AOE_RANGE = 10
|
|
41
|
+
# Base HP drain per step outside healing AOE (from regen_amounts)
|
|
42
|
+
HP_DRAIN_BASE = 1
|
|
43
|
+
# Additional HP drain per step when near enemy buildings (from attack_aoe)
|
|
44
|
+
HP_DRAIN_ENEMY_AOE = 1
|
|
45
|
+
# Enemy AOE range
|
|
46
|
+
ENEMY_AOE_RANGE = 10
|
|
47
|
+
# Safety margin - return home with this much HP buffer
|
|
48
|
+
HP_SAFETY_MARGIN = 5
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MinerAgentPolicyImpl(CogsguardAgentPolicyImpl):
|
|
52
|
+
"""Miner agent: gather resources and deposit at nearest aligned building."""
|
|
53
|
+
|
|
54
|
+
ROLE = Role.MINER
|
|
55
|
+
RESOURCE_ORDER = ["carbon", "oxygen", "germanium", "silicon"]
|
|
56
|
+
|
|
57
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
58
|
+
super().__init__(*args, **kwargs)
|
|
59
|
+
self._preferred_resource = self.RESOURCE_ORDER[self._agent_id % len(self.RESOURCE_ORDER)]
|
|
60
|
+
|
|
61
|
+
def _get_nearest_aligned_depot(self, s: CogsguardAgentState) -> tuple[int, int] | None:
|
|
62
|
+
"""Find the nearest aligned building that accepts deposits.
|
|
63
|
+
|
|
64
|
+
Returns position of nearest cogs-aligned building (hub or junction).
|
|
65
|
+
These buildings have healing AOE and accept resource deposits.
|
|
66
|
+
"""
|
|
67
|
+
candidates: list[tuple[int, tuple[int, int]]] = []
|
|
68
|
+
|
|
69
|
+
# Hub is always aligned to cogs
|
|
70
|
+
hub_pos = s.get_structure_position(StructureType.HUB)
|
|
71
|
+
if hub_pos:
|
|
72
|
+
dist = abs(hub_pos[0] - s.row) + abs(hub_pos[1] - s.col)
|
|
73
|
+
candidates.append((dist, hub_pos))
|
|
74
|
+
|
|
75
|
+
# Check for cogs-aligned junctions
|
|
76
|
+
junctions = s.get_structures_by_type(StructureType.CHARGER)
|
|
77
|
+
for junction in junctions:
|
|
78
|
+
if junction.alignment == "cogs":
|
|
79
|
+
dist = abs(junction.position[0] - s.row) + abs(junction.position[1] - s.col)
|
|
80
|
+
candidates.append((dist, junction.position))
|
|
81
|
+
|
|
82
|
+
if not candidates:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# Return nearest
|
|
86
|
+
candidates.sort(key=lambda x: x[0])
|
|
87
|
+
return candidates[0][1]
|
|
88
|
+
|
|
89
|
+
def execute_role(self, s: CogsguardAgentState) -> Action:
|
|
90
|
+
"""Execute miner behavior: continuously gather resources and deposit at hub.
|
|
91
|
+
|
|
92
|
+
If gear is lost, collect resources without gear and check for gear on each dropoff.
|
|
93
|
+
Retry failed mine actions up to MAX_RETRIES times.
|
|
94
|
+
HP-aware: Return to healing territory before HP gets too low to avoid losing gear.
|
|
95
|
+
"""
|
|
96
|
+
# Use dynamic properties - cargo_capacity updates if gear is lost
|
|
97
|
+
total_cargo = s.total_cargo
|
|
98
|
+
cargo_capacity = s.cargo_capacity
|
|
99
|
+
has_gear = s.miner > 0
|
|
100
|
+
|
|
101
|
+
if DEBUG and s.step_count <= 50:
|
|
102
|
+
num_extractors = len(s.get_structures_by_type(StructureType.EXTRACTOR))
|
|
103
|
+
mode = "deposit" if total_cargo >= cargo_capacity - 2 else "gather"
|
|
104
|
+
gear_status = "GEAR" if has_gear else "NO_GEAR"
|
|
105
|
+
steps_to_heal = self._get_steps_to_healing(s)
|
|
106
|
+
drain_rate = self._get_hp_drain_rate(s)
|
|
107
|
+
nearest_depot = self._get_nearest_aligned_depot(s)
|
|
108
|
+
print(
|
|
109
|
+
f"[A{s.agent_id}] MINER step={s.step_count}: pos=({s.row},{s.col}) "
|
|
110
|
+
f"cargo={total_cargo}/{cargo_capacity} mode={mode} ext={num_extractors} "
|
|
111
|
+
f"{gear_status} energy={s.energy} hp={s.hp} steps_to_heal={steps_to_heal} "
|
|
112
|
+
f"drain={drain_rate}/step depot={nearest_depot}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# === HP check - highest priority ===
|
|
116
|
+
# If HP is getting low, head back to healing territory immediately
|
|
117
|
+
if self._should_return_for_healing(s):
|
|
118
|
+
if DEBUG:
|
|
119
|
+
steps = self._get_steps_to_healing(s)
|
|
120
|
+
print(f"[A{s.agent_id}] MINER: HP LOW ({s.hp}), returning to heal! Steps to safety: {steps}")
|
|
121
|
+
return self._return_to_healing(s)
|
|
122
|
+
|
|
123
|
+
# Check if last action succeeded (for retry logic)
|
|
124
|
+
# Actions can fail due to insufficient energy - agents auto-regen so just retry
|
|
125
|
+
if s._pending_action_type == "mine":
|
|
126
|
+
if s.check_action_success():
|
|
127
|
+
if DEBUG:
|
|
128
|
+
print(f"[A{s.agent_id}] MINER: Previous mine succeeded!")
|
|
129
|
+
elif s.should_retry_action(MAX_RETRIES):
|
|
130
|
+
retry_count = s.increment_retry()
|
|
131
|
+
if DEBUG:
|
|
132
|
+
print(
|
|
133
|
+
f"[A{s.agent_id}] MINER: Mine failed, retrying ({retry_count}/{MAX_RETRIES}) "
|
|
134
|
+
f"at {s._pending_action_target}"
|
|
135
|
+
)
|
|
136
|
+
# Retry the same action - agent will have auto-regenerated some energy
|
|
137
|
+
if s._pending_action_target and is_adjacent((s.row, s.col), s._pending_action_target):
|
|
138
|
+
return self._use_object_at(s, s._pending_action_target)
|
|
139
|
+
else:
|
|
140
|
+
if DEBUG:
|
|
141
|
+
print(f"[A{s.agent_id}] MINER: Mine failed after {MAX_RETRIES} retries, moving on")
|
|
142
|
+
s.clear_pending_action()
|
|
143
|
+
|
|
144
|
+
# === Gear re-acquisition logic ===
|
|
145
|
+
if not has_gear:
|
|
146
|
+
return self._handle_no_gear(s, total_cargo, cargo_capacity)
|
|
147
|
+
|
|
148
|
+
# === Normal mining loop (has gear) ===
|
|
149
|
+
# Simple loop: gather until full, deposit, repeat
|
|
150
|
+
if total_cargo >= cargo_capacity - 2:
|
|
151
|
+
return self._do_deposit(s)
|
|
152
|
+
|
|
153
|
+
# Otherwise gather resources
|
|
154
|
+
return self._do_gather(s)
|
|
155
|
+
|
|
156
|
+
def _get_steps_to_healing(self, s: CogsguardAgentState) -> int:
|
|
157
|
+
"""Calculate steps needed to reach healing territory (nearest aligned building AOE).
|
|
158
|
+
|
|
159
|
+
Returns the number of steps to get within any aligned building's healing AOE.
|
|
160
|
+
If no aligned buildings known, returns a large number to be conservative.
|
|
161
|
+
"""
|
|
162
|
+
depot_pos = self._get_nearest_aligned_depot(s)
|
|
163
|
+
if depot_pos is None:
|
|
164
|
+
return 100 # Unknown - be conservative
|
|
165
|
+
|
|
166
|
+
dist = abs(depot_pos[0] - s.row) + abs(depot_pos[1] - s.col)
|
|
167
|
+
return max(0, dist - HEALING_AOE_RANGE)
|
|
168
|
+
|
|
169
|
+
def _should_return_for_healing(self, s: CogsguardAgentState) -> bool:
|
|
170
|
+
"""Check if miner should return to healing territory based on HP.
|
|
171
|
+
|
|
172
|
+
Returns True if HP is low enough that we need to head back now to survive.
|
|
173
|
+
Accounts for faster HP drain when near enemy buildings.
|
|
174
|
+
"""
|
|
175
|
+
steps_to_healing = self._get_steps_to_healing(s)
|
|
176
|
+
current_drain = self._get_hp_drain_rate(s)
|
|
177
|
+
|
|
178
|
+
# Use current drain rate (which may be elevated if near enemies)
|
|
179
|
+
# to calculate HP needed to survive the trip
|
|
180
|
+
hp_needed = (steps_to_healing * current_drain) + HP_SAFETY_MARGIN
|
|
181
|
+
|
|
182
|
+
return s.hp <= hp_needed
|
|
183
|
+
|
|
184
|
+
def _get_hp_drain_rate(self, s: CogsguardAgentState) -> int:
|
|
185
|
+
"""Calculate current HP drain rate based on proximity to enemy buildings.
|
|
186
|
+
|
|
187
|
+
Base drain is HP_DRAIN_BASE per step outside healing AOE.
|
|
188
|
+
Additional HP_DRAIN_ENEMY_AOE per step when near enemy junctions.
|
|
189
|
+
"""
|
|
190
|
+
drain_rate = HP_DRAIN_BASE
|
|
191
|
+
|
|
192
|
+
# Check if near any enemy junctions
|
|
193
|
+
junctions = s.get_structures_by_type(StructureType.CHARGER)
|
|
194
|
+
for junction in junctions:
|
|
195
|
+
if junction.alignment == "cogs":
|
|
196
|
+
continue # Friendly junction
|
|
197
|
+
dist = abs(junction.position[0] - s.row) + abs(junction.position[1] - s.col)
|
|
198
|
+
if dist <= ENEMY_AOE_RANGE:
|
|
199
|
+
drain_rate += HP_DRAIN_ENEMY_AOE
|
|
200
|
+
if DEBUG:
|
|
201
|
+
print(f"[A{s.agent_id}] MINER: Near enemy junction at {junction.position}, drain_rate={drain_rate}")
|
|
202
|
+
break # Only count once even if near multiple enemies
|
|
203
|
+
|
|
204
|
+
return drain_rate
|
|
205
|
+
|
|
206
|
+
def _return_to_healing(self, s: CogsguardAgentState) -> Action:
|
|
207
|
+
"""Return to nearest aligned building to heal. Deposit any cargo while there."""
|
|
208
|
+
depot_pos = self._get_nearest_aligned_depot(s)
|
|
209
|
+
|
|
210
|
+
if depot_pos is None:
|
|
211
|
+
# Don't know where any aligned building is - explore to find one
|
|
212
|
+
if DEBUG:
|
|
213
|
+
print(f"[A{s.agent_id}] MINER_HEAL: No aligned building known, exploring!")
|
|
214
|
+
return self._explore(s)
|
|
215
|
+
|
|
216
|
+
# Check if we're already in healing range
|
|
217
|
+
dist_to_depot = abs(depot_pos[0] - s.row) + abs(depot_pos[1] - s.col)
|
|
218
|
+
if dist_to_depot <= HEALING_AOE_RANGE:
|
|
219
|
+
# We're in healing range - if we have cargo, deposit it
|
|
220
|
+
if s.total_cargo > 0 and is_adjacent((s.row, s.col), depot_pos):
|
|
221
|
+
if DEBUG:
|
|
222
|
+
print(f"[A{s.agent_id}] MINER_HEAL: In range, depositing cargo={s.total_cargo}")
|
|
223
|
+
return self._use_object_at(s, depot_pos)
|
|
224
|
+
# Otherwise just wait here to heal (or move closer to deposit)
|
|
225
|
+
if s.total_cargo > 0:
|
|
226
|
+
return self._move_towards(s, depot_pos, reach_adjacent=True)
|
|
227
|
+
# No cargo, just noop to heal
|
|
228
|
+
if DEBUG and s.step_count % 10 == 0:
|
|
229
|
+
print(f"[A{s.agent_id}] MINER_HEAL: Healing at HP={s.hp}")
|
|
230
|
+
return self._noop()
|
|
231
|
+
|
|
232
|
+
# Move towards nearest aligned building
|
|
233
|
+
if DEBUG and s.step_count % 10 == 0:
|
|
234
|
+
print(f"[A{s.agent_id}] MINER_HEAL: Moving to aligned building at {depot_pos}, dist={dist_to_depot}")
|
|
235
|
+
return self._move_towards(s, depot_pos, reach_adjacent=True)
|
|
236
|
+
|
|
237
|
+
def _handle_no_gear(self, s: CogsguardAgentState, total_cargo: int, cargo_capacity: int) -> Action:
|
|
238
|
+
"""Handle behavior when miner doesn't have gear.
|
|
239
|
+
|
|
240
|
+
Strategy: Collect resources even without gear (reduced capacity/extraction rate).
|
|
241
|
+
Check for gear availability on each dropoff at the hub.
|
|
242
|
+
"""
|
|
243
|
+
# If cargo is full (even with small capacity), deposit and check for gear
|
|
244
|
+
if total_cargo >= cargo_capacity - 1:
|
|
245
|
+
return self._do_deposit_and_check_gear(s)
|
|
246
|
+
|
|
247
|
+
# If we just deposited, check for gear before gathering again
|
|
248
|
+
if total_cargo == 0 and s._resources_deposited_since_gear_attempt > 0:
|
|
249
|
+
return self._do_deposit_and_check_gear(s)
|
|
250
|
+
|
|
251
|
+
# Otherwise, continue gathering resources (at reduced rate without gear)
|
|
252
|
+
if DEBUG and s.step_count % 20 == 0:
|
|
253
|
+
print(f"[A{s.agent_id}] MINER_NO_GEAR: Gathering without gear, cargo={total_cargo}/{cargo_capacity}")
|
|
254
|
+
return self._do_gather(s)
|
|
255
|
+
|
|
256
|
+
def _do_deposit_and_check_gear(self, s: CogsguardAgentState) -> Action:
|
|
257
|
+
"""Deposit resources at nearest aligned building, then check gear station.
|
|
258
|
+
|
|
259
|
+
After depositing, immediately try to get gear before continuing to mine.
|
|
260
|
+
"""
|
|
261
|
+
depot_pos = self._get_nearest_aligned_depot(s)
|
|
262
|
+
station_pos = s.get_structure_position(StructureType.MINER_STATION)
|
|
263
|
+
|
|
264
|
+
# If we still have cargo, deposit first at nearest aligned building
|
|
265
|
+
if s.total_cargo > 0:
|
|
266
|
+
if depot_pos is None:
|
|
267
|
+
if DEBUG:
|
|
268
|
+
print(f"[A{s.agent_id}] MINER_NO_GEAR: No aligned building known, exploring")
|
|
269
|
+
return self._explore(s)
|
|
270
|
+
|
|
271
|
+
if not is_adjacent((s.row, s.col), depot_pos):
|
|
272
|
+
if DEBUG and s.step_count % 20 == 0:
|
|
273
|
+
print(f"[A{s.agent_id}] MINER_NO_GEAR: Moving to deposit at {depot_pos}")
|
|
274
|
+
return self._move_towards(s, depot_pos, reach_adjacent=True)
|
|
275
|
+
|
|
276
|
+
# At depot - deposit
|
|
277
|
+
cargo_to_deposit = s.total_cargo
|
|
278
|
+
s._resources_deposited_since_gear_attempt += cargo_to_deposit
|
|
279
|
+
if DEBUG:
|
|
280
|
+
print(
|
|
281
|
+
f"[A{s.agent_id}] MINER_NO_GEAR: Depositing cargo={cargo_to_deposit} at {depot_pos}, "
|
|
282
|
+
f"total_deposited={s._resources_deposited_since_gear_attempt}"
|
|
283
|
+
)
|
|
284
|
+
return self._use_object_at(s, depot_pos)
|
|
285
|
+
|
|
286
|
+
# Cargo deposited, now try to get gear
|
|
287
|
+
if station_pos is None:
|
|
288
|
+
if DEBUG:
|
|
289
|
+
print(f"[A{s.agent_id}] MINER_NO_GEAR: Station unknown, exploring")
|
|
290
|
+
return self._explore(s)
|
|
291
|
+
|
|
292
|
+
if not is_adjacent((s.row, s.col), station_pos):
|
|
293
|
+
if DEBUG and s.step_count % 20 == 0:
|
|
294
|
+
print(f"[A{s.agent_id}] MINER_NO_GEAR: Checking gear station at {station_pos}")
|
|
295
|
+
return self._move_towards(s, station_pos, reach_adjacent=True)
|
|
296
|
+
|
|
297
|
+
# At station - try to get gear (will fail if commons lacks resources, that's ok)
|
|
298
|
+
if DEBUG:
|
|
299
|
+
print(f"[A{s.agent_id}] MINER_NO_GEAR: Attempting to get gear")
|
|
300
|
+
s._resources_deposited_since_gear_attempt = 0
|
|
301
|
+
return self._use_object_at(s, station_pos)
|
|
302
|
+
|
|
303
|
+
def _do_gather(self, s: CogsguardAgentState) -> Action:
|
|
304
|
+
"""Gather resources from nearest extractor.
|
|
305
|
+
|
|
306
|
+
Tracks mining attempts for retry logic.
|
|
307
|
+
Note: moves require energy. If move fails due to low energy,
|
|
308
|
+
action failure detection will catch it and we'll retry next step
|
|
309
|
+
(agents auto-regen energy every step, and regen full near aligned buildings)
|
|
310
|
+
"""
|
|
311
|
+
# Use structures map for most up-to-date extractor info
|
|
312
|
+
# Prefer extractors that are safe (not near clips junctions)
|
|
313
|
+
extractor = self._get_safe_extractor(s, preferred_resource=self._preferred_resource)
|
|
314
|
+
|
|
315
|
+
if extractor is None:
|
|
316
|
+
# No usable extractors known - explore to find more
|
|
317
|
+
all_extractors = s.get_structures_by_type(StructureType.EXTRACTOR)
|
|
318
|
+
usable_extractors = s.get_usable_extractors()
|
|
319
|
+
total_extractors = len(all_extractors)
|
|
320
|
+
total_usable = len(usable_extractors)
|
|
321
|
+
if total_extractors > 0 and DEBUG:
|
|
322
|
+
# Log why extractors are not usable (empty, clipped, etc.)
|
|
323
|
+
empty_count = sum(1 for e in all_extractors if e.inventory_amount <= 0)
|
|
324
|
+
clipped_count = sum(1 for e in all_extractors if e.clipped)
|
|
325
|
+
print(
|
|
326
|
+
f"[A{s.agent_id}] GATHER: {total_extractors} extractors known, "
|
|
327
|
+
f"{total_usable} usable (empty={empty_count}, clipped={clipped_count}), "
|
|
328
|
+
f"exploring for more"
|
|
329
|
+
)
|
|
330
|
+
return self._explore_for_extractors(s)
|
|
331
|
+
|
|
332
|
+
# Navigate to extractor
|
|
333
|
+
ext_pos = extractor.position
|
|
334
|
+
agent_pos = (s.row, s.col)
|
|
335
|
+
adjacent = is_adjacent(agent_pos, ext_pos)
|
|
336
|
+
if DEBUG and s.step_count <= 60:
|
|
337
|
+
print(f"[A{s.agent_id}] GATHER: agent@{agent_pos} ext@{ext_pos} adjacent={adjacent}")
|
|
338
|
+
if not adjacent:
|
|
339
|
+
if DEBUG and s.step_count <= 50:
|
|
340
|
+
print(f"[A{s.agent_id}] GATHER: Moving to extractor at {extractor.position}")
|
|
341
|
+
return self._move_towards(s, extractor.position, reach_adjacent=True)
|
|
342
|
+
|
|
343
|
+
# At extractor - get CURRENT state from structures map (updated from observation)
|
|
344
|
+
current_extractor = s.get_structure_at(extractor.position)
|
|
345
|
+
|
|
346
|
+
# Check if extractor is still usable (might have been depleted since we started moving)
|
|
347
|
+
if current_extractor is None or not current_extractor.is_usable_extractor():
|
|
348
|
+
# Log why extractor is not usable
|
|
349
|
+
if DEBUG and current_extractor:
|
|
350
|
+
reason = []
|
|
351
|
+
if current_extractor.inventory_amount <= 0:
|
|
352
|
+
reason.append(f"empty(inv={current_extractor.inventory_amount})")
|
|
353
|
+
if current_extractor.clipped:
|
|
354
|
+
reason.append("clipped")
|
|
355
|
+
if current_extractor.remaining_uses <= 0:
|
|
356
|
+
reason.append(f"depleted(uses={current_extractor.remaining_uses})")
|
|
357
|
+
print(
|
|
358
|
+
f"[A{s.agent_id}] GATHER: Extractor at {extractor.position} not usable: "
|
|
359
|
+
f"{', '.join(reason) if reason else 'unknown'}. Switching."
|
|
360
|
+
)
|
|
361
|
+
# Find another extractor
|
|
362
|
+
other = s.get_nearest_usable_extractor(exclude=extractor.position)
|
|
363
|
+
if other is not None:
|
|
364
|
+
if DEBUG:
|
|
365
|
+
print(f"[A{s.agent_id}] GATHER: Switching to extractor at {other.position}")
|
|
366
|
+
return self._move_towards(s, other.position, reach_adjacent=True)
|
|
367
|
+
# No usable extractors - explore to find more
|
|
368
|
+
if DEBUG:
|
|
369
|
+
total_ext = len(s.get_structures_by_type(StructureType.EXTRACTOR))
|
|
370
|
+
usable_ext = len(s.get_usable_extractors())
|
|
371
|
+
print(f"[A{s.agent_id}] GATHER: No usable extractors! total={total_ext}, usable={usable_ext}")
|
|
372
|
+
return self._explore_for_extractors(s)
|
|
373
|
+
|
|
374
|
+
# Check if another agent is blocking the extractor
|
|
375
|
+
if extractor.position in s.agent_occupancy:
|
|
376
|
+
if DEBUG:
|
|
377
|
+
print(f"[A{s.agent_id}] GATHER: Extractor at {extractor.position} blocked by another agent")
|
|
378
|
+
# Try another extractor
|
|
379
|
+
other = s.get_nearest_usable_extractor(exclude=extractor.position)
|
|
380
|
+
if other is not None:
|
|
381
|
+
if DEBUG:
|
|
382
|
+
print(f"[A{s.agent_id}] GATHER: Switching to extractor at {other.position}")
|
|
383
|
+
return self._move_towards(s, other.position, reach_adjacent=True)
|
|
384
|
+
# Wait a bit - other agent should move soon
|
|
385
|
+
return self._noop()
|
|
386
|
+
|
|
387
|
+
# At extractor - check cooldown
|
|
388
|
+
if current_extractor.cooldown_remaining > 0:
|
|
389
|
+
if DEBUG and s.step_count <= 50:
|
|
390
|
+
print(f"[A{s.agent_id}] GATHER: Extractor on cooldown={current_extractor.cooldown_remaining}")
|
|
391
|
+
# Try another extractor while this one cools down
|
|
392
|
+
other = s.get_nearest_usable_extractor(exclude=extractor.position)
|
|
393
|
+
if other is not None and other.cooldown_remaining == 0:
|
|
394
|
+
return self._move_towards(s, other.position, reach_adjacent=True)
|
|
395
|
+
# Wait for cooldown - noop
|
|
396
|
+
return self._noop()
|
|
397
|
+
|
|
398
|
+
# Start tracking this mine attempt
|
|
399
|
+
s.start_action_attempt("mine", current_extractor.position)
|
|
400
|
+
|
|
401
|
+
# Extract!
|
|
402
|
+
if DEBUG and s.step_count <= 60:
|
|
403
|
+
print(f"[A{s.agent_id}] GATHER: MINING at {current_extractor.position} (energy={s.energy})!")
|
|
404
|
+
return self._use_object_at(s, current_extractor.position)
|
|
405
|
+
|
|
406
|
+
def _explore_for_extractors(self, s: CogsguardAgentState) -> Action:
|
|
407
|
+
"""Explore towards map corners where extractors are located."""
|
|
408
|
+
# Track exploration progress - rotate through corners quickly
|
|
409
|
+
# Each miner starts at a different corner, rotates every 50 steps
|
|
410
|
+
miner_idx = s.agent_id % 4 # Spread across all 4 corners
|
|
411
|
+
# Rotate corner based on step count - faster rotation (every 50 steps) to find all resources
|
|
412
|
+
corner_rotation = s.step_count // 50
|
|
413
|
+
corner_idx = (miner_idx + corner_rotation) % len(CORNER_OFFSETS)
|
|
414
|
+
dr, dc = CORNER_OFFSETS[corner_idx]
|
|
415
|
+
|
|
416
|
+
# Target a point away from center in the corner direction
|
|
417
|
+
target_r = max(10, min(s.map_height - 10, s.row + dr))
|
|
418
|
+
target_c = max(10, min(s.map_width - 10, s.col + dc))
|
|
419
|
+
|
|
420
|
+
# If we've reached our corner area, switch to regular exploration
|
|
421
|
+
at_corner = ((dr < 0 and s.row < 95) or (dr > 0 and s.row > 105)) and (
|
|
422
|
+
(dc < 0 and s.col < 95) or (dc > 0 and s.col > 105)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if at_corner:
|
|
426
|
+
# In corner area, explore locally to find extractors
|
|
427
|
+
return self._explore(s)
|
|
428
|
+
|
|
429
|
+
# Move towards corner
|
|
430
|
+
if DEBUG and s.step_count <= 20:
|
|
431
|
+
print(f"[A{s.agent_id}] MINER: exploring towards corner {corner_idx}, target=({target_r},{target_c})")
|
|
432
|
+
return self._move_towards(s, (target_r, target_c), reach_adjacent=False)
|
|
433
|
+
|
|
434
|
+
def _get_safe_extractor(
|
|
435
|
+
self,
|
|
436
|
+
s: CogsguardAgentState,
|
|
437
|
+
preferred_resource: str | None = None,
|
|
438
|
+
) -> "StructureInfo | None":
|
|
439
|
+
"""Get extractor prioritized by distance to aligned stations.
|
|
440
|
+
|
|
441
|
+
Prioritizes extractors nearest to aligned buildings (hub/cogs junctions)
|
|
442
|
+
for shorter, safer mining routes.
|
|
443
|
+
|
|
444
|
+
Considers:
|
|
445
|
+
1. Distance from extractor to nearest aligned station (primary sort)
|
|
446
|
+
2. Distance from clips junctions (enemy AOE damage)
|
|
447
|
+
3. HP-based range limit - only select extractors we can reach and return from safely
|
|
448
|
+
"""
|
|
449
|
+
# Avoid extractors within enemy AOE so "safe" picks never drain faster than expected.
|
|
450
|
+
danger_range = ENEMY_AOE_RANGE
|
|
451
|
+
|
|
452
|
+
# Get all clips junctions
|
|
453
|
+
junctions = s.get_structures_by_type(StructureType.CHARGER)
|
|
454
|
+
danger_zones = [c.position for c in junctions if c.alignment != "cogs"]
|
|
455
|
+
|
|
456
|
+
# Calculate max safe operating distance based on HP
|
|
457
|
+
# We need HP to get to extractor AND back to healing zone
|
|
458
|
+
max_safe_dist = self._get_max_safe_distance(s)
|
|
459
|
+
|
|
460
|
+
# Get usable extractors
|
|
461
|
+
extractors = s.get_usable_extractors()
|
|
462
|
+
if preferred_resource:
|
|
463
|
+
preferred = [ext for ext in extractors if ext.resource_type == preferred_resource]
|
|
464
|
+
if preferred:
|
|
465
|
+
extractors = preferred
|
|
466
|
+
|
|
467
|
+
# Sort by distance to aligned station and prefer safe ones within HP range
|
|
468
|
+
safe_extractors: list[tuple[int, int, StructureInfo]] = [] # (dist_to_depot, dist_to_ext, ext)
|
|
469
|
+
risky_extractors: list[tuple[int, int, StructureInfo]] = []
|
|
470
|
+
fallback_preferred: list[tuple[int, int, StructureInfo]] = []
|
|
471
|
+
|
|
472
|
+
# Get nearest aligned depot for round-trip calculations
|
|
473
|
+
nearest_depot = self._get_nearest_aligned_depot(s)
|
|
474
|
+
steps_currently_to_healing = self._get_steps_to_healing(s)
|
|
475
|
+
|
|
476
|
+
for ext in extractors:
|
|
477
|
+
dist_to_ext = abs(ext.position[0] - s.row) + abs(ext.position[1] - s.col)
|
|
478
|
+
|
|
479
|
+
# Calculate distance from extractor to nearest aligned building
|
|
480
|
+
if nearest_depot:
|
|
481
|
+
dist_ext_to_depot = abs(ext.position[0] - nearest_depot[0]) + abs(ext.position[1] - nearest_depot[1])
|
|
482
|
+
steps_from_ext_to_healing = max(0, dist_ext_to_depot - HEALING_AOE_RANGE)
|
|
483
|
+
if steps_currently_to_healing == 0:
|
|
484
|
+
# Already in healing AOE; only count steps outside AOE both ways.
|
|
485
|
+
round_trip = 2 * steps_from_ext_to_healing
|
|
486
|
+
else:
|
|
487
|
+
round_trip = dist_to_ext + steps_from_ext_to_healing
|
|
488
|
+
else:
|
|
489
|
+
# Unknown depot - use conservative estimate
|
|
490
|
+
dist_ext_to_depot = 100 # Large number to deprioritize
|
|
491
|
+
round_trip = dist_to_ext * 2
|
|
492
|
+
|
|
493
|
+
# Skip extractors that are too far for our current HP
|
|
494
|
+
if round_trip > max_safe_dist:
|
|
495
|
+
if preferred_resource and ext.resource_type == preferred_resource:
|
|
496
|
+
fallback_preferred.append((dist_ext_to_depot, dist_to_ext, ext))
|
|
497
|
+
if DEBUG:
|
|
498
|
+
print(
|
|
499
|
+
f"[A{s.agent_id}] MINER: Skipping extractor at {ext.position}, "
|
|
500
|
+
f"round_trip={round_trip} > max_safe={max_safe_dist}"
|
|
501
|
+
)
|
|
502
|
+
continue
|
|
503
|
+
|
|
504
|
+
# Check if extractor is in danger zone (enemy AOE)
|
|
505
|
+
is_safe = True
|
|
506
|
+
for danger_pos in danger_zones:
|
|
507
|
+
danger_dist = abs(ext.position[0] - danger_pos[0]) + abs(ext.position[1] - danger_pos[1])
|
|
508
|
+
if danger_dist < danger_range:
|
|
509
|
+
is_safe = False
|
|
510
|
+
break
|
|
511
|
+
|
|
512
|
+
# Store with dist_to_depot as primary sort key, dist_to_ext as tiebreaker
|
|
513
|
+
if is_safe:
|
|
514
|
+
safe_extractors.append((dist_ext_to_depot, dist_to_ext, ext))
|
|
515
|
+
else:
|
|
516
|
+
risky_extractors.append((dist_ext_to_depot, dist_to_ext, ext))
|
|
517
|
+
|
|
518
|
+
# Prefer safe extractors, sorted by distance to aligned station
|
|
519
|
+
if safe_extractors:
|
|
520
|
+
safe_extractors.sort(key=lambda x: (x[0], x[1])) # Primary: depot dist, Secondary: agent dist
|
|
521
|
+
return safe_extractors[0][2]
|
|
522
|
+
|
|
523
|
+
# Fall back to risky extractors if no safe ones
|
|
524
|
+
if risky_extractors:
|
|
525
|
+
risky_extractors.sort(key=lambda x: (x[0], x[1]))
|
|
526
|
+
return risky_extractors[0][2]
|
|
527
|
+
|
|
528
|
+
if fallback_preferred:
|
|
529
|
+
fallback_preferred.sort(key=lambda x: (x[0], x[1]))
|
|
530
|
+
return fallback_preferred[0][2]
|
|
531
|
+
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
def _get_max_safe_distance(self, s: CogsguardAgentState) -> int:
|
|
535
|
+
"""Calculate max round-trip distance based on current HP.
|
|
536
|
+
|
|
537
|
+
Returns the maximum total distance (to target + back to healing) that's safe.
|
|
538
|
+
Uses current drain rate which may be elevated if near enemy buildings.
|
|
539
|
+
"""
|
|
540
|
+
# Reserve HP_SAFETY_MARGIN for emergencies
|
|
541
|
+
available_hp = max(0, s.hp - HP_SAFETY_MARGIN)
|
|
542
|
+
|
|
543
|
+
# Use current drain rate (may be faster near enemies)
|
|
544
|
+
drain_rate = self._get_hp_drain_rate(s)
|
|
545
|
+
|
|
546
|
+
# Max steps we can take before HP runs out
|
|
547
|
+
max_steps = available_hp // drain_rate if drain_rate > 0 else available_hp
|
|
548
|
+
|
|
549
|
+
return max_steps
|
|
550
|
+
|
|
551
|
+
def _do_deposit(self, s: CogsguardAgentState) -> Action:
|
|
552
|
+
"""Deposit resources at the nearest aligned building.
|
|
553
|
+
|
|
554
|
+
Energy-aware: checks if we have enough energy to reach the depot.
|
|
555
|
+
"""
|
|
556
|
+
depot_pos = self._get_nearest_aligned_depot(s)
|
|
557
|
+
|
|
558
|
+
if depot_pos is None:
|
|
559
|
+
if DEBUG:
|
|
560
|
+
print(f"[A{s.agent_id}] DEPOSIT: No aligned building found, exploring")
|
|
561
|
+
return self._explore(s)
|
|
562
|
+
|
|
563
|
+
# Check if we have enough energy to reach the depot
|
|
564
|
+
dist = abs(depot_pos[0] - s.row) + abs(depot_pos[1] - s.col)
|
|
565
|
+
if not s.has_enough_energy_for_moves(dist + 2): # +2 for safety margin
|
|
566
|
+
if DEBUG:
|
|
567
|
+
print(
|
|
568
|
+
f"[A{s.agent_id}] DEPOSIT: Not enough energy ({s.energy}) to reach "
|
|
569
|
+
f"depot at dist={dist}, but going anyway (AOE will recharge)"
|
|
570
|
+
)
|
|
571
|
+
# Note: we need to recharge BUT we're carrying cargo, so go to depot anyway
|
|
572
|
+
# since aligned buildings provide energy AOE - recharging and depositing are the same trip!
|
|
573
|
+
pass # Continue to move to depot - we'll recharge there
|
|
574
|
+
|
|
575
|
+
if not is_adjacent((s.row, s.col), depot_pos):
|
|
576
|
+
if DEBUG and s.step_count % 20 == 0:
|
|
577
|
+
print(f"[A{s.agent_id}] DEPOSIT: Moving to depot at {depot_pos}")
|
|
578
|
+
return self._move_towards(s, depot_pos, reach_adjacent=True)
|
|
579
|
+
|
|
580
|
+
# Track resources deposited (for gear re-acquisition logic)
|
|
581
|
+
cargo_to_deposit = s.total_cargo
|
|
582
|
+
s._resources_deposited_since_gear_attempt += cargo_to_deposit
|
|
583
|
+
if DEBUG:
|
|
584
|
+
print(
|
|
585
|
+
f"[A{s.agent_id}] DEPOSIT: At depot {depot_pos}, cargo={cargo_to_deposit}, "
|
|
586
|
+
f"total_deposited={s._resources_deposited_since_gear_attempt}"
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
return self._use_object_at(s, depot_pos)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from mettagrid.simulator import Action
|
|
7
|
+
|
|
8
|
+
from .types import CogsguardAgentState
|
|
9
|
+
|
|
10
|
+
OptionPredicate = Callable[[CogsguardAgentState], bool]
|
|
11
|
+
OptionAction = Callable[[CogsguardAgentState], Action]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class OptionDef:
|
|
16
|
+
name: str
|
|
17
|
+
can_start: OptionPredicate
|
|
18
|
+
act: OptionAction
|
|
19
|
+
should_terminate: OptionPredicate
|
|
20
|
+
interruptible: bool = True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def options_always_can_start(_: CogsguardAgentState) -> bool:
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def options_always_terminate(_: CogsguardAgentState) -> bool:
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_options(s: CogsguardAgentState, options: list[OptionDef]) -> Action:
|
|
32
|
+
if not options:
|
|
33
|
+
return Action(name="noop")
|
|
34
|
+
|
|
35
|
+
def reset_active() -> None:
|
|
36
|
+
s.active_option_id = -1
|
|
37
|
+
s.active_option_ticks = 0
|
|
38
|
+
|
|
39
|
+
option_count = len(options)
|
|
40
|
+
if 0 <= s.active_option_id < option_count:
|
|
41
|
+
active_idx = s.active_option_id
|
|
42
|
+
active = options[active_idx]
|
|
43
|
+
if active.interruptible:
|
|
44
|
+
for idx in range(active_idx):
|
|
45
|
+
if options[idx].can_start(s):
|
|
46
|
+
s.active_option_id = idx
|
|
47
|
+
s.active_option_ticks = 0
|
|
48
|
+
active_idx = idx
|
|
49
|
+
active = options[idx]
|
|
50
|
+
break
|
|
51
|
+
s.active_option_ticks += 1
|
|
52
|
+
action = active.act(s)
|
|
53
|
+
if active.should_terminate(s):
|
|
54
|
+
reset_active()
|
|
55
|
+
return action
|
|
56
|
+
|
|
57
|
+
for idx, opt in enumerate(options):
|
|
58
|
+
if not opt.can_start(s):
|
|
59
|
+
continue
|
|
60
|
+
s.active_option_id = idx
|
|
61
|
+
s.active_option_ticks = 1
|
|
62
|
+
action = opt.act(s)
|
|
63
|
+
if opt.should_terminate(s):
|
|
64
|
+
reset_active()
|
|
65
|
+
return action
|
|
66
|
+
|
|
67
|
+
return Action(name="noop")
|