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,197 @@
1
+ """GetGearGoal — navigate to a station to acquire gear."""
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
+ if TYPE_CHECKING:
12
+ from cogames_agents.policy.scripted_agent.cogas.context import CogasContext
13
+
14
+
15
+ class GetGearGoal(Goal):
16
+ """Navigate to a station to acquire gear for a role.
17
+
18
+ If the team lacks resources to produce gear, the station won't give any.
19
+ Checks collective resources before attempting, to avoid wasting time bumping
20
+ a station that can't dispense gear.
21
+ """
22
+
23
+ # How many bump attempts at dist=1 before exploring for another route
24
+ MAX_BUMPS_AT_STATION = 5
25
+ # How many total steps trying to get gear before giving up temporarily
26
+ MAX_TOTAL_ATTEMPTS = 80
27
+ # How many steps to wait before trying again
28
+ RETRY_INTERVAL = 150
29
+
30
+ def __init__(
31
+ self,
32
+ gear_attr: str,
33
+ station_type: str,
34
+ goal_name: str,
35
+ gear_cost: dict[str, int] | None = None,
36
+ ) -> None:
37
+ self.name = goal_name
38
+ self._gear_attr = gear_attr # e.g. "miner_gear"
39
+ self._station_type = station_type # e.g. "miner_station"
40
+ self._gear_cost = gear_cost or {}
41
+ self._bb_attempts_key = f"{goal_name}_total_attempts"
42
+ self._bb_giveup_step_key = f"{goal_name}_giveup_step"
43
+ self._bb_bump_count_key = f"{goal_name}_bump_count"
44
+ self._bb_last_dist_key = f"{goal_name}_last_dist"
45
+
46
+ # Minimum collective resource reserve — don't consume below this level
47
+ # Reduced from 3 to 1 for faster gear acquisition
48
+ RESOURCE_RESERVE = 1
49
+
50
+ def _collective_can_afford(self, ctx: CogasContext) -> bool:
51
+ """Check if the collective can afford gear while maintaining reserves."""
52
+ if not self._gear_cost:
53
+ return True
54
+ s = ctx.state
55
+ collective = {
56
+ "carbon": s.collective_carbon,
57
+ "oxygen": s.collective_oxygen,
58
+ "germanium": s.collective_germanium,
59
+ "silicon": s.collective_silicon,
60
+ }
61
+ # Must have cost + reserve for each resource
62
+ return all(collective.get(res, 0) >= amt + self.RESOURCE_RESERVE for res, amt in self._gear_cost.items())
63
+
64
+ def _get_hub_center(self, ctx: CogasContext) -> tuple[int, int]:
65
+ """Find hub center from observations, falling back to current position."""
66
+ # Check if we already cached the hub position
67
+ cached_hub = ctx.blackboard.get("_hub_center")
68
+ if cached_hub is not None:
69
+ return cached_hub
70
+
71
+ # Try to find hub from entity map
72
+ pf = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
73
+ hub = ctx.map.find_nearest(ctx.state.position, type_contains="hub", property_filter=pf)
74
+ if hub is not None:
75
+ hub_pos, _ = hub
76
+ ctx.blackboard["_hub_center"] = hub_pos
77
+ return hub_pos
78
+
79
+ # Fall back to current position if hub not visible yet
80
+ return ctx.state.position
81
+
82
+ def is_satisfied(self, ctx: CogasContext) -> bool:
83
+ # Satisfied if we have the gear
84
+ if getattr(ctx.state, self._gear_attr, False):
85
+ # Got gear - reset attempts for next time
86
+ ctx.blackboard[self._bb_attempts_key] = 0
87
+ ctx.blackboard[self._bb_bump_count_key] = 0
88
+ return True
89
+ # Also "satisfied" (skip) if we gave up recently
90
+ giveup_step = ctx.blackboard.get(self._bb_giveup_step_key, -9999)
91
+ if ctx.step - giveup_step < self.RETRY_INTERVAL:
92
+ return True
93
+ # Skip if collective can't afford this gear
94
+ if not self._collective_can_afford(ctx):
95
+ if ctx.trace:
96
+ ctx.trace.skip(self.name, "collective lacks resources")
97
+ return True
98
+ return False
99
+
100
+ def execute(self, ctx: CogasContext) -> Optional[Action]:
101
+ # Track total attempts regardless of distance
102
+ attempts = ctx.blackboard.get(self._bb_attempts_key, 0) + 1
103
+ ctx.blackboard[self._bb_attempts_key] = attempts
104
+
105
+ if attempts > self.MAX_TOTAL_ATTEMPTS:
106
+ # Give up - team probably lacks resources or station unreachable
107
+ ctx.blackboard[self._bb_giveup_step_key] = ctx.step
108
+ ctx.blackboard[self._bb_attempts_key] = 0
109
+ ctx.blackboard[self._bb_bump_count_key] = 0
110
+ if ctx.trace:
111
+ ctx.trace.activate(self.name, "giving up after max attempts")
112
+ return None # Skip to next goal
113
+
114
+ # Find station by type (filter to own team if known)
115
+ pf = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
116
+ result = ctx.map.find_nearest(ctx.state.position, type_contains=self._station_type, property_filter=pf)
117
+ if result is None:
118
+ # Station not discovered yet — navigate toward hub where stations are
119
+ hub_pos = self._get_hub_center(ctx)
120
+ hub_dist = _manhattan(ctx.state.position, hub_pos)
121
+ if ctx.trace:
122
+ ctx.trace.activate(self.name, f"exploring for {self._station_type} (hub dist={hub_dist})")
123
+ if hub_dist > 3:
124
+ # Navigate toward hub
125
+ return ctx.navigator.get_action(ctx.state.position, hub_pos, ctx.map, reach_adjacent=True)
126
+ # At hub — explore nearby to find the station
127
+ return ctx.navigator.explore(ctx.state.position, ctx.map)
128
+
129
+ station_pos, _ = result
130
+ dist = _manhattan(ctx.state.position, station_pos)
131
+
132
+ if ctx.trace:
133
+ ctx.trace.nav_target = station_pos
134
+
135
+ # Track if we're making progress toward the station
136
+ last_dist = ctx.blackboard.get(self._bb_last_dist_key, 999)
137
+ ctx.blackboard[self._bb_last_dist_key] = dist
138
+
139
+ if dist <= 1:
140
+ # Adjacent to station — try to bump into it
141
+ bump_count = ctx.blackboard.get(self._bb_bump_count_key, 0) + 1
142
+ ctx.blackboard[self._bb_bump_count_key] = bump_count
143
+
144
+ if bump_count > self.MAX_BUMPS_AT_STATION:
145
+ # Stuck at dist=1 - explore to find another path
146
+ ctx.blackboard[self._bb_bump_count_key] = 0
147
+ if ctx.trace:
148
+ ctx.trace.activate(self.name, "stuck at dist=1, exploring")
149
+ # Clear navigator cache and explore a random direction
150
+ ctx.navigator._cached_path = None
151
+ ctx.navigator._cached_target = None
152
+ return ctx.navigator.explore(ctx.state.position, ctx.map)
153
+
154
+ if ctx.trace:
155
+ ctx.trace.activate(self.name, f"bump {bump_count}/{self.MAX_BUMPS_AT_STATION}")
156
+ return _move_toward(ctx.state.position, station_pos)
157
+
158
+ # Not adjacent yet - navigate toward station
159
+ ctx.blackboard[self._bb_bump_count_key] = 0
160
+
161
+ # If we're not making progress (dist not decreasing), clear cache and try fresh path
162
+ if dist >= last_dist and attempts > 10:
163
+ ctx.navigator._cached_path = None
164
+ ctx.navigator._cached_target = None
165
+
166
+ return ctx.navigator.get_action(ctx.state.position, station_pos, ctx.map, reach_adjacent=True)
167
+
168
+
169
+ def _move_toward(current: tuple[int, int], target: tuple[int, int]) -> Action:
170
+ """Move one step toward target, trying the most direct direction."""
171
+ dr = target[0] - current[0]
172
+ dc = target[1] - current[1]
173
+
174
+ # When exactly adjacent (dist=1), we want to bump INTO the target
175
+ # Return the direction that would move us onto the target
176
+ if dr == 1 and dc == 0:
177
+ return Action(name="move_south")
178
+ if dr == -1 and dc == 0:
179
+ return Action(name="move_north")
180
+ if dr == 0 and dc == 1:
181
+ return Action(name="move_east")
182
+ if dr == 0 and dc == -1:
183
+ return Action(name="move_west")
184
+
185
+ # For larger distances, prefer the longer axis
186
+ if abs(dr) >= abs(dc):
187
+ if dr > 0:
188
+ return Action(name="move_south")
189
+ elif dr < 0:
190
+ return Action(name="move_north")
191
+ if dc > 0:
192
+ return Action(name="move_east")
193
+ elif dc < 0:
194
+ return Action(name="move_west")
195
+
196
+ # On target — shouldn't happen, but bump north as fallback
197
+ return Action(name="move_north")
@@ -0,0 +1,441 @@
1
+ """Miner goals — pick resource, mine, deposit."""
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
+
17
+ class GetMinerGearGoal(GetGearGoal):
18
+ """Get miner gear (costs C1 O1 G3 S1 from collective).
19
+
20
+ Miners always get gear regardless of reserves — they produce resources.
21
+ """
22
+
23
+ def __init__(self) -> None:
24
+ super().__init__(
25
+ gear_attr="miner_gear",
26
+ station_type="miner_station",
27
+ goal_name="GetMinerGear",
28
+ gear_cost={"carbon": 1, "oxygen": 1, "germanium": 3, "silicon": 1},
29
+ )
30
+
31
+ def _collective_can_afford(self, ctx: "CogasContext") -> bool:
32
+ """Miners always get gear — they're the resource producers.
33
+
34
+ But skip if collective is already well-stocked (no need to mine).
35
+ """
36
+ if _collective_resources_sufficient(ctx):
37
+ return False
38
+ if not self._gear_cost:
39
+ return True
40
+ s = ctx.state
41
+ collective = {
42
+ "carbon": s.collective_carbon,
43
+ "oxygen": s.collective_oxygen,
44
+ "germanium": s.collective_germanium,
45
+ "silicon": s.collective_silicon,
46
+ }
47
+ # No reserve requirement for miners — just need the cost
48
+ return all(collective.get(res, 0) >= amt for res, amt in self._gear_cost.items())
49
+
50
+
51
+ # Resource types that can be mined
52
+ RESOURCE_TYPES = ["carbon", "oxygen", "germanium", "silicon"]
53
+
54
+ # When the collective has more than this amount of every resource, stop mining.
55
+ COLLECTIVE_SUFFICIENT_THRESHOLD = 100
56
+
57
+
58
+ def _collective_resources_sufficient(ctx: "CogasContext") -> bool:
59
+ """Return True when the collective has >COLLECTIVE_SUFFICIENT_THRESHOLD of every resource."""
60
+ s = ctx.state
61
+ return (
62
+ s.collective_carbon > COLLECTIVE_SUFFICIENT_THRESHOLD
63
+ and s.collective_oxygen > COLLECTIVE_SUFFICIENT_THRESHOLD
64
+ and s.collective_germanium > COLLECTIVE_SUFFICIENT_THRESHOLD
65
+ and s.collective_silicon > COLLECTIVE_SUFFICIENT_THRESHOLD
66
+ )
67
+
68
+
69
+ class ExploreHubGoal(Goal):
70
+ """Explore the hub to discover all 4 extractors before mining.
71
+
72
+ Finds the actual hub from observations (not hardcoded position) and
73
+ explores nearby corners to discover extractors.
74
+ """
75
+
76
+ name = "ExploreHub"
77
+ # Hub corner offsets from hub center — extractors at these positions
78
+ HUB_OFFSETS = [(-5, -5), (-5, 5), (5, 5), (5, -5)]
79
+
80
+ def is_satisfied(self, ctx: CogasContext) -> bool:
81
+ found = sum(1 for r in RESOURCE_TYPES if ctx.map.find(type=f"{r}_extractor"))
82
+ if found >= 4:
83
+ return True
84
+ # Time limit: don't explore forever, but give more time (30 steps)
85
+ if ctx.step > 30:
86
+ return True
87
+ return False
88
+
89
+ def execute(self, ctx: CogasContext) -> Optional[Action]:
90
+ # Find actual hub position from observations
91
+ hub_center = self._get_hub_center(ctx)
92
+
93
+ corner_idx = ctx.blackboard.get("_hub_corner_idx", ctx.agent_id % 4)
94
+ offsets = self.HUB_OFFSETS
95
+ target = (hub_center[0] + offsets[corner_idx][0], hub_center[1] + offsets[corner_idx][1])
96
+
97
+ dist = _manhattan(ctx.state.position, target)
98
+ if dist <= 2:
99
+ corner_idx = (corner_idx + 1) % 4
100
+ ctx.blackboard["_hub_corner_idx"] = corner_idx
101
+ target = (hub_center[0] + offsets[corner_idx][0], hub_center[1] + offsets[corner_idx][1])
102
+
103
+ if ctx.trace:
104
+ ctx.trace.nav_target = target
105
+ found = sum(1 for r in RESOURCE_TYPES if ctx.map.find(type=f"{r}_extractor"))
106
+ ctx.trace.activate(self.name, f"corner={corner_idx} found={found}/4 hub={hub_center}")
107
+
108
+ # Check dist to avoid navigator returning noop
109
+ new_dist = _manhattan(ctx.state.position, target)
110
+ if new_dist <= 1:
111
+ return _move_toward(ctx.state.position, target)
112
+ return ctx.navigator.get_action(ctx.state.position, target, ctx.map, reach_adjacent=True)
113
+
114
+ def _get_hub_center(self, ctx: CogasContext) -> tuple[int, int]:
115
+ """Find hub center from observations, falling back to current position."""
116
+ # Check if we already cached the hub position
117
+ cached_hub = ctx.blackboard.get("_hub_center")
118
+ if cached_hub is not None:
119
+ return cached_hub
120
+
121
+ # Try to find hub from entity map
122
+ pf = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
123
+ hub = ctx.map.find_nearest(ctx.state.position, type_contains="hub", property_filter=pf)
124
+ if hub is not None:
125
+ hub_pos, _ = hub
126
+ ctx.blackboard["_hub_center"] = hub_pos
127
+ return hub_pos
128
+
129
+ # Fall back to current position if hub not visible yet
130
+ # (agent will explore from where it spawned)
131
+ return ctx.state.position
132
+
133
+
134
+ class PickResourceGoal(Goal):
135
+ """Select a target resource based on collective needs.
136
+
137
+ Prioritizes the resource that the collective has the least of,
138
+ ensuring balanced gathering for heart production.
139
+ Re-evaluates every 50 steps to adapt to changing needs.
140
+ """
141
+
142
+ name = "PickResource"
143
+ REEVALUATE_INTERVAL = 50
144
+
145
+ def is_satisfied(self, ctx: CogasContext) -> bool:
146
+ # Don't bother picking a resource if collective is well-stocked
147
+ if _collective_resources_sufficient(ctx):
148
+ return True
149
+
150
+ if "target_resource" not in ctx.blackboard:
151
+ return False
152
+
153
+ # Re-evaluate periodically to ensure we're mining what's needed
154
+ last_pick = ctx.blackboard.get("_target_resource_step", 0)
155
+ if ctx.step - last_pick >= self.REEVALUATE_INTERVAL:
156
+ # Clear to force re-evaluation
157
+ ctx.blackboard.pop("target_resource", None)
158
+ return False
159
+
160
+ return True
161
+
162
+ def execute(self, ctx: CogasContext) -> Optional[Action]:
163
+ # Get collective resource levels
164
+ collective = {
165
+ "carbon": ctx.state.collective_carbon,
166
+ "oxygen": ctx.state.collective_oxygen,
167
+ "germanium": ctx.state.collective_germanium,
168
+ "silicon": ctx.state.collective_silicon,
169
+ }
170
+
171
+ # Find resources with available extractors
172
+ available_resources: list[tuple[int, str]] = []
173
+ for resource in RESOURCE_TYPES:
174
+ extractors = ctx.map.find(type=f"{resource}_extractor")
175
+ usable = [
176
+ (pos, e)
177
+ for pos, e in extractors
178
+ if e.properties.get("remaining_uses", 999) > 0
179
+ and e.properties.get("inventory_amount", -1) != 0
180
+ and not _extractor_recently_failed(ctx, pos)
181
+ ]
182
+ if usable:
183
+ # Score by collective amount (lower = higher priority)
184
+ available_resources.append((collective.get(resource, 0), resource))
185
+
186
+ if not available_resources:
187
+ # No extractors known — pick carbon as default, MineResource will explore
188
+ ctx.blackboard["target_resource"] = "carbon"
189
+ ctx.blackboard["_target_resource_step"] = ctx.step
190
+ if ctx.trace:
191
+ ctx.trace.activate(self.name, "no extractors known, defaulting to carbon")
192
+ # Return None to immediately continue to MineResource goal
193
+ return None
194
+
195
+ # Pick the resource the collective has least of (that we can mine)
196
+ available_resources.sort()
197
+ best_resource = available_resources[0][1]
198
+
199
+ if ctx.trace:
200
+ ctx.trace.activate(self.name, f"need={best_resource} coll={collective}")
201
+
202
+ ctx.blackboard["target_resource"] = best_resource
203
+ ctx.blackboard["_target_resource_step"] = ctx.step
204
+ # Return None to immediately continue to next goal (mining)
205
+ return None
206
+
207
+
208
+ def _extractor_recently_failed(ctx: CogasContext, pos: tuple[int, int]) -> bool:
209
+ """Check if we recently failed to mine from this extractor."""
210
+ failed_step = ctx.blackboard.get(f"mine_failed_{pos}", -9999)
211
+ return ctx.step - failed_step < 100 # 100 step cooldown - extractors may refill
212
+
213
+
214
+ class DepositCargoGoal(Goal):
215
+ """Deposit resources at nearest cogs-aligned building when cargo is reasonably full.
216
+
217
+ Triggers when cargo is >= 50% full (or >= 10 resources for small capacity).
218
+ Once triggered, keeps depositing until cargo is EMPTY.
219
+ Tracks attempts and marks depots as failed if cargo doesn't decrease.
220
+ """
221
+
222
+ name = "DepositCargo"
223
+ MAX_ATTEMPTS_PER_DEPOT = 5
224
+
225
+ def is_satisfied(self, ctx: CogasContext) -> bool:
226
+ cargo = ctx.state.cargo_total
227
+
228
+ # If we're currently depositing (flag set), keep going until empty
229
+ if ctx.blackboard.get("_depositing", False):
230
+ if cargo == 0:
231
+ ctx.blackboard["_depositing"] = False
232
+ return True
233
+ return False # Keep depositing until empty
234
+
235
+ # Not currently depositing - check if we should start
236
+ # Deposit when at least 50% full (but always deposit if cargo == capacity)
237
+ capacity = ctx.state.cargo_capacity
238
+ threshold = max(2, capacity // 2)
239
+
240
+ if cargo >= threshold:
241
+ ctx.blackboard["_depositing"] = True
242
+ return False # Start depositing
243
+
244
+ return True # Don't need to deposit yet
245
+
246
+ def execute(self, ctx: CogasContext) -> Optional[Action]:
247
+ # Track cargo to detect successful deposit
248
+ prev_cargo = ctx.blackboard.get("prev_deposit_cargo", ctx.state.cargo_total)
249
+ current_cargo = ctx.state.cargo_total
250
+ ctx.blackboard["prev_deposit_cargo"] = current_cargo
251
+
252
+ # Find nearest cogs depot
253
+ depot_pos = _find_cogs_depot(ctx)
254
+ if depot_pos is None:
255
+ return ctx.navigator.explore(ctx.state.position, ctx.map)
256
+
257
+ if ctx.trace:
258
+ ctx.trace.nav_target = depot_pos
259
+
260
+ dist = _manhattan(ctx.state.position, depot_pos)
261
+ if dist <= 1:
262
+ if ctx.trace:
263
+ hub_dbg_filter = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
264
+ hubs = ctx.map.find(type_contains="hub", property_filter=hub_dbg_filter)
265
+ depot_entity = ctx.map.entities.get(depot_pos)
266
+ print(
267
+ f"[deposit-debug] agent={ctx.agent_id} t={ctx.step} pos={ctx.state.position}"
268
+ f" depot={depot_pos} depot_type={depot_entity.type if depot_entity else 'NONE'}"
269
+ f" depot_align={depot_entity.properties.get('alignment') if depot_entity else 'N/A'}"
270
+ f" cargo={current_cargo} prev={prev_cargo}"
271
+ f" hubs={[(p, e.properties.get('alignment')) for p, e in hubs]}"
272
+ )
273
+ # Adjacent to depot - track attempts
274
+ attempts_key = f"deposit_attempts_{depot_pos}"
275
+ attempts = ctx.blackboard.get(attempts_key, 0) + 1
276
+
277
+ # Reset if cargo decreased (deposit succeeded)
278
+ if current_cargo < prev_cargo:
279
+ ctx.blackboard[attempts_key] = 0
280
+ else:
281
+ ctx.blackboard[attempts_key] = attempts
282
+
283
+ if attempts > self.MAX_ATTEMPTS_PER_DEPOT:
284
+ # Mark as failed temporarily
285
+ ctx.blackboard[f"deposit_failed_{depot_pos}"] = ctx.step
286
+ ctx.blackboard[attempts_key] = 0
287
+ if ctx.trace:
288
+ ctx.trace.activate(self.name, f"giving up on {depot_pos}")
289
+ return ctx.navigator.explore(ctx.state.position, ctx.map)
290
+
291
+ return _move_toward(ctx.state.position, depot_pos)
292
+
293
+ # Not adjacent - reset attempts
294
+ ctx.blackboard[f"deposit_attempts_{depot_pos}"] = 0
295
+ return ctx.navigator.get_action(ctx.state.position, depot_pos, ctx.map, reach_adjacent=True)
296
+
297
+
298
+ class MineResourceGoal(Goal):
299
+ """Navigate to extractor for target_resource and bump it.
300
+
301
+ Tracks attempts at each extractor and marks them as failed if
302
+ cargo doesn't increase after several bumps (extractor empty/broken).
303
+
304
+ NEVER satisfied - miners should always mine or explore, never noop.
305
+ """
306
+
307
+ name = "MineResource"
308
+ MAX_ATTEMPTS_PER_EXTRACTOR = 3 # Reduced from 5 - fail faster
309
+
310
+ def is_satisfied(self, ctx: CogasContext) -> bool:
311
+ # Never satisfied - always mine or explore to avoid noops
312
+ # Even when collective is well-stocked, keep contributing
313
+ return False
314
+
315
+ def execute(self, ctx: CogasContext) -> Optional[Action]:
316
+ target_resource = ctx.blackboard.get("target_resource", "carbon")
317
+
318
+ # Track cargo to detect successful mining
319
+ prev_cargo = ctx.blackboard.get("prev_cargo", 0)
320
+ current_cargo = ctx.state.cargo_total
321
+ ctx.blackboard["prev_cargo"] = current_cargo
322
+
323
+ # Find nearest usable extractor for this resource
324
+ target_pos = self._find_extractor(ctx, target_resource)
325
+
326
+ if target_pos is None:
327
+ # Try any resource type
328
+ for resource in RESOURCE_TYPES:
329
+ if resource == target_resource:
330
+ continue
331
+ target_pos = self._find_extractor(ctx, resource)
332
+ if target_pos:
333
+ ctx.blackboard["target_resource"] = resource
334
+ ctx.blackboard["_target_resource_step"] = ctx.step
335
+ break
336
+
337
+ if target_pos is None:
338
+ # No extractors found — explore in agent-specific direction to discover them
339
+ ctx.blackboard.pop("target_resource", None)
340
+ directions = ["north", "east", "south", "west"]
341
+ return ctx.navigator.explore(
342
+ ctx.state.position,
343
+ ctx.map,
344
+ direction_bias=directions[ctx.agent_id % 4],
345
+ )
346
+
347
+ if ctx.trace:
348
+ ctx.trace.nav_target = target_pos
349
+
350
+ dist = _manhattan(ctx.state.position, target_pos)
351
+ if dist <= 1:
352
+ # Adjacent to extractor — track attempts
353
+ attempts_key = f"mine_attempts_{target_pos}"
354
+ attempts = ctx.blackboard.get(attempts_key, 0) + 1
355
+
356
+ # Reset attempts if cargo increased (mining succeeded)
357
+ if current_cargo > prev_cargo:
358
+ ctx.blackboard[attempts_key] = 0
359
+ else:
360
+ ctx.blackboard[attempts_key] = attempts
361
+
362
+ if attempts > self.MAX_ATTEMPTS_PER_EXTRACTOR:
363
+ # Mark as failed permanently for this episode
364
+ ctx.blackboard[f"mine_failed_{target_pos}"] = ctx.step
365
+ ctx.blackboard[attempts_key] = 0
366
+ # Also clear target resource to force re-evaluation
367
+ ctx.blackboard.pop("target_resource", None)
368
+ if ctx.trace:
369
+ ctx.trace.activate(self.name, f"giving up on {target_pos}")
370
+ return ctx.navigator.explore(
371
+ ctx.state.position,
372
+ ctx.map,
373
+ direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
374
+ )
375
+
376
+ return _move_toward(ctx.state.position, target_pos)
377
+
378
+ # Don't reset attempts when moving away - only reset on successful mine
379
+ return ctx.navigator.get_action(ctx.state.position, target_pos, ctx.map, reach_adjacent=True)
380
+
381
+ def _find_extractor(self, ctx: CogasContext, resource: str) -> Optional[tuple[int, int]]:
382
+ """Find nearest usable extractor."""
383
+ extractors = ctx.map.find(type=f"{resource}_extractor")
384
+ usable = [
385
+ (pos, e)
386
+ for pos, e in extractors
387
+ if e.properties.get("remaining_uses", 999) > 0
388
+ and e.properties.get("inventory_amount", -1) != 0
389
+ and not _extractor_recently_failed(ctx, pos)
390
+ ]
391
+
392
+ if not usable:
393
+ return None
394
+
395
+ # Sort by distance to agent
396
+ usable.sort(key=lambda x: _manhattan(ctx.state.position, x[0]))
397
+ return usable[0][0]
398
+
399
+
400
+ def _find_cogs_depot(ctx: CogasContext) -> tuple[int, int] | None:
401
+ """Find nearest cogs-aligned depot, prioritizing hub."""
402
+ from cogames_agents.policy.scripted_agent.cogas.policy import SPAWN_POS
403
+
404
+ pos = ctx.state.position
405
+
406
+ def recently_failed(p: tuple[int, int]) -> bool:
407
+ failed_step = ctx.blackboard.get(f"deposit_failed_{p}", -9999)
408
+ return ctx.step - failed_step < 100
409
+
410
+ # Prioritize own team's hub
411
+ hub_filter = {"collective_id": ctx.my_collective_id} if ctx.my_collective_id is not None else None
412
+ for apos, _ in ctx.map.find(type_contains="hub", property_filter=hub_filter):
413
+ if not recently_failed(apos):
414
+ return apos
415
+
416
+ # Fallback: nearest cogs junction near hub
417
+ candidates: list[tuple[int, tuple[int, int]]] = []
418
+ for jpos, _ in ctx.map.find(type_contains="junction", property_filter={"alignment": "cogs"}):
419
+ if not recently_failed(jpos) and _manhattan(jpos, SPAWN_POS) <= 15:
420
+ candidates.append((_manhattan(pos, jpos), jpos))
421
+
422
+ if not candidates:
423
+ # Last resort: navigate to hub area
424
+ return SPAWN_POS
425
+ candidates.sort()
426
+ return candidates[0][1]
427
+
428
+
429
+ def _move_toward(current: tuple[int, int], target: tuple[int, int]) -> Action:
430
+ dr = target[0] - current[0]
431
+ dc = target[1] - current[1]
432
+ if abs(dr) >= abs(dc):
433
+ if dr > 0:
434
+ return Action(name="move_south")
435
+ elif dr < 0:
436
+ return Action(name="move_north")
437
+ if dc > 0:
438
+ return Action(name="move_east")
439
+ elif dc < 0:
440
+ return Action(name="move_west")
441
+ return Action(name="move_north")
@@ -0,0 +1,40 @@
1
+ """Scout goals — explore the map."""
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
+ from .gear import GetGearGoal
11
+
12
+ if TYPE_CHECKING:
13
+ from cogames_agents.policy.scripted_agent.cogas.context import CogasContext
14
+
15
+
16
+ class GetScoutGearGoal(GetGearGoal):
17
+ """Get scout gear (costs C1 O1 G1 S3 from collective)."""
18
+
19
+ def __init__(self) -> None:
20
+ super().__init__(
21
+ gear_attr="scout_gear",
22
+ station_type="scout_station",
23
+ goal_name="GetScoutGear",
24
+ gear_cost={"carbon": 1, "oxygen": 1, "germanium": 1, "silicon": 3},
25
+ )
26
+
27
+
28
+ class ExploreGoal(Goal):
29
+ """Explore the map by navigating to frontier cells."""
30
+
31
+ name = "Explore"
32
+
33
+ def is_satisfied(self, ctx: CogasContext) -> bool:
34
+ # Never satisfied — always explore
35
+ return False
36
+
37
+ def execute(self, ctx: CogasContext) -> Action:
38
+ directions = ["north", "east", "south", "west"]
39
+ direction_bias = directions[ctx.agent_id % 4]
40
+ return ctx.navigator.explore(ctx.state.position, ctx.map, direction_bias=direction_bias)