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.
- cogames_agents/__init__.py +0 -0
- cogames_agents/evals/__init__.py +5 -0
- cogames_agents/evals/planky_evals.py +415 -0
- cogames_agents/policy/__init__.py +0 -0
- cogames_agents/policy/evolution/__init__.py +0 -0
- cogames_agents/policy/evolution/cogsguard/__init__.py +0 -0
- cogames_agents/policy/evolution/cogsguard/evolution.py +695 -0
- cogames_agents/policy/evolution/cogsguard/evolutionary_coordinator.py +540 -0
- cogames_agents/policy/nim_agents/__init__.py +20 -0
- cogames_agents/policy/nim_agents/agents.py +98 -0
- cogames_agents/policy/nim_agents/bindings/generated/libnim_agents.dylib +0 -0
- cogames_agents/policy/nim_agents/bindings/generated/nim_agents.py +215 -0
- cogames_agents/policy/nim_agents/cogsguard_agents.nim +555 -0
- cogames_agents/policy/nim_agents/cogsguard_align_all_agents.nim +569 -0
- cogames_agents/policy/nim_agents/common.nim +1054 -0
- cogames_agents/policy/nim_agents/install.sh +1 -0
- cogames_agents/policy/nim_agents/ladybug_agent.nim +954 -0
- cogames_agents/policy/nim_agents/nim_agents.nim +68 -0
- cogames_agents/policy/nim_agents/nim_agents.nims +14 -0
- cogames_agents/policy/nim_agents/nimby.lock +3 -0
- cogames_agents/policy/nim_agents/racecar_agents.nim +844 -0
- cogames_agents/policy/nim_agents/random_agents.nim +68 -0
- cogames_agents/policy/nim_agents/test_agents.py +53 -0
- cogames_agents/policy/nim_agents/thinky_agents.nim +677 -0
- cogames_agents/policy/nim_agents/thinky_eval.py +230 -0
- cogames_agents/policy/scripted_agent/README.md +360 -0
- cogames_agents/policy/scripted_agent/__init__.py +0 -0
- cogames_agents/policy/scripted_agent/baseline_agent.py +1031 -0
- cogames_agents/policy/scripted_agent/cogas/__init__.py +5 -0
- cogames_agents/policy/scripted_agent/cogas/context.py +68 -0
- cogames_agents/policy/scripted_agent/cogas/entity_map.py +152 -0
- cogames_agents/policy/scripted_agent/cogas/goal.py +115 -0
- cogames_agents/policy/scripted_agent/cogas/goals/__init__.py +27 -0
- cogames_agents/policy/scripted_agent/cogas/goals/aligner.py +160 -0
- cogames_agents/policy/scripted_agent/cogas/goals/gear.py +197 -0
- cogames_agents/policy/scripted_agent/cogas/goals/miner.py +441 -0
- cogames_agents/policy/scripted_agent/cogas/goals/scout.py +40 -0
- cogames_agents/policy/scripted_agent/cogas/goals/scrambler.py +174 -0
- cogames_agents/policy/scripted_agent/cogas/goals/shared.py +160 -0
- cogames_agents/policy/scripted_agent/cogas/goals/stem.py +60 -0
- cogames_agents/policy/scripted_agent/cogas/goals/survive.py +100 -0
- cogames_agents/policy/scripted_agent/cogas/navigator.py +401 -0
- cogames_agents/policy/scripted_agent/cogas/obs_parser.py +238 -0
- cogames_agents/policy/scripted_agent/cogas/policy.py +525 -0
- cogames_agents/policy/scripted_agent/cogas/trace.py +69 -0
- cogames_agents/policy/scripted_agent/cogsguard/CLAUDE.md +517 -0
- cogames_agents/policy/scripted_agent/cogsguard/README.md +252 -0
- cogames_agents/policy/scripted_agent/cogsguard/__init__.py +74 -0
- cogames_agents/policy/scripted_agent/cogsguard/aligned_junction_held_investigation.md +152 -0
- cogames_agents/policy/scripted_agent/cogsguard/aligner.py +333 -0
- cogames_agents/policy/scripted_agent/cogsguard/behavior_hooks.py +44 -0
- cogames_agents/policy/scripted_agent/cogsguard/control_agent.py +323 -0
- cogames_agents/policy/scripted_agent/cogsguard/debug_agent.py +533 -0
- cogames_agents/policy/scripted_agent/cogsguard/miner.py +589 -0
- cogames_agents/policy/scripted_agent/cogsguard/options.py +67 -0
- cogames_agents/policy/scripted_agent/cogsguard/parity_metrics.py +36 -0
- cogames_agents/policy/scripted_agent/cogsguard/policy.py +1967 -0
- cogames_agents/policy/scripted_agent/cogsguard/prereq_trace.py +33 -0
- cogames_agents/policy/scripted_agent/cogsguard/role_trace.py +50 -0
- cogames_agents/policy/scripted_agent/cogsguard/roles.py +31 -0
- cogames_agents/policy/scripted_agent/cogsguard/rollout_trace.py +40 -0
- cogames_agents/policy/scripted_agent/cogsguard/scout.py +69 -0
- cogames_agents/policy/scripted_agent/cogsguard/scrambler.py +350 -0
- cogames_agents/policy/scripted_agent/cogsguard/targeted_agent.py +418 -0
- cogames_agents/policy/scripted_agent/cogsguard/teacher.py +224 -0
- cogames_agents/policy/scripted_agent/cogsguard/types.py +381 -0
- cogames_agents/policy/scripted_agent/cogsguard/v2_agent.py +49 -0
- cogames_agents/policy/scripted_agent/common/__init__.py +0 -0
- cogames_agents/policy/scripted_agent/common/geometry.py +24 -0
- cogames_agents/policy/scripted_agent/common/roles.py +34 -0
- cogames_agents/policy/scripted_agent/common/tag_utils.py +48 -0
- cogames_agents/policy/scripted_agent/demo_policy.py +242 -0
- cogames_agents/policy/scripted_agent/pathfinding.py +126 -0
- cogames_agents/policy/scripted_agent/pinky/DESIGN.md +317 -0
- cogames_agents/policy/scripted_agent/pinky/__init__.py +5 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/__init__.py +17 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/aligner.py +400 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/base.py +119 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/miner.py +632 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/scout.py +138 -0
- cogames_agents/policy/scripted_agent/pinky/behaviors/scrambler.py +433 -0
- cogames_agents/policy/scripted_agent/pinky/policy.py +570 -0
- cogames_agents/policy/scripted_agent/pinky/services/__init__.py +7 -0
- cogames_agents/policy/scripted_agent/pinky/services/map_tracker.py +808 -0
- cogames_agents/policy/scripted_agent/pinky/services/navigator.py +864 -0
- cogames_agents/policy/scripted_agent/pinky/services/safety.py +189 -0
- cogames_agents/policy/scripted_agent/pinky/state.py +299 -0
- cogames_agents/policy/scripted_agent/pinky/types.py +138 -0
- cogames_agents/policy/scripted_agent/planky/CLAUDE.md +124 -0
- cogames_agents/policy/scripted_agent/planky/IMPROVEMENTS.md +160 -0
- cogames_agents/policy/scripted_agent/planky/NOTES.md +153 -0
- cogames_agents/policy/scripted_agent/planky/PLAN.md +254 -0
- cogames_agents/policy/scripted_agent/planky/README.md +214 -0
- cogames_agents/policy/scripted_agent/planky/STRATEGY.md +100 -0
- cogames_agents/policy/scripted_agent/planky/__init__.py +5 -0
- cogames_agents/policy/scripted_agent/planky/context.py +68 -0
- cogames_agents/policy/scripted_agent/planky/entity_map.py +152 -0
- cogames_agents/policy/scripted_agent/planky/goal.py +107 -0
- cogames_agents/policy/scripted_agent/planky/goals/__init__.py +27 -0
- cogames_agents/policy/scripted_agent/planky/goals/aligner.py +168 -0
- cogames_agents/policy/scripted_agent/planky/goals/gear.py +179 -0
- cogames_agents/policy/scripted_agent/planky/goals/miner.py +416 -0
- cogames_agents/policy/scripted_agent/planky/goals/scout.py +40 -0
- cogames_agents/policy/scripted_agent/planky/goals/scrambler.py +174 -0
- cogames_agents/policy/scripted_agent/planky/goals/shared.py +160 -0
- cogames_agents/policy/scripted_agent/planky/goals/stem.py +49 -0
- cogames_agents/policy/scripted_agent/planky/goals/survive.py +96 -0
- cogames_agents/policy/scripted_agent/planky/navigator.py +388 -0
- cogames_agents/policy/scripted_agent/planky/obs_parser.py +238 -0
- cogames_agents/policy/scripted_agent/planky/policy.py +485 -0
- cogames_agents/policy/scripted_agent/planky/tests/__init__.py +0 -0
- cogames_agents/policy/scripted_agent/planky/tests/conftest.py +66 -0
- cogames_agents/policy/scripted_agent/planky/tests/helpers.py +152 -0
- cogames_agents/policy/scripted_agent/planky/tests/test_aligner.py +24 -0
- cogames_agents/policy/scripted_agent/planky/tests/test_miner.py +30 -0
- cogames_agents/policy/scripted_agent/planky/tests/test_scout.py +15 -0
- cogames_agents/policy/scripted_agent/planky/tests/test_scrambler.py +29 -0
- cogames_agents/policy/scripted_agent/planky/tests/test_stem.py +36 -0
- cogames_agents/policy/scripted_agent/planky/trace.py +69 -0
- cogames_agents/policy/scripted_agent/types.py +239 -0
- cogames_agents/policy/scripted_agent/unclipping_agent.py +461 -0
- cogames_agents/policy/scripted_agent/utils.py +381 -0
- cogames_agents/policy/scripted_registry.py +80 -0
- cogames_agents/py.typed +0 -0
- cogames_agents-0.0.0.7.dist-info/METADATA +98 -0
- cogames_agents-0.0.0.7.dist-info/RECORD +128 -0
- cogames_agents-0.0.0.7.dist-info/WHEEL +6 -0
- 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,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
|
+
]
|