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,381 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data types and structures for CoGsGuard scripted agents.
|
|
3
|
+
|
|
4
|
+
This module contains state, enums, and type definitions for role-based agents.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import TYPE_CHECKING, Optional
|
|
12
|
+
|
|
13
|
+
from cogames_agents.policy.scripted_agent.common.roles import ROLE_TO_GEAR, ROLE_TO_STATION, Role
|
|
14
|
+
from mettagrid.simulator import Action
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from mettagrid.simulator.interface import AgentObservation
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CogsguardPhase(Enum):
|
|
21
|
+
"""Phases for CoGsGuard agents."""
|
|
22
|
+
|
|
23
|
+
GET_GEAR = "get_gear" # Find and equip role-specific gear
|
|
24
|
+
EXECUTE_ROLE = "execute_role" # Execute role-specific behavior
|
|
25
|
+
RECHARGE = "recharge" # Recharge energy at junction
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StructureType(Enum):
|
|
29
|
+
"""Types of structures in the game."""
|
|
30
|
+
|
|
31
|
+
HUB = "hub" # Main hub / resource deposit point
|
|
32
|
+
CHARGER = "junction" # Supply depot
|
|
33
|
+
MINER_STATION = "miner_station"
|
|
34
|
+
SCOUT_STATION = "scout_station"
|
|
35
|
+
ALIGNER_STATION = "aligner_station"
|
|
36
|
+
SCRAMBLER_STATION = "scrambler_station"
|
|
37
|
+
EXTRACTOR = "extractor" # Resource extractor/chest
|
|
38
|
+
CHEST = "chest" # Heart acquisition point
|
|
39
|
+
WALL = "wall"
|
|
40
|
+
UNKNOWN = "unknown"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class StructureInfo:
|
|
45
|
+
"""Information about a discovered structure."""
|
|
46
|
+
|
|
47
|
+
position: tuple[int, int]
|
|
48
|
+
structure_type: StructureType
|
|
49
|
+
name: str # Original object name
|
|
50
|
+
|
|
51
|
+
# Common attributes
|
|
52
|
+
last_seen_step: int = 0
|
|
53
|
+
|
|
54
|
+
# Alignment (for depots/hubs): "cogs", "clips", or None (neutral)
|
|
55
|
+
alignment: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
# Extractor-specific attributes
|
|
58
|
+
resource_type: Optional[str] = None # carbon, oxygen, germanium, silicon
|
|
59
|
+
remaining_uses: int = 999
|
|
60
|
+
cooldown_remaining: int = 0
|
|
61
|
+
clipped: bool = False # True if owned by clips
|
|
62
|
+
inventory_amount: int = 999 # Current resource amount in extractor inventory
|
|
63
|
+
|
|
64
|
+
def is_usable_extractor(self) -> bool:
|
|
65
|
+
"""Check if this is a usable extractor (not depleted, not clipped, has resources).
|
|
66
|
+
|
|
67
|
+
An extractor is usable if:
|
|
68
|
+
- It's an extractor structure type
|
|
69
|
+
- It has remaining uses (not permanently depleted)
|
|
70
|
+
- It has resources to extract (inventory_amount > 0)
|
|
71
|
+
- It's not owned by clips (clipped)
|
|
72
|
+
"""
|
|
73
|
+
if self.structure_type != StructureType.EXTRACTOR:
|
|
74
|
+
return False
|
|
75
|
+
if self.remaining_uses <= 0:
|
|
76
|
+
return False
|
|
77
|
+
if self.inventory_amount <= 0:
|
|
78
|
+
return False
|
|
79
|
+
if self.clipped:
|
|
80
|
+
return False
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Map roles to their gear station structure types
|
|
85
|
+
ROLE_TO_STRUCTURE_TYPE = {
|
|
86
|
+
Role.MINER: StructureType.MINER_STATION,
|
|
87
|
+
Role.SCOUT: StructureType.SCOUT_STATION,
|
|
88
|
+
Role.ALIGNER: StructureType.ALIGNER_STATION,
|
|
89
|
+
Role.SCRAMBLER: StructureType.SCRAMBLER_STATION,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class CogsguardAgentState:
|
|
95
|
+
"""State for a CoGsGuard agent."""
|
|
96
|
+
|
|
97
|
+
agent_id: int
|
|
98
|
+
role: Role
|
|
99
|
+
|
|
100
|
+
# Current phase
|
|
101
|
+
phase: CogsguardPhase = CogsguardPhase.GET_GEAR
|
|
102
|
+
|
|
103
|
+
# Current vibe (read from observation)
|
|
104
|
+
current_vibe: str = "default"
|
|
105
|
+
|
|
106
|
+
# Role switching guardrails (for smart-role logic)
|
|
107
|
+
last_role_switch_step: int = 0
|
|
108
|
+
role_lock_until_step: int = 0
|
|
109
|
+
|
|
110
|
+
step_count: int = 0
|
|
111
|
+
|
|
112
|
+
# Position tracking (origin-relative)
|
|
113
|
+
row: int = 0
|
|
114
|
+
col: int = 0
|
|
115
|
+
energy: int = 100
|
|
116
|
+
|
|
117
|
+
# Map knowledge
|
|
118
|
+
map_height: int = 200
|
|
119
|
+
map_width: int = 200
|
|
120
|
+
occupancy: list[list[int]] = field(default_factory=list)
|
|
121
|
+
# Track which cells have been observed (explored)
|
|
122
|
+
explored: list[list[bool]] = field(default_factory=list)
|
|
123
|
+
|
|
124
|
+
# === Unified structure map ===
|
|
125
|
+
# All discovered structures: position -> StructureInfo
|
|
126
|
+
structures: dict[tuple[int, int], StructureInfo] = field(default_factory=dict)
|
|
127
|
+
stations: dict[str, Optional[tuple[int, int]]] = field(default_factory=dict)
|
|
128
|
+
supply_depots: list[tuple[tuple[int, int], Optional[str]]] = field(default_factory=list)
|
|
129
|
+
|
|
130
|
+
# Alignment overrides from our own actions (pos -> alignment).
|
|
131
|
+
alignment_overrides: dict[tuple[int, int], Optional[str]] = field(default_factory=dict)
|
|
132
|
+
|
|
133
|
+
# Inventory - gear items
|
|
134
|
+
miner: int = 0
|
|
135
|
+
scout: int = 0
|
|
136
|
+
aligner: int = 0
|
|
137
|
+
scrambler: int = 0
|
|
138
|
+
|
|
139
|
+
# Inventory - resources
|
|
140
|
+
carbon: int = 0
|
|
141
|
+
oxygen: int = 0
|
|
142
|
+
germanium: int = 0
|
|
143
|
+
silicon: int = 0
|
|
144
|
+
heart: int = 0
|
|
145
|
+
influence: int = 0
|
|
146
|
+
hp: int = 100
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def cargo_capacity(self) -> int:
|
|
150
|
+
"""Current cargo capacity based on miner gear.
|
|
151
|
+
|
|
152
|
+
Base capacity is 4. Miner gear sets capacity to 40 per item.
|
|
153
|
+
This matches ResourceLimitsConfig semantics (max(min, sum(modifiers))).
|
|
154
|
+
"""
|
|
155
|
+
base = 4
|
|
156
|
+
bonus = 40 * self.miner
|
|
157
|
+
return max(base, bonus)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def total_cargo(self) -> int:
|
|
161
|
+
"""Total resources currently carried."""
|
|
162
|
+
return self.carbon + self.oxygen + self.germanium + self.silicon
|
|
163
|
+
|
|
164
|
+
# Track last action for position updates
|
|
165
|
+
last_action: Action = field(default_factory=lambda: Action(name="noop"))
|
|
166
|
+
|
|
167
|
+
# Track what action the simulator actually executed (from observation)
|
|
168
|
+
last_action_executed: Optional[str] = None
|
|
169
|
+
|
|
170
|
+
# Navigation state
|
|
171
|
+
target_position: Optional[tuple[int, int]] = None
|
|
172
|
+
cached_path: Optional[list[tuple[int, int]]] = None
|
|
173
|
+
cached_path_target: Optional[tuple[int, int]] = None
|
|
174
|
+
cached_path_reach_adjacent: bool = False
|
|
175
|
+
|
|
176
|
+
# Exploration state
|
|
177
|
+
exploration_target: Optional[str] = None
|
|
178
|
+
exploration_target_step: int = 0
|
|
179
|
+
|
|
180
|
+
# Agent collision detection
|
|
181
|
+
agent_occupancy: set[tuple[int, int]] = field(default_factory=set)
|
|
182
|
+
|
|
183
|
+
# Position history for stuck detection
|
|
184
|
+
position_history: list[tuple[int, int]] = field(default_factory=list)
|
|
185
|
+
|
|
186
|
+
# Object interaction tracking
|
|
187
|
+
using_object_this_step: bool = False
|
|
188
|
+
|
|
189
|
+
# Current observation reference
|
|
190
|
+
current_obs: Optional[AgentObservation] = None
|
|
191
|
+
|
|
192
|
+
# Track junctions we've worked on (position -> last interaction step)
|
|
193
|
+
# Used by aligners/scramblers to avoid getting stuck on the same junction
|
|
194
|
+
worked_junctions: dict[tuple[int, int], int] = field(default_factory=dict)
|
|
195
|
+
|
|
196
|
+
# Scrambler-specific tracking for heart acquisition timeout
|
|
197
|
+
_heart_wait_start: int = 0
|
|
198
|
+
_last_heart_count: int = 0
|
|
199
|
+
|
|
200
|
+
# Action retry tracking
|
|
201
|
+
_pending_action_type: Optional[str] = None # "scramble", "align", "mine"
|
|
202
|
+
_pending_action_target: Optional[tuple[int, int]] = None
|
|
203
|
+
_action_retry_count: int = 0
|
|
204
|
+
_pre_action_heart: int = 0 # Heart count before action attempt
|
|
205
|
+
_pre_action_cargo: int = 0 # Cargo count before action attempt
|
|
206
|
+
_pending_alignment_target: Optional[tuple[int, int]] = None
|
|
207
|
+
|
|
208
|
+
# Miner gear acquisition tracking
|
|
209
|
+
_gear_attempt_step: int = 0 # Step when we last tried to get gear
|
|
210
|
+
_resources_deposited_since_gear_attempt: int = 0 # Resources deposited since last gear attempt
|
|
211
|
+
_gear_attempts_failed: int = 0 # Count of failed gear acquisition attempts (for scrambler)
|
|
212
|
+
|
|
213
|
+
# Option execution state
|
|
214
|
+
active_option_id: int = -1
|
|
215
|
+
active_option_ticks: int = 0
|
|
216
|
+
|
|
217
|
+
# Energy costs (can be overridden based on game config)
|
|
218
|
+
MOVE_ENERGY_COST: int = 3 # Default move energy cost
|
|
219
|
+
|
|
220
|
+
def has_gear(self) -> bool:
|
|
221
|
+
"""Check if agent has their role's gear equipped."""
|
|
222
|
+
gear_name = ROLE_TO_GEAR[self.role]
|
|
223
|
+
return getattr(self, gear_name, 0) > 0
|
|
224
|
+
|
|
225
|
+
def get_gear_station_name(self) -> str:
|
|
226
|
+
"""Get the station name for this agent's role."""
|
|
227
|
+
return ROLE_TO_STATION[self.role]
|
|
228
|
+
|
|
229
|
+
def get_gear_station_type(self) -> StructureType:
|
|
230
|
+
"""Get the structure type for this agent's gear station."""
|
|
231
|
+
return ROLE_TO_STRUCTURE_TYPE[self.role]
|
|
232
|
+
|
|
233
|
+
def get_structure_position(self, structure_type: StructureType) -> Optional[tuple[int, int]]:
|
|
234
|
+
"""Get the nearest structure position for the given type."""
|
|
235
|
+
structure = self.get_nearest_structure(structure_type)
|
|
236
|
+
return structure.position if structure else None
|
|
237
|
+
|
|
238
|
+
# === Structure query methods ===
|
|
239
|
+
|
|
240
|
+
def get_structures_by_type(self, structure_type: StructureType) -> list[StructureInfo]:
|
|
241
|
+
"""Get all structures of a given type."""
|
|
242
|
+
return [s for s in self.structures.values() if s.structure_type == structure_type]
|
|
243
|
+
|
|
244
|
+
def get_structure_at(self, pos: tuple[int, int]) -> Optional[StructureInfo]:
|
|
245
|
+
"""Get structure at a specific position."""
|
|
246
|
+
return self.structures.get(pos)
|
|
247
|
+
|
|
248
|
+
def get_nearest_structure(
|
|
249
|
+
self,
|
|
250
|
+
structure_type: StructureType,
|
|
251
|
+
exclude: Optional[tuple[int, int]] = None,
|
|
252
|
+
) -> Optional[StructureInfo]:
|
|
253
|
+
"""Find the nearest structure of a given type."""
|
|
254
|
+
best: Optional[StructureInfo] = None
|
|
255
|
+
best_dist = float("inf")
|
|
256
|
+
|
|
257
|
+
for struct in self.structures.values():
|
|
258
|
+
if struct.structure_type != structure_type:
|
|
259
|
+
continue
|
|
260
|
+
if exclude and struct.position == exclude:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
dist = abs(struct.position[0] - self.row) + abs(struct.position[1] - self.col)
|
|
264
|
+
if dist < best_dist:
|
|
265
|
+
best = struct
|
|
266
|
+
best_dist = dist
|
|
267
|
+
|
|
268
|
+
return best
|
|
269
|
+
|
|
270
|
+
def get_usable_extractors(self) -> list[StructureInfo]:
|
|
271
|
+
"""Get all usable extractors (not depleted, not clipped)."""
|
|
272
|
+
return [s for s in self.structures.values() if s.is_usable_extractor()]
|
|
273
|
+
|
|
274
|
+
def get_nearest_usable_extractor(self, exclude: Optional[tuple[int, int]] = None) -> Optional[StructureInfo]:
|
|
275
|
+
"""Find nearest usable extractor."""
|
|
276
|
+
best: Optional[StructureInfo] = None
|
|
277
|
+
best_dist = float("inf")
|
|
278
|
+
|
|
279
|
+
for struct in self.structures.values():
|
|
280
|
+
if not struct.is_usable_extractor():
|
|
281
|
+
continue
|
|
282
|
+
if exclude and struct.position == exclude:
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
dist = abs(struct.position[0] - self.row) + abs(struct.position[1] - self.col)
|
|
286
|
+
if dist < best_dist:
|
|
287
|
+
best = struct
|
|
288
|
+
best_dist = dist
|
|
289
|
+
|
|
290
|
+
return best
|
|
291
|
+
|
|
292
|
+
# === Action retry helpers ===
|
|
293
|
+
|
|
294
|
+
def has_enough_energy_for_moves(self, num_moves: int) -> bool:
|
|
295
|
+
"""Check if agent has enough energy to make num_moves moves."""
|
|
296
|
+
return self.energy >= num_moves * self.MOVE_ENERGY_COST
|
|
297
|
+
|
|
298
|
+
def start_action_attempt(self, action_type: str, target: tuple[int, int]) -> None:
|
|
299
|
+
"""Start tracking an action attempt for retry purposes."""
|
|
300
|
+
self._pending_action_type = action_type
|
|
301
|
+
self._pending_action_target = target
|
|
302
|
+
self._action_retry_count = 0
|
|
303
|
+
self._pre_action_heart = self.heart
|
|
304
|
+
self._pre_action_cargo = self.total_cargo
|
|
305
|
+
|
|
306
|
+
def record_alignment_override(self, pos: tuple[int, int], alignment: Optional[str]) -> None:
|
|
307
|
+
"""Persist alignment changes from successful actions."""
|
|
308
|
+
self.alignment_overrides[pos] = alignment
|
|
309
|
+
struct = self.structures.get(pos)
|
|
310
|
+
if struct is not None:
|
|
311
|
+
struct.alignment = alignment
|
|
312
|
+
|
|
313
|
+
def check_action_success(self) -> bool:
|
|
314
|
+
"""Check if the last action attempt succeeded based on state changes.
|
|
315
|
+
|
|
316
|
+
Returns True if action succeeded or no action was pending.
|
|
317
|
+
|
|
318
|
+
For moves: checks if last_action_executed matches last_action (intended).
|
|
319
|
+
For scramble/align: checks if heart count decreased.
|
|
320
|
+
For mine: checks if cargo increased.
|
|
321
|
+
"""
|
|
322
|
+
if self._pending_action_type is None:
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
action_type = self._pending_action_type
|
|
326
|
+
|
|
327
|
+
# First check: did our intended action actually execute?
|
|
328
|
+
# If we intended to move but executed noop, the action failed
|
|
329
|
+
intended = self.last_action.name if self.last_action else None
|
|
330
|
+
executed = self.last_action_executed
|
|
331
|
+
if intended and executed and intended != executed:
|
|
332
|
+
# Action failed at the move level - don't clear, allow retry
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
# Check based on action type
|
|
336
|
+
if action_type in ("scramble", "align"):
|
|
337
|
+
# These actions consume 1 heart on success
|
|
338
|
+
if self.heart < self._pre_action_heart:
|
|
339
|
+
if self._pending_action_target:
|
|
340
|
+
target = self._pending_action_target
|
|
341
|
+
new_alignment = "cogs" if action_type == "align" else None
|
|
342
|
+
self.record_alignment_override(target, new_alignment)
|
|
343
|
+
for idx, (pos, _alignment) in enumerate(self.supply_depots):
|
|
344
|
+
if pos == target:
|
|
345
|
+
self.supply_depots[idx] = (pos, new_alignment)
|
|
346
|
+
if action_type == "scramble":
|
|
347
|
+
self._pending_alignment_target = target
|
|
348
|
+
elif action_type == "align" and target == self._pending_alignment_target:
|
|
349
|
+
self._pending_alignment_target = None
|
|
350
|
+
# Heart was consumed - action succeeded
|
|
351
|
+
self.clear_pending_action()
|
|
352
|
+
return True
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
elif action_type == "mine":
|
|
356
|
+
# Mining increases cargo
|
|
357
|
+
if self.total_cargo > self._pre_action_cargo:
|
|
358
|
+
self.clear_pending_action()
|
|
359
|
+
return True
|
|
360
|
+
return False
|
|
361
|
+
|
|
362
|
+
# Unknown action type - assume success
|
|
363
|
+
self.clear_pending_action()
|
|
364
|
+
return True
|
|
365
|
+
|
|
366
|
+
def increment_retry(self) -> int:
|
|
367
|
+
"""Increment retry count and return current count."""
|
|
368
|
+
self._action_retry_count += 1
|
|
369
|
+
return self._action_retry_count
|
|
370
|
+
|
|
371
|
+
def clear_pending_action(self) -> None:
|
|
372
|
+
"""Clear pending action tracking."""
|
|
373
|
+
self._pending_action_type = None
|
|
374
|
+
self._pending_action_target = None
|
|
375
|
+
self._action_retry_count = 0
|
|
376
|
+
|
|
377
|
+
def should_retry_action(self, max_retries: int = 3) -> bool:
|
|
378
|
+
"""Check if we should retry the pending action."""
|
|
379
|
+
if self._pending_action_type is None:
|
|
380
|
+
return False
|
|
381
|
+
return self._action_retry_count < max_retries
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""CogsGuard scripted policy with a tuned default role mix."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mettagrid.policy.policy_env_interface import PolicyEnvInterface
|
|
8
|
+
|
|
9
|
+
from .policy import CogsguardPolicy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _default_role_counts(num_agents: int) -> dict[str, int]:
|
|
13
|
+
if num_agents <= 1:
|
|
14
|
+
return {"miner": 1}
|
|
15
|
+
if num_agents == 2:
|
|
16
|
+
return {"scrambler": 1, "miner": 1}
|
|
17
|
+
if num_agents == 3:
|
|
18
|
+
return {"scrambler": 1, "miner": 1, "scout": 1}
|
|
19
|
+
if num_agents <= 7:
|
|
20
|
+
scramblers = 1
|
|
21
|
+
aligners = 1
|
|
22
|
+
scouts = 1
|
|
23
|
+
else:
|
|
24
|
+
scramblers = max(2, num_agents // 6)
|
|
25
|
+
aligners = max(2, num_agents // 6)
|
|
26
|
+
scouts = 1
|
|
27
|
+
miners = max(1, num_agents - scramblers - scouts - aligners)
|
|
28
|
+
return {
|
|
29
|
+
"scrambler": scramblers,
|
|
30
|
+
"aligner": aligners,
|
|
31
|
+
"miner": miners,
|
|
32
|
+
"scout": scouts,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CogsguardV2Agent(CogsguardPolicy):
|
|
37
|
+
"""Scripted cogsguard policy with better default role allocation."""
|
|
38
|
+
|
|
39
|
+
short_names = ["cogsguard_v2"]
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
policy_env_info: PolicyEnvInterface,
|
|
44
|
+
device: str = "cpu",
|
|
45
|
+
**vibe_counts: Any,
|
|
46
|
+
):
|
|
47
|
+
if not any(isinstance(v, int) for v in vibe_counts.values()):
|
|
48
|
+
vibe_counts = {**vibe_counts, **_default_role_counts(policy_env_info.num_agents)}
|
|
49
|
+
super().__init__(policy_env_info, device=device, **vibe_counts)
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
MOVE_DELTAS: dict[str, tuple[int, int]] = {
|
|
4
|
+
"north": (-1, 0),
|
|
5
|
+
"south": (1, 0),
|
|
6
|
+
"east": (0, 1),
|
|
7
|
+
"west": (0, -1),
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
DIRECTIONS = ["north", "south", "east", "west"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def manhattan(a: tuple[int, int], b: tuple[int, int]) -> int:
|
|
14
|
+
return abs(a[0] - b[0]) + abs(a[1] - b[1])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def manhattan_distance(a: tuple[int, int], b: tuple[int, int]) -> int:
|
|
18
|
+
return manhattan(a, b)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_adjacent(pos1: tuple[int, int], pos2: tuple[int, int]) -> bool:
|
|
22
|
+
dr = abs(pos1[0] - pos2[0])
|
|
23
|
+
dc = abs(pos1[1] - pos2[1])
|
|
24
|
+
return (dr == 1 and dc == 0) or (dr == 0 and dc == 1)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Role(Enum):
|
|
7
|
+
MINER = "miner"
|
|
8
|
+
SCOUT = "scout"
|
|
9
|
+
ALIGNER = "aligner"
|
|
10
|
+
SCRAMBLER = "scrambler"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ROLE_TO_STATION: dict[Role, str] = {
|
|
14
|
+
Role.MINER: "miner_station",
|
|
15
|
+
Role.SCOUT: "scout_station",
|
|
16
|
+
Role.ALIGNER: "aligner_station",
|
|
17
|
+
Role.SCRAMBLER: "scrambler_station",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ROLE_TO_GEAR: dict[Role, str] = {
|
|
21
|
+
Role.MINER: "miner",
|
|
22
|
+
Role.SCOUT: "scout",
|
|
23
|
+
Role.ALIGNER: "aligner",
|
|
24
|
+
Role.SCRAMBLER: "scrambler",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ROLE_VIBES = ["miner", "scout", "aligner", "scrambler"]
|
|
28
|
+
|
|
29
|
+
VIBE_TO_ROLE: dict[str, Role] = {
|
|
30
|
+
"miner": Role.MINER,
|
|
31
|
+
"scout": Role.SCOUT,
|
|
32
|
+
"aligner": Role.ALIGNER,
|
|
33
|
+
"scrambler": Role.SCRAMBLER,
|
|
34
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def select_primary_tag(tags: list[str], *, priority_objects: Optional[set[str]] = None) -> str:
|
|
7
|
+
if not tags:
|
|
8
|
+
return "unknown"
|
|
9
|
+
|
|
10
|
+
for tag in tags:
|
|
11
|
+
if tag.startswith("type:"):
|
|
12
|
+
return tag.split(":", 1)[1]
|
|
13
|
+
|
|
14
|
+
if priority_objects:
|
|
15
|
+
for tag in tags:
|
|
16
|
+
if tag and not tag.startswith("collective:") and tag in priority_objects:
|
|
17
|
+
return tag
|
|
18
|
+
|
|
19
|
+
for tag in tags:
|
|
20
|
+
if tag and not tag.startswith("collective:"):
|
|
21
|
+
return tag
|
|
22
|
+
|
|
23
|
+
for tag in tags:
|
|
24
|
+
if tag:
|
|
25
|
+
return tag
|
|
26
|
+
|
|
27
|
+
return "unknown"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def derive_alignment(
|
|
31
|
+
obj_name: str,
|
|
32
|
+
clipped: int,
|
|
33
|
+
collective_id: Optional[int],
|
|
34
|
+
*,
|
|
35
|
+
cogs_collective_id: Optional[int],
|
|
36
|
+
clips_collective_id: Optional[int],
|
|
37
|
+
) -> Optional[str]:
|
|
38
|
+
if collective_id is not None:
|
|
39
|
+
if collective_id == cogs_collective_id:
|
|
40
|
+
return "cogs"
|
|
41
|
+
if collective_id == clips_collective_id:
|
|
42
|
+
return "clips"
|
|
43
|
+
|
|
44
|
+
if "cogs" in obj_name:
|
|
45
|
+
return "cogs"
|
|
46
|
+
if "clips" in obj_name or clipped > 0:
|
|
47
|
+
return "clips"
|
|
48
|
+
return None
|