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,5 @@
1
+ """Cogas policy - goal-tree scripted agent."""
2
+
3
+ from .policy import CogasPolicy
4
+
5
+ __all__ = ["CogasPolicy"]
@@ -0,0 +1,68 @@
1
+ """Context and state snapshot for Cogas policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any, Optional
7
+
8
+ if TYPE_CHECKING:
9
+ from .entity_map import EntityMap
10
+ from .navigator import Navigator
11
+ from .trace import TraceLog
12
+
13
+
14
+ @dataclass
15
+ class StateSnapshot:
16
+ """Rebuilt every tick from observation tokens. Observation is source of truth."""
17
+
18
+ position: tuple[int, int] = (0, 0)
19
+
20
+ # Inventory
21
+ carbon: int = 0
22
+ oxygen: int = 0
23
+ germanium: int = 0
24
+ silicon: int = 0
25
+ heart: int = 0
26
+ influence: int = 0
27
+ hp: int = 100
28
+ energy: int = 100
29
+
30
+ # Gear flags
31
+ miner_gear: bool = False
32
+ scout_gear: bool = False
33
+ aligner_gear: bool = False
34
+ scrambler_gear: bool = False
35
+
36
+ # Vibe
37
+ vibe: str = "default"
38
+
39
+ # Collective inventory
40
+ collective_carbon: int = 0
41
+ collective_oxygen: int = 0
42
+ collective_germanium: int = 0
43
+ collective_silicon: int = 0
44
+ collective_heart: int = 0
45
+ collective_influence: int = 0
46
+
47
+ @property
48
+ def cargo_total(self) -> int:
49
+ return self.carbon + self.oxygen + self.germanium + self.silicon
50
+
51
+ @property
52
+ def cargo_capacity(self) -> int:
53
+ return 40 if self.miner_gear else 4
54
+
55
+
56
+ @dataclass
57
+ class CogasContext:
58
+ """Passed to all goals, bundles everything needed for decision-making."""
59
+
60
+ state: StateSnapshot
61
+ map: EntityMap
62
+ blackboard: dict[str, Any]
63
+ navigator: Navigator
64
+ trace: Optional[TraceLog]
65
+ action_names: list[str]
66
+ agent_id: int
67
+ step: int
68
+ my_collective_id: Optional[int] = None
@@ -0,0 +1,152 @@
1
+ """Sparse entity map for Cogas policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+
9
+ @dataclass
10
+ class Entity:
11
+ """An object on the map."""
12
+
13
+ type: str # e.g. "carbon_extractor", "miner_station", "wall", "agent"
14
+ properties: dict # alignment, remaining_uses, inventory_amount, cooldown, etc.
15
+ last_seen: int = 0
16
+
17
+
18
+ class EntityMap:
19
+ """Sparse map of entities. Only stores non-empty cells."""
20
+
21
+ def __init__(self) -> None:
22
+ self.entities: dict[tuple[int, int], Entity] = {}
23
+ self.explored: set[tuple[int, int]] = set()
24
+
25
+ def update_from_observation(
26
+ self,
27
+ agent_pos: tuple[int, int],
28
+ obs_half_height: int,
29
+ obs_half_width: int,
30
+ visible_entities: dict[tuple[int, int], Entity],
31
+ step: int,
32
+ ) -> None:
33
+ """Update map from current observation window.
34
+
35
+ All cells in the observation window are marked as explored.
36
+ Entities in the window are overwritten with fresh data.
37
+ Entities no longer visible in the window are removed.
38
+ """
39
+ # Mark all cells in observation window as explored
40
+ for obs_r in range(2 * obs_half_height + 1):
41
+ for obs_c in range(2 * obs_half_width + 1):
42
+ r = obs_r - obs_half_height + agent_pos[0]
43
+ c = obs_c - obs_half_width + agent_pos[1]
44
+ self.explored.add((r, c))
45
+
46
+ # Remove entities in observation window that are no longer visible
47
+ window_min_r = agent_pos[0] - obs_half_height
48
+ window_max_r = agent_pos[0] + obs_half_height
49
+ window_min_c = agent_pos[1] - obs_half_width
50
+ window_max_c = agent_pos[1] + obs_half_width
51
+
52
+ to_remove = []
53
+ for pos in self.entities:
54
+ if window_min_r <= pos[0] <= window_max_r and window_min_c <= pos[1] <= window_max_c:
55
+ if pos not in visible_entities:
56
+ to_remove.append(pos)
57
+ for pos in to_remove:
58
+ del self.entities[pos]
59
+
60
+ # Add/update visible entities
61
+ for pos, entity in visible_entities.items():
62
+ entity.last_seen = step
63
+ self.entities[pos] = entity
64
+
65
+ def find(
66
+ self,
67
+ type: Optional[str] = None,
68
+ type_contains: Optional[str] = None,
69
+ property_filter: Optional[dict] = None,
70
+ ) -> list[tuple[tuple[int, int], Entity]]:
71
+ """Query entities by type and/or properties.
72
+
73
+ Args:
74
+ type: Exact type match
75
+ type_contains: Substring match on type
76
+ property_filter: Dict of property key-value pairs that must match
77
+ """
78
+ results = []
79
+ for pos, entity in self.entities.items():
80
+ if type is not None and entity.type != type:
81
+ continue
82
+ if type_contains is not None and type_contains not in entity.type:
83
+ continue
84
+ if property_filter is not None:
85
+ match = all(entity.properties.get(k) == v for k, v in property_filter.items())
86
+ if not match:
87
+ continue
88
+ results.append((pos, entity))
89
+ return results
90
+
91
+ def find_nearest(
92
+ self,
93
+ from_pos: tuple[int, int],
94
+ type: Optional[str] = None,
95
+ type_contains: Optional[str] = None,
96
+ property_filter: Optional[dict] = None,
97
+ max_dist: Optional[int] = None,
98
+ ) -> Optional[tuple[tuple[int, int], Entity]]:
99
+ """Find nearest entity matching criteria."""
100
+ matches = self.find(type=type, type_contains=type_contains, property_filter=property_filter)
101
+ if not matches:
102
+ return None
103
+
104
+ best = None
105
+ best_dist = float("inf")
106
+ for pos, entity in matches:
107
+ dist = abs(pos[0] - from_pos[0]) + abs(pos[1] - from_pos[1])
108
+ if max_dist is not None and dist > max_dist:
109
+ continue
110
+ if dist < best_dist:
111
+ best = (pos, entity)
112
+ best_dist = dist
113
+ return best
114
+
115
+ def is_passable(self, pos: tuple[int, int]) -> bool:
116
+ """Check if a position is passable (explored and not a wall/obstacle)."""
117
+ if pos not in self.explored:
118
+ return False
119
+ entity = self.entities.get(pos)
120
+ if entity is None:
121
+ return True # Explored empty cell
122
+ # Agents are temporary obstacles, everything else is permanent
123
+ if entity.type == "agent":
124
+ return False
125
+ # Walls are obstacles
126
+ if entity.type == "wall":
127
+ return False
128
+ # Structures are obstacles (stations, extractors, junctions, etc.)
129
+ # But we don't block pathfinding through them — goals that need adjacency
130
+ # handle that via reach_adjacent=True
131
+ return True # Structures are passable for pathfinding
132
+
133
+ def is_wall(self, pos: tuple[int, int]) -> bool:
134
+ """Check if position is a wall."""
135
+ entity = self.entities.get(pos)
136
+ return entity is not None and entity.type == "wall"
137
+
138
+ def is_structure(self, pos: tuple[int, int]) -> bool:
139
+ """Check if position has a structure (non-wall, non-agent entity)."""
140
+ entity = self.entities.get(pos)
141
+ if entity is None:
142
+ return False
143
+ return entity.type not in ("wall", "agent")
144
+
145
+ def is_free(self, pos: tuple[int, int]) -> bool:
146
+ """Check if position is explored and has no entity."""
147
+ return pos in self.explored and pos not in self.entities
148
+
149
+ def has_agent(self, pos: tuple[int, int]) -> bool:
150
+ """Check if position has an agent."""
151
+ entity = self.entities.get(pos)
152
+ return entity is not None and entity.type == "agent"
@@ -0,0 +1,115 @@
1
+ """Goal base class and evaluation logic for Cogas policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ from mettagrid.simulator import Action
8
+
9
+ if TYPE_CHECKING:
10
+ from .context import CogasContext
11
+
12
+
13
+ class Goal:
14
+ """Base class for all goals in the goal tree.
15
+
16
+ Subclasses implement:
17
+ - is_satisfied(ctx) -> bool: whether this goal is already met
18
+ - preconditions() -> list[Goal]: sub-goals that must be satisfied first
19
+ - execute(ctx) -> Action | None: produce an action, or None to skip/defer
20
+ """
21
+
22
+ name: str = "Goal"
23
+
24
+ def is_satisfied(self, ctx: CogasContext) -> bool:
25
+ """Check if this goal is already satisfied."""
26
+ return False
27
+
28
+ def preconditions(self) -> list[Goal]:
29
+ """Return sub-goals that must be satisfied before this goal can execute."""
30
+ return []
31
+
32
+ def execute(self, ctx: CogasContext) -> Optional[Action]:
33
+ """Produce an action to work toward this goal, or None to skip."""
34
+ return Action(name="noop")
35
+
36
+
37
+ def evaluate_goals(goals: list[Goal], ctx: CogasContext) -> Action:
38
+ """Evaluate a priority-ordered goal list and return an action.
39
+
40
+ Walks the list top-down. The first unsatisfied goal becomes active.
41
+ Recursively checks preconditions to find the deepest unsatisfied leaf.
42
+ That leaf's execute() produces the action.
43
+
44
+ If execute() returns None, the goal is skipped and evaluation continues
45
+ with the next goal (allows goals to voluntarily defer).
46
+ """
47
+ for goal in goals:
48
+ if goal.is_satisfied(ctx):
49
+ if ctx.trace:
50
+ ctx.trace.skip(goal.name, _satisfaction_detail(goal, ctx))
51
+ continue
52
+
53
+ # Found unsatisfied goal — recurse into preconditions
54
+ leaf = _deepest_unsatisfied(goal, ctx)
55
+ action = leaf.execute(ctx)
56
+
57
+ # None means "skip me for now" — continue to next goal
58
+ if action is None:
59
+ if ctx.trace:
60
+ ctx.trace.skip(leaf.name, "deferred")
61
+ continue
62
+
63
+ if ctx.trace:
64
+ ctx.trace.active_goal_chain = _build_chain(goal, leaf)
65
+ ctx.trace.action_name = action.name
66
+
67
+ return action
68
+
69
+ # All goals satisfied - explore as fallback instead of nooping
70
+ if ctx.trace:
71
+ ctx.trace.active_goal_chain = "AllGoalsSatisfied"
72
+ directions = ["north", "east", "south", "west"]
73
+ return ctx.navigator.explore(
74
+ ctx.state.position,
75
+ ctx.map,
76
+ direction_bias=directions[ctx.agent_id % 4],
77
+ )
78
+
79
+
80
+ def _deepest_unsatisfied(goal: Goal, ctx: CogasContext) -> Goal:
81
+ """Find the deepest unsatisfied precondition in the goal tree."""
82
+ for pre in goal.preconditions():
83
+ if not pre.is_satisfied(ctx):
84
+ if ctx.trace:
85
+ ctx.trace.activate(pre.name)
86
+ return _deepest_unsatisfied(pre, ctx)
87
+ return goal
88
+
89
+
90
+ def _build_chain(root: Goal, leaf: Goal) -> str:
91
+ """Build a display chain like 'MineCarbon>BeNearExtractor'."""
92
+ if root is leaf:
93
+ return root.name
94
+ # Walk preconditions to find the path
95
+ chain = [root.name]
96
+ _find_path(root, leaf, chain)
97
+ return ">".join(chain)
98
+
99
+
100
+ def _find_path(current: Goal, target: Goal, chain: list[str]) -> bool:
101
+ """DFS to find path from current to target goal."""
102
+ for pre in current.preconditions():
103
+ if pre is target:
104
+ chain.append(pre.name)
105
+ return True
106
+ chain.append(pre.name)
107
+ if _find_path(pre, target, chain):
108
+ return True
109
+ chain.pop()
110
+ return False
111
+
112
+
113
+ def _satisfaction_detail(goal: Goal, ctx: CogasContext) -> str:
114
+ """Generate a short detail string for why a goal is satisfied."""
115
+ return "ok"
@@ -0,0 +1,27 @@
1
+ """Goal classes for Cogas policy."""
2
+
3
+ from .aligner import AlignJunctionGoal, GetAlignerGearGoal
4
+ from .gear import GetGearGoal
5
+ from .miner import DepositCargoGoal, GetMinerGearGoal, MineResourceGoal, PickResourceGoal
6
+ from .scout import ExploreGoal, GetScoutGearGoal
7
+ from .scrambler import GetScramblerGearGoal, ScrambleJunctionGoal
8
+ from .shared import GetHeartsGoal
9
+ from .stem import SelectRoleGoal
10
+ from .survive import SurviveGoal
11
+
12
+ __all__ = [
13
+ "SurviveGoal",
14
+ "GetGearGoal",
15
+ "GetAlignerGearGoal",
16
+ "GetMinerGearGoal",
17
+ "GetScoutGearGoal",
18
+ "GetScramblerGearGoal",
19
+ "GetHeartsGoal",
20
+ "PickResourceGoal",
21
+ "DepositCargoGoal",
22
+ "MineResourceGoal",
23
+ "ExploreGoal",
24
+ "AlignJunctionGoal",
25
+ "ScrambleJunctionGoal",
26
+ "SelectRoleGoal",
27
+ ]
@@ -0,0 +1,160 @@
1
+ """Aligner goals — align neutral 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 GetAlignerGearGoal(GetGearGoal):
20
+ """Get aligner gear (costs C3 O1 G1 S1 from collective)."""
21
+
22
+ def __init__(self) -> None:
23
+ super().__init__(
24
+ gear_attr="aligner_gear",
25
+ station_type="aligner_station",
26
+ goal_name="GetAlignerGear",
27
+ gear_cost={"carbon": 3, "oxygen": 1, "germanium": 1, "silicon": 1},
28
+ )
29
+
30
+
31
+ class AlignJunctionGoal(Goal):
32
+ """Find and align a neutral junction to cogs.
33
+
34
+ Tracks attempts per junction to avoid getting stuck on one that
35
+ can't be captured (e.g., already aligned but map hasn't updated).
36
+ """
37
+
38
+ name = "AlignJunction"
39
+ MAX_ATTEMPTS_PER_TARGET = 15 # Increased from 5 - 55% move fail needs more attempts
40
+ MAX_NAV_STEPS_PER_TARGET = 80 # Increased from 40 - give more time to navigate
41
+ COOLDOWN_STEPS = 30 # Reduced from 50 - try junctions again sooner
42
+
43
+ def is_satisfied(self, ctx: CogasContext) -> bool:
44
+ # Can't align without gear and a heart
45
+ if not ctx.state.aligner_gear:
46
+ if ctx.trace:
47
+ ctx.trace.skip(self.name, "no gear")
48
+ return True
49
+ if ctx.state.heart < 1:
50
+ if ctx.trace:
51
+ ctx.trace.skip(self.name, "no heart")
52
+ return True
53
+ return False
54
+
55
+ def execute(self, ctx: CogasContext) -> Optional[Action]:
56
+ nav_key = "_align_nav_steps"
57
+ nav_target_key = "_align_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
+ # Nav timeout — mark target as failed
78
+ if nav_steps > self.MAX_NAV_STEPS_PER_TARGET:
79
+ failed_key = f"align_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"align_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"align_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
+ # Clear and try a different junction next tick
108
+ return ctx.navigator.explore(
109
+ ctx.state.position,
110
+ ctx.map,
111
+ direction_bias=["north", "east", "south", "west"][ctx.agent_id % 4],
112
+ )
113
+
114
+ if ctx.trace:
115
+ ctx.trace.activate(self.name, f"bump {attempts}/{self.MAX_ATTEMPTS_PER_TARGET}")
116
+ return _move_toward(ctx.state.position, target)
117
+
118
+ # Not adjacent - reset attempts for this target
119
+ attempts_key = f"align_attempts_{target}"
120
+ ctx.blackboard[attempts_key] = 0
121
+ return ctx.navigator.get_action(ctx.state.position, target, ctx.map, reach_adjacent=True)
122
+
123
+ def _find_best_target(self, ctx: CogasContext) -> tuple[int, int] | None:
124
+ """Find nearest neutral junction, including contested ones."""
125
+ pos = ctx.state.position
126
+
127
+ def recently_failed(p: tuple[int, int]) -> bool:
128
+ failed_step = ctx.blackboard.get(f"align_failed_{p}", -9999)
129
+ return ctx.step - failed_step < self.COOLDOWN_STEPS
130
+
131
+ # Find neutral junctions (no AOE filter — aligners go where needed)
132
+ candidates: list[tuple[int, tuple[int, int]]] = []
133
+
134
+ for jpos, e in ctx.map.find(type_contains="junction"):
135
+ alignment = e.properties.get("alignment")
136
+ if alignment is not None:
137
+ continue # Not neutral
138
+ if recently_failed(jpos):
139
+ continue
140
+ candidates.append((_manhattan(pos, jpos), jpos))
141
+
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")