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