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,174 @@
|
|
|
1
|
+
"""Scrambler goals — neutralize enemy junctions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
from cogames_agents.policy.scripted_agent.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 GetScramblerGearGoal(GetGearGoal):
|
|
20
|
+
"""Get scrambler gear (costs C1 O3 G1 S1 from collective)."""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
super().__init__(
|
|
24
|
+
gear_attr="scrambler_gear",
|
|
25
|
+
station_type="scrambler_station",
|
|
26
|
+
goal_name="GetScramblerGear",
|
|
27
|
+
gear_cost={"carbon": 1, "oxygen": 3, "germanium": 1, "silicon": 1},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ScrambleJunctionGoal(Goal):
|
|
32
|
+
"""Find and scramble enemy (clips) junctions.
|
|
33
|
+
|
|
34
|
+
Tracks attempts per junction to avoid getting stuck.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name = "ScrambleJunction"
|
|
38
|
+
MAX_ATTEMPTS_PER_TARGET = 5
|
|
39
|
+
MAX_NAV_STEPS_PER_TARGET = 40 # Give up navigating to a target after this many steps
|
|
40
|
+
COOLDOWN_STEPS = 50
|
|
41
|
+
|
|
42
|
+
def is_satisfied(self, ctx: CogasContext) -> bool:
|
|
43
|
+
# Can't scramble without gear and a heart
|
|
44
|
+
if not ctx.state.scrambler_gear:
|
|
45
|
+
if ctx.trace:
|
|
46
|
+
ctx.trace.skip(self.name, "no gear")
|
|
47
|
+
return True
|
|
48
|
+
if ctx.state.heart < 1:
|
|
49
|
+
if ctx.trace:
|
|
50
|
+
ctx.trace.skip(self.name, "no heart")
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def execute(self, ctx: CogasContext) -> Optional[Action]:
|
|
55
|
+
# Track navigation steps toward current target to detect stuck
|
|
56
|
+
nav_key = "_scramble_nav_steps"
|
|
57
|
+
nav_target_key = "_scramble_nav_target"
|
|
58
|
+
nav_steps = ctx.blackboard.get(nav_key, 0) + 1
|
|
59
|
+
ctx.blackboard[nav_key] = nav_steps
|
|
60
|
+
|
|
61
|
+
target = self._find_best_target(ctx)
|
|
62
|
+
if target is None:
|
|
63
|
+
ctx.blackboard[nav_key] = 0
|
|
64
|
+
return ctx.navigator.explore(
|
|
65
|
+
ctx.state.position,
|
|
66
|
+
ctx.map,
|
|
67
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Reset nav counter if target changed
|
|
71
|
+
prev_target = ctx.blackboard.get(nav_target_key)
|
|
72
|
+
if prev_target != target:
|
|
73
|
+
ctx.blackboard[nav_key] = 0
|
|
74
|
+
nav_steps = 0
|
|
75
|
+
ctx.blackboard[nav_target_key] = target
|
|
76
|
+
|
|
77
|
+
# If we've been navigating too long, mark target as failed
|
|
78
|
+
if nav_steps > self.MAX_NAV_STEPS_PER_TARGET:
|
|
79
|
+
failed_key = f"scramble_failed_{target}"
|
|
80
|
+
ctx.blackboard[failed_key] = ctx.step
|
|
81
|
+
ctx.blackboard[nav_key] = 0
|
|
82
|
+
if ctx.trace:
|
|
83
|
+
ctx.trace.activate(self.name, f"nav timeout on {target}")
|
|
84
|
+
return ctx.navigator.explore(
|
|
85
|
+
ctx.state.position,
|
|
86
|
+
ctx.map,
|
|
87
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if ctx.trace:
|
|
91
|
+
ctx.trace.nav_target = target
|
|
92
|
+
|
|
93
|
+
dist = _manhattan(ctx.state.position, target)
|
|
94
|
+
if dist <= 1:
|
|
95
|
+
# Track attempts on this specific junction
|
|
96
|
+
attempts_key = f"scramble_attempts_{target}"
|
|
97
|
+
attempts = ctx.blackboard.get(attempts_key, 0) + 1
|
|
98
|
+
ctx.blackboard[attempts_key] = attempts
|
|
99
|
+
|
|
100
|
+
if attempts > self.MAX_ATTEMPTS_PER_TARGET:
|
|
101
|
+
# Mark this junction as failed temporarily
|
|
102
|
+
failed_key = f"scramble_failed_{target}"
|
|
103
|
+
ctx.blackboard[failed_key] = ctx.step
|
|
104
|
+
ctx.blackboard[attempts_key] = 0
|
|
105
|
+
if ctx.trace:
|
|
106
|
+
ctx.trace.activate(self.name, f"giving up on {target}")
|
|
107
|
+
return ctx.navigator.explore(
|
|
108
|
+
ctx.state.position,
|
|
109
|
+
ctx.map,
|
|
110
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if ctx.trace:
|
|
114
|
+
ctx.trace.activate(self.name, f"bump {attempts}/{self.MAX_ATTEMPTS_PER_TARGET}")
|
|
115
|
+
return _move_toward(ctx.state.position, target)
|
|
116
|
+
|
|
117
|
+
# Not adjacent - reset attempts for this target
|
|
118
|
+
attempts_key = f"scramble_attempts_{target}"
|
|
119
|
+
ctx.blackboard[attempts_key] = 0
|
|
120
|
+
return ctx.navigator.get_action(ctx.state.position, target, ctx.map, reach_adjacent=True)
|
|
121
|
+
|
|
122
|
+
def _find_best_target(self, ctx: CogasContext) -> tuple[int, int] | None:
|
|
123
|
+
"""Find enemy junction to scramble, prioritized by blocking count."""
|
|
124
|
+
pos = ctx.state.position
|
|
125
|
+
|
|
126
|
+
def recently_failed(p: tuple[int, int]) -> bool:
|
|
127
|
+
failed_step = ctx.blackboard.get(f"scramble_failed_{p}", -9999)
|
|
128
|
+
return ctx.step - failed_step < self.COOLDOWN_STEPS
|
|
129
|
+
|
|
130
|
+
# Get clips junctions
|
|
131
|
+
enemy: list[tuple[tuple[int, int], dict]] = []
|
|
132
|
+
for jpos, e in ctx.map.find(type_contains="junction", property_filter={"alignment": "clips"}):
|
|
133
|
+
if not recently_failed(jpos):
|
|
134
|
+
enemy.append((jpos, e.properties))
|
|
135
|
+
for cpos, e in ctx.map.find(type_contains="junction", property_filter={"alignment": "clips"}):
|
|
136
|
+
if not recently_failed(cpos):
|
|
137
|
+
enemy.append((cpos, e.properties))
|
|
138
|
+
|
|
139
|
+
if not enemy:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
# Get neutral junctions for scoring
|
|
143
|
+
neutral_positions: list[tuple[int, int]] = []
|
|
144
|
+
for jpos, e in ctx.map.find(type_contains="junction"):
|
|
145
|
+
if e.properties.get("alignment") is None:
|
|
146
|
+
neutral_positions.append(jpos)
|
|
147
|
+
for cpos, e in ctx.map.find(type_contains="junction"):
|
|
148
|
+
if e.properties.get("alignment") is None:
|
|
149
|
+
neutral_positions.append(cpos)
|
|
150
|
+
|
|
151
|
+
# Score by: how many neutrals this enemy blocks, then by distance
|
|
152
|
+
scored: list[tuple[int, int, tuple[int, int]]] = []
|
|
153
|
+
for epos, _ in enemy:
|
|
154
|
+
blocked = sum(1 for np in neutral_positions if _manhattan(epos, np) <= JUNCTION_AOE_RANGE)
|
|
155
|
+
dist = _manhattan(pos, epos)
|
|
156
|
+
scored.append((-blocked, dist, epos)) # Negative blocked for descending sort
|
|
157
|
+
|
|
158
|
+
scored.sort()
|
|
159
|
+
return scored[0][2]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _move_toward(current: tuple[int, int], target: tuple[int, int]) -> Action:
|
|
163
|
+
dr = target[0] - current[0]
|
|
164
|
+
dc = target[1] - current[1]
|
|
165
|
+
if abs(dr) >= abs(dc):
|
|
166
|
+
if dr > 0:
|
|
167
|
+
return Action(name="move_south")
|
|
168
|
+
elif dr < 0:
|
|
169
|
+
return Action(name="move_north")
|
|
170
|
+
if dc > 0:
|
|
171
|
+
return Action(name="move_east")
|
|
172
|
+
elif dc < 0:
|
|
173
|
+
return Action(name="move_west")
|
|
174
|
+
return Action(name="move_north")
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Shared goals used by multiple roles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
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
|
+
if TYPE_CHECKING:
|
|
12
|
+
from cogames_agents.policy.scripted_agent.cogas.context import CogasContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GetHeartsGoal(Goal):
|
|
16
|
+
"""Navigate to a chest to acquire hearts.
|
|
17
|
+
|
|
18
|
+
Hearts cost 1 of each element from the collective. Skip if the
|
|
19
|
+
collective can't afford it to avoid wasting time at the chest.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name = "GetHearts"
|
|
23
|
+
# Cost per heart: 1 of each element
|
|
24
|
+
HEART_COST = {"carbon": 1, "oxygen": 1, "germanium": 1, "silicon": 1}
|
|
25
|
+
|
|
26
|
+
def __init__(self, min_hearts: int = 1) -> None:
|
|
27
|
+
self._min_hearts = min_hearts
|
|
28
|
+
|
|
29
|
+
# Minimum collective resource reserve — don't consume below this level
|
|
30
|
+
# Reduced from 3 to 1 to allow earlier heart acquisition
|
|
31
|
+
RESOURCE_RESERVE = 1
|
|
32
|
+
|
|
33
|
+
def _collective_can_afford_heart(self, ctx: CogasContext) -> bool:
|
|
34
|
+
s = ctx.state
|
|
35
|
+
r = self.RESOURCE_RESERVE
|
|
36
|
+
return (
|
|
37
|
+
s.collective_carbon >= 1 + r
|
|
38
|
+
and s.collective_oxygen >= 1 + r
|
|
39
|
+
and s.collective_germanium >= 1 + r
|
|
40
|
+
and s.collective_silicon >= 1 + r
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def is_satisfied(self, ctx: CogasContext) -> bool:
|
|
44
|
+
if ctx.state.heart >= self._min_hearts:
|
|
45
|
+
return True
|
|
46
|
+
# Skip if collective can't afford a heart
|
|
47
|
+
if not self._collective_can_afford_heart(ctx):
|
|
48
|
+
if ctx.trace:
|
|
49
|
+
ctx.trace.skip(self.name, "collective lacks resources for heart")
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def execute(self, ctx: CogasContext) -> Action:
|
|
54
|
+
# Find own team's chest
|
|
55
|
+
pf = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
|
|
56
|
+
result = ctx.map.find_nearest(ctx.state.position, type_contains="chest", property_filter=pf)
|
|
57
|
+
if result is None:
|
|
58
|
+
# Try hub as fallback
|
|
59
|
+
result = ctx.map.find_nearest(ctx.state.position, type_contains="hub", property_filter=pf)
|
|
60
|
+
if result is None:
|
|
61
|
+
return ctx.navigator.explore(ctx.state.position, ctx.map)
|
|
62
|
+
|
|
63
|
+
chest_pos, _ = result
|
|
64
|
+
if ctx.trace:
|
|
65
|
+
ctx.trace.nav_target = chest_pos
|
|
66
|
+
|
|
67
|
+
dist = _manhattan(ctx.state.position, chest_pos)
|
|
68
|
+
if dist <= 1:
|
|
69
|
+
return _move_toward(ctx.state.position, chest_pos)
|
|
70
|
+
return ctx.navigator.get_action(ctx.state.position, chest_pos, ctx.map, reach_adjacent=True)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FallbackMineGoal(Goal):
|
|
74
|
+
"""Fallback: mine resources when combat roles can't act.
|
|
75
|
+
|
|
76
|
+
Used at the bottom of aligner/scrambler goal lists so they contribute
|
|
77
|
+
to the economy instead of idling when they lack gear or hearts.
|
|
78
|
+
|
|
79
|
+
NEVER satisfied - always provides something to do rather than noop.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
name = "FallbackMine"
|
|
83
|
+
|
|
84
|
+
def is_satisfied(self, ctx: CogasContext) -> bool:
|
|
85
|
+
# Never satisfied - always mine/explore as fallback to avoid noops
|
|
86
|
+
# This ensures aligners/scramblers always have productive work
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def execute(self, ctx: CogasContext) -> Action:
|
|
90
|
+
from .miner import RESOURCE_TYPES, _extractor_recently_failed
|
|
91
|
+
|
|
92
|
+
# If carrying resources, deposit first
|
|
93
|
+
if ctx.state.cargo_total > 0:
|
|
94
|
+
depot_pos = _find_deposit(ctx)
|
|
95
|
+
if depot_pos is not None:
|
|
96
|
+
if ctx.trace:
|
|
97
|
+
ctx.trace.nav_target = depot_pos
|
|
98
|
+
dist = _manhattan(ctx.state.position, depot_pos)
|
|
99
|
+
if dist <= 1:
|
|
100
|
+
return _move_toward(ctx.state.position, depot_pos)
|
|
101
|
+
return ctx.navigator.get_action(ctx.state.position, depot_pos, ctx.map, reach_adjacent=True)
|
|
102
|
+
|
|
103
|
+
# Find nearest usable extractor (any resource type)
|
|
104
|
+
best: tuple[int, tuple[int, int]] | None = None
|
|
105
|
+
for resource in RESOURCE_TYPES:
|
|
106
|
+
for pos, e in ctx.map.find(type=f"{resource}_extractor"):
|
|
107
|
+
if e.properties.get("remaining_uses", 999) <= 0:
|
|
108
|
+
continue
|
|
109
|
+
if e.properties.get("inventory_amount", -1) == 0:
|
|
110
|
+
continue
|
|
111
|
+
if _extractor_recently_failed(ctx, pos):
|
|
112
|
+
continue
|
|
113
|
+
d = _manhattan(ctx.state.position, pos)
|
|
114
|
+
if best is None or d < best[0]:
|
|
115
|
+
best = (d, pos)
|
|
116
|
+
|
|
117
|
+
if best is not None:
|
|
118
|
+
if ctx.trace:
|
|
119
|
+
ctx.trace.nav_target = best[1]
|
|
120
|
+
dist = best[0]
|
|
121
|
+
if dist <= 1:
|
|
122
|
+
return _move_toward(ctx.state.position, best[1])
|
|
123
|
+
return ctx.navigator.get_action(ctx.state.position, best[1], ctx.map, reach_adjacent=True)
|
|
124
|
+
|
|
125
|
+
# No extractors known — explore
|
|
126
|
+
return ctx.navigator.explore(
|
|
127
|
+
ctx.state.position,
|
|
128
|
+
ctx.map,
|
|
129
|
+
direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _find_deposit(ctx: "CogasContext") -> tuple[int, int] | None:
|
|
134
|
+
"""Find nearest cogs-aligned depot for depositing resources."""
|
|
135
|
+
pos = ctx.state.position
|
|
136
|
+
hub_filter = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
|
|
137
|
+
candidates: list[tuple[int, tuple[int, int]]] = []
|
|
138
|
+
for apos, _ in ctx.map.find(type_contains="hub", property_filter=hub_filter):
|
|
139
|
+
candidates.append((_manhattan(pos, apos), apos))
|
|
140
|
+
for jpos, _ in ctx.map.find(type_contains="junction", property_filter={"alignment": "cogs"}):
|
|
141
|
+
candidates.append((_manhattan(pos, jpos), jpos))
|
|
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")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Stem goal — select a role based on map and collective state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from cogames_agents.policy.scripted_agent.cogas.goal import Goal
|
|
8
|
+
from mettagrid.simulator import Action
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from cogames_agents.policy.scripted_agent.cogas.context import CogasContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SelectRoleGoal(Goal):
|
|
15
|
+
"""Evaluate map + collective inventory to select a role.
|
|
16
|
+
|
|
17
|
+
Once a role is selected, the agent's goal list is replaced with
|
|
18
|
+
the selected role's goal list. This is a one-time decision.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name = "SelectRole"
|
|
22
|
+
|
|
23
|
+
def __init__(self, role_goal_lists: dict | None = None) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Args:
|
|
26
|
+
role_goal_lists: Deprecated, ignored. Roles are now vibe-driven.
|
|
27
|
+
"""
|
|
28
|
+
self._selected = False
|
|
29
|
+
|
|
30
|
+
def is_satisfied(self, ctx: CogasContext) -> bool:
|
|
31
|
+
return self._selected
|
|
32
|
+
|
|
33
|
+
def execute(self, ctx: CogasContext) -> Action:
|
|
34
|
+
role = self._select_role(ctx)
|
|
35
|
+
ctx.blackboard["selected_role"] = role
|
|
36
|
+
ctx.blackboard["change_role"] = role
|
|
37
|
+
self._selected = True
|
|
38
|
+
|
|
39
|
+
if ctx.trace:
|
|
40
|
+
ctx.trace.activate(self.name, f"selected={role}")
|
|
41
|
+
|
|
42
|
+
# Return change_vibe action to immediately start the new role
|
|
43
|
+
return Action(name=f"change_vibe_{role}")
|
|
44
|
+
|
|
45
|
+
def _select_role(self, ctx: CogasContext) -> str:
|
|
46
|
+
"""Distribute roles to match planky's defaults: 3 miners + 5 aligners.
|
|
47
|
+
|
|
48
|
+
For small teams, prioritize mining since resources are needed for hearts.
|
|
49
|
+
Uses planky's exact distribution pattern, tiled across agent IDs.
|
|
50
|
+
"""
|
|
51
|
+
agent_id = ctx.agent_id
|
|
52
|
+
|
|
53
|
+
# Planky's default pattern: [miner, miner, miner, aligner, aligner, aligner, aligner, aligner]
|
|
54
|
+
# Index into this pattern based on agent_id
|
|
55
|
+
planky_pattern_size = 8 # 3 miners + 5 aligners
|
|
56
|
+
pattern_index = agent_id % planky_pattern_size
|
|
57
|
+
|
|
58
|
+
if pattern_index < 3:
|
|
59
|
+
return "miner"
|
|
60
|
+
return "aligner"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""SurviveGoal — retreat to safety when HP is critical."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from cogames_agents.policy.scripted_agent.cogas.goal import Goal
|
|
8
|
+
from cogames_agents.policy.scripted_agent.cogas.navigator import _manhattan, _move_action
|
|
9
|
+
from mettagrid.simulator import Action
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from cogames_agents.policy.scripted_agent.cogas.context import CogasContext
|
|
13
|
+
|
|
14
|
+
# Game constants
|
|
15
|
+
JUNCTION_AOE_RANGE = 10
|
|
16
|
+
HP_SAFETY_MARGIN = 10
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SurviveGoal(Goal):
|
|
20
|
+
"""Retreat to nearest safe zone when HP is low."""
|
|
21
|
+
|
|
22
|
+
name = "Survive"
|
|
23
|
+
|
|
24
|
+
def __init__(self, hp_threshold: int = 30) -> None:
|
|
25
|
+
self._hp_threshold = hp_threshold
|
|
26
|
+
|
|
27
|
+
def is_satisfied(self, ctx: CogasContext) -> bool:
|
|
28
|
+
# If we're in a safe zone, we're fine
|
|
29
|
+
if _is_in_safe_zone(ctx):
|
|
30
|
+
return True
|
|
31
|
+
# If HP is above threshold, we're fine
|
|
32
|
+
safe_pos = _nearest_safe_zone(ctx)
|
|
33
|
+
if safe_pos is None:
|
|
34
|
+
return ctx.state.hp > 20 # No known safe zone, be conservative
|
|
35
|
+
steps_to_safety = max(0, _manhattan(ctx.state.position, safe_pos) - JUNCTION_AOE_RANGE)
|
|
36
|
+
hp_needed = steps_to_safety + HP_SAFETY_MARGIN
|
|
37
|
+
return ctx.state.hp > hp_needed
|
|
38
|
+
|
|
39
|
+
def execute(self, ctx: CogasContext) -> Action:
|
|
40
|
+
safe_pos = _nearest_safe_zone(ctx)
|
|
41
|
+
if safe_pos is None:
|
|
42
|
+
return ctx.navigator.explore(ctx.state.position, ctx.map)
|
|
43
|
+
if ctx.trace:
|
|
44
|
+
ctx.trace.nav_target = safe_pos
|
|
45
|
+
# Check dist to avoid navigator returning noop
|
|
46
|
+
dist = _manhattan(ctx.state.position, safe_pos)
|
|
47
|
+
if dist <= 1:
|
|
48
|
+
return _move_action(ctx.state.position, safe_pos)
|
|
49
|
+
return ctx.navigator.get_action(ctx.state.position, safe_pos, ctx.map, reach_adjacent=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_in_safe_zone(ctx: CogasContext) -> bool:
|
|
53
|
+
"""Check if agent is within AOE of any cogs structure."""
|
|
54
|
+
pos = ctx.state.position
|
|
55
|
+
# Check hub
|
|
56
|
+
hubs = ctx.map.find(type="hub")
|
|
57
|
+
for apos, _ in hubs:
|
|
58
|
+
if _manhattan(pos, apos) <= JUNCTION_AOE_RANGE:
|
|
59
|
+
return True
|
|
60
|
+
# Check cogs junctions
|
|
61
|
+
junctions = ctx.map.find(type_contains="junction", property_filter={"alignment": "cogs"})
|
|
62
|
+
for jpos, _ in junctions:
|
|
63
|
+
if _manhattan(pos, jpos) <= JUNCTION_AOE_RANGE:
|
|
64
|
+
return True
|
|
65
|
+
# Check cogs junctions
|
|
66
|
+
junctions = ctx.map.find(type_contains="junction", property_filter={"alignment": "cogs"})
|
|
67
|
+
for cpos, _ in junctions:
|
|
68
|
+
if _manhattan(pos, cpos) <= JUNCTION_AOE_RANGE:
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_in_enemy_aoe(ctx: CogasContext) -> bool:
|
|
74
|
+
"""Check if agent is within AOE of any clips structure."""
|
|
75
|
+
pos = ctx.state.position
|
|
76
|
+
for jpos, _ in ctx.map.find(type_contains="junction", property_filter={"alignment": "clips"}):
|
|
77
|
+
if _manhattan(pos, jpos) <= JUNCTION_AOE_RANGE:
|
|
78
|
+
return True
|
|
79
|
+
for cpos, _ in ctx.map.find(type_contains="junction", property_filter={"alignment": "clips"}):
|
|
80
|
+
if _manhattan(pos, cpos) <= JUNCTION_AOE_RANGE:
|
|
81
|
+
return True
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _nearest_safe_zone(ctx: CogasContext) -> tuple[int, int] | None:
|
|
86
|
+
"""Find nearest cogs-aligned structure."""
|
|
87
|
+
pos = ctx.state.position
|
|
88
|
+
candidates: list[tuple[int, tuple[int, int]]] = []
|
|
89
|
+
|
|
90
|
+
for apos, _ in ctx.map.find(type="hub"):
|
|
91
|
+
candidates.append((_manhattan(pos, apos), apos))
|
|
92
|
+
for jpos, _ in ctx.map.find(type_contains="junction", property_filter={"alignment": "cogs"}):
|
|
93
|
+
candidates.append((_manhattan(pos, jpos), jpos))
|
|
94
|
+
for cpos, _ in ctx.map.find(type_contains="junction", property_filter={"alignment": "cogs"}):
|
|
95
|
+
candidates.append((_manhattan(pos, cpos), cpos))
|
|
96
|
+
|
|
97
|
+
if not candidates:
|
|
98
|
+
return None
|
|
99
|
+
candidates.sort()
|
|
100
|
+
return candidates[0][1]
|