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,214 @@
1
+ # Planky — Goal-Tree Scripted Agent
2
+
3
+ Planky is a goal-tree scripted policy where each agent evaluates a priority-ordered list of goals each tick. The first
4
+ unsatisfied goal decomposes into preconditions, and the deepest unsatisfied leaf produces an action.
5
+
6
+ ## Quick Start
7
+
8
+ ```bash
9
+ # Watch planky play a cogsguard match (GUI mode)
10
+ cogames play --mission cogsguard_machina_1.basic \
11
+ --policy "metta://policy/planky"
12
+
13
+ # Terminal mode (no GUI needed)
14
+ cogames play --mission cogsguard_machina_1.basic \
15
+ --policy "metta://policy/planky" \
16
+ --render unicode
17
+
18
+ # Run a multi-episode scrimmage
19
+ cogames scrimmage --mission cogsguard_machina_1.basic \
20
+ --policy "metta://policy/planky" \
21
+ --episodes 10
22
+ ```
23
+
24
+ ## Policy URI Parameters
25
+
26
+ Configure agent role counts and tracing via query string:
27
+
28
+ ```
29
+ metta://policy/planky?miner=4&scout=0&aligner=2&scrambler=4&stem=0&trace=0
30
+ ```
31
+
32
+ | Parameter | Default | Description |
33
+ | ------------- | ------- | --------------------------------------------- |
34
+ | `miner` | 0 | Number of miner agents |
35
+ | `scout` | 0 | Number of scout agents |
36
+ | `aligner` | 0 | Number of aligner agents |
37
+ | `scrambler` | 4 | Number of scrambler agents |
38
+ | `stem` | 0 | Number of stem agents (auto-select role) |
39
+ | `trace` | 0 | Enable tracing (1=on) |
40
+ | `trace_level` | 1 | Trace verbosity: 1=minimal, 2=context, 3=full |
41
+ | `trace_agent` | -1 | Trace only this agent ID (-1=all) |
42
+
43
+ Agents beyond the total count stay on "default" vibe (inactive/noop).
44
+
45
+ ## Debugging with Tracing
46
+
47
+ ### Enable trace output
48
+
49
+ ```bash
50
+ # Trace all agents at level 1 (one line per tick: goal chain + action)
51
+ cogames play --mission cogsguard_machina_1.basic \
52
+ --policy "metta://policy/planky?miner=2&scrambler=2&trace=1"
53
+
54
+ # Trace only agent 0 at level 2 (shows why each goal was skipped)
55
+ cogames play --mission cogsguard_machina_1.basic \
56
+ --policy "metta://policy/planky?miner=2&scrambler=2&trace=1&trace_agent=0&trace_level=2"
57
+
58
+ # Maximum detail — level 3
59
+ cogames play --mission cogsguard_machina_1.basic \
60
+ --policy "metta://policy/planky?miner=1&trace=1&trace_agent=0&trace_level=3"
61
+ ```
62
+
63
+ ### Trace output format
64
+
65
+ **Level 1** — Goal chain and action:
66
+
67
+ ```
68
+ [planky] [t=142 a=2 miner (105,98) hp=73] MineResource>BeNearExtractor → move_east
69
+ ```
70
+
71
+ **Level 2** — Adds skip reasons, blackboard, navigation target:
72
+
73
+ ```
74
+ [planky] [t=142 a=2 miner (105,98) hp=73] skip:Survive(ok) skip:GetMinerGear(ok) skip:DepositCargo(ok) → MineResource dist=13 → move_east | bb={target_resource=carbon}
75
+ ```
76
+
77
+ **Level 3** — Full detail including all goal evaluations:
78
+
79
+ ```
80
+ [planky] [t=142 a=2 miner (105,98) hp=73] skip:Survive(ok) skip:GetMinerGear(ok) skip:PickResource(ok) skip:DepositCargo(ok) ACTIVE:MineResource() nav_target=(110,95) → move_east bb={target_resource=carbon}
81
+ ```
82
+
83
+ ### Filtering trace output
84
+
85
+ Pipe through grep to focus on specific events:
86
+
87
+ ```bash
88
+ # Only retreat events
89
+ cogames play -m cogsguard_machina_1.basic \
90
+ -p "metta://policy/planky?miner=2&trace=1&trace_level=2" 2>&1 | grep Survive
91
+
92
+ # Only a specific agent
93
+ cogames play -m cogsguard_machina_1.basic \
94
+ -p "metta://policy/planky?miner=4&trace=1" 2>&1 | grep "a=2"
95
+
96
+ # Watch goal transitions (when active goal changes)
97
+ cogames play -m cogsguard_machina_1.basic \
98
+ -p "metta://policy/planky?miner=2&trace=1" --render log 2>&1 | grep planky
99
+ ```
100
+
101
+ ## Role Configurations
102
+
103
+ ### Mining-heavy (resource gathering)
104
+
105
+ ```bash
106
+ cogames play -m cogsguard_machina_1.basic \
107
+ -p "metta://policy/planky?miner=6&aligner=2&scrambler=2"
108
+ ```
109
+
110
+ ### Balanced (default)
111
+
112
+ ```bash
113
+ cogames play -m cogsguard_machina_1.basic \
114
+ -p "metta://policy/planky?miner=4&aligner=2&scrambler=4"
115
+ ```
116
+
117
+ ### Combat-heavy (territory control)
118
+
119
+ ```bash
120
+ cogames play -m cogsguard_machina_1.basic \
121
+ -p "metta://policy/planky?miner=2&aligner=3&scrambler=5"
122
+ ```
123
+
124
+ ### With scouting
125
+
126
+ ```bash
127
+ cogames play -m cogsguard_machina_1.basic \
128
+ -p "metta://policy/planky?miner=3&scout=2&aligner=2&scrambler=3"
129
+ ```
130
+
131
+ ### Stem agents (auto-role selection)
132
+
133
+ ```bash
134
+ cogames play -m cogsguard_machina_1.basic \
135
+ -p "metta://policy/planky?stem=10"
136
+ ```
137
+
138
+ ## Comparing Planky vs Pinky
139
+
140
+ Run both agents on the same mission and compare:
141
+
142
+ ```bash
143
+ # Planky scrimmage
144
+ cogames scrimmage -m cogsguard_machina_1.basic \
145
+ -p "metta://policy/planky?miner=4&aligner=2&scrambler=4" \
146
+ --episodes 10
147
+
148
+ # Pinky scrimmage
149
+ cogames scrimmage -m cogsguard_machina_1.basic \
150
+ -p "metta://policy/pinky?miner=4&aligner=2&scrambler=4" \
151
+ --episodes 10
152
+ ```
153
+
154
+ Or head-to-head with `cogames run`:
155
+
156
+ ```bash
157
+ cogames run -m cogsguard_machina_1.basic \
158
+ -p "metta://policy/planky?miner=4&aligner=2&scrambler=4" \
159
+ -p "metta://policy/pinky?miner=4&aligner=2&scrambler=4" \
160
+ --episodes 10
161
+ ```
162
+
163
+ ## Alternative Policy Specification
164
+
165
+ All of these are equivalent:
166
+
167
+ ```bash
168
+ # URI format
169
+ -p "metta://policy/planky?miner=4&trace=1"
170
+
171
+ # class= format with kw. prefix
172
+ -p "class=planky,kw.miner=4,kw.trace=1"
173
+
174
+ # shorthand with kw. prefix
175
+ -p "planky,kw.miner=4,kw.trace=1"
176
+ ```
177
+
178
+ ## Architecture Overview
179
+
180
+ ```
181
+ Observation → StateSnapshot → Goal Planner → Action
182
+
183
+ EntityMap update
184
+ ```
185
+
186
+ Each tick:
187
+
188
+ 1. Parse observation into `StateSnapshot` (source of truth — no internal drift)
189
+ 2. Update sparse `EntityMap` with visible entities
190
+ 3. Evaluate role's priority-ordered goal list top-down
191
+ 4. First unsatisfied goal decomposes via `preconditions()` recursion
192
+ 5. Deepest unsatisfied leaf calls `execute()` → returns an `Action`
193
+
194
+ ### File Structure
195
+
196
+ ```
197
+ planky/
198
+ ├── policy.py # PlankyPolicy + PlankyBrain (entry point)
199
+ ├── context.py # PlankyContext, StateSnapshot
200
+ ├── entity_map.py # Sparse EntityMap with find/query
201
+ ├── navigator.py # A* pathfinding, stuck detection, exploration
202
+ ├── obs_parser.py # Observation token → StateSnapshot + entities
203
+ ├── goal.py # Goal base class, evaluate_goals()
204
+ ├── trace.py # TraceLog with 3 verbosity levels
205
+ └── goals/
206
+ ├── survive.py # SurviveGoal (HP-based retreat)
207
+ ├── gear.py # GetGearGoal (generic station navigation)
208
+ ├── shared.py # GetHeartsGoal (used by aligner + scrambler)
209
+ ├── miner.py # PickResource, DepositCargo, MineResource
210
+ ├── scout.py # ExploreGoal, GetScoutGearGoal
211
+ ├── aligner.py # AlignJunctionGoal (neutral, outside enemy AOE)
212
+ ├── scrambler.py # ScrambleJunctionGoal (enemy, scored by blocking)
213
+ └── stem.py # SelectRoleGoal (heuristic role selection)
214
+ ```
@@ -0,0 +1,100 @@
1
+ # CogsGuard Strategy Overview
2
+
3
+ **CogsGuard** is a territory control game where your team (Cogs) competes against an AI opponent (Clips) to control
4
+ **junctions** on the map.
5
+
6
+ ## Game Mechanics
7
+
8
+ ### Junction States
9
+
10
+ Junctions (junctions on the map) have three states:
11
+
12
+ - **Neutral** (unaligned)
13
+ - **Cogs-aligned** (your team controls)
14
+ - **Clips-aligned** (enemy controls)
15
+
16
+ ### Junction AOE Effects (10 tile radius)
17
+
18
+ - Friendly junctions give you **+10 influence, +100 energy, +100 HP** per tick
19
+ - Enemy junctions **attack you**: -1 HP, -100 influence per tick
20
+
21
+ ### Clips Behavior (AI opponent)
22
+
23
+ - At timestep 10, Clips claims one initial junction
24
+ - Every ~100 steps, Clips **scrambles** a nearby Cogs junction to neutral
25
+ - Every ~100 steps, Clips **aligns** a nearby neutral junction to Clips
26
+ - Clips expands outward from their controlled junctions (25 tile radius)
27
+
28
+ ### Reward
29
+
30
+ Points based on how many junctions Cogs controls over time (scaled: 100 / max_steps per junction held).
31
+
32
+ ## Role System
33
+
34
+ | Role | Gear Cost | Purpose |
35
+ | ---------------- | --------------- | ------------------------------------------------------------ |
36
+ | **Miner** ⛏️ | C1 O1 **G3** S1 | Extract resources faster (+40 cargo), deposits to collective |
37
+ | **Scout** 🔭 | C1 O1 G1 **S3** | Explore map (+100 energy, +400 HP) |
38
+ | **Aligner** 🔗 | **C3** O1 G1 S1 | Convert neutral junctions to Cogs (+20 influence) |
39
+ | **Scrambler** 🌀 | C1 **O3** G1 S1 | Convert Clips junctions to neutral (+200 HP) |
40
+
41
+ ### Critical Costs
42
+
43
+ - **Heart** (required for align/scramble): 1 of each element from collective
44
+ - **Align**: 1 heart + 1 influence + aligner gear
45
+ - **Scramble**: 1 heart + scrambler gear
46
+
47
+ ## The Strategic Loop
48
+
49
+ ```
50
+ Resources (extractors) → Collective → Hearts (chest) → Junction control
51
+ ```
52
+
53
+ 1. **Miners** gather resources from extractors → deposit at Hub/Junction → funds collective
54
+ 2. **Collective** resources can buy hearts at chest (1 of each element)
55
+ 3. **Aligners** spend hearts to convert neutral junctions
56
+ 4. **Scramblers** spend hearts to break enemy junctions
57
+
58
+ ## Key Strategic Considerations
59
+
60
+ ### Economy Priority
61
+
62
+ You need a steady stream of hearts. Without miners depositing resources, aligners/scramblers can't act.
63
+
64
+ ### Territory Expansion
65
+
66
+ Clips expands from existing junctions. The optimal counter is:
67
+
68
+ - **Scramble** enemy junctions to break their expansion radius
69
+ - **Align** neutral junctions **outside** enemy AOE (the aligner goal already checks this)
70
+
71
+ ### Junction Targeting
72
+
73
+ Current scrambler logic prioritizes junctions that block the most neutral junctions from being captured.
74
+
75
+ ### Role Balance
76
+
77
+ Default is `stem=10` which lets agents dynamically choose roles based on game state. Only use explicit role counts when
78
+ testing specific role behaviors.
79
+
80
+ ## Improvement Areas
81
+
82
+ ### Early Game Economy
83
+
84
+ - Bootstrap resource gathering before combat roles become effective
85
+ - Consider dynamic role allocation based on collective resources
86
+
87
+ ### Smarter Role Transitions (Stem Agents)
88
+
89
+ - Stem agents can auto-select roles based on game state
90
+ - Could be improved to respond to economy/territory balance
91
+
92
+ ### Better Junction Targeting Heuristics
93
+
94
+ - Prioritize junctions that would give strategic map control
95
+ - Consider path distances and clustering
96
+
97
+ ### Coordination Between Roles
98
+
99
+ - Scramblers and aligners could coordinate to chain-capture junctions
100
+ - Miners could prioritize resources needed for hearts vs gear
@@ -0,0 +1,5 @@
1
+ """Planky policy - goal-tree scripted agent."""
2
+
3
+ from .policy import PlankyPolicy
4
+
5
+ __all__ = ["PlankyPolicy"]
@@ -0,0 +1,68 @@
1
+ """Context and state snapshot for Planky 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 PlankyContext:
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 Planky 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,107 @@
1
+ """Goal base class and evaluation logic for Planky 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 PlankyContext
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: PlankyContext) -> 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: PlankyContext) -> 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: PlankyContext) -> 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
+ return Action(name="noop")
70
+
71
+
72
+ def _deepest_unsatisfied(goal: Goal, ctx: PlankyContext) -> Goal:
73
+ """Find the deepest unsatisfied precondition in the goal tree."""
74
+ for pre in goal.preconditions():
75
+ if not pre.is_satisfied(ctx):
76
+ if ctx.trace:
77
+ ctx.trace.activate(pre.name)
78
+ return _deepest_unsatisfied(pre, ctx)
79
+ return goal
80
+
81
+
82
+ def _build_chain(root: Goal, leaf: Goal) -> str:
83
+ """Build a display chain like 'MineCarbon>BeNearExtractor'."""
84
+ if root is leaf:
85
+ return root.name
86
+ # Walk preconditions to find the path
87
+ chain = [root.name]
88
+ _find_path(root, leaf, chain)
89
+ return ">".join(chain)
90
+
91
+
92
+ def _find_path(current: Goal, target: Goal, chain: list[str]) -> bool:
93
+ """DFS to find path from current to target goal."""
94
+ for pre in current.preconditions():
95
+ if pre is target:
96
+ chain.append(pre.name)
97
+ return True
98
+ chain.append(pre.name)
99
+ if _find_path(pre, target, chain):
100
+ return True
101
+ chain.pop()
102
+ return False
103
+
104
+
105
+ def _satisfaction_detail(goal: Goal, ctx: PlankyContext) -> str:
106
+ """Generate a short detail string for why a goal is satisfied."""
107
+ return "ok"
@@ -0,0 +1,27 @@
1
+ """Goal classes for Planky 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
+ ]