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,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def prereq_missing(
|
|
5
|
+
action_type: str,
|
|
6
|
+
*,
|
|
7
|
+
gear: int,
|
|
8
|
+
heart: int,
|
|
9
|
+
influence: int,
|
|
10
|
+
) -> dict[str, bool]:
|
|
11
|
+
if action_type not in {"align", "scramble"}:
|
|
12
|
+
raise ValueError(f"Unsupported action_type: {action_type}")
|
|
13
|
+
missing = {"gear": gear < 1, "heart": heart < 1}
|
|
14
|
+
if action_type == "align":
|
|
15
|
+
missing["influence"] = influence < 1
|
|
16
|
+
return missing
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def format_prereq_trace_line(
|
|
20
|
+
*,
|
|
21
|
+
step: int,
|
|
22
|
+
agent_id: int,
|
|
23
|
+
action_type: str,
|
|
24
|
+
gear: int,
|
|
25
|
+
heart: int,
|
|
26
|
+
influence: int,
|
|
27
|
+
missing: dict[str, bool],
|
|
28
|
+
) -> str:
|
|
29
|
+
missing_str = ",".join(key for key, is_missing in missing.items() if is_missing) or "-"
|
|
30
|
+
return (
|
|
31
|
+
f"step={step} agent={agent_id} action={action_type} "
|
|
32
|
+
f"gear={gear} heart={heart} influence={influence} missing[{missing_str}]"
|
|
33
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def summarize_role_counts(
|
|
8
|
+
role_counts_history: list[dict[str, int]],
|
|
9
|
+
roles: Iterable[str],
|
|
10
|
+
) -> dict[str, dict[str, float]]:
|
|
11
|
+
summary: dict[str, dict[str, float]] = {}
|
|
12
|
+
total_steps = max(len(role_counts_history), 1)
|
|
13
|
+
for role in roles:
|
|
14
|
+
counts = [counts_map.get(role, 0) for counts_map in role_counts_history]
|
|
15
|
+
summary[role] = {
|
|
16
|
+
"min": min(counts) if counts else 0,
|
|
17
|
+
"max": max(counts) if counts else 0,
|
|
18
|
+
"avg": sum(counts) / total_steps,
|
|
19
|
+
}
|
|
20
|
+
return summary
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def count_steps_with_roles(
|
|
24
|
+
role_counts_history: list[dict[str, int]],
|
|
25
|
+
required_roles: Iterable[str],
|
|
26
|
+
) -> int:
|
|
27
|
+
required = list(required_roles)
|
|
28
|
+
return sum(1 for counts_map in role_counts_history if all(counts_map.get(role, 0) > 0 for role in required))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def count_role_transitions(
|
|
32
|
+
transitions: list[tuple[str, str]],
|
|
33
|
+
) -> dict[tuple[str, str], int]:
|
|
34
|
+
transition_counts: dict[tuple[str, str], int] = defaultdict(int)
|
|
35
|
+
for prev_role, next_role in transitions:
|
|
36
|
+
transition_counts[(prev_role, next_role)] += 1
|
|
37
|
+
return dict(transition_counts)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_role_trace_line(
|
|
41
|
+
*,
|
|
42
|
+
step: int,
|
|
43
|
+
role_counts: dict[str, int],
|
|
44
|
+
roles: Iterable[str],
|
|
45
|
+
transitions: int,
|
|
46
|
+
) -> str:
|
|
47
|
+
role_list = list(roles)
|
|
48
|
+
counts_str = " ".join(f"{role}={role_counts.get(role, 0)}" for role in role_list)
|
|
49
|
+
present_str = ",".join(role for role in role_list if role_counts.get(role, 0) > 0) or "-"
|
|
50
|
+
return f"step={step} roles[{counts_str}] present[{present_str}] transitions={transitions}"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from cogames_agents.policy.scripted_agent.cogsguard.policy import CogsguardPolicy
|
|
4
|
+
from mettagrid.policy.policy_env_interface import PolicyEnvInterface
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _CogsguardRolePolicy(CogsguardPolicy):
|
|
8
|
+
role_name: str = ""
|
|
9
|
+
|
|
10
|
+
def __init__(self, policy_env_info: PolicyEnvInterface, device: str = "cpu", **_ignored: int) -> None:
|
|
11
|
+
super().__init__(policy_env_info, device=device, **{self.role_name: policy_env_info.num_agents})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MinerPolicy(_CogsguardRolePolicy):
|
|
15
|
+
short_names = ["miner"]
|
|
16
|
+
role_name = "miner"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ScoutPolicy(_CogsguardRolePolicy):
|
|
20
|
+
short_names = ["scout"]
|
|
21
|
+
role_name = "scout"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AlignerPolicy(_CogsguardRolePolicy):
|
|
25
|
+
short_names = ["aligner"]
|
|
26
|
+
role_name = "aligner"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ScramblerPolicy(_CogsguardRolePolicy):
|
|
30
|
+
short_names = ["scrambler"]
|
|
31
|
+
role_name = "scrambler"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
|
|
5
|
+
from cogames.cogs_vs_clips.stations import GEAR_COSTS
|
|
6
|
+
|
|
7
|
+
TRACE_RESOURCES = tuple(sorted({resource for costs in GEAR_COSTS.values() for resource in costs}))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def inventory_snapshot(collective_inv: dict[str, int], resources: Iterable[str]) -> dict[str, int]:
|
|
11
|
+
return {resource: int(collective_inv.get(resource, 0)) for resource in resources}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def inventory_delta(previous: dict[str, int] | None, current: dict[str, int]) -> dict[str, int]:
|
|
15
|
+
if previous is None:
|
|
16
|
+
return {resource: 0 for resource in current}
|
|
17
|
+
return {resource: current[resource] - previous.get(resource, 0) for resource in current}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def format_resource_trace_line(
|
|
21
|
+
*,
|
|
22
|
+
step: int,
|
|
23
|
+
inventory: dict[str, int],
|
|
24
|
+
delta: dict[str, int],
|
|
25
|
+
station_uses: dict[str, int],
|
|
26
|
+
station_uses_with_resources: dict[str, int],
|
|
27
|
+
adjacent_roles: dict[str, bool],
|
|
28
|
+
available_roles: dict[str, bool],
|
|
29
|
+
) -> str:
|
|
30
|
+
inv_str = " ".join(f"{resource}={inventory[resource]}" for resource in TRACE_RESOURCES)
|
|
31
|
+
delta_str = " ".join(f"{resource}={delta[resource]:+d}" for resource in TRACE_RESOURCES)
|
|
32
|
+
uses_str = " ".join(f"{role}={station_uses.get(role, 0)}" for role in GEAR_COSTS)
|
|
33
|
+
uses_with_str = " ".join(f"{role}={station_uses_with_resources.get(role, 0)}" for role in GEAR_COSTS)
|
|
34
|
+
adjacent_str = ",".join(role for role, adjacent in adjacent_roles.items() if adjacent) or "-"
|
|
35
|
+
available_str = ",".join(role for role, available in available_roles.items() if available) or "-"
|
|
36
|
+
return (
|
|
37
|
+
f"step={step} inv[{inv_str}] delta[{delta_str}] "
|
|
38
|
+
f"station_uses[{uses_str}] station_uses_with_resources[{uses_with_str}] "
|
|
39
|
+
f"adjacent_roles[{adjacent_str}] available_roles[{available_str}]"
|
|
40
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scout role for CoGsGuard.
|
|
3
|
+
|
|
4
|
+
Scouts explore the map and patrol to discover objects.
|
|
5
|
+
With scout gear, they get +400 HP and +100 energy capacity.
|
|
6
|
+
|
|
7
|
+
Scouts prioritize filling out their internal map by:
|
|
8
|
+
1. Moving towards unexplored frontiers (unexplored cells adjacent to explored cells)
|
|
9
|
+
2. Using systematic patrol when no clear frontier is available
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from mettagrid.simulator import Action
|
|
15
|
+
|
|
16
|
+
from .policy import CogsguardAgentPolicyImpl
|
|
17
|
+
from .types import CogsguardAgentState, Role
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ScoutAgentPolicyImpl(CogsguardAgentPolicyImpl):
|
|
21
|
+
"""Scout agent: explore and patrol the map to fill out internal knowledge."""
|
|
22
|
+
|
|
23
|
+
ROLE = Role.SCOUT
|
|
24
|
+
|
|
25
|
+
def execute_role(self, s: CogsguardAgentState) -> Action:
|
|
26
|
+
"""Execute scout behavior: prioritize filling out unexplored areas."""
|
|
27
|
+
# Try frontier-based exploration first
|
|
28
|
+
frontier_action = self._explore_frontier(s)
|
|
29
|
+
if frontier_action is not None:
|
|
30
|
+
return frontier_action
|
|
31
|
+
|
|
32
|
+
# Fall back to systematic patrol if no frontier found
|
|
33
|
+
return self._patrol(s)
|
|
34
|
+
|
|
35
|
+
def _patrol(self, s: CogsguardAgentState) -> Action:
|
|
36
|
+
"""Fall back patrol behavior when no frontier is available."""
|
|
37
|
+
# Use longer exploration persistence for scouts
|
|
38
|
+
if s.exploration_target is not None and isinstance(s.exploration_target, str):
|
|
39
|
+
steps_in_direction = s.step_count - s.exploration_target_step
|
|
40
|
+
# Scouts persist longer in each direction (25 steps vs 15)
|
|
41
|
+
if steps_in_direction < 25:
|
|
42
|
+
dr, dc = self._move_deltas.get(s.exploration_target, (0, 0))
|
|
43
|
+
next_r, next_c = s.row + dr, s.col + dc
|
|
44
|
+
if 0 <= next_r < s.map_height and 0 <= next_c < s.map_width:
|
|
45
|
+
if s.occupancy[next_r][next_c] == 1: # FREE
|
|
46
|
+
if (next_r, next_c) not in s.agent_occupancy:
|
|
47
|
+
return self._move(s.exploration_target)
|
|
48
|
+
|
|
49
|
+
# Cycle through directions systematically
|
|
50
|
+
direction_cycle = ["north", "east", "south", "west"]
|
|
51
|
+
current_dir = s.exploration_target
|
|
52
|
+
if current_dir in direction_cycle:
|
|
53
|
+
idx = direction_cycle.index(current_dir)
|
|
54
|
+
next_idx = (idx + 1) % 4
|
|
55
|
+
else:
|
|
56
|
+
next_idx = 0
|
|
57
|
+
|
|
58
|
+
for i in range(4):
|
|
59
|
+
direction = direction_cycle[(next_idx + i) % 4]
|
|
60
|
+
dr, dc = self._move_deltas[direction]
|
|
61
|
+
next_r, next_c = s.row + dr, s.col + dc
|
|
62
|
+
if 0 <= next_r < s.map_height and 0 <= next_c < s.map_width:
|
|
63
|
+
if s.occupancy[next_r][next_c] == 1: # FREE
|
|
64
|
+
if (next_r, next_c) not in s.agent_occupancy:
|
|
65
|
+
s.exploration_target = direction
|
|
66
|
+
s.exploration_target_step = s.step_count
|
|
67
|
+
return self._move(direction)
|
|
68
|
+
|
|
69
|
+
return self._noop()
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scrambler role for CoGsGuard.
|
|
3
|
+
|
|
4
|
+
Scramblers find enemy-aligned supply depots and scramble them to take control.
|
|
5
|
+
With scrambler gear, they get +200 HP.
|
|
6
|
+
|
|
7
|
+
Strategy:
|
|
8
|
+
- Find ALL junctions on the map
|
|
9
|
+
- Prioritize scrambling enemy (clips) aligned junctions
|
|
10
|
+
- Systematically work through all junctions to take them over
|
|
11
|
+
- Check energy before moving to targets
|
|
12
|
+
- Retry failed scramble actions up to MAX_RETRIES times
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from cogames_agents.policy.scripted_agent.pathfinding import is_traversable
|
|
20
|
+
from cogames_agents.policy.scripted_agent.types import CellType
|
|
21
|
+
from cogames_agents.policy.scripted_agent.utils import is_adjacent
|
|
22
|
+
from mettagrid.simulator import Action
|
|
23
|
+
|
|
24
|
+
from .policy import DEBUG, CogsguardAgentPolicyImpl
|
|
25
|
+
from .types import CogsguardAgentState, Role, StructureType
|
|
26
|
+
|
|
27
|
+
# Maximum number of times to retry a failed scramble action
|
|
28
|
+
MAX_RETRIES = 3
|
|
29
|
+
# HP buffer to start returning to the hub before gear is lost.
|
|
30
|
+
HP_RETURN_BUFFER = 12
|
|
31
|
+
# Scramblers should switch to aligner gear after making some neutral junctions.
|
|
32
|
+
SCRAMBLE_TO_ALIGN_THRESHOLD = 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ScramblerAgentPolicyImpl(CogsguardAgentPolicyImpl):
|
|
36
|
+
"""Scrambler agent: scramble enemy supply depots to take control."""
|
|
37
|
+
|
|
38
|
+
ROLE = Role.SCRAMBLER
|
|
39
|
+
|
|
40
|
+
def execute_role(self, s: CogsguardAgentState) -> Action:
|
|
41
|
+
"""Execute scrambler behavior: find and scramble ALL enemy depots.
|
|
42
|
+
|
|
43
|
+
Energy-aware behavior:
|
|
44
|
+
- Check if we have enough energy before attempting to move to targets
|
|
45
|
+
- If energy is low, go recharge at the nexus
|
|
46
|
+
- Retry failed scramble actions up to MAX_RETRIES times
|
|
47
|
+
- If gear is lost, go back to base to re-equip
|
|
48
|
+
- If gear acquisition fails repeatedly, get hearts first (gear may require hearts)
|
|
49
|
+
"""
|
|
50
|
+
if DEBUG and s.step_count % 100 == 0:
|
|
51
|
+
num_junctions = len(s.get_structures_by_type(StructureType.CHARGER))
|
|
52
|
+
clips_junctions = len(
|
|
53
|
+
[c for c in s.get_structures_by_type(StructureType.CHARGER) if c.alignment == "clips"]
|
|
54
|
+
)
|
|
55
|
+
num_worked = len(s.worked_junctions)
|
|
56
|
+
print(
|
|
57
|
+
f"[A{s.agent_id}] SCRAMBLER: step={s.step_count} heart={s.heart} energy={s.energy} gear={s.scrambler} "
|
|
58
|
+
f"junctions={num_junctions} clips={clips_junctions} scrambled={num_worked}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
hub_pos = s.get_structure_position(StructureType.HUB)
|
|
62
|
+
if hub_pos is not None:
|
|
63
|
+
dist_to_hub = abs(hub_pos[0] - s.row) + abs(hub_pos[1] - s.col)
|
|
64
|
+
if s.hp <= dist_to_hub + HP_RETURN_BUFFER:
|
|
65
|
+
if DEBUG and s.step_count % 10 == 0:
|
|
66
|
+
print(f"[A{s.agent_id}] SCRAMBLER: Low HP ({s.hp}), returning to hub")
|
|
67
|
+
return self._do_recharge(s)
|
|
68
|
+
|
|
69
|
+
# === Resource check: need both gear AND heart to scramble ===
|
|
70
|
+
has_gear = s.scrambler >= 1
|
|
71
|
+
has_heart = s.heart >= 1
|
|
72
|
+
|
|
73
|
+
# Check if last action succeeded (for retry logic)
|
|
74
|
+
# Actions can fail due to insufficient energy - agents auto-regen so just retry
|
|
75
|
+
if s._pending_action_type == "scramble":
|
|
76
|
+
target = s._pending_action_target
|
|
77
|
+
if s.check_action_success():
|
|
78
|
+
if DEBUG:
|
|
79
|
+
print(f"[A{s.agent_id}] SCRAMBLER: Previous scramble succeeded!")
|
|
80
|
+
if target is not None and self._smart_role_coordinator is not None:
|
|
81
|
+
hub_pos = s.stations.get("hub")
|
|
82
|
+
self._smart_role_coordinator.register_junction_alignment(
|
|
83
|
+
target,
|
|
84
|
+
None,
|
|
85
|
+
hub_pos,
|
|
86
|
+
s.step_count,
|
|
87
|
+
)
|
|
88
|
+
elif s.should_retry_action(MAX_RETRIES):
|
|
89
|
+
retry_count = s.increment_retry()
|
|
90
|
+
if DEBUG:
|
|
91
|
+
print(
|
|
92
|
+
f"[A{s.agent_id}] SCRAMBLER: Scramble failed, retrying ({retry_count}/{MAX_RETRIES}) "
|
|
93
|
+
f"at {s._pending_action_target}"
|
|
94
|
+
)
|
|
95
|
+
# Retry the same action - agent will have auto-regenerated some energy
|
|
96
|
+
if has_heart and s._pending_action_target and is_adjacent((s.row, s.col), s._pending_action_target):
|
|
97
|
+
return self._use_object_at(s, s._pending_action_target)
|
|
98
|
+
else:
|
|
99
|
+
if DEBUG:
|
|
100
|
+
print(f"[A{s.agent_id}] SCRAMBLER: Scramble failed after {MAX_RETRIES} retries, moving on")
|
|
101
|
+
s.clear_pending_action()
|
|
102
|
+
|
|
103
|
+
# If we don't have gear, try to get it
|
|
104
|
+
if not has_gear:
|
|
105
|
+
return self._handle_no_gear(s)
|
|
106
|
+
|
|
107
|
+
# If we have gear but no heart, go get hearts
|
|
108
|
+
if not has_heart:
|
|
109
|
+
if DEBUG and s.step_count % 10 == 0:
|
|
110
|
+
print(f"[A{s.agent_id}] SCRAMBLER: Have gear but no heart, getting hearts first")
|
|
111
|
+
return self._get_hearts(s)
|
|
112
|
+
|
|
113
|
+
junctions = s.get_structures_by_type(StructureType.CHARGER)
|
|
114
|
+
enemy_junctions = [c for c in junctions if c.alignment == "clips" or c.clipped]
|
|
115
|
+
neutral_junctions = [c for c in junctions if c.alignment is None]
|
|
116
|
+
|
|
117
|
+
if has_gear and len(s.alignment_overrides) >= SCRAMBLE_TO_ALIGN_THRESHOLD:
|
|
118
|
+
if DEBUG and s.step_count % 10 == 0:
|
|
119
|
+
print(f"[A{s.agent_id}] SCRAMBLER: Swapping to aligner gear after scrambles")
|
|
120
|
+
action = self._switch_to_aligner_gear(s)
|
|
121
|
+
if action is not None:
|
|
122
|
+
return action
|
|
123
|
+
|
|
124
|
+
if has_gear and not enemy_junctions and neutral_junctions:
|
|
125
|
+
if DEBUG and s.step_count % 10 == 0:
|
|
126
|
+
print(f"[A{s.agent_id}] SCRAMBLER: No enemy junctions; swapping to aligner gear")
|
|
127
|
+
action = self._switch_to_aligner_gear(s)
|
|
128
|
+
if action is not None:
|
|
129
|
+
return action
|
|
130
|
+
|
|
131
|
+
# Find the best enemy depot to scramble (prioritize closest enemy junction)
|
|
132
|
+
target_depot = self._find_best_target(s)
|
|
133
|
+
|
|
134
|
+
if target_depot is None:
|
|
135
|
+
# No known enemy depots, explore to find more junctions
|
|
136
|
+
if DEBUG:
|
|
137
|
+
junctions = s.get_structures_by_type(StructureType.CHARGER)
|
|
138
|
+
print(f"[A{s.agent_id}] SCRAMBLER: No targets (total junctions={len(junctions)}), exploring")
|
|
139
|
+
return self._explore_for_junctions(s)
|
|
140
|
+
|
|
141
|
+
# Navigate to depot
|
|
142
|
+
# Note: moves require energy. If move fails due to low energy,
|
|
143
|
+
# action failure detection will catch it and we'll retry next step
|
|
144
|
+
# (agents auto-regen energy every step, and regen full near aligned buildings)
|
|
145
|
+
dist = abs(target_depot[0] - s.row) + abs(target_depot[1] - s.col)
|
|
146
|
+
if not is_adjacent((s.row, s.col), target_depot):
|
|
147
|
+
if DEBUG and s.step_count % 10 == 0:
|
|
148
|
+
print(f"[A{s.agent_id}] SCRAMBLER: Moving to junction at {target_depot} (dist={dist})")
|
|
149
|
+
return self._move_towards(s, target_depot, reach_adjacent=True)
|
|
150
|
+
|
|
151
|
+
# Scramble the depot by bumping it
|
|
152
|
+
# Mark this junction as worked
|
|
153
|
+
s.worked_junctions[target_depot] = s.step_count
|
|
154
|
+
|
|
155
|
+
# Start tracking this scramble attempt
|
|
156
|
+
s.start_action_attempt("scramble", target_depot)
|
|
157
|
+
|
|
158
|
+
if DEBUG:
|
|
159
|
+
junction = s.get_structure_at(target_depot)
|
|
160
|
+
alignment = junction.alignment if junction else "unknown"
|
|
161
|
+
print(
|
|
162
|
+
f"[A{s.agent_id}] SCRAMBLER: SCRAMBLING junction at {target_depot} "
|
|
163
|
+
f"(alignment={alignment}, heart={s.heart}, energy={s.energy})!"
|
|
164
|
+
)
|
|
165
|
+
return self._use_object_at(s, target_depot)
|
|
166
|
+
|
|
167
|
+
def _switch_to_aligner_gear(self, s: CogsguardAgentState) -> Optional[Action]:
|
|
168
|
+
aligner_station = s.get_structure_position(StructureType.ALIGNER_STATION)
|
|
169
|
+
if aligner_station is None:
|
|
170
|
+
return None
|
|
171
|
+
if not is_adjacent((s.row, s.col), aligner_station):
|
|
172
|
+
return self._move_towards(s, aligner_station, reach_adjacent=True)
|
|
173
|
+
return self._use_object_at(s, aligner_station)
|
|
174
|
+
|
|
175
|
+
def _handle_no_gear(self, s: CogsguardAgentState) -> Action:
|
|
176
|
+
"""Handle behavior when scrambler doesn't have gear.
|
|
177
|
+
|
|
178
|
+
Strategy: Go to gear station and wait there until gear is available.
|
|
179
|
+
Can't do much without gear, so just wait.
|
|
180
|
+
"""
|
|
181
|
+
station_pos = s.get_structure_position(StructureType.SCRAMBLER_STATION)
|
|
182
|
+
|
|
183
|
+
# If we don't know where the station is, explore to find it
|
|
184
|
+
if station_pos is None:
|
|
185
|
+
if DEBUG:
|
|
186
|
+
print(f"[A{s.agent_id}] SCRAMBLER_NO_GEAR: Station unknown, exploring")
|
|
187
|
+
return self._explore(s)
|
|
188
|
+
|
|
189
|
+
# Go to gear station
|
|
190
|
+
if not is_adjacent((s.row, s.col), station_pos):
|
|
191
|
+
if DEBUG and s.step_count % 10 == 0:
|
|
192
|
+
print(f"[A{s.agent_id}] SCRAMBLER_NO_GEAR: Moving to station at {station_pos}")
|
|
193
|
+
return self._move_towards(s, station_pos, reach_adjacent=True)
|
|
194
|
+
|
|
195
|
+
# At station - keep trying to get gear
|
|
196
|
+
if DEBUG and s.step_count % 10 == 0:
|
|
197
|
+
print(f"[A{s.agent_id}] SCRAMBLER_NO_GEAR: At station, waiting for gear")
|
|
198
|
+
return self._use_object_at(s, station_pos)
|
|
199
|
+
|
|
200
|
+
def _get_hearts(self, s: CogsguardAgentState) -> Action:
|
|
201
|
+
"""Get hearts from chest (primary source for hearts).
|
|
202
|
+
|
|
203
|
+
The chest can produce hearts from resources:
|
|
204
|
+
1. First tries to withdraw existing hearts from cogs commons (get_heart handler)
|
|
205
|
+
2. If no hearts available, converts 1 of each element into 1 heart (make_heart handler)
|
|
206
|
+
|
|
207
|
+
So as long as miners deposit resources, scramblers can get hearts.
|
|
208
|
+
If we've been trying to get hearts for too long, go explore instead.
|
|
209
|
+
"""
|
|
210
|
+
# If we've waited more than 40 steps for hearts, go explore instead
|
|
211
|
+
# This prevents getting stuck when commons is out of resources
|
|
212
|
+
if s._heart_wait_start == 0:
|
|
213
|
+
s._heart_wait_start = s.step_count
|
|
214
|
+
if s.step_count - s._heart_wait_start > 40:
|
|
215
|
+
if DEBUG:
|
|
216
|
+
print(f"[A{s.agent_id}] SCRAMBLER: Waited 40+ steps for hearts, exploring instead")
|
|
217
|
+
s._heart_wait_start = 0
|
|
218
|
+
return self._explore_for_junctions(s)
|
|
219
|
+
|
|
220
|
+
# Try chest first - it's the primary heart source
|
|
221
|
+
chest_pos = s.get_structure_position(StructureType.CHEST)
|
|
222
|
+
if chest_pos is not None:
|
|
223
|
+
if DEBUG and s.step_count % 10 == 0:
|
|
224
|
+
adj = is_adjacent((s.row, s.col), chest_pos)
|
|
225
|
+
print(f"[A{s.agent_id}] SCRAMBLER: Getting hearts from chest at {chest_pos}, adjacent={adj}")
|
|
226
|
+
if not is_adjacent((s.row, s.col), chest_pos):
|
|
227
|
+
return self._move_towards(s, chest_pos, reach_adjacent=True)
|
|
228
|
+
return self._use_object_at(s, chest_pos)
|
|
229
|
+
|
|
230
|
+
# Try hub as fallback (may have heart AOE or deposit function)
|
|
231
|
+
hub_pos = s.get_structure_position(StructureType.HUB)
|
|
232
|
+
if hub_pos is not None:
|
|
233
|
+
if DEBUG:
|
|
234
|
+
print(f"[A{s.agent_id}] SCRAMBLER: No chest found, trying hub at {hub_pos}")
|
|
235
|
+
if not is_adjacent((s.row, s.col), hub_pos):
|
|
236
|
+
return self._move_towards(s, hub_pos, reach_adjacent=True)
|
|
237
|
+
return self._use_object_at(s, hub_pos)
|
|
238
|
+
|
|
239
|
+
# Neither found - explore to find them
|
|
240
|
+
if DEBUG:
|
|
241
|
+
print(f"[A{s.agent_id}] SCRAMBLER: No chest/hub found, exploring")
|
|
242
|
+
s._heart_wait_start = 0
|
|
243
|
+
return self._explore(s)
|
|
244
|
+
|
|
245
|
+
def _find_best_target(self, s: CogsguardAgentState) -> Optional[tuple[int, int]]:
|
|
246
|
+
"""Find the best junction to scramble - prioritize enemy (clips) aligned ones.
|
|
247
|
+
|
|
248
|
+
Skips junctions that were recently worked on to ensure we visit multiple junctions.
|
|
249
|
+
"""
|
|
250
|
+
# Get all known junctions from structures map
|
|
251
|
+
junctions = s.get_structures_by_type(StructureType.CHARGER)
|
|
252
|
+
|
|
253
|
+
# How long to ignore a junction after working on it (steps)
|
|
254
|
+
cooldown = 50
|
|
255
|
+
|
|
256
|
+
# Collect junctions and sort by distance, skipping recently worked ones
|
|
257
|
+
enemy_junctions: list[tuple[int, tuple[int, int]]] = []
|
|
258
|
+
any_junctions: list[tuple[int, tuple[int, int]]] = []
|
|
259
|
+
|
|
260
|
+
if DEBUG and s.step_count % 20 == 1:
|
|
261
|
+
print(f"[A{s.agent_id}] FIND_TARGET: {len(junctions)} junctions in structures map")
|
|
262
|
+
for ch in junctions:
|
|
263
|
+
print(f" - {ch.position}: alignment={ch.alignment}, clipped={ch.clipped}")
|
|
264
|
+
|
|
265
|
+
for junction in junctions:
|
|
266
|
+
pos = junction.position
|
|
267
|
+
dist = abs(pos[0] - s.row) + abs(pos[1] - s.col)
|
|
268
|
+
|
|
269
|
+
if DEBUG and s.step_count % 20 == 1:
|
|
270
|
+
print(f" LOOP junction@{pos}: alignment='{junction.alignment}' clipped={junction.clipped} dist={dist}")
|
|
271
|
+
|
|
272
|
+
# Skip recently worked junctions (only if actually worked before)
|
|
273
|
+
last_worked = s.worked_junctions.get(pos, 0)
|
|
274
|
+
if last_worked > 0 and s.step_count - last_worked < cooldown:
|
|
275
|
+
if DEBUG and s.step_count % 20 == 1:
|
|
276
|
+
print(f" SKIP: on cooldown (worked {s.step_count - last_worked} steps ago)")
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
# Skip cogs-aligned junctions (already ours)
|
|
280
|
+
if junction.alignment == "cogs":
|
|
281
|
+
if DEBUG and s.step_count % 20 == 1:
|
|
282
|
+
print(" SKIP: cogs-aligned (ours)")
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
# Check alignment - prioritize clips (enemy) junctions
|
|
286
|
+
if junction.alignment == "clips" or junction.clipped:
|
|
287
|
+
if DEBUG and s.step_count % 20 == 1:
|
|
288
|
+
print(" ADD to enemy_junctions")
|
|
289
|
+
enemy_junctions.append((dist, pos))
|
|
290
|
+
else:
|
|
291
|
+
any_junctions.append((dist, pos))
|
|
292
|
+
|
|
293
|
+
if DEBUG and s.step_count % 20 == 1:
|
|
294
|
+
print(f" enemy_junctions={enemy_junctions} any={any_junctions}")
|
|
295
|
+
|
|
296
|
+
# First try enemy junctions (sorted by distance)
|
|
297
|
+
if enemy_junctions:
|
|
298
|
+
enemy_junctions.sort()
|
|
299
|
+
if DEBUG:
|
|
300
|
+
print(f"[A{s.agent_id}] FIND_TARGET: Returning enemy junction at {enemy_junctions[0][1]}")
|
|
301
|
+
target_idx = 0
|
|
302
|
+
if self._smart_role_coordinator is not None:
|
|
303
|
+
scrambler_ids = sorted(
|
|
304
|
+
agent_id
|
|
305
|
+
for agent_id, snapshot in self._smart_role_coordinator.agent_snapshots.items()
|
|
306
|
+
if snapshot.role == Role.SCRAMBLER
|
|
307
|
+
)
|
|
308
|
+
if scrambler_ids:
|
|
309
|
+
target_idx = scrambler_ids.index(s.agent_id) if s.agent_id in scrambler_ids else 0
|
|
310
|
+
return enemy_junctions[target_idx % len(enemy_junctions)][1]
|
|
311
|
+
|
|
312
|
+
# Then try any non-cogs junction (unknown alignment)
|
|
313
|
+
if any_junctions:
|
|
314
|
+
any_junctions.sort()
|
|
315
|
+
if DEBUG:
|
|
316
|
+
print(f"[A{s.agent_id}] FIND_TARGET: Returning any junction at {any_junctions[0][1]}")
|
|
317
|
+
target_idx = 0
|
|
318
|
+
if self._smart_role_coordinator is not None:
|
|
319
|
+
scrambler_ids = sorted(
|
|
320
|
+
agent_id
|
|
321
|
+
for agent_id, snapshot in self._smart_role_coordinator.agent_snapshots.items()
|
|
322
|
+
if snapshot.role == Role.SCRAMBLER
|
|
323
|
+
)
|
|
324
|
+
if scrambler_ids:
|
|
325
|
+
target_idx = scrambler_ids.index(s.agent_id) if s.agent_id in scrambler_ids else 0
|
|
326
|
+
return any_junctions[target_idx % len(any_junctions)][1]
|
|
327
|
+
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def _explore_for_junctions(self, s: CogsguardAgentState) -> Action:
|
|
331
|
+
"""Explore aggressively to find more junctions spread around the map."""
|
|
332
|
+
frontier_action = self._explore_frontier(s)
|
|
333
|
+
if frontier_action is not None:
|
|
334
|
+
return frontier_action
|
|
335
|
+
|
|
336
|
+
# Move in a direction based on agent ID and step count to spread out
|
|
337
|
+
# Chargers are spread around the map, so cover different areas
|
|
338
|
+
directions = ["north", "south", "east", "west"]
|
|
339
|
+
# Cycle through directions, spending 20 steps in each direction
|
|
340
|
+
dir_idx = (s.agent_id + s.step_count // 20) % 4
|
|
341
|
+
direction = directions[dir_idx]
|
|
342
|
+
|
|
343
|
+
dr, dc = self._move_deltas[direction]
|
|
344
|
+
next_r, next_c = s.row + dr, s.col + dc
|
|
345
|
+
|
|
346
|
+
if is_traversable(s, next_r, next_c, CellType): # type: ignore[arg-type]
|
|
347
|
+
return self._move(direction)
|
|
348
|
+
|
|
349
|
+
# Fall back to regular exploration if blocked
|
|
350
|
+
return self._explore(s)
|