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.
Files changed (128) hide show
  1. cogames_agents/__init__.py +0 -0
  2. cogames_agents/evals/__init__.py +5 -0
  3. cogames_agents/evals/planky_evals.py +415 -0
  4. cogames_agents/policy/__init__.py +0 -0
  5. cogames_agents/policy/evolution/__init__.py +0 -0
  6. cogames_agents/policy/evolution/cogsguard/__init__.py +0 -0
  7. cogames_agents/policy/evolution/cogsguard/evolution.py +695 -0
  8. cogames_agents/policy/evolution/cogsguard/evolutionary_coordinator.py +540 -0
  9. cogames_agents/policy/nim_agents/__init__.py +20 -0
  10. cogames_agents/policy/nim_agents/agents.py +98 -0
  11. cogames_agents/policy/nim_agents/bindings/generated/libnim_agents.dylib +0 -0
  12. cogames_agents/policy/nim_agents/bindings/generated/nim_agents.py +215 -0
  13. cogames_agents/policy/nim_agents/cogsguard_agents.nim +555 -0
  14. cogames_agents/policy/nim_agents/cogsguard_align_all_agents.nim +569 -0
  15. cogames_agents/policy/nim_agents/common.nim +1054 -0
  16. cogames_agents/policy/nim_agents/install.sh +1 -0
  17. cogames_agents/policy/nim_agents/ladybug_agent.nim +954 -0
  18. cogames_agents/policy/nim_agents/nim_agents.nim +68 -0
  19. cogames_agents/policy/nim_agents/nim_agents.nims +14 -0
  20. cogames_agents/policy/nim_agents/nimby.lock +3 -0
  21. cogames_agents/policy/nim_agents/racecar_agents.nim +844 -0
  22. cogames_agents/policy/nim_agents/random_agents.nim +68 -0
  23. cogames_agents/policy/nim_agents/test_agents.py +53 -0
  24. cogames_agents/policy/nim_agents/thinky_agents.nim +677 -0
  25. cogames_agents/policy/nim_agents/thinky_eval.py +230 -0
  26. cogames_agents/policy/scripted_agent/README.md +360 -0
  27. cogames_agents/policy/scripted_agent/__init__.py +0 -0
  28. cogames_agents/policy/scripted_agent/baseline_agent.py +1031 -0
  29. cogames_agents/policy/scripted_agent/cogas/__init__.py +5 -0
  30. cogames_agents/policy/scripted_agent/cogas/context.py +68 -0
  31. cogames_agents/policy/scripted_agent/cogas/entity_map.py +152 -0
  32. cogames_agents/policy/scripted_agent/cogas/goal.py +115 -0
  33. cogames_agents/policy/scripted_agent/cogas/goals/__init__.py +27 -0
  34. cogames_agents/policy/scripted_agent/cogas/goals/aligner.py +160 -0
  35. cogames_agents/policy/scripted_agent/cogas/goals/gear.py +197 -0
  36. cogames_agents/policy/scripted_agent/cogas/goals/miner.py +441 -0
  37. cogames_agents/policy/scripted_agent/cogas/goals/scout.py +40 -0
  38. cogames_agents/policy/scripted_agent/cogas/goals/scrambler.py +174 -0
  39. cogames_agents/policy/scripted_agent/cogas/goals/shared.py +160 -0
  40. cogames_agents/policy/scripted_agent/cogas/goals/stem.py +60 -0
  41. cogames_agents/policy/scripted_agent/cogas/goals/survive.py +100 -0
  42. cogames_agents/policy/scripted_agent/cogas/navigator.py +401 -0
  43. cogames_agents/policy/scripted_agent/cogas/obs_parser.py +238 -0
  44. cogames_agents/policy/scripted_agent/cogas/policy.py +525 -0
  45. cogames_agents/policy/scripted_agent/cogas/trace.py +69 -0
  46. cogames_agents/policy/scripted_agent/cogsguard/CLAUDE.md +517 -0
  47. cogames_agents/policy/scripted_agent/cogsguard/README.md +252 -0
  48. cogames_agents/policy/scripted_agent/cogsguard/__init__.py +74 -0
  49. cogames_agents/policy/scripted_agent/cogsguard/aligned_junction_held_investigation.md +152 -0
  50. cogames_agents/policy/scripted_agent/cogsguard/aligner.py +333 -0
  51. cogames_agents/policy/scripted_agent/cogsguard/behavior_hooks.py +44 -0
  52. cogames_agents/policy/scripted_agent/cogsguard/control_agent.py +323 -0
  53. cogames_agents/policy/scripted_agent/cogsguard/debug_agent.py +533 -0
  54. cogames_agents/policy/scripted_agent/cogsguard/miner.py +589 -0
  55. cogames_agents/policy/scripted_agent/cogsguard/options.py +67 -0
  56. cogames_agents/policy/scripted_agent/cogsguard/parity_metrics.py +36 -0
  57. cogames_agents/policy/scripted_agent/cogsguard/policy.py +1967 -0
  58. cogames_agents/policy/scripted_agent/cogsguard/prereq_trace.py +33 -0
  59. cogames_agents/policy/scripted_agent/cogsguard/role_trace.py +50 -0
  60. cogames_agents/policy/scripted_agent/cogsguard/roles.py +31 -0
  61. cogames_agents/policy/scripted_agent/cogsguard/rollout_trace.py +40 -0
  62. cogames_agents/policy/scripted_agent/cogsguard/scout.py +69 -0
  63. cogames_agents/policy/scripted_agent/cogsguard/scrambler.py +350 -0
  64. cogames_agents/policy/scripted_agent/cogsguard/targeted_agent.py +418 -0
  65. cogames_agents/policy/scripted_agent/cogsguard/teacher.py +224 -0
  66. cogames_agents/policy/scripted_agent/cogsguard/types.py +381 -0
  67. cogames_agents/policy/scripted_agent/cogsguard/v2_agent.py +49 -0
  68. cogames_agents/policy/scripted_agent/common/__init__.py +0 -0
  69. cogames_agents/policy/scripted_agent/common/geometry.py +24 -0
  70. cogames_agents/policy/scripted_agent/common/roles.py +34 -0
  71. cogames_agents/policy/scripted_agent/common/tag_utils.py +48 -0
  72. cogames_agents/policy/scripted_agent/demo_policy.py +242 -0
  73. cogames_agents/policy/scripted_agent/pathfinding.py +126 -0
  74. cogames_agents/policy/scripted_agent/pinky/DESIGN.md +317 -0
  75. cogames_agents/policy/scripted_agent/pinky/__init__.py +5 -0
  76. cogames_agents/policy/scripted_agent/pinky/behaviors/__init__.py +17 -0
  77. cogames_agents/policy/scripted_agent/pinky/behaviors/aligner.py +400 -0
  78. cogames_agents/policy/scripted_agent/pinky/behaviors/base.py +119 -0
  79. cogames_agents/policy/scripted_agent/pinky/behaviors/miner.py +632 -0
  80. cogames_agents/policy/scripted_agent/pinky/behaviors/scout.py +138 -0
  81. cogames_agents/policy/scripted_agent/pinky/behaviors/scrambler.py +433 -0
  82. cogames_agents/policy/scripted_agent/pinky/policy.py +570 -0
  83. cogames_agents/policy/scripted_agent/pinky/services/__init__.py +7 -0
  84. cogames_agents/policy/scripted_agent/pinky/services/map_tracker.py +808 -0
  85. cogames_agents/policy/scripted_agent/pinky/services/navigator.py +864 -0
  86. cogames_agents/policy/scripted_agent/pinky/services/safety.py +189 -0
  87. cogames_agents/policy/scripted_agent/pinky/state.py +299 -0
  88. cogames_agents/policy/scripted_agent/pinky/types.py +138 -0
  89. cogames_agents/policy/scripted_agent/planky/CLAUDE.md +124 -0
  90. cogames_agents/policy/scripted_agent/planky/IMPROVEMENTS.md +160 -0
  91. cogames_agents/policy/scripted_agent/planky/NOTES.md +153 -0
  92. cogames_agents/policy/scripted_agent/planky/PLAN.md +254 -0
  93. cogames_agents/policy/scripted_agent/planky/README.md +214 -0
  94. cogames_agents/policy/scripted_agent/planky/STRATEGY.md +100 -0
  95. cogames_agents/policy/scripted_agent/planky/__init__.py +5 -0
  96. cogames_agents/policy/scripted_agent/planky/context.py +68 -0
  97. cogames_agents/policy/scripted_agent/planky/entity_map.py +152 -0
  98. cogames_agents/policy/scripted_agent/planky/goal.py +107 -0
  99. cogames_agents/policy/scripted_agent/planky/goals/__init__.py +27 -0
  100. cogames_agents/policy/scripted_agent/planky/goals/aligner.py +168 -0
  101. cogames_agents/policy/scripted_agent/planky/goals/gear.py +179 -0
  102. cogames_agents/policy/scripted_agent/planky/goals/miner.py +416 -0
  103. cogames_agents/policy/scripted_agent/planky/goals/scout.py +40 -0
  104. cogames_agents/policy/scripted_agent/planky/goals/scrambler.py +174 -0
  105. cogames_agents/policy/scripted_agent/planky/goals/shared.py +160 -0
  106. cogames_agents/policy/scripted_agent/planky/goals/stem.py +49 -0
  107. cogames_agents/policy/scripted_agent/planky/goals/survive.py +96 -0
  108. cogames_agents/policy/scripted_agent/planky/navigator.py +388 -0
  109. cogames_agents/policy/scripted_agent/planky/obs_parser.py +238 -0
  110. cogames_agents/policy/scripted_agent/planky/policy.py +485 -0
  111. cogames_agents/policy/scripted_agent/planky/tests/__init__.py +0 -0
  112. cogames_agents/policy/scripted_agent/planky/tests/conftest.py +66 -0
  113. cogames_agents/policy/scripted_agent/planky/tests/helpers.py +152 -0
  114. cogames_agents/policy/scripted_agent/planky/tests/test_aligner.py +24 -0
  115. cogames_agents/policy/scripted_agent/planky/tests/test_miner.py +30 -0
  116. cogames_agents/policy/scripted_agent/planky/tests/test_scout.py +15 -0
  117. cogames_agents/policy/scripted_agent/planky/tests/test_scrambler.py +29 -0
  118. cogames_agents/policy/scripted_agent/planky/tests/test_stem.py +36 -0
  119. cogames_agents/policy/scripted_agent/planky/trace.py +69 -0
  120. cogames_agents/policy/scripted_agent/types.py +239 -0
  121. cogames_agents/policy/scripted_agent/unclipping_agent.py +461 -0
  122. cogames_agents/policy/scripted_agent/utils.py +381 -0
  123. cogames_agents/policy/scripted_registry.py +80 -0
  124. cogames_agents/py.typed +0 -0
  125. cogames_agents-0.0.0.7.dist-info/METADATA +98 -0
  126. cogames_agents-0.0.0.7.dist-info/RECORD +128 -0
  127. cogames_agents-0.0.0.7.dist-info/WHEEL +6 -0
  128. 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)