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,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scout behavior for Pinky policy.
|
|
3
|
+
|
|
4
|
+
Scouts explore the map to discover structures for other roles.
|
|
5
|
+
Strategy: Frontier-based exploration, venture deep with +400 HP from gear.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import deque
|
|
11
|
+
from typing import TYPE_CHECKING, Optional
|
|
12
|
+
|
|
13
|
+
from cogames_agents.policy.scripted_agent.pinky.behaviors.base import Services, is_adjacent
|
|
14
|
+
from cogames_agents.policy.scripted_agent.pinky.types import DEBUG, ROLE_TO_STATION, RiskTolerance, Role
|
|
15
|
+
from mettagrid.simulator import Action
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from cogames_agents.policy.scripted_agent.pinky.state import AgentState
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ScoutBehavior:
|
|
22
|
+
"""Scout agent: explore and discover the map."""
|
|
23
|
+
|
|
24
|
+
role = Role.SCOUT
|
|
25
|
+
risk_tolerance = RiskTolerance.AGGRESSIVE
|
|
26
|
+
|
|
27
|
+
def act(self, state: AgentState, services: Services) -> Action:
|
|
28
|
+
"""Execute scout behavior."""
|
|
29
|
+
# Priority 1: Retreat only if critically low HP (scouts are tanky)
|
|
30
|
+
if state.hp < 50:
|
|
31
|
+
if DEBUG:
|
|
32
|
+
print(f"[A{state.agent_id}] SCOUT: Retreating! HP={state.hp}")
|
|
33
|
+
return self._retreat_to_safety(state, services)
|
|
34
|
+
|
|
35
|
+
# Priority 2: Get gear if missing (high priority - +400 HP is huge)
|
|
36
|
+
if self.needs_gear(state):
|
|
37
|
+
return self._get_gear(state, services)
|
|
38
|
+
|
|
39
|
+
# Priority 3: Frontier-based exploration
|
|
40
|
+
return self._explore_frontier(state, services)
|
|
41
|
+
|
|
42
|
+
def needs_gear(self, state: AgentState) -> bool:
|
|
43
|
+
"""Scouts need scout gear for +400 HP."""
|
|
44
|
+
return not state.scout_gear
|
|
45
|
+
|
|
46
|
+
def has_resources_to_act(self, state: AgentState) -> bool:
|
|
47
|
+
"""Scouts don't need resources to explore."""
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
def _retreat_to_safety(self, state: AgentState, services: Services) -> Action:
|
|
51
|
+
"""Return to nearest safe zone."""
|
|
52
|
+
safe_pos = services.safety.nearest_safe_zone(state)
|
|
53
|
+
if safe_pos is None:
|
|
54
|
+
# No safe zone known, just explore
|
|
55
|
+
return services.navigator.explore(state)
|
|
56
|
+
return services.navigator.move_to(state, safe_pos, reach_adjacent=True)
|
|
57
|
+
|
|
58
|
+
def _get_gear(self, state: AgentState, services: Services) -> Action:
|
|
59
|
+
"""Get scout gear from station."""
|
|
60
|
+
station_name = ROLE_TO_STATION[Role.SCOUT]
|
|
61
|
+
station_pos = state.map.stations.get(station_name)
|
|
62
|
+
|
|
63
|
+
if station_pos is None:
|
|
64
|
+
# No gear station found after initial search, proceed without gear
|
|
65
|
+
if state.step > 20:
|
|
66
|
+
return self._explore_frontier(state, services)
|
|
67
|
+
if DEBUG:
|
|
68
|
+
print(f"[A{state.agent_id}] SCOUT: Station not found, exploring")
|
|
69
|
+
return services.navigator.explore(state)
|
|
70
|
+
|
|
71
|
+
if is_adjacent(state.pos, station_pos):
|
|
72
|
+
if DEBUG:
|
|
73
|
+
print(f"[A{state.agent_id}] SCOUT: Getting gear from {station_pos}")
|
|
74
|
+
return services.navigator.use_object_at(state, station_pos)
|
|
75
|
+
|
|
76
|
+
return services.navigator.move_to(state, station_pos, reach_adjacent=True)
|
|
77
|
+
|
|
78
|
+
def _explore_frontier(self, state: AgentState, services: Services) -> Action:
|
|
79
|
+
"""Find and move toward nearest unexplored frontier cell."""
|
|
80
|
+
frontier = self._find_nearest_frontier(state)
|
|
81
|
+
|
|
82
|
+
if frontier is None:
|
|
83
|
+
# Map fully explored or boxed in - patrol
|
|
84
|
+
if DEBUG and state.step % 50 == 0:
|
|
85
|
+
explored = sum(sum(row) for row in state.map.explored)
|
|
86
|
+
total = state.map.grid_size * state.map.grid_size
|
|
87
|
+
print(f"[A{state.agent_id}] SCOUT: No frontier, explored={explored}/{total}")
|
|
88
|
+
return services.navigator.explore(state)
|
|
89
|
+
|
|
90
|
+
return services.navigator.move_to(state, frontier)
|
|
91
|
+
|
|
92
|
+
def _find_nearest_frontier(self, state: AgentState) -> Optional[tuple[int, int]]:
|
|
93
|
+
"""BFS to find nearest unexplored cell adjacent to explored cell.
|
|
94
|
+
|
|
95
|
+
A frontier is an unexplored cell next to an explored cell.
|
|
96
|
+
"""
|
|
97
|
+
if not state.map.explored:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
start = state.pos
|
|
101
|
+
visited: set[tuple[int, int]] = {start}
|
|
102
|
+
queue: deque[tuple[tuple[int, int], Optional[tuple[int, int]]]] = deque()
|
|
103
|
+
queue.append((start, None))
|
|
104
|
+
|
|
105
|
+
directions = [(-1, 0), (1, 0), (0, 1), (0, -1)]
|
|
106
|
+
|
|
107
|
+
while queue:
|
|
108
|
+
pos, first_step = queue.popleft()
|
|
109
|
+
r, c = pos
|
|
110
|
+
|
|
111
|
+
for dr, dc in directions:
|
|
112
|
+
nr, nc = r + dr, c + dc
|
|
113
|
+
|
|
114
|
+
# Check bounds
|
|
115
|
+
if not (0 <= nr < state.map.grid_size and 0 <= nc < state.map.grid_size):
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if (nr, nc) in visited:
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
visited.add((nr, nc))
|
|
122
|
+
|
|
123
|
+
# Found unexplored cell - this is our frontier target
|
|
124
|
+
if not state.map.explored[nr][nc]:
|
|
125
|
+
if first_step is None:
|
|
126
|
+
return (nr, nc)
|
|
127
|
+
return first_step
|
|
128
|
+
|
|
129
|
+
# Continue BFS through explored, free cells
|
|
130
|
+
from cogames_agents.policy.scripted_agent.pinky.types import CellType
|
|
131
|
+
|
|
132
|
+
if state.map.occupancy[nr][nc] == CellType.FREE.value:
|
|
133
|
+
next_first_step = first_step
|
|
134
|
+
if first_step is None and (r, c) == start:
|
|
135
|
+
next_first_step = (nr, nc)
|
|
136
|
+
queue.append(((nr, nc), next_first_step))
|
|
137
|
+
|
|
138
|
+
return None
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scrambler behavior for Pinky policy.
|
|
3
|
+
|
|
4
|
+
Scramblers raid enemy junctions to neutralize them, enabling aligners.
|
|
5
|
+
Strategy: Get hearts, target enemy junctions that block neutral territory.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Optional
|
|
11
|
+
|
|
12
|
+
from cogames_agents.policy.scripted_agent.pinky.behaviors.base import (
|
|
13
|
+
Services,
|
|
14
|
+
explore_for_station,
|
|
15
|
+
get_explore_direction_for_agent,
|
|
16
|
+
is_adjacent,
|
|
17
|
+
manhattan_distance,
|
|
18
|
+
)
|
|
19
|
+
from cogames_agents.policy.scripted_agent.pinky.types import (
|
|
20
|
+
DEBUG,
|
|
21
|
+
JUNCTION_AOE_RANGE,
|
|
22
|
+
ROLE_TO_STATION,
|
|
23
|
+
DebugInfo,
|
|
24
|
+
RiskTolerance,
|
|
25
|
+
Role,
|
|
26
|
+
StructureInfo,
|
|
27
|
+
)
|
|
28
|
+
from mettagrid.simulator import Action
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from cogames_agents.policy.scripted_agent.pinky.state import AgentState
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ScramblerBehavior:
|
|
35
|
+
"""Scrambler agent: neutralize enemy junctions."""
|
|
36
|
+
|
|
37
|
+
role = Role.SCRAMBLER
|
|
38
|
+
risk_tolerance = RiskTolerance.AGGRESSIVE
|
|
39
|
+
|
|
40
|
+
# How many steps to explore before giving up on gear
|
|
41
|
+
EXPLORATION_STEPS = 100
|
|
42
|
+
|
|
43
|
+
# How many ticks to explore before retrying gear station
|
|
44
|
+
GEAR_RETRY_INTERVAL = 100
|
|
45
|
+
|
|
46
|
+
# How many steps before junction alignment data is considered stale
|
|
47
|
+
# Since alignment can change (e.g., enemy aligners convert junctions),
|
|
48
|
+
# scramblers should revisit old junctions to check for new targets
|
|
49
|
+
JUNCTION_STALE_THRESHOLD = 50
|
|
50
|
+
|
|
51
|
+
def act(self, state: AgentState, services: Services) -> Action:
|
|
52
|
+
"""Execute scrambler behavior.
|
|
53
|
+
|
|
54
|
+
Priority order:
|
|
55
|
+
1. Stuck detection - break out of stuck loops (using navigator's escape handling)
|
|
56
|
+
2. Critical HP retreat - survive first
|
|
57
|
+
3. Get gear (REQUIRED) - must have scrambler gear to scramble effectively
|
|
58
|
+
4. Get hearts (REQUIRED) - must have hearts to scramble
|
|
59
|
+
5. Hunt and scramble enemy junctions
|
|
60
|
+
"""
|
|
61
|
+
# Track gear state
|
|
62
|
+
state.had_gear_last_step = state.scrambler_gear
|
|
63
|
+
|
|
64
|
+
# Priority 0: Check for stuck patterns and handle escape mode (via navigator)
|
|
65
|
+
escape_action = services.navigator.check_and_handle_escape(state)
|
|
66
|
+
if escape_action:
|
|
67
|
+
debug_info = services.navigator.get_escape_debug_info(state)
|
|
68
|
+
state.debug_info = DebugInfo(**debug_info)
|
|
69
|
+
return escape_action
|
|
70
|
+
|
|
71
|
+
# Priority 1: Retreat only if critically low (scramblers are tanky)
|
|
72
|
+
# Only retreat when HP drops below 30 (they start with 50)
|
|
73
|
+
if state.hp < 30:
|
|
74
|
+
if DEBUG:
|
|
75
|
+
print(f"[A{state.agent_id}] SCRAMBLER: Retreating! HP={state.hp}")
|
|
76
|
+
state.debug_info = DebugInfo(mode="retreat", goal="safety", target_object="safe_zone", signal="hp_low")
|
|
77
|
+
return self._retreat_to_safety(state, services)
|
|
78
|
+
|
|
79
|
+
# Priority 2: Get gear (REQUIRED) - scrambler gear is needed to scramble effectively
|
|
80
|
+
# Gear gives +200 HP which is essential for surviving in enemy territory
|
|
81
|
+
if self.needs_gear(state):
|
|
82
|
+
return self._get_gear(state, services)
|
|
83
|
+
|
|
84
|
+
# Priority 3: Get hearts (REQUIRED) - hearts are needed to scramble junctions
|
|
85
|
+
if not self.has_resources_to_act(state):
|
|
86
|
+
return self._get_hearts(state, services)
|
|
87
|
+
|
|
88
|
+
# Priority 4: Hunt and scramble enemy junctions
|
|
89
|
+
return self._scramble_junction(state, services)
|
|
90
|
+
|
|
91
|
+
def needs_gear(self, state: AgentState) -> bool:
|
|
92
|
+
"""Scramblers MUST have scrambler gear to scramble effectively.
|
|
93
|
+
|
|
94
|
+
Gear provides +200 HP which is essential for surviving enemy territory.
|
|
95
|
+
Without gear, scramblers would die too quickly to be effective.
|
|
96
|
+
"""
|
|
97
|
+
return not state.scrambler_gear
|
|
98
|
+
|
|
99
|
+
def has_resources_to_act(self, state: AgentState) -> bool:
|
|
100
|
+
"""Scramblers need hearts to scramble junctions."""
|
|
101
|
+
return state.heart >= 1
|
|
102
|
+
|
|
103
|
+
def _retreat_to_safety(self, state: AgentState, services: Services) -> Action:
|
|
104
|
+
"""Return to nearest safe zone."""
|
|
105
|
+
safe_pos = services.safety.nearest_safe_zone(state)
|
|
106
|
+
if safe_pos is None:
|
|
107
|
+
return services.navigator.explore(state)
|
|
108
|
+
return services.navigator.move_to(state, safe_pos, reach_adjacent=True)
|
|
109
|
+
|
|
110
|
+
def _get_gear(self, state: AgentState, services: Services) -> Action:
|
|
111
|
+
"""Get scrambler gear from station."""
|
|
112
|
+
station_name = ROLE_TO_STATION[Role.SCRAMBLER]
|
|
113
|
+
|
|
114
|
+
# First try visible scrambler_station in current observation
|
|
115
|
+
if state.last_obs is not None:
|
|
116
|
+
result = services.map_tracker.get_direction_to_nearest(state, state.last_obs, frozenset({station_name}))
|
|
117
|
+
if result:
|
|
118
|
+
direction, target_pos = result
|
|
119
|
+
if DEBUG:
|
|
120
|
+
print(f"[A{state.agent_id}] SCRAMBLER: Station visible at {target_pos}, moving {direction}")
|
|
121
|
+
state.debug_info = DebugInfo(
|
|
122
|
+
mode="get_gear", goal="scrambler_station", target_object=station_name, target_pos=target_pos
|
|
123
|
+
)
|
|
124
|
+
return Action(name=f"move_{direction}")
|
|
125
|
+
|
|
126
|
+
# Use accumulated map knowledge if station was found
|
|
127
|
+
station_pos = state.map.stations.get(station_name)
|
|
128
|
+
|
|
129
|
+
if station_pos is not None:
|
|
130
|
+
dist = manhattan_distance(state.pos, station_pos)
|
|
131
|
+
|
|
132
|
+
# If ON the station, we should have received gear from walking in
|
|
133
|
+
if state.pos == station_pos:
|
|
134
|
+
if DEBUG:
|
|
135
|
+
print(f"[A{state.agent_id}] SCRAMBLER: ON station {station_pos}, no gear - explore")
|
|
136
|
+
state.debug_info = DebugInfo(
|
|
137
|
+
mode="get_gear", goal="on_station_no_gear", target_object=station_name, target_pos=station_pos
|
|
138
|
+
)
|
|
139
|
+
state.last_gear_attempt_step = state.step
|
|
140
|
+
return Action(name="move_east")
|
|
141
|
+
|
|
142
|
+
if is_adjacent(state.pos, station_pos):
|
|
143
|
+
if DEBUG:
|
|
144
|
+
print(f"[A{state.agent_id}] SCRAMBLER: Getting gear from {station_pos}")
|
|
145
|
+
state.debug_info = DebugInfo(
|
|
146
|
+
mode="get_gear", goal="use_station", target_object=station_name, target_pos=station_pos
|
|
147
|
+
)
|
|
148
|
+
return services.navigator.use_object_at(state, station_pos)
|
|
149
|
+
|
|
150
|
+
if DEBUG and state.step % 10 == 0:
|
|
151
|
+
print(f"[A{state.agent_id}] SCRAMBLER: Moving to station at {station_pos} (dist={dist})")
|
|
152
|
+
state.debug_info = DebugInfo(
|
|
153
|
+
mode="get_gear", goal=f"move_to_station({dist})", target_object=station_name, target_pos=station_pos
|
|
154
|
+
)
|
|
155
|
+
return services.navigator.move_to(state, station_pos, reach_adjacent=True)
|
|
156
|
+
|
|
157
|
+
# Station not found yet - explore
|
|
158
|
+
if DEBUG and state.step % 10 == 0:
|
|
159
|
+
print(f"[A{state.agent_id}] SCRAMBLER: Exploring for station (step {state.step})")
|
|
160
|
+
state.debug_info = DebugInfo(mode="explore", goal="find_station", target_object=station_name)
|
|
161
|
+
return self._explore_for_station(state, services)
|
|
162
|
+
|
|
163
|
+
def _explore_for_station(self, state: AgentState, services: Services) -> Action:
|
|
164
|
+
"""Explore to find the scrambler station."""
|
|
165
|
+
# Spread scramblers out by giving each agent a different primary direction
|
|
166
|
+
direction = get_explore_direction_for_agent(state.agent_id)
|
|
167
|
+
return explore_for_station(state, services, primary_direction=direction)
|
|
168
|
+
|
|
169
|
+
# How often to cycle exploration direction (in steps)
|
|
170
|
+
EXPLORE_DIRECTION_CYCLE = 100
|
|
171
|
+
|
|
172
|
+
def _explore_for_enemy_junctions(self, state: AgentState, services: Services) -> Action:
|
|
173
|
+
"""Explore to find clips junctions, ensuring whole-map coverage.
|
|
174
|
+
|
|
175
|
+
Strategy:
|
|
176
|
+
1. Revisit stale junctions (alignment data may be outdated) - least recently seen first
|
|
177
|
+
2. If no stale junctions, patrol the map by cycling through different directions
|
|
178
|
+
3. Reset exploration origin periodically to cover new areas
|
|
179
|
+
"""
|
|
180
|
+
# Find junctions that haven't been visited recently (alignment could have changed)
|
|
181
|
+
# Prioritize least recently seen - these are most likely to have outdated alignment info
|
|
182
|
+
stale_junctions: list[tuple[int, int, StructureInfo]] = [] # (steps_since_seen, dist, junction)
|
|
183
|
+
|
|
184
|
+
for junction in state.map.get_junctions():
|
|
185
|
+
steps_since_seen = state.step - junction.last_seen_step
|
|
186
|
+
|
|
187
|
+
# Only consider junctions we haven't seen recently
|
|
188
|
+
if steps_since_seen < self.JUNCTION_STALE_THRESHOLD:
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
# Skip known clips junctions (we already found them, _find_best_target handles them)
|
|
192
|
+
# Focus on neutral and unknown junctions that could have become clips
|
|
193
|
+
if junction.is_clips_aligned():
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
dist = manhattan_distance(state.pos, junction.position)
|
|
197
|
+
stale_junctions.append((steps_since_seen, dist, junction))
|
|
198
|
+
|
|
199
|
+
if stale_junctions:
|
|
200
|
+
# Sort by: most stale first (highest steps_since_seen), then by distance
|
|
201
|
+
stale_junctions.sort(key=lambda x: (-x[0], x[1]))
|
|
202
|
+
target = stale_junctions[0][2]
|
|
203
|
+
|
|
204
|
+
if DEBUG and state.step % 50 == 0:
|
|
205
|
+
print(
|
|
206
|
+
f"[A{state.agent_id}] SCRAMBLER: Revisiting stale junction at {target.position} "
|
|
207
|
+
f"(last_seen={target.last_seen_step}, age={state.step - target.last_seen_step})"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
state.debug_info = DebugInfo(
|
|
211
|
+
mode="explore",
|
|
212
|
+
goal=f"revisit_stale(age={state.step - target.last_seen_step})",
|
|
213
|
+
target_object="junction",
|
|
214
|
+
target_pos=target.position,
|
|
215
|
+
)
|
|
216
|
+
return services.navigator.move_to(state, target.position, reach_adjacent=True)
|
|
217
|
+
|
|
218
|
+
# No stale junctions - patrol the map to find enemy territory
|
|
219
|
+
# Cycle through directions over time to ensure whole-map coverage
|
|
220
|
+
# Each scrambler starts at a different direction (agent_id offset) and cycles through all 4
|
|
221
|
+
directions = ["north", "east", "south", "west"]
|
|
222
|
+
current_cycle = state.step // self.EXPLORE_DIRECTION_CYCLE
|
|
223
|
+
cycle_index = (current_cycle + state.agent_id) % 4
|
|
224
|
+
direction_bias = directions[cycle_index]
|
|
225
|
+
|
|
226
|
+
# Reset exploration origin when we've been exploring from the same origin for too long
|
|
227
|
+
# This prevents getting stuck in one area - forces movement to new regions
|
|
228
|
+
if state.nav.explore_origin is not None:
|
|
229
|
+
steps_at_origin = state.step - state.nav.explore_start_step
|
|
230
|
+
if steps_at_origin >= self.EXPLORE_DIRECTION_CYCLE:
|
|
231
|
+
# Time to move to a new area - reset origin to current position
|
|
232
|
+
state.nav.explore_origin = state.pos
|
|
233
|
+
state.nav.explore_start_step = state.step
|
|
234
|
+
if DEBUG:
|
|
235
|
+
print(
|
|
236
|
+
f"[A{state.agent_id}] SCRAMBLER: Resetting patrol origin to {state.pos}, "
|
|
237
|
+
f"direction={direction_bias}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if DEBUG and state.step % 50 == 0:
|
|
241
|
+
clips_count = len(state.map.get_clips_junctions())
|
|
242
|
+
all_count = len(state.map.get_junctions())
|
|
243
|
+
print(
|
|
244
|
+
f"[A{state.agent_id}] SCRAMBLER: Patrolling (clips={clips_count}, "
|
|
245
|
+
f"all={all_count}, direction={direction_bias}, cycle={current_cycle})"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
state.debug_info = DebugInfo(mode="explore", goal=f"patrol_{direction_bias}", target_object="junction")
|
|
249
|
+
return services.navigator.explore(state, direction_bias=direction_bias)
|
|
250
|
+
|
|
251
|
+
def _get_hearts(self, state: AgentState, services: Services) -> Action:
|
|
252
|
+
"""Get hearts from chest."""
|
|
253
|
+
# First try visible chest in current observation
|
|
254
|
+
if state.last_obs is not None:
|
|
255
|
+
result = services.map_tracker.get_direction_to_nearest(state, state.last_obs, frozenset({"chest"}))
|
|
256
|
+
if result:
|
|
257
|
+
direction, target_pos = result
|
|
258
|
+
if DEBUG:
|
|
259
|
+
print(f"[A{state.agent_id}] SCRAMBLER: Chest visible at {target_pos}, moving {direction}")
|
|
260
|
+
state.debug_info = DebugInfo(
|
|
261
|
+
mode="get_hearts", goal="chest", target_object="chest", target_pos=target_pos
|
|
262
|
+
)
|
|
263
|
+
return Action(name=f"move_{direction}")
|
|
264
|
+
|
|
265
|
+
# Use accumulated map knowledge
|
|
266
|
+
chest_pos = state.map.stations.get("chest")
|
|
267
|
+
|
|
268
|
+
if chest_pos is None:
|
|
269
|
+
# Try hub as fallback
|
|
270
|
+
hub_pos = state.map.stations.get("hub")
|
|
271
|
+
if hub_pos is not None:
|
|
272
|
+
chest_pos = hub_pos
|
|
273
|
+
else:
|
|
274
|
+
if DEBUG:
|
|
275
|
+
print(f"[A{state.agent_id}] SCRAMBLER: No chest/hub, exploring")
|
|
276
|
+
state.debug_info = DebugInfo(mode="explore", goal="find_chest", target_object="chest")
|
|
277
|
+
return services.navigator.explore(state)
|
|
278
|
+
|
|
279
|
+
dist = manhattan_distance(state.pos, chest_pos)
|
|
280
|
+
|
|
281
|
+
if is_adjacent(state.pos, chest_pos):
|
|
282
|
+
if DEBUG:
|
|
283
|
+
print(f"[A{state.agent_id}] SCRAMBLER: Getting hearts from {chest_pos}")
|
|
284
|
+
state.debug_info = DebugInfo(
|
|
285
|
+
mode="get_hearts", goal="use_chest", target_object="chest", target_pos=chest_pos
|
|
286
|
+
)
|
|
287
|
+
return services.navigator.use_object_at(state, chest_pos)
|
|
288
|
+
|
|
289
|
+
state.debug_info = DebugInfo(
|
|
290
|
+
mode="get_hearts", goal=f"move_to_chest(dist={dist})", target_object="chest", target_pos=chest_pos
|
|
291
|
+
)
|
|
292
|
+
return services.navigator.move_to(state, chest_pos, reach_adjacent=True)
|
|
293
|
+
|
|
294
|
+
def _scramble_junction(self, state: AgentState, services: Services) -> Action:
|
|
295
|
+
"""Find and scramble an enemy (clips) junction.
|
|
296
|
+
|
|
297
|
+
Strategy: ONLY target clips-aligned junctions (save hearts for real scrambles).
|
|
298
|
+
Always verify alignment AND resources before using - both can change at any time.
|
|
299
|
+
"""
|
|
300
|
+
# SAFETY CHECK: Verify we still have the required resources
|
|
301
|
+
# This is a defensive check - act() should have already verified this
|
|
302
|
+
if self.needs_gear(state):
|
|
303
|
+
if DEBUG:
|
|
304
|
+
print(f"[A{state.agent_id}] SCRAMBLER: In _scramble_junction but missing gear, getting gear")
|
|
305
|
+
return self._get_gear(state, services)
|
|
306
|
+
|
|
307
|
+
if not self.has_resources_to_act(state):
|
|
308
|
+
if DEBUG:
|
|
309
|
+
print(f"[A{state.agent_id}] SCRAMBLER: In _scramble_junction but no hearts, getting hearts")
|
|
310
|
+
return self._get_hearts(state, services)
|
|
311
|
+
|
|
312
|
+
# First try to find CLIPS junction in current observation
|
|
313
|
+
if state.last_obs is not None:
|
|
314
|
+
result = services.map_tracker.get_direction_to_nearest(
|
|
315
|
+
state, state.last_obs, frozenset({"junction", "supply_depot"})
|
|
316
|
+
)
|
|
317
|
+
if result:
|
|
318
|
+
direction, target_pos = result
|
|
319
|
+
struct = state.map.get_structure_at(target_pos)
|
|
320
|
+
|
|
321
|
+
# ONLY target confirmed clips-aligned junctions
|
|
322
|
+
# Don't waste time on neutral/unknown - let exploration handle discovery
|
|
323
|
+
if struct is not None and struct.is_clips_aligned():
|
|
324
|
+
if DEBUG:
|
|
325
|
+
print(f"[A{state.agent_id}] SCRAMBLER: Enemy junction at {target_pos}, moving {direction}")
|
|
326
|
+
state.debug_info = DebugInfo(
|
|
327
|
+
mode="scramble", goal="enemy_junction", target_object="junction", target_pos=target_pos
|
|
328
|
+
)
|
|
329
|
+
return Action(name=f"move_{direction}")
|
|
330
|
+
# If not clips-aligned, don't target it - fall through to map knowledge or explore
|
|
331
|
+
|
|
332
|
+
# Fall back to map knowledge - find known clips junctions
|
|
333
|
+
target = self._find_best_target(state, services)
|
|
334
|
+
|
|
335
|
+
if target is not None:
|
|
336
|
+
dist = manhattan_distance(state.pos, target.position)
|
|
337
|
+
|
|
338
|
+
if is_adjacent(state.pos, target.position):
|
|
339
|
+
# IMPORTANT: Re-verify alignment before using!
|
|
340
|
+
# Alignment could have changed while we were moving toward it
|
|
341
|
+
current_struct = state.map.get_structure_at(target.position)
|
|
342
|
+
if current_struct is None or not current_struct.is_clips_aligned():
|
|
343
|
+
if DEBUG:
|
|
344
|
+
alignment = current_struct.alignment if current_struct else "unknown"
|
|
345
|
+
print(
|
|
346
|
+
f"[A{state.agent_id}] SCRAMBLER: Target at {target.position} no longer clips "
|
|
347
|
+
f"(now {alignment}), finding new target"
|
|
348
|
+
)
|
|
349
|
+
state.debug_info = DebugInfo(
|
|
350
|
+
mode="scramble",
|
|
351
|
+
goal="target_alignment_changed",
|
|
352
|
+
target_object="junction",
|
|
353
|
+
signal="alignment_changed",
|
|
354
|
+
)
|
|
355
|
+
# Target is no longer clips - explore to find a new one
|
|
356
|
+
return self._explore_for_enemy_junctions(state, services)
|
|
357
|
+
|
|
358
|
+
# CRITICAL: Final check before spending hearts - verify we have resources
|
|
359
|
+
if not self.has_resources_to_act(state):
|
|
360
|
+
if DEBUG:
|
|
361
|
+
print(
|
|
362
|
+
f"[A{state.agent_id}] SCRAMBLER: Adjacent to junction but no hearts! Getting hearts first."
|
|
363
|
+
)
|
|
364
|
+
return self._get_hearts(state, services)
|
|
365
|
+
|
|
366
|
+
if DEBUG:
|
|
367
|
+
print(f"[A{state.agent_id}] SCRAMBLER: Scrambling junction at {target.position}")
|
|
368
|
+
state.debug_info = DebugInfo(
|
|
369
|
+
mode="scramble", goal="use_junction", target_object="junction", target_pos=target.position
|
|
370
|
+
)
|
|
371
|
+
return services.navigator.use_object_at(state, target.position)
|
|
372
|
+
|
|
373
|
+
# Not adjacent yet - verify target is still clips before continuing to move toward it
|
|
374
|
+
current_struct = state.map.get_structure_at(target.position)
|
|
375
|
+
if current_struct is None or not current_struct.is_clips_aligned():
|
|
376
|
+
if DEBUG:
|
|
377
|
+
alignment = current_struct.alignment if current_struct else "unknown"
|
|
378
|
+
print(
|
|
379
|
+
f"[A{state.agent_id}] SCRAMBLER: Target at {target.position} no longer clips "
|
|
380
|
+
f"(now {alignment}), finding new target"
|
|
381
|
+
)
|
|
382
|
+
# Target changed - find a new one next tick
|
|
383
|
+
return self._explore_for_enemy_junctions(state, services)
|
|
384
|
+
|
|
385
|
+
state.debug_info = DebugInfo(
|
|
386
|
+
mode="scramble",
|
|
387
|
+
goal=f"move_to_junction(dist={dist})",
|
|
388
|
+
target_object="junction",
|
|
389
|
+
target_pos=target.position,
|
|
390
|
+
)
|
|
391
|
+
return services.navigator.move_to(state, target.position, reach_adjacent=True)
|
|
392
|
+
|
|
393
|
+
# No known clips junctions - explore to find them or revisit stale junctions
|
|
394
|
+
# Since alignment can change, revisit old junctions (least recently seen first)
|
|
395
|
+
return self._explore_for_enemy_junctions(state, services)
|
|
396
|
+
|
|
397
|
+
def _find_best_target(self, state: AgentState, services: Services) -> Optional[StructureInfo]:
|
|
398
|
+
"""Find enemy junction to scramble.
|
|
399
|
+
|
|
400
|
+
ONLY returns confirmed clips-aligned junctions.
|
|
401
|
+
Stale/neutral junctions are handled by exploration, not targeting.
|
|
402
|
+
"""
|
|
403
|
+
max_dist = services.safety.max_safe_distance(state, self.risk_tolerance)
|
|
404
|
+
|
|
405
|
+
enemy_junctions = state.map.get_clips_junctions()
|
|
406
|
+
neutral_junctions = state.map.get_neutral_junctions()
|
|
407
|
+
|
|
408
|
+
if not enemy_junctions:
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
# Score each enemy junction by how many neutrals it blocks
|
|
412
|
+
scored: list[tuple[int, int, StructureInfo]] = [] # (blocked_count, dist, junction)
|
|
413
|
+
|
|
414
|
+
for enemy in enemy_junctions:
|
|
415
|
+
dist = manhattan_distance(state.pos, enemy.position)
|
|
416
|
+
if dist > max_dist:
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
# Count how many neutral junctions this enemy is blocking
|
|
420
|
+
blocked = sum(
|
|
421
|
+
1
|
|
422
|
+
for neutral in neutral_junctions
|
|
423
|
+
if manhattan_distance(enemy.position, neutral.position) <= JUNCTION_AOE_RANGE
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
scored.append((blocked, dist, enemy))
|
|
427
|
+
|
|
428
|
+
if not scored:
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
# Sort by: most blocked neutrals first, then by distance
|
|
432
|
+
scored.sort(key=lambda x: (-x[0], x[1]))
|
|
433
|
+
return scored[0][2]
|