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,68 @@
|
|
|
1
|
+
"""Context and state snapshot for Cogas policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .entity_map import EntityMap
|
|
10
|
+
from .navigator import Navigator
|
|
11
|
+
from .trace import TraceLog
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class StateSnapshot:
|
|
16
|
+
"""Rebuilt every tick from observation tokens. Observation is source of truth."""
|
|
17
|
+
|
|
18
|
+
position: tuple[int, int] = (0, 0)
|
|
19
|
+
|
|
20
|
+
# Inventory
|
|
21
|
+
carbon: int = 0
|
|
22
|
+
oxygen: int = 0
|
|
23
|
+
germanium: int = 0
|
|
24
|
+
silicon: int = 0
|
|
25
|
+
heart: int = 0
|
|
26
|
+
influence: int = 0
|
|
27
|
+
hp: int = 100
|
|
28
|
+
energy: int = 100
|
|
29
|
+
|
|
30
|
+
# Gear flags
|
|
31
|
+
miner_gear: bool = False
|
|
32
|
+
scout_gear: bool = False
|
|
33
|
+
aligner_gear: bool = False
|
|
34
|
+
scrambler_gear: bool = False
|
|
35
|
+
|
|
36
|
+
# Vibe
|
|
37
|
+
vibe: str = "default"
|
|
38
|
+
|
|
39
|
+
# Collective inventory
|
|
40
|
+
collective_carbon: int = 0
|
|
41
|
+
collective_oxygen: int = 0
|
|
42
|
+
collective_germanium: int = 0
|
|
43
|
+
collective_silicon: int = 0
|
|
44
|
+
collective_heart: int = 0
|
|
45
|
+
collective_influence: int = 0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def cargo_total(self) -> int:
|
|
49
|
+
return self.carbon + self.oxygen + self.germanium + self.silicon
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def cargo_capacity(self) -> int:
|
|
53
|
+
return 40 if self.miner_gear else 4
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class CogasContext:
|
|
58
|
+
"""Passed to all goals, bundles everything needed for decision-making."""
|
|
59
|
+
|
|
60
|
+
state: StateSnapshot
|
|
61
|
+
map: EntityMap
|
|
62
|
+
blackboard: dict[str, Any]
|
|
63
|
+
navigator: Navigator
|
|
64
|
+
trace: Optional[TraceLog]
|
|
65
|
+
action_names: list[str]
|
|
66
|
+
agent_id: int
|
|
67
|
+
step: int
|
|
68
|
+
my_collective_id: Optional[int] = None
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Sparse entity map for Cogas policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Entity:
|
|
11
|
+
"""An object on the map."""
|
|
12
|
+
|
|
13
|
+
type: str # e.g. "carbon_extractor", "miner_station", "wall", "agent"
|
|
14
|
+
properties: dict # alignment, remaining_uses, inventory_amount, cooldown, etc.
|
|
15
|
+
last_seen: int = 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EntityMap:
|
|
19
|
+
"""Sparse map of entities. Only stores non-empty cells."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self.entities: dict[tuple[int, int], Entity] = {}
|
|
23
|
+
self.explored: set[tuple[int, int]] = set()
|
|
24
|
+
|
|
25
|
+
def update_from_observation(
|
|
26
|
+
self,
|
|
27
|
+
agent_pos: tuple[int, int],
|
|
28
|
+
obs_half_height: int,
|
|
29
|
+
obs_half_width: int,
|
|
30
|
+
visible_entities: dict[tuple[int, int], Entity],
|
|
31
|
+
step: int,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Update map from current observation window.
|
|
34
|
+
|
|
35
|
+
All cells in the observation window are marked as explored.
|
|
36
|
+
Entities in the window are overwritten with fresh data.
|
|
37
|
+
Entities no longer visible in the window are removed.
|
|
38
|
+
"""
|
|
39
|
+
# Mark all cells in observation window as explored
|
|
40
|
+
for obs_r in range(2 * obs_half_height + 1):
|
|
41
|
+
for obs_c in range(2 * obs_half_width + 1):
|
|
42
|
+
r = obs_r - obs_half_height + agent_pos[0]
|
|
43
|
+
c = obs_c - obs_half_width + agent_pos[1]
|
|
44
|
+
self.explored.add((r, c))
|
|
45
|
+
|
|
46
|
+
# Remove entities in observation window that are no longer visible
|
|
47
|
+
window_min_r = agent_pos[0] - obs_half_height
|
|
48
|
+
window_max_r = agent_pos[0] + obs_half_height
|
|
49
|
+
window_min_c = agent_pos[1] - obs_half_width
|
|
50
|
+
window_max_c = agent_pos[1] + obs_half_width
|
|
51
|
+
|
|
52
|
+
to_remove = []
|
|
53
|
+
for pos in self.entities:
|
|
54
|
+
if window_min_r <= pos[0] <= window_max_r and window_min_c <= pos[1] <= window_max_c:
|
|
55
|
+
if pos not in visible_entities:
|
|
56
|
+
to_remove.append(pos)
|
|
57
|
+
for pos in to_remove:
|
|
58
|
+
del self.entities[pos]
|
|
59
|
+
|
|
60
|
+
# Add/update visible entities
|
|
61
|
+
for pos, entity in visible_entities.items():
|
|
62
|
+
entity.last_seen = step
|
|
63
|
+
self.entities[pos] = entity
|
|
64
|
+
|
|
65
|
+
def find(
|
|
66
|
+
self,
|
|
67
|
+
type: Optional[str] = None,
|
|
68
|
+
type_contains: Optional[str] = None,
|
|
69
|
+
property_filter: Optional[dict] = None,
|
|
70
|
+
) -> list[tuple[tuple[int, int], Entity]]:
|
|
71
|
+
"""Query entities by type and/or properties.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
type: Exact type match
|
|
75
|
+
type_contains: Substring match on type
|
|
76
|
+
property_filter: Dict of property key-value pairs that must match
|
|
77
|
+
"""
|
|
78
|
+
results = []
|
|
79
|
+
for pos, entity in self.entities.items():
|
|
80
|
+
if type is not None and entity.type != type:
|
|
81
|
+
continue
|
|
82
|
+
if type_contains is not None and type_contains not in entity.type:
|
|
83
|
+
continue
|
|
84
|
+
if property_filter is not None:
|
|
85
|
+
match = all(entity.properties.get(k) == v for k, v in property_filter.items())
|
|
86
|
+
if not match:
|
|
87
|
+
continue
|
|
88
|
+
results.append((pos, entity))
|
|
89
|
+
return results
|
|
90
|
+
|
|
91
|
+
def find_nearest(
|
|
92
|
+
self,
|
|
93
|
+
from_pos: tuple[int, int],
|
|
94
|
+
type: Optional[str] = None,
|
|
95
|
+
type_contains: Optional[str] = None,
|
|
96
|
+
property_filter: Optional[dict] = None,
|
|
97
|
+
max_dist: Optional[int] = None,
|
|
98
|
+
) -> Optional[tuple[tuple[int, int], Entity]]:
|
|
99
|
+
"""Find nearest entity matching criteria."""
|
|
100
|
+
matches = self.find(type=type, type_contains=type_contains, property_filter=property_filter)
|
|
101
|
+
if not matches:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
best = None
|
|
105
|
+
best_dist = float("inf")
|
|
106
|
+
for pos, entity in matches:
|
|
107
|
+
dist = abs(pos[0] - from_pos[0]) + abs(pos[1] - from_pos[1])
|
|
108
|
+
if max_dist is not None and dist > max_dist:
|
|
109
|
+
continue
|
|
110
|
+
if dist < best_dist:
|
|
111
|
+
best = (pos, entity)
|
|
112
|
+
best_dist = dist
|
|
113
|
+
return best
|
|
114
|
+
|
|
115
|
+
def is_passable(self, pos: tuple[int, int]) -> bool:
|
|
116
|
+
"""Check if a position is passable (explored and not a wall/obstacle)."""
|
|
117
|
+
if pos not in self.explored:
|
|
118
|
+
return False
|
|
119
|
+
entity = self.entities.get(pos)
|
|
120
|
+
if entity is None:
|
|
121
|
+
return True # Explored empty cell
|
|
122
|
+
# Agents are temporary obstacles, everything else is permanent
|
|
123
|
+
if entity.type == "agent":
|
|
124
|
+
return False
|
|
125
|
+
# Walls are obstacles
|
|
126
|
+
if entity.type == "wall":
|
|
127
|
+
return False
|
|
128
|
+
# Structures are obstacles (stations, extractors, junctions, etc.)
|
|
129
|
+
# But we don't block pathfinding through them — goals that need adjacency
|
|
130
|
+
# handle that via reach_adjacent=True
|
|
131
|
+
return True # Structures are passable for pathfinding
|
|
132
|
+
|
|
133
|
+
def is_wall(self, pos: tuple[int, int]) -> bool:
|
|
134
|
+
"""Check if position is a wall."""
|
|
135
|
+
entity = self.entities.get(pos)
|
|
136
|
+
return entity is not None and entity.type == "wall"
|
|
137
|
+
|
|
138
|
+
def is_structure(self, pos: tuple[int, int]) -> bool:
|
|
139
|
+
"""Check if position has a structure (non-wall, non-agent entity)."""
|
|
140
|
+
entity = self.entities.get(pos)
|
|
141
|
+
if entity is None:
|
|
142
|
+
return False
|
|
143
|
+
return entity.type not in ("wall", "agent")
|
|
144
|
+
|
|
145
|
+
def is_free(self, pos: tuple[int, int]) -> bool:
|
|
146
|
+
"""Check if position is explored and has no entity."""
|
|
147
|
+
return pos in self.explored and pos not in self.entities
|
|
148
|
+
|
|
149
|
+
def has_agent(self, pos: tuple[int, int]) -> bool:
|
|
150
|
+
"""Check if position has an agent."""
|
|
151
|
+
entity = self.entities.get(pos)
|
|
152
|
+
return entity is not None and entity.type == "agent"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Goal base class and evaluation logic for Cogas policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
from mettagrid.simulator import Action
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .context import CogasContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Goal:
|
|
14
|
+
"""Base class for all goals in the goal tree.
|
|
15
|
+
|
|
16
|
+
Subclasses implement:
|
|
17
|
+
- is_satisfied(ctx) -> bool: whether this goal is already met
|
|
18
|
+
- preconditions() -> list[Goal]: sub-goals that must be satisfied first
|
|
19
|
+
- execute(ctx) -> Action | None: produce an action, or None to skip/defer
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str = "Goal"
|
|
23
|
+
|
|
24
|
+
def is_satisfied(self, ctx: CogasContext) -> bool:
|
|
25
|
+
"""Check if this goal is already satisfied."""
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
def preconditions(self) -> list[Goal]:
|
|
29
|
+
"""Return sub-goals that must be satisfied before this goal can execute."""
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
def execute(self, ctx: CogasContext) -> Optional[Action]:
|
|
33
|
+
"""Produce an action to work toward this goal, or None to skip."""
|
|
34
|
+
return Action(name="noop")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def evaluate_goals(goals: list[Goal], ctx: CogasContext) -> Action:
|
|
38
|
+
"""Evaluate a priority-ordered goal list and return an action.
|
|
39
|
+
|
|
40
|
+
Walks the list top-down. The first unsatisfied goal becomes active.
|
|
41
|
+
Recursively checks preconditions to find the deepest unsatisfied leaf.
|
|
42
|
+
That leaf's execute() produces the action.
|
|
43
|
+
|
|
44
|
+
If execute() returns None, the goal is skipped and evaluation continues
|
|
45
|
+
with the next goal (allows goals to voluntarily defer).
|
|
46
|
+
"""
|
|
47
|
+
for goal in goals:
|
|
48
|
+
if goal.is_satisfied(ctx):
|
|
49
|
+
if ctx.trace:
|
|
50
|
+
ctx.trace.skip(goal.name, _satisfaction_detail(goal, ctx))
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# Found unsatisfied goal — recurse into preconditions
|
|
54
|
+
leaf = _deepest_unsatisfied(goal, ctx)
|
|
55
|
+
action = leaf.execute(ctx)
|
|
56
|
+
|
|
57
|
+
# None means "skip me for now" — continue to next goal
|
|
58
|
+
if action is None:
|
|
59
|
+
if ctx.trace:
|
|
60
|
+
ctx.trace.skip(leaf.name, "deferred")
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
if ctx.trace:
|
|
64
|
+
ctx.trace.active_goal_chain = _build_chain(goal, leaf)
|
|
65
|
+
ctx.trace.action_name = action.name
|
|
66
|
+
|
|
67
|
+
return action
|
|
68
|
+
|
|
69
|
+
# All goals satisfied - explore as fallback instead of nooping
|
|
70
|
+
if ctx.trace:
|
|
71
|
+
ctx.trace.active_goal_chain = "AllGoalsSatisfied"
|
|
72
|
+
directions = ["north", "east", "south", "west"]
|
|
73
|
+
return ctx.navigator.explore(
|
|
74
|
+
ctx.state.position,
|
|
75
|
+
ctx.map,
|
|
76
|
+
direction_bias=directions[ctx.agent_id % 4],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _deepest_unsatisfied(goal: Goal, ctx: CogasContext) -> Goal:
|
|
81
|
+
"""Find the deepest unsatisfied precondition in the goal tree."""
|
|
82
|
+
for pre in goal.preconditions():
|
|
83
|
+
if not pre.is_satisfied(ctx):
|
|
84
|
+
if ctx.trace:
|
|
85
|
+
ctx.trace.activate(pre.name)
|
|
86
|
+
return _deepest_unsatisfied(pre, ctx)
|
|
87
|
+
return goal
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _build_chain(root: Goal, leaf: Goal) -> str:
|
|
91
|
+
"""Build a display chain like 'MineCarbon>BeNearExtractor'."""
|
|
92
|
+
if root is leaf:
|
|
93
|
+
return root.name
|
|
94
|
+
# Walk preconditions to find the path
|
|
95
|
+
chain = [root.name]
|
|
96
|
+
_find_path(root, leaf, chain)
|
|
97
|
+
return ">".join(chain)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _find_path(current: Goal, target: Goal, chain: list[str]) -> bool:
|
|
101
|
+
"""DFS to find path from current to target goal."""
|
|
102
|
+
for pre in current.preconditions():
|
|
103
|
+
if pre is target:
|
|
104
|
+
chain.append(pre.name)
|
|
105
|
+
return True
|
|
106
|
+
chain.append(pre.name)
|
|
107
|
+
if _find_path(pre, target, chain):
|
|
108
|
+
return True
|
|
109
|
+
chain.pop()
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _satisfaction_detail(goal: Goal, ctx: CogasContext) -> str:
|
|
114
|
+
"""Generate a short detail string for why a goal is satisfied."""
|
|
115
|
+
return "ok"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Goal classes for Cogas policy."""
|
|
2
|
+
|
|
3
|
+
from .aligner import AlignJunctionGoal, GetAlignerGearGoal
|
|
4
|
+
from .gear import GetGearGoal
|
|
5
|
+
from .miner import DepositCargoGoal, GetMinerGearGoal, MineResourceGoal, PickResourceGoal
|
|
6
|
+
from .scout import ExploreGoal, GetScoutGearGoal
|
|
7
|
+
from .scrambler import GetScramblerGearGoal, ScrambleJunctionGoal
|
|
8
|
+
from .shared import GetHeartsGoal
|
|
9
|
+
from .stem import SelectRoleGoal
|
|
10
|
+
from .survive import SurviveGoal
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"SurviveGoal",
|
|
14
|
+
"GetGearGoal",
|
|
15
|
+
"GetAlignerGearGoal",
|
|
16
|
+
"GetMinerGearGoal",
|
|
17
|
+
"GetScoutGearGoal",
|
|
18
|
+
"GetScramblerGearGoal",
|
|
19
|
+
"GetHeartsGoal",
|
|
20
|
+
"PickResourceGoal",
|
|
21
|
+
"DepositCargoGoal",
|
|
22
|
+
"MineResourceGoal",
|
|
23
|
+
"ExploreGoal",
|
|
24
|
+
"AlignJunctionGoal",
|
|
25
|
+
"ScrambleJunctionGoal",
|
|
26
|
+
"SelectRoleGoal",
|
|
27
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Aligner goals — align neutral junctions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
from cogames_agents.policy.scripted_agent.cogas.goal import Goal
|
|
8
|
+
from cogames_agents.policy.scripted_agent.cogas.navigator import _manhattan
|
|
9
|
+
from mettagrid.simulator import Action
|
|
10
|
+
|
|
11
|
+
from .gear import GetGearGoal
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from cogames_agents.policy.scripted_agent.cogas.context import CogasContext
|
|
15
|
+
|
|
16
|
+
JUNCTION_AOE_RANGE = 10
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GetAlignerGearGoal(GetGearGoal):
|
|
20
|
+
"""Get aligner gear (costs C3 O1 G1 S1 from collective)."""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
super().__init__(
|
|
24
|
+
gear_attr="aligner_gear",
|
|
25
|
+
station_type="aligner_station",
|
|
26
|
+
goal_name="GetAlignerGear",
|
|
27
|
+
gear_cost={"carbon": 3, "oxygen": 1, "germanium": 1, "silicon": 1},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AlignJunctionGoal(Goal):
|
|
32
|
+
"""Find and align a neutral junction to cogs.
|
|
33
|
+
|
|
34
|
+
Tracks attempts per junction to avoid getting stuck on one that
|
|
35
|
+
can't be captured (e.g., already aligned but map hasn't updated).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name = "AlignJunction"
|
|
39
|
+
MAX_ATTEMPTS_PER_TARGET = 15 # Increased from 5 - 55% move fail needs more attempts
|
|
40
|
+
MAX_NAV_STEPS_PER_TARGET = 80 # Increased from 40 - give more time to navigate
|
|
41
|
+
COOLDOWN_STEPS = 30 # Reduced from 50 - try junctions again sooner
|
|
42
|
+
|
|
43
|
+
def is_satisfied(self, ctx: CogasContext) -> bool:
|
|
44
|
+
# Can't align without gear and a heart
|
|
45
|
+
if not ctx.state.aligner_gear:
|
|
46
|
+
if ctx.trace:
|
|
47
|
+
ctx.trace.skip(self.name, "no gear")
|
|
48
|
+
return True
|
|
49
|
+
if ctx.state.heart < 1:
|
|
50
|
+
if ctx.trace:
|
|
51
|
+
ctx.trace.skip(self.name, "no heart")
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def execute(self, ctx: CogasContext) -> Optional[Action]:
|
|
56
|
+
nav_key = "_align_nav_steps"
|
|
57
|
+
nav_target_key = "_align_nav_target"
|
|
58
|
+
nav_steps = ctx.blackboard.get(nav_key, 0) + 1
|
|
59
|
+
ctx.blackboard[nav_key] = nav_steps
|
|
60
|
+
|
|
61
|
+
target = self._find_best_target(ctx)
|
|
62
|
+
if target is None:
|
|
63
|
+
ctx.blackboard[nav_key] = 0
|
|
64
|
+
return ctx.navigator.explore(
|
|
65
|
+
ctx.state.position,
|
|
66
|
+
ctx.map,
|
|
67
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Reset nav counter if target changed
|
|
71
|
+
prev_target = ctx.blackboard.get(nav_target_key)
|
|
72
|
+
if prev_target != target:
|
|
73
|
+
ctx.blackboard[nav_key] = 0
|
|
74
|
+
nav_steps = 0
|
|
75
|
+
ctx.blackboard[nav_target_key] = target
|
|
76
|
+
|
|
77
|
+
# Nav timeout — mark target as failed
|
|
78
|
+
if nav_steps > self.MAX_NAV_STEPS_PER_TARGET:
|
|
79
|
+
failed_key = f"align_failed_{target}"
|
|
80
|
+
ctx.blackboard[failed_key] = ctx.step
|
|
81
|
+
ctx.blackboard[nav_key] = 0
|
|
82
|
+
if ctx.trace:
|
|
83
|
+
ctx.trace.activate(self.name, f"nav timeout on {target}")
|
|
84
|
+
return ctx.navigator.explore(
|
|
85
|
+
ctx.state.position,
|
|
86
|
+
ctx.map,
|
|
87
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if ctx.trace:
|
|
91
|
+
ctx.trace.nav_target = target
|
|
92
|
+
|
|
93
|
+
dist = _manhattan(ctx.state.position, target)
|
|
94
|
+
if dist <= 1:
|
|
95
|
+
# Track attempts on this specific junction
|
|
96
|
+
attempts_key = f"align_attempts_{target}"
|
|
97
|
+
attempts = ctx.blackboard.get(attempts_key, 0) + 1
|
|
98
|
+
ctx.blackboard[attempts_key] = attempts
|
|
99
|
+
|
|
100
|
+
if attempts > self.MAX_ATTEMPTS_PER_TARGET:
|
|
101
|
+
# Mark this junction as failed temporarily
|
|
102
|
+
failed_key = f"align_failed_{target}"
|
|
103
|
+
ctx.blackboard[failed_key] = ctx.step
|
|
104
|
+
ctx.blackboard[attempts_key] = 0
|
|
105
|
+
if ctx.trace:
|
|
106
|
+
ctx.trace.activate(self.name, f"giving up on {target}")
|
|
107
|
+
# Clear and try a different junction next tick
|
|
108
|
+
return ctx.navigator.explore(
|
|
109
|
+
ctx.state.position,
|
|
110
|
+
ctx.map,
|
|
111
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if ctx.trace:
|
|
115
|
+
ctx.trace.activate(self.name, f"bump {attempts}/{self.MAX_ATTEMPTS_PER_TARGET}")
|
|
116
|
+
return _move_toward(ctx.state.position, target)
|
|
117
|
+
|
|
118
|
+
# Not adjacent - reset attempts for this target
|
|
119
|
+
attempts_key = f"align_attempts_{target}"
|
|
120
|
+
ctx.blackboard[attempts_key] = 0
|
|
121
|
+
return ctx.navigator.get_action(ctx.state.position, target, ctx.map, reach_adjacent=True)
|
|
122
|
+
|
|
123
|
+
def _find_best_target(self, ctx: CogasContext) -> tuple[int, int] | None:
|
|
124
|
+
"""Find nearest neutral junction, including contested ones."""
|
|
125
|
+
pos = ctx.state.position
|
|
126
|
+
|
|
127
|
+
def recently_failed(p: tuple[int, int]) -> bool:
|
|
128
|
+
failed_step = ctx.blackboard.get(f"align_failed_{p}", -9999)
|
|
129
|
+
return ctx.step - failed_step < self.COOLDOWN_STEPS
|
|
130
|
+
|
|
131
|
+
# Find neutral junctions (no AOE filter — aligners go where needed)
|
|
132
|
+
candidates: list[tuple[int, tuple[int, int]]] = []
|
|
133
|
+
|
|
134
|
+
for jpos, e in ctx.map.find(type_contains="junction"):
|
|
135
|
+
alignment = e.properties.get("alignment")
|
|
136
|
+
if alignment is not None:
|
|
137
|
+
continue # Not neutral
|
|
138
|
+
if recently_failed(jpos):
|
|
139
|
+
continue
|
|
140
|
+
candidates.append((_manhattan(pos, jpos), jpos))
|
|
141
|
+
|
|
142
|
+
if not candidates:
|
|
143
|
+
return None
|
|
144
|
+
candidates.sort()
|
|
145
|
+
return candidates[0][1]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _move_toward(current: tuple[int, int], target: tuple[int, int]) -> Action:
|
|
149
|
+
dr = target[0] - current[0]
|
|
150
|
+
dc = target[1] - current[1]
|
|
151
|
+
if abs(dr) >= abs(dc):
|
|
152
|
+
if dr > 0:
|
|
153
|
+
return Action(name="move_south")
|
|
154
|
+
elif dr < 0:
|
|
155
|
+
return Action(name="move_north")
|
|
156
|
+
if dc > 0:
|
|
157
|
+
return Action(name="move_east")
|
|
158
|
+
elif dc < 0:
|
|
159
|
+
return Action(name="move_west")
|
|
160
|
+
return Action(name="move_north")
|