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,24 @@
1
+ """Capability tests for the Aligner role."""
2
+
3
+ from cogames_agents.policy.scripted_agent.planky.tests.helpers import EpisodeResult
4
+
5
+
6
+ def test_aligner_acquires_gear(aligner_episode: EpisodeResult):
7
+ gained = aligner_episode.gear_gained("aligner")
8
+ assert gained > 0, (
9
+ f"Aligner did not acquire gear (aligner.gained={gained})\nTrace:\n{aligner_episode.trace.summary()}"
10
+ )
11
+
12
+
13
+ def test_aligner_acquires_hearts(aligner_episode: EpisodeResult):
14
+ hearts = aligner_episode.hearts_gained()
15
+ assert hearts > 0, (
16
+ f"Aligner did not acquire hearts (heart.gained={hearts})\nTrace:\n{aligner_episode.trace.summary()}"
17
+ )
18
+
19
+
20
+ def test_aligner_aligns_junctions(aligner_episode: EpisodeResult):
21
+ aligned = aligner_episode.junctions_aligned()
22
+ assert aligned > 0, (
23
+ f"Aligner did not align any junctions (junction.gained={aligned})\nTrace:\n{aligner_episode.trace.summary()}"
24
+ )
@@ -0,0 +1,30 @@
1
+ """Capability tests for the Miner role."""
2
+
3
+ from cogames_agents.policy.scripted_agent.planky.tests.helpers import EpisodeResult
4
+
5
+
6
+ def test_miner_acquires_gear(miner_episode: EpisodeResult):
7
+ gained = miner_episode.gear_gained("miner")
8
+ assert gained > 0, f"Miner did not acquire gear (miner.gained={gained})\nTrace:\n{miner_episode.trace.summary()}"
9
+
10
+
11
+ def test_miner_deposits_resources(miner_episode: EpisodeResult):
12
+ total = miner_episode.total_deposited()
13
+ assert total > 0, (
14
+ f"Miner did not deposit any resources (total_deposited={total})\nTrace:\n{miner_episode.trace.summary()}"
15
+ )
16
+
17
+
18
+ def test_miner_mines_multiple_elements(miner_episode: EpisodeResult):
19
+ elements = ["carbon", "oxygen", "germanium", "silicon"]
20
+ deposited = {e: miner_episode.resource_deposited(e) for e in elements}
21
+ non_zero = [e for e, v in deposited.items() if v > 0]
22
+ assert len(non_zero) >= 1, f"Miner did not mine any elements: {deposited}\nTrace:\n{miner_episode.trace.summary()}"
23
+
24
+
25
+ def test_miner_stays_productive(miner_episode: EpisodeResult):
26
+ """Miner should deposit a meaningful amount of resources in 500 steps."""
27
+ total = miner_episode.total_deposited()
28
+ assert total >= 10, (
29
+ f"Miner deposited too few resources ({total}), may be stuck or idle\nTrace:\n{miner_episode.trace.summary()}"
30
+ )
@@ -0,0 +1,15 @@
1
+ """Capability tests for the Scout role."""
2
+
3
+ from cogames_agents.policy.scripted_agent.planky.tests.helpers import EpisodeResult
4
+
5
+
6
+ def test_scout_acquires_gear(scout_episode: EpisodeResult):
7
+ gained = scout_episode.gear_gained("scout")
8
+ assert gained > 0, f"Scout did not acquire gear (scout.gained={gained})\nTrace:\n{scout_episode.trace.summary()}"
9
+
10
+
11
+ def test_scout_explores(scout_episode: EpisodeResult):
12
+ """Scout should be actively exploring (Explore goal activated in trace)."""
13
+ assert scout_episode.trace.had_goal("Explore"), (
14
+ f"Scout never activated Explore goal\nTrace:\n{scout_episode.trace.summary()}"
15
+ )
@@ -0,0 +1,29 @@
1
+ """Capability tests for the Scrambler role."""
2
+
3
+ import pytest
4
+
5
+ from cogames_agents.policy.scripted_agent.planky.tests.helpers import EpisodeResult
6
+
7
+
8
+ def test_scrambler_acquires_gear(scrambler_episode: EpisodeResult):
9
+ gained = scrambler_episode.gear_gained("scrambler")
10
+ assert gained > 0, (
11
+ f"Scrambler did not acquire gear (scrambler.gained={gained})\nTrace:\n{scrambler_episode.trace.summary()}"
12
+ )
13
+
14
+
15
+ def test_scrambler_acquires_hearts(scrambler_episode: EpisodeResult):
16
+ hearts = scrambler_episode.hearts_gained()
17
+ assert hearts > 0, (
18
+ f"Scrambler did not acquire hearts (heart.gained={hearts})\nTrace:\n{scrambler_episode.trace.summary()}"
19
+ )
20
+
21
+
22
+ @pytest.mark.xfail(reason="Scrambler cannot reach enemy junctions in 500 steps with limited economy")
23
+ def test_scrambler_scrambles_junctions(scrambler_episode: EpisodeResult):
24
+ # Check for scrambled junctions in clips stats (enemy junctions neutralized)
25
+ clips_aligned = int(scrambler_episode.clips_stats.get("junction.lost", 0))
26
+ assert clips_aligned > 0, (
27
+ f"Scrambler did not scramble any enemy junctions (clips junction.lost={clips_aligned})\n"
28
+ f"Trace:\n{scrambler_episode.trace.summary()}"
29
+ )
@@ -0,0 +1,36 @@
1
+ """Capability tests for the Stem (dynamic role selection) system."""
2
+
3
+ from cogames_agents.policy.scripted_agent.planky.tests.helpers import EpisodeResult
4
+
5
+
6
+ def test_stem_agents_select_roles(stem_episode: EpisodeResult):
7
+ changes = stem_episode.trace.role_changes
8
+ assert len(changes) > 0, f"No role changes detected in trace\nTrace:\n{stem_episode.trace.summary()}"
9
+
10
+
11
+ def test_stem_multiple_roles_represented(stem_episode: EpisodeResult):
12
+ roles_seen = set()
13
+ for line in stem_episode.trace.role_changes:
14
+ # Format: "[planky][t=5 a=1] role: stem→miner"
15
+ if "→" in line:
16
+ role = line.split("→")[-1].strip()
17
+ roles_seen.add(role)
18
+ assert len(roles_seen) >= 2, (
19
+ f"Only {len(roles_seen)} role(s) selected: {roles_seen}. "
20
+ f"Expected at least 2 distinct roles.\n"
21
+ f"Trace:\n{stem_episode.trace.summary()}"
22
+ )
23
+
24
+
25
+ def test_stem_economy_produces_resources(stem_episode: EpisodeResult):
26
+ total = stem_episode.total_deposited()
27
+ assert total > 0, (
28
+ f"Stem team did not deposit any resources (total_deposited={total})\nTrace:\n{stem_episode.trace.summary()}"
29
+ )
30
+
31
+
32
+ def test_stem_pipeline_aligns_junctions(stem_episode: EpisodeResult):
33
+ aligned = stem_episode.junctions_aligned()
34
+ assert aligned > 0, (
35
+ f"Stem team did not align any junctions (junction.gained={aligned})\nTrace:\n{stem_episode.trace.summary()}"
36
+ )
@@ -0,0 +1,69 @@
1
+ """Tracing system for Planky policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Optional
7
+
8
+
9
+ @dataclass
10
+ class TraceEntry:
11
+ """One goal evaluation entry."""
12
+
13
+ goal_name: str
14
+ satisfied: bool
15
+ detail: str = ""
16
+
17
+
18
+ @dataclass
19
+ class TraceLog:
20
+ """Collects trace information during a single tick."""
21
+
22
+ entries: list[TraceEntry] = field(default_factory=list)
23
+ active_goal_chain: str = ""
24
+ action_name: str = ""
25
+ blackboard_summary: str = ""
26
+ nav_target: Optional[tuple[int, int]] = None
27
+ steps_since_useful: int = 0 # Steps since last useful action (mine/deposit/align/scramble)
28
+
29
+ def skip(self, goal_name: str, reason: str = "ok") -> None:
30
+ """Record a satisfied (skipped) goal."""
31
+ self.entries.append(TraceEntry(goal_name=goal_name, satisfied=True, detail=reason))
32
+
33
+ def activate(self, goal_name: str, detail: str = "") -> None:
34
+ """Record an activated (unsatisfied) goal."""
35
+ self.entries.append(TraceEntry(goal_name=goal_name, satisfied=False, detail=detail))
36
+
37
+ def format_line(
38
+ self,
39
+ step: int,
40
+ agent_id: int,
41
+ role: str,
42
+ pos: tuple[int, int],
43
+ hp: int,
44
+ level: int,
45
+ ) -> str:
46
+ """Format the trace as a single line."""
47
+ # Include idle indicator if agent hasn't done anything useful recently
48
+ idle_str = f" IDLE={self.steps_since_useful}" if self.steps_since_useful >= 20 else ""
49
+ prefix = f"[t={step} a={agent_id} {role} ({pos[0]},{pos[1]}) hp={hp}{idle_str}]"
50
+
51
+ if level == 1:
52
+ return f"{prefix} {self.active_goal_chain} → {self.action_name}"
53
+
54
+ if level == 2:
55
+ skips = " ".join(f"skip:{e.goal_name}({e.detail})" for e in self.entries if e.satisfied)
56
+ target_str = ""
57
+ if self.nav_target:
58
+ dist = abs(self.nav_target[0] - pos[0]) + abs(self.nav_target[1] - pos[1])
59
+ target_str = f" dist={dist}"
60
+ bb = f" | bb={{{self.blackboard_summary}}}" if self.blackboard_summary else ""
61
+ idle_detail = f" idle={self.steps_since_useful}" if self.steps_since_useful > 0 else ""
62
+ return f"{prefix} {skips} → {self.active_goal_chain}{target_str} → {self.action_name}{bb}{idle_detail}"
63
+
64
+ # Level 3 — full detail
65
+ all_entries = " ".join(f"{'skip' if e.satisfied else 'ACTIVE'}:{e.goal_name}({e.detail})" for e in self.entries)
66
+ target_str = f" nav_target={self.nav_target}" if self.nav_target else ""
67
+ bb = f" bb={{{self.blackboard_summary}}}" if self.blackboard_summary else ""
68
+ idle_detail = f" idle={self.steps_since_useful}"
69
+ return f"{prefix} {all_entries}{target_str} → {self.action_name}{bb}{idle_detail}"
@@ -0,0 +1,239 @@
1
+ """
2
+ Data types and structures for scripted agents.
3
+
4
+ This module contains all the dataclasses, enums, and type definitions
5
+ used by the baseline and unclipping agents.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+ from typing import Optional
13
+
14
+ from mettagrid.simulator import Action
15
+ from mettagrid.simulator.interface import AgentObservation
16
+
17
+
18
+ @dataclass
19
+ class BaselineHyperparameters:
20
+ """Hyperparameters controlling baseline agent behavior."""
21
+
22
+ # Energy management (recharge timing)
23
+ recharge_threshold_low: int = 35 # Enter RECHARGE phase when energy < this
24
+ recharge_threshold_high: int = 85 # Exit RECHARGE phase when energy >= this
25
+
26
+ # Stuck detection and escape
27
+ stuck_detection_enabled: bool = True # Enable loop detection
28
+ stuck_escape_distance: int = 12 # Minimum distance for escape target
29
+
30
+ # Exploration parameters
31
+ position_history_size: int = 30 # Size of position history buffer
32
+ exploration_area_check_window: int = 30 # Steps to check for stuck area
33
+ exploration_area_size_threshold: int = 7 # Max area size (height/width) to trigger escape
34
+ exploration_escape_duration: int = 10 # Steps to navigate to hub when stuck
35
+ exploration_direction_persistence: int = 10 # Steps to persist in one direction
36
+ exploration_hub_distance_threshold: int = 10 # Min distance from hub to trigger escape
37
+
38
+
39
+ # Hyperparameter Presets for Ensemble Creation
40
+ BASELINE_HYPERPARAMETER_PRESETS = {
41
+ "default": BaselineHyperparameters(
42
+ recharge_threshold_low=35, # Moderate energy management
43
+ recharge_threshold_high=85,
44
+ stuck_detection_enabled=True,
45
+ stuck_escape_distance=12,
46
+ position_history_size=40, # Thorough exploration: longer history
47
+ exploration_area_check_window=35, # Thorough exploration: longer check window
48
+ exploration_area_size_threshold=9, # Thorough exploration: larger area tolerance
49
+ exploration_escape_duration=8, # Thorough exploration: shorter escape duration
50
+ exploration_direction_persistence=18, # Thorough exploration: longer persistence
51
+ exploration_hub_distance_threshold=12, # Thorough exploration: larger distance threshold
52
+ ),
53
+ "conservative": BaselineHyperparameters(
54
+ recharge_threshold_low=50, # Recharge early
55
+ recharge_threshold_high=95, # Stay charged
56
+ stuck_detection_enabled=True,
57
+ stuck_escape_distance=8, # Shorter escape distance
58
+ position_history_size=30,
59
+ exploration_area_check_window=30,
60
+ exploration_area_size_threshold=7,
61
+ exploration_escape_duration=10,
62
+ exploration_direction_persistence=10,
63
+ exploration_hub_distance_threshold=10,
64
+ ),
65
+ "aggressive": BaselineHyperparameters(
66
+ recharge_threshold_low=20, # Low energy tolerance
67
+ recharge_threshold_high=80, # Don't wait for full charge
68
+ stuck_detection_enabled=True,
69
+ stuck_escape_distance=15, # Longer escape distance
70
+ position_history_size=30,
71
+ exploration_area_check_window=30,
72
+ exploration_area_size_threshold=7,
73
+ exploration_escape_duration=10,
74
+ exploration_direction_persistence=10,
75
+ exploration_hub_distance_threshold=10,
76
+ ),
77
+ }
78
+
79
+
80
+ class CellType(Enum):
81
+ """Occupancy map cell states."""
82
+
83
+ FREE = 1 # Passable (can walk through)
84
+ OBSTACLE = 2 # Impassable (walls, stations, extractors)
85
+
86
+
87
+ class Phase(Enum):
88
+ """Goal-driven phases for the baseline agent."""
89
+
90
+ GATHER = "gather" # Collect resources (explore if needed)
91
+ ASSEMBLE = "assemble" # Make hearts at hub
92
+ DELIVER = "deliver" # Deposit hearts to chest
93
+ RECHARGE = "recharge" # Recharge energy at junction
94
+ CRAFT_UNCLIP = "craft_unclip" # Craft unclip items at hub
95
+ UNCLIP = "unclip" # Unclip extractors
96
+
97
+
98
+ @dataclass
99
+ class ExtractorInfo:
100
+ """Tracks a discovered extractor with full state."""
101
+
102
+ position: tuple[int, int]
103
+ resource_type: str # "carbon", "oxygen", "germanium", "silicon"
104
+ last_seen_step: int
105
+ times_used: int = 0
106
+
107
+ # Extractor state from observations
108
+ cooldown_remaining: int = 0 # Steps until ready
109
+ clipped: bool = False # Is it depleted?
110
+ remaining_uses: int = 999 # How many uses left
111
+
112
+
113
+ @dataclass
114
+ class ObjectState:
115
+ """State of a single object at a position."""
116
+
117
+ name: str
118
+ tags: list[str] = field(default_factory=list)
119
+
120
+ # Extractor/station features
121
+ cooldown_remaining: int = 0
122
+ clipped: int = 0
123
+ remaining_uses: int = 999
124
+
125
+ # Inventory (for chests/extractors - maps resource name to amount)
126
+ inventory: dict[str, int] = field(default_factory=dict)
127
+
128
+ # Protocol details (recipes for hubs/extractors)
129
+ protocol_inputs: dict[str, int] = field(default_factory=dict)
130
+ protocol_outputs: dict[str, int] = field(default_factory=dict)
131
+
132
+ # Agent features (when object is another agent)
133
+ agent_id: int = -1 # Which agent (-1 if not an agent) - NOT in observations, kept for API
134
+ agent_group: int = -1 # Team/group
135
+ agent_frozen: int = 0 # Is frozen?
136
+
137
+
138
+ @dataclass
139
+ class ParsedObservation:
140
+ """Parsed observation data in a clean format."""
141
+
142
+ # Agent state
143
+ row: int
144
+ col: int
145
+ energy: int
146
+
147
+ # Inventory
148
+ carbon: int
149
+ oxygen: int
150
+ germanium: int
151
+ silicon: int
152
+ hearts: int
153
+ decoder: int
154
+ modulator: int
155
+ resonator: int
156
+ scrambler: int
157
+
158
+ # Nearby objects with full state (position -> ObjectState)
159
+ nearby_objects: dict[tuple[int, int], ObjectState]
160
+
161
+
162
+ @dataclass
163
+ class SimpleAgentState:
164
+ """State for a single agent."""
165
+
166
+ agent_id: int
167
+
168
+ phase: Phase = Phase.GATHER # Start gathering immediately
169
+ step_count: int = 0
170
+
171
+ # Current position (origin-relative, starting at (0, 0))
172
+ row: int = 0
173
+ col: int = 0
174
+ energy: int = 100
175
+
176
+ # Per-agent discovered extractors and stations (no shared state, each agent tracks independently)
177
+ extractors: dict[str, list[ExtractorInfo]] = field(
178
+ default_factory=lambda: {"carbon": [], "oxygen": [], "germanium": [], "silicon": []}
179
+ )
180
+ stations: dict[str, tuple[int, int] | None] = field(
181
+ default_factory=lambda: {"hub": None, "chest": None, "junction": None}
182
+ )
183
+
184
+ # Inventory
185
+ carbon: int = 0
186
+ oxygen: int = 0
187
+ germanium: int = 0
188
+ silicon: int = 0
189
+ hearts: int = 0
190
+ decoder: int = 0
191
+ modulator: int = 0
192
+ resonator: int = 0
193
+ scrambler: int = 0
194
+
195
+ # Current target
196
+ target_position: Optional[tuple[int, int]] = None
197
+ target_resource: Optional[str] = None
198
+
199
+ # Map knowledge
200
+ map_height: int = 0
201
+ map_width: int = 0
202
+ occupancy: list[list[int]] = field(default_factory=list) # 1=free, 2=obstacle (initialized in reset)
203
+
204
+ # Track last action for position updates
205
+ last_action: Action = field(default_factory=lambda: Action(name="noop"))
206
+
207
+ # Current glyph (vibe) for interacting with hub
208
+ current_glyph: str = "default"
209
+
210
+ # Discovered hub recipe (dynamically discovered from observations)
211
+ heart_recipe: Optional[dict[str, int]] = None
212
+
213
+ # Extractor activation state
214
+ waiting_at_extractor: Optional[tuple[int, int]] = None
215
+ wait_steps: int = 0
216
+ pending_use_resource: Optional[str] = None
217
+ pending_use_amount: int = 0
218
+
219
+ # Directional exploration state
220
+ exploration_target: Optional[str] = None # Current direction ("north", "south", "east", "west")
221
+ exploration_target_step: int = 0 # When we set the direction
222
+ exploration_escape_until_step: int = 0 # If > 0, we're in escape mode until this step
223
+
224
+ # Agent positions (for collision detection)
225
+ agent_occupancy: set[tuple[int, int]] = field(default_factory=set)
226
+
227
+ # Stuck detection
228
+ position_history: list[tuple[int, int]] = field(default_factory=list) # Last 30 positions
229
+ stuck_loop_detected: bool = False
230
+ stuck_escape_step: int = 0
231
+
232
+ # Path caching for efficient navigation (per-agent)
233
+ cached_path: Optional[list[tuple[int, int]]] = None
234
+ cached_path_target: Optional[tuple[int, int]] = None
235
+ cached_path_reach_adjacent: bool = False
236
+ using_object_this_step: bool = False # Flag to prevent position update when using objects
237
+
238
+ # Current observation (for collision detection and state updates)
239
+ current_obs: Optional[AgentObservation] = None