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,461 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UnclippingAgent - Extends SimpleBaselineAgent with unclipping capabilities.
|
|
3
|
+
|
|
4
|
+
This agent can detect clipped extractors and craft unclip items to restore them.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import TYPE_CHECKING, Optional, cast
|
|
11
|
+
|
|
12
|
+
from mettagrid.policy.policy import MultiAgentPolicy, StatefulAgentPolicy
|
|
13
|
+
from mettagrid.policy.policy_env_interface import PolicyEnvInterface
|
|
14
|
+
from mettagrid.simulator import Action
|
|
15
|
+
from mettagrid.simulator.interface import AgentObservation
|
|
16
|
+
|
|
17
|
+
from .baseline_agent import BaselineAgentPolicyImpl
|
|
18
|
+
from .types import (
|
|
19
|
+
BaselineHyperparameters,
|
|
20
|
+
ExtractorInfo,
|
|
21
|
+
Phase,
|
|
22
|
+
SimpleAgentState,
|
|
23
|
+
)
|
|
24
|
+
from .utils import is_adjacent, use_object_at
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class UnclippingHyperparameters(BaselineHyperparameters):
|
|
32
|
+
"""Extends baseline hyperparameters with unclipping-specific parameters."""
|
|
33
|
+
|
|
34
|
+
# Unclipping strategy
|
|
35
|
+
unclip_priority_order: tuple[str, ...] = ("oxygen", "silicon", "carbon", "germanium") # Order to unclip resources
|
|
36
|
+
craft_unclip_items_early: bool = True # Craft unclip items proactively vs on-demand
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class UnclippingAgentState(SimpleAgentState):
|
|
41
|
+
"""Extended state for unclipping agent.
|
|
42
|
+
|
|
43
|
+
Note: decoder, modulator, resonator, scrambler are already defined in SimpleAgentState.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# Unclip tracking
|
|
47
|
+
blocked_by_clipped_extractor: Optional[tuple[int, int]] = None
|
|
48
|
+
unclip_target_resource: Optional[str] = None # Which resource is clipped
|
|
49
|
+
|
|
50
|
+
# Discovered unclip recipes from observations (resource_type -> (unclip_item, craft_recipe))
|
|
51
|
+
# e.g., "oxygen" -> ("decoder", {"carbon": 1})
|
|
52
|
+
unclip_recipes: dict[str, tuple[str, dict[str, int]]] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
# Craft recipes for unclip items (item_name -> craft_recipe)
|
|
55
|
+
# e.g., "decoder" -> {"carbon": 1}
|
|
56
|
+
# Initialized from hub protocols
|
|
57
|
+
unclip_craft_recipes: dict[str, dict[str, int]] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UnclippingAgentPolicyImpl(BaselineAgentPolicyImpl):
|
|
61
|
+
"""
|
|
62
|
+
Agent that can unclip extractors by crafting and using unclip items.
|
|
63
|
+
|
|
64
|
+
Unclip item mapping:
|
|
65
|
+
- decoder (from carbon) unclips oxygen extractors
|
|
66
|
+
- modulator (from oxygen) unclips carbon extractors
|
|
67
|
+
- resonator (from silicon) unclips germanium extractors
|
|
68
|
+
- scrambler (from germanium) unclips silicon extractors
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
policy_env_info: PolicyEnvInterface,
|
|
74
|
+
agent_id: int,
|
|
75
|
+
hyperparams: UnclippingHyperparameters,
|
|
76
|
+
):
|
|
77
|
+
super().__init__(policy_env_info, agent_id, hyperparams)
|
|
78
|
+
self._priority_order = hyperparams.unclip_priority_order
|
|
79
|
+
|
|
80
|
+
def initial_agent_state(self) -> UnclippingAgentState:
|
|
81
|
+
"""Create initial state for unclipping agent."""
|
|
82
|
+
# Get the base state from parent class
|
|
83
|
+
base_state = super().initial_agent_state()
|
|
84
|
+
|
|
85
|
+
# Initialize unclip recipes from hub protocols
|
|
86
|
+
# Map: unclip_item -> craft_recipe (e.g., "decoder" -> {"carbon": 1})
|
|
87
|
+
unclip_craft_recipes = {}
|
|
88
|
+
for protocol in getattr(self._policy_env_info, "hub_protocols", []):
|
|
89
|
+
for output_item, output_amount in protocol.output_resources.items():
|
|
90
|
+
if output_amount > 0 and output_item in ("decoder", "modulator", "resonator", "scrambler"):
|
|
91
|
+
craft_recipe = dict(protocol.input_resources)
|
|
92
|
+
craft_recipe.pop("energy", None)
|
|
93
|
+
unclip_craft_recipes[output_item] = craft_recipe
|
|
94
|
+
|
|
95
|
+
# Create unclipping state by extending base state
|
|
96
|
+
# Convert base_state dict to UnclippingAgentState with additional fields
|
|
97
|
+
return UnclippingAgentState(
|
|
98
|
+
**base_state.__dict__,
|
|
99
|
+
unclip_recipes={}, # Will be discovered from clipped extractor observations
|
|
100
|
+
unclip_craft_recipes=unclip_craft_recipes, # Store craft recipes separately
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _discover_unclip_recipe(self, s: UnclippingAgentState, parsed_observation) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Discover unclip recipes from clipped extractor observations.
|
|
106
|
+
|
|
107
|
+
When we observe a clipped extractor, its protocol_outputs tell us what unclip item is needed.
|
|
108
|
+
We then need to observe the hub with the correct vibe to learn how to craft that item.
|
|
109
|
+
"""
|
|
110
|
+
for _pos, obj_state in parsed_observation.nearby_objects.items():
|
|
111
|
+
obj_name = obj_state.name.lower()
|
|
112
|
+
# Check if this is a clipped extractor
|
|
113
|
+
if "extractor" in obj_name and obj_state.clipped > 0:
|
|
114
|
+
# Extract resource type from name (e.g., "oxygen_extractor" -> "oxygen")
|
|
115
|
+
resource_type = obj_name.replace("_extractor", "")
|
|
116
|
+
# Check if we already know this recipe
|
|
117
|
+
if resource_type in s.unclip_recipes:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# Read the unclip item from protocol_inputs (unclipping protocols have inputs, not outputs!)
|
|
121
|
+
if obj_state.protocol_inputs:
|
|
122
|
+
# The clipped extractor's protocol_inputs shows what unclip item is needed
|
|
123
|
+
for item_name, amount in obj_state.protocol_inputs.items():
|
|
124
|
+
if amount > 0 and item_name in ("decoder", "modulator", "resonator", "scrambler"):
|
|
125
|
+
# Get the craft recipe from our initialized recipes
|
|
126
|
+
craft_recipe = s.unclip_craft_recipes.get(item_name, {})
|
|
127
|
+
s.unclip_recipes[resource_type] = (item_name, craft_recipe)
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
# No need to discover craft recipes from hub - we already have them from init!
|
|
131
|
+
|
|
132
|
+
def _get_unclip_item_name(self, s: UnclippingAgentState, clipped_resource: str) -> Optional[str]:
|
|
133
|
+
recipe = s.unclip_recipes.get(clipped_resource)
|
|
134
|
+
if recipe is None:
|
|
135
|
+
return None
|
|
136
|
+
item_name, _ = recipe
|
|
137
|
+
return item_name
|
|
138
|
+
|
|
139
|
+
def _has_unclip_item(self, s: UnclippingAgentState) -> bool:
|
|
140
|
+
"""Check if agent has the unclip item for the blocked extractor."""
|
|
141
|
+
if s.blocked_by_clipped_extractor is None:
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
if s.unclip_target_resource is None:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
item_name = self._get_unclip_item_name(s, s.unclip_target_resource)
|
|
148
|
+
if item_name is None:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
has_item = getattr(s, item_name, 0) > 0
|
|
152
|
+
return has_item
|
|
153
|
+
|
|
154
|
+
def _get_unclip_info(
|
|
155
|
+
self, s: UnclippingAgentState, resource: Optional[str]
|
|
156
|
+
) -> Optional[tuple[str, dict[str, int]]]:
|
|
157
|
+
"""Get unclip item name and craft recipe for a resource type."""
|
|
158
|
+
if resource is None:
|
|
159
|
+
return None
|
|
160
|
+
return s.unclip_recipes.get(resource)
|
|
161
|
+
|
|
162
|
+
def _clear_unclip_state(self, s: UnclippingAgentState) -> None:
|
|
163
|
+
s.blocked_by_clipped_extractor = None
|
|
164
|
+
s.unclip_target_resource = None
|
|
165
|
+
|
|
166
|
+
def _set_unclip_state(self, s: UnclippingAgentState, resource_type: str, extractor: ExtractorInfo) -> None:
|
|
167
|
+
# Only set unclip state if we've discovered the recipe for this resource
|
|
168
|
+
if resource_type not in s.unclip_recipes:
|
|
169
|
+
return
|
|
170
|
+
s.blocked_by_clipped_extractor = extractor.position
|
|
171
|
+
s.unclip_target_resource = resource_type
|
|
172
|
+
|
|
173
|
+
def _get_vibe_for_phase(self, phase: Phase, state: UnclippingAgentState) -> str:
|
|
174
|
+
"""Override to set correct vibe for CRAFT_UNCLIP and UNCLIP phases."""
|
|
175
|
+
# For crafting unclip items at the hub, use "gear" vibe
|
|
176
|
+
if phase == Phase.CRAFT_UNCLIP:
|
|
177
|
+
return "gear"
|
|
178
|
+
|
|
179
|
+
# For unclipping extractors, use "gear" vibe
|
|
180
|
+
if phase == Phase.UNCLIP:
|
|
181
|
+
return "gear"
|
|
182
|
+
|
|
183
|
+
# Otherwise use baseline logic
|
|
184
|
+
return super()._get_vibe_for_phase(phase, state)
|
|
185
|
+
|
|
186
|
+
def _update_phase(self, s: UnclippingAgentState) -> None:
|
|
187
|
+
"""Override to add unclipping phase priorities before gathering."""
|
|
188
|
+
old_phase = s.phase
|
|
189
|
+
|
|
190
|
+
# Priority 1-2: Recharge and Deliver (handled by parent)
|
|
191
|
+
if s.energy < self._hyperparams.recharge_threshold_low or s.phase == Phase.RECHARGE or s.hearts > 0:
|
|
192
|
+
super()._update_phase(s)
|
|
193
|
+
if old_phase != s.phase:
|
|
194
|
+
s.cached_path = None
|
|
195
|
+
s.cached_path_target = None
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# Priority 3: Assemble if possible (and not currently blocked by clipped resource)
|
|
199
|
+
heart_recipe = s.heart_recipe or {}
|
|
200
|
+
can_assemble = all(
|
|
201
|
+
getattr(s, res, 0) >= heart_recipe.get(res, 0) for res in ("carbon", "oxygen", "germanium", "silicon")
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if can_assemble and s.blocked_by_clipped_extractor is None:
|
|
205
|
+
if s.phase != Phase.ASSEMBLE:
|
|
206
|
+
s.phase = Phase.ASSEMBLE
|
|
207
|
+
s.pending_use_resource = None
|
|
208
|
+
s.pending_use_amount = 0
|
|
209
|
+
s.waiting_at_extractor = None
|
|
210
|
+
if old_phase != s.phase:
|
|
211
|
+
s.cached_path = None
|
|
212
|
+
s.cached_path_target = None
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
# Priority 4: Unclipping workflow (before gathering)
|
|
216
|
+
if s.blocked_by_clipped_extractor is not None and s.unclip_target_resource is not None:
|
|
217
|
+
# First, check if the extractor is still clipped
|
|
218
|
+
target_pos = s.blocked_by_clipped_extractor
|
|
219
|
+
extractors = s.extractors.get(s.unclip_target_resource, [])
|
|
220
|
+
target_extractor = None
|
|
221
|
+
for ext in extractors:
|
|
222
|
+
if ext.position == target_pos:
|
|
223
|
+
target_extractor = ext
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
# If extractor is no longer clipped or not found, clear unclip state
|
|
227
|
+
if target_extractor is None or not target_extractor.clipped:
|
|
228
|
+
self._clear_unclip_state(s)
|
|
229
|
+
# Fall through to normal phase logic
|
|
230
|
+
else:
|
|
231
|
+
# Still clipped, continue unclipping workflow
|
|
232
|
+
info = self._get_unclip_info(s, s.unclip_target_resource)
|
|
233
|
+
|
|
234
|
+
if info is not None:
|
|
235
|
+
item_name, craft_recipe = info
|
|
236
|
+
item_count = getattr(s, item_name, 0)
|
|
237
|
+
|
|
238
|
+
if item_count > 0:
|
|
239
|
+
if s.phase != Phase.UNCLIP:
|
|
240
|
+
s.phase = Phase.UNCLIP
|
|
241
|
+
return
|
|
242
|
+
elif craft_recipe and all(getattr(s, res, 0) >= amt for res, amt in craft_recipe.items()):
|
|
243
|
+
if s.phase != Phase.CRAFT_UNCLIP:
|
|
244
|
+
s.phase = Phase.CRAFT_UNCLIP
|
|
245
|
+
return
|
|
246
|
+
else:
|
|
247
|
+
# Need to gather craft resources
|
|
248
|
+
if s.phase != Phase.GATHER:
|
|
249
|
+
s.phase = Phase.GATHER
|
|
250
|
+
if old_phase != s.phase:
|
|
251
|
+
s.cached_path = None
|
|
252
|
+
s.cached_path_target = None
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# Priority 5: Default to GATHER (handled by parent)
|
|
256
|
+
super()._update_phase(s)
|
|
257
|
+
if old_phase != s.phase:
|
|
258
|
+
s.cached_path = None
|
|
259
|
+
s.cached_path_target = None
|
|
260
|
+
|
|
261
|
+
def _find_any_needed_extractor(self, s: UnclippingAgentState) -> Optional[tuple[ExtractorInfo, str]]:
|
|
262
|
+
"""
|
|
263
|
+
Override to detect clipped extractors and trigger unclipping workflow.
|
|
264
|
+
"""
|
|
265
|
+
# Try baseline logic first
|
|
266
|
+
result = super()._find_any_needed_extractor(s)
|
|
267
|
+
if result is not None:
|
|
268
|
+
self._clear_unclip_state(s)
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
deficits = self._calculate_deficits(s)
|
|
272
|
+
|
|
273
|
+
def distance(pos: tuple[int, int]) -> int:
|
|
274
|
+
return abs(pos[0] - s.row) + abs(pos[1] - s.col)
|
|
275
|
+
|
|
276
|
+
# If we are already blocked, ensure we gather craft resources if needed
|
|
277
|
+
if s.blocked_by_clipped_extractor is not None:
|
|
278
|
+
info = self._get_unclip_info(s, s.unclip_target_resource)
|
|
279
|
+
if info is not None:
|
|
280
|
+
item_name, craft_recipe = info
|
|
281
|
+
if getattr(s, item_name, 0) > 0:
|
|
282
|
+
return None # Ready to unclip
|
|
283
|
+
|
|
284
|
+
# Need craft resources before crafting
|
|
285
|
+
if craft_recipe:
|
|
286
|
+
for craft_resource, needed_amount in craft_recipe.items():
|
|
287
|
+
if getattr(s, craft_resource, 0) < needed_amount:
|
|
288
|
+
craft_extractors = s.extractors.get(craft_resource, [])
|
|
289
|
+
available = [e for e in craft_extractors if not e.clipped and e.remaining_uses > 0]
|
|
290
|
+
if available:
|
|
291
|
+
nearest = min(available, key=lambda e: distance(e.position))
|
|
292
|
+
return (nearest, craft_resource)
|
|
293
|
+
|
|
294
|
+
clipped = [e for e in craft_extractors if e.clipped and e.remaining_uses > 0]
|
|
295
|
+
if clipped:
|
|
296
|
+
nearest = min(clipped, key=lambda e: distance(e.position))
|
|
297
|
+
self._set_unclip_state(s, craft_resource, nearest)
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
# Look for deficits that are blocked by clipped extractors
|
|
301
|
+
for resource_type in self._priority_order:
|
|
302
|
+
if deficits.get(resource_type, 0) <= 0:
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
extractors = s.extractors.get(resource_type, [])
|
|
306
|
+
if not extractors:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
available = [e for e in extractors if not e.clipped and e.remaining_uses > 0]
|
|
310
|
+
if available:
|
|
311
|
+
nearest = min(available, key=lambda e: distance(e.position))
|
|
312
|
+
return (nearest, resource_type)
|
|
313
|
+
|
|
314
|
+
clipped = [e for e in extractors if e.clipped and e.remaining_uses > 0]
|
|
315
|
+
if clipped:
|
|
316
|
+
nearest = min(clipped, key=lambda e: distance(e.position))
|
|
317
|
+
self._set_unclip_state(s, resource_type, nearest)
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
# No special action
|
|
321
|
+
self._clear_unclip_state(s)
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
def step_with_state(
|
|
325
|
+
self, obs: AgentObservation, state: UnclippingAgentState
|
|
326
|
+
) -> tuple[Action, UnclippingAgentState]:
|
|
327
|
+
"""Override to discover unclip recipes from observations."""
|
|
328
|
+
# First, let the base class handle observation parsing and state updates
|
|
329
|
+
action, updated_state = super().step_with_state(obs, state)
|
|
330
|
+
state = cast(UnclippingAgentState, updated_state)
|
|
331
|
+
|
|
332
|
+
# Now discover unclip recipes from the parsed observation
|
|
333
|
+
# We need to parse again to get the nearby_objects
|
|
334
|
+
parsed = self.parse_observation(state, obs)
|
|
335
|
+
self._discover_unclip_recipe(state, parsed)
|
|
336
|
+
|
|
337
|
+
return action, state
|
|
338
|
+
|
|
339
|
+
def _execute_phase(self, s: UnclippingAgentState) -> Action:
|
|
340
|
+
"""Override to handle CRAFT_UNCLIP and UNCLIP phases."""
|
|
341
|
+
if s.phase == Phase.CRAFT_UNCLIP:
|
|
342
|
+
return self._do_craft_unclip(s)
|
|
343
|
+
elif s.phase == Phase.UNCLIP:
|
|
344
|
+
return self._do_unclip(s)
|
|
345
|
+
# All other phases handled by parent (GATHER, ASSEMBLE, DELIVER, RECHARGE)
|
|
346
|
+
return super()._execute_phase(s)
|
|
347
|
+
|
|
348
|
+
def _do_craft_unclip(self, s: UnclippingAgentState) -> Action:
|
|
349
|
+
"""Craft unclip item at hub."""
|
|
350
|
+
info = self._get_unclip_info(s, s.unclip_target_resource)
|
|
351
|
+
if info is None:
|
|
352
|
+
self._clear_unclip_state(s)
|
|
353
|
+
s.phase = Phase.GATHER
|
|
354
|
+
return Action(name="noop")
|
|
355
|
+
|
|
356
|
+
item_name, craft_recipe = info
|
|
357
|
+
|
|
358
|
+
# Check if we have all craft resources
|
|
359
|
+
if craft_recipe and not all(getattr(s, res, 0) >= amt for res, amt in craft_recipe.items()):
|
|
360
|
+
# Need to gather craft resources first
|
|
361
|
+
s.phase = Phase.GATHER
|
|
362
|
+
return Action(name="noop")
|
|
363
|
+
|
|
364
|
+
# Explore until we find hub
|
|
365
|
+
explore_action = self._explore_until(
|
|
366
|
+
s, condition=lambda: s.stations.get("hub") is not None, reason="Need hub for crafting"
|
|
367
|
+
)
|
|
368
|
+
if explore_action is not None:
|
|
369
|
+
return explore_action
|
|
370
|
+
|
|
371
|
+
# Vibe is automatically set by _get_vibe_for_phase to the input resource (e.g., "carbon" for decoder)
|
|
372
|
+
|
|
373
|
+
hub = s.stations.get("hub")
|
|
374
|
+
if hub is None:
|
|
375
|
+
return Action(name="noop")
|
|
376
|
+
|
|
377
|
+
ar, ac = hub
|
|
378
|
+
if is_adjacent((s.row, s.col), hub):
|
|
379
|
+
return use_object_at(s, hub)
|
|
380
|
+
|
|
381
|
+
return self._move_towards(s, hub, reach_adjacent=True)
|
|
382
|
+
|
|
383
|
+
def _do_unclip(self, s: UnclippingAgentState) -> Action:
|
|
384
|
+
"""Use unclip item on clipped extractor."""
|
|
385
|
+
if s.blocked_by_clipped_extractor is None:
|
|
386
|
+
s.phase = Phase.GATHER
|
|
387
|
+
self._clear_unclip_state(s)
|
|
388
|
+
return Action(name="noop")
|
|
389
|
+
|
|
390
|
+
info = self._get_unclip_info(s, s.unclip_target_resource)
|
|
391
|
+
if info is None:
|
|
392
|
+
self._clear_unclip_state(s)
|
|
393
|
+
s.phase = Phase.GATHER
|
|
394
|
+
return Action(name="noop")
|
|
395
|
+
|
|
396
|
+
item_name, _ = info
|
|
397
|
+
if getattr(s, item_name, 0) <= 0:
|
|
398
|
+
# Lost the item before reaching extractor
|
|
399
|
+
self._clear_unclip_state(s)
|
|
400
|
+
s.phase = Phase.GATHER
|
|
401
|
+
return Action(name="noop")
|
|
402
|
+
|
|
403
|
+
# Navigate to clipped extractor
|
|
404
|
+
target = s.blocked_by_clipped_extractor
|
|
405
|
+
tr, tc = target
|
|
406
|
+
is_at_target = s.row == tr and s.col == tc
|
|
407
|
+
|
|
408
|
+
if is_at_target:
|
|
409
|
+
# Already on the extractor - it should be unclipped now
|
|
410
|
+
# Wait for next step to verify and clear state
|
|
411
|
+
return Action(name="noop")
|
|
412
|
+
|
|
413
|
+
if is_adjacent((s.row, s.col), target):
|
|
414
|
+
# Adjacent to clipped extractor - use it to unclip (like using any other object)
|
|
415
|
+
action = use_object_at(s, target)
|
|
416
|
+
# Don't clear unclip state yet - wait until next step to verify it worked
|
|
417
|
+
# The state will be cleared in _update_phase when we see the extractor is unclipped
|
|
418
|
+
return action
|
|
419
|
+
|
|
420
|
+
# Not adjacent yet, move towards it
|
|
421
|
+
return self._move_towards(s, target, reach_adjacent=True)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ============================================================================
|
|
425
|
+
# Policy Wrapper Class
|
|
426
|
+
# ============================================================================
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class UnclippingPolicy(MultiAgentPolicy):
|
|
430
|
+
"""Multi-agent policy wrapper for UnclippingAgent.
|
|
431
|
+
|
|
432
|
+
This class wraps UnclippingAgent to work with the policy interface.
|
|
433
|
+
It handles multiple agents, each with their own UnclippingAgent instance.
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
short_names = ["ladybug_py"]
|
|
437
|
+
|
|
438
|
+
def __init__(
|
|
439
|
+
self,
|
|
440
|
+
policy_env_info: PolicyEnvInterface,
|
|
441
|
+
device: str = "cpu",
|
|
442
|
+
hyperparams: Optional[UnclippingHyperparameters] = None,
|
|
443
|
+
):
|
|
444
|
+
super().__init__(policy_env_info, device=device)
|
|
445
|
+
self._agent_policies: dict[int, StatefulAgentPolicy[UnclippingAgentState]] = {}
|
|
446
|
+
self._hyperparams = hyperparams or UnclippingHyperparameters()
|
|
447
|
+
|
|
448
|
+
def agent_policy(self, agent_id: int) -> StatefulAgentPolicy[UnclippingAgentState]:
|
|
449
|
+
if agent_id not in self._agent_policies:
|
|
450
|
+
# UnclippingAgentPolicyImpl uses UnclippingAgentState but inherits from
|
|
451
|
+
# BaselineAgentPolicyImpl typed with SimpleAgentState, requiring a cast
|
|
452
|
+
policy = cast(
|
|
453
|
+
StatefulAgentPolicy[UnclippingAgentState],
|
|
454
|
+
StatefulAgentPolicy(
|
|
455
|
+
UnclippingAgentPolicyImpl(self._policy_env_info, agent_id, self._hyperparams),
|
|
456
|
+
self._policy_env_info,
|
|
457
|
+
agent_id=agent_id,
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
self._agent_policies[agent_id] = policy
|
|
461
|
+
return self._agent_policies[agent_id]
|