Spaces:
Configuration error
Configuration error
EZTIME2025 commited on
Commit ·
6016237
1
Parent(s): 61ad06f
make state smallet
Browse files- .github/instructions/WORK_PLAN.md +67 -36
- examples/ai_testing/my_games/current_state.json +0 -885
- examples/ai_testing/my_games/test_prompt_output.json +792 -0
- examples/ai_testing/play_and_capture.py +208 -4
- examples/ai_testing/sample_states/captured_game.json +22 -90
- examples/ai_testing/sample_states/captured_game_optimized.json +7 -0
- examples/ai_testing/sample_states/captured_game_optimized.txt +25 -0
- examples/ai_testing/test_optimized_prompts.py +215 -0
- examples/test_ai_config.py +24 -45
- examples/test_prompt_manager.py +332 -0
- pycatan/ai/__init__.py +10 -5
- pycatan/ai/config.py +3 -29
- pycatan/ai/prompt_manager.py +301 -0
- pycatan/ai/prompt_templates.py +423 -0
- pycatan/ai/state_filter.py +336 -0
- pycatan/management/game_manager.py +4 -0
- temp_viz_console.py +0 -31
.github/instructions/WORK_PLAN.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
# 🗺️ AI Agent Development Work Plan
|
| 2 |
|
| 3 |
**Date:** January 3, 2026
|
| 4 |
-
**Status:**
|
|
|
|
| 5 |
|
| 6 |
## 🎯 Project Goal
|
| 7 |
|
|
@@ -17,45 +18,75 @@ Build a fully functional LLM-based AI agent that can play Settlers of Catan auto
|
|
| 17 |
### Phase 1: Foundation & Infrastructure 🏗️
|
| 18 |
**Goal:** Build the core infrastructure needed to support AI agents
|
| 19 |
|
| 20 |
-
#### 1.1 Configuration Management
|
| 21 |
-
- [
|
| 22 |
-
- [
|
| 23 |
-
- [
|
| 24 |
-
- [
|
| 25 |
-
- [
|
| 26 |
-
- [
|
| 27 |
-
- [
|
| 28 |
-
- [
|
| 29 |
-
|
| 30 |
-
**Files
|
| 31 |
-
- `pycatan/ai/config.py` - Configuration management
|
| 32 |
-
- `pycatan/ai/config_example.yaml` - Example configuration file
|
|
|
|
|
|
|
| 33 |
|
| 34 |
---
|
| 35 |
|
| 36 |
-
#### 1.2 Prompt Management Layer
|
| 37 |
-
- [
|
| 38 |
-
- [
|
| 39 |
-
- [
|
| 40 |
-
- [
|
| 41 |
-
- [
|
| 42 |
-
- [
|
| 43 |
-
- [
|
| 44 |
-
- [
|
| 45 |
-
- [
|
| 46 |
-
- [
|
| 47 |
-
- [
|
| 48 |
-
- [
|
| 49 |
-
- [
|
| 50 |
-
- [
|
| 51 |
-
- [
|
| 52 |
-
- [
|
| 53 |
-
- [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
---
|
| 61 |
|
|
|
|
| 1 |
# 🗺️ AI Agent Development Work Plan
|
| 2 |
|
| 3 |
**Date:** January 3, 2026
|
| 4 |
+
**Status:** ✅ Phase 1 - Foundation & Infrastructure (90% Complete)
|
| 5 |
+
**Current Task:** Response Parser (1.3) - **NEXT**
|
| 6 |
|
| 7 |
## 🎯 Project Goal
|
| 8 |
|
|
|
|
| 18 |
### Phase 1: Foundation & Infrastructure 🏗️
|
| 19 |
**Goal:** Build the core infrastructure needed to support AI agents
|
| 20 |
|
| 21 |
+
#### 1.1 Configuration Management ✅ **COMPLETED**
|
| 22 |
+
- [x] Create centralized configuration system
|
| 23 |
+
- [x] LLM settings (model, temperature, max_tokens, etc.)
|
| 24 |
+
- [x] API credentials management
|
| 25 |
+
- [x] Agent parameters (custom instructions only)
|
| 26 |
+
- [x] Performance settings (timeouts, retries, caching)
|
| 27 |
+
- [x] Create config file format (YAML)
|
| 28 |
+
- [x] Build configuration loader and validator
|
| 29 |
+
- [x] Add environment variable support for sensitive data
|
| 30 |
+
|
| 31 |
+
**Files created:**
|
| 32 |
+
- ✅ `pycatan/ai/config.py` - Configuration management
|
| 33 |
+
- ✅ `pycatan/ai/config_example.yaml` - Example configuration file
|
| 34 |
+
- ✅ `pycatan/ai/config_dev.yaml` - Default dev configuration
|
| 35 |
+
- ✅ `.env.example` - Environment variables template
|
| 36 |
|
| 37 |
---
|
| 38 |
|
| 39 |
+
#### 1.2 Prompt Management Layer ✅ **COMPLETED**
|
| 40 |
+
- [x] Design prompt processing pipeline
|
| 41 |
+
- [x] Implement game state filtering
|
| 42 |
+
- [x] Hide opponent's private information
|
| 43 |
+
- [x] Filter development cards
|
| 44 |
+
- [x] Remove non-visible game elements
|
| 45 |
+
- [x] Build perspective transformation
|
| 46 |
+
- [x] Convert game state to agent's viewpoint
|
| 47 |
+
- [x] Format resources and points
|
| 48 |
+
- [x] Present relative positioning
|
| 49 |
+
- [x] Create prompt template system
|
| 50 |
+
- [x] Meta data section
|
| 51 |
+
- [x] Task context section
|
| 52 |
+
- [x] Game state section
|
| 53 |
+
- [x] Social context section
|
| 54 |
+
- [x] Memory section
|
| 55 |
+
- [x] Constraints section
|
| 56 |
+
- [x] Build custom instruction injection per agent
|
| 57 |
+
|
| 58 |
+
**Files created:**
|
| 59 |
+
- ✅ `pycatan/ai/prompt_manager.py` - Main prompt processing
|
| 60 |
+
- ✅ `pycatan/ai/state_filter.py` - Game state filtering logic
|
| 61 |
+
- ✅ `pycatan/ai/prompt_templates.py` - Template definitions
|
| 62 |
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
#### 1.2.5 Game State Optimization ✅ **COMPLETED**
|
| 66 |
+
**Goal:** Optimize the game state capture and representation for better LLM consumption
|
| 67 |
+
|
| 68 |
+
- [x] Review current game state structure from `play_and_capture.py`
|
| 69 |
+
- [x] Design improved game state format
|
| 70 |
+
- [x] Compress player information structure
|
| 71 |
+
- [x] Improve board representation (lookup tables H & N)
|
| 72 |
+
- [x] Add resource/harbor code mappings
|
| 73 |
+
- [x] Reduce redundancy and token usage (removed pixel_coords, board_graph)
|
| 74 |
+
- [x] Add status flags (Longest Road, Largest Army)
|
| 75 |
+
- [x] Create optimized state format with legend
|
| 76 |
+
- [x] Update game state capture to save both formats (.json + .txt)
|
| 77 |
+
- [x] Fix timing: capture state at turn START (not just after actions)
|
| 78 |
+
- [x] Test with real game scenarios
|
| 79 |
+
|
| 80 |
+
**Files modified:**
|
| 81 |
+
- ✅ `examples/ai_testing/play_and_capture.py` - Optimized state capture
|
| 82 |
+
- ✅ `pycatan/management/game_manager.py` - Added state capture at turn start
|
| 83 |
+
|
| 84 |
+
**Key achievements:**
|
| 85 |
+
- 🎯 State representation optimized by ~60% (removed redundant fields)
|
| 86 |
+
- 📊 Compressed format with lookup tables (H=hexes, N=nodes)
|
| 87 |
+
- 🔄 Real-time state updates at `current_state_optimized.txt`
|
| 88 |
+
- 📝 Clear legend/documentation included in output
|
| 89 |
+
- ✅ Captures state at decision point (turn start)
|
| 90 |
|
| 91 |
---
|
| 92 |
|
examples/ai_testing/my_games/current_state.json
DELETED
|
@@ -1,885 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"timestamp": "2025-12-26T16:21:38.408731",
|
| 3 |
-
"state_number": 1,
|
| 4 |
-
"state": {
|
| 5 |
-
"hexes": [
|
| 6 |
-
{
|
| 7 |
-
"id": 1,
|
| 8 |
-
"type": "wood",
|
| 9 |
-
"number": 12,
|
| 10 |
-
"has_robber": false
|
| 11 |
-
},
|
| 12 |
-
{
|
| 13 |
-
"id": 2,
|
| 14 |
-
"type": "sheep",
|
| 15 |
-
"number": 5,
|
| 16 |
-
"has_robber": false
|
| 17 |
-
},
|
| 18 |
-
{
|
| 19 |
-
"id": 3,
|
| 20 |
-
"type": "wood",
|
| 21 |
-
"number": 4,
|
| 22 |
-
"has_robber": false
|
| 23 |
-
},
|
| 24 |
-
{
|
| 25 |
-
"id": 4,
|
| 26 |
-
"type": "sheep",
|
| 27 |
-
"number": 8,
|
| 28 |
-
"has_robber": false
|
| 29 |
-
},
|
| 30 |
-
{
|
| 31 |
-
"id": 5,
|
| 32 |
-
"type": "brick",
|
| 33 |
-
"number": 6,
|
| 34 |
-
"has_robber": false
|
| 35 |
-
},
|
| 36 |
-
{
|
| 37 |
-
"id": 6,
|
| 38 |
-
"type": "wood",
|
| 39 |
-
"number": 3,
|
| 40 |
-
"has_robber": false
|
| 41 |
-
},
|
| 42 |
-
{
|
| 43 |
-
"id": 7,
|
| 44 |
-
"type": "wheat",
|
| 45 |
-
"number": 8,
|
| 46 |
-
"has_robber": false
|
| 47 |
-
},
|
| 48 |
-
{
|
| 49 |
-
"id": 8,
|
| 50 |
-
"type": "brick",
|
| 51 |
-
"number": 10,
|
| 52 |
-
"has_robber": false
|
| 53 |
-
},
|
| 54 |
-
{
|
| 55 |
-
"id": 9,
|
| 56 |
-
"type": "wood",
|
| 57 |
-
"number": 11,
|
| 58 |
-
"has_robber": false
|
| 59 |
-
},
|
| 60 |
-
{
|
| 61 |
-
"id": 10,
|
| 62 |
-
"type": "desert",
|
| 63 |
-
"number": null,
|
| 64 |
-
"has_robber": true
|
| 65 |
-
},
|
| 66 |
-
{
|
| 67 |
-
"id": 11,
|
| 68 |
-
"type": "ore",
|
| 69 |
-
"number": 3,
|
| 70 |
-
"has_robber": false
|
| 71 |
-
},
|
| 72 |
-
{
|
| 73 |
-
"id": 12,
|
| 74 |
-
"type": "sheep",
|
| 75 |
-
"number": 4,
|
| 76 |
-
"has_robber": false
|
| 77 |
-
},
|
| 78 |
-
{
|
| 79 |
-
"id": 13,
|
| 80 |
-
"type": "brick",
|
| 81 |
-
"number": 10,
|
| 82 |
-
"has_robber": false
|
| 83 |
-
},
|
| 84 |
-
{
|
| 85 |
-
"id": 14,
|
| 86 |
-
"type": "wheat",
|
| 87 |
-
"number": 9,
|
| 88 |
-
"has_robber": false
|
| 89 |
-
},
|
| 90 |
-
{
|
| 91 |
-
"id": 15,
|
| 92 |
-
"type": "wheat",
|
| 93 |
-
"number": 6,
|
| 94 |
-
"has_robber": false
|
| 95 |
-
},
|
| 96 |
-
{
|
| 97 |
-
"id": 16,
|
| 98 |
-
"type": "sheep",
|
| 99 |
-
"number": 11,
|
| 100 |
-
"has_robber": false
|
| 101 |
-
},
|
| 102 |
-
{
|
| 103 |
-
"id": 17,
|
| 104 |
-
"type": "ore",
|
| 105 |
-
"number": 5,
|
| 106 |
-
"has_robber": false
|
| 107 |
-
},
|
| 108 |
-
{
|
| 109 |
-
"id": 18,
|
| 110 |
-
"type": "wheat",
|
| 111 |
-
"number": 9,
|
| 112 |
-
"has_robber": false
|
| 113 |
-
},
|
| 114 |
-
{
|
| 115 |
-
"id": 19,
|
| 116 |
-
"type": "ore",
|
| 117 |
-
"number": 2,
|
| 118 |
-
"has_robber": false
|
| 119 |
-
}
|
| 120 |
-
],
|
| 121 |
-
"settlements": [],
|
| 122 |
-
"cities": [],
|
| 123 |
-
"roads": [],
|
| 124 |
-
"harbors": [
|
| 125 |
-
{
|
| 126 |
-
"id": 1,
|
| 127 |
-
"type": "any",
|
| 128 |
-
"ratio": 3,
|
| 129 |
-
"point_one": 9,
|
| 130 |
-
"point_two": 8
|
| 131 |
-
},
|
| 132 |
-
{
|
| 133 |
-
"id": 2,
|
| 134 |
-
"type": "sheep",
|
| 135 |
-
"ratio": 2,
|
| 136 |
-
"point_one": 17,
|
| 137 |
-
"point_two": 28
|
| 138 |
-
},
|
| 139 |
-
{
|
| 140 |
-
"id": 3,
|
| 141 |
-
"type": "wood",
|
| 142 |
-
"ratio": 2,
|
| 143 |
-
"point_one": 40,
|
| 144 |
-
"point_two": 48
|
| 145 |
-
},
|
| 146 |
-
{
|
| 147 |
-
"id": 4,
|
| 148 |
-
"type": "any",
|
| 149 |
-
"ratio": 3,
|
| 150 |
-
"point_one": 50,
|
| 151 |
-
"point_two": 51
|
| 152 |
-
},
|
| 153 |
-
{
|
| 154 |
-
"id": 5,
|
| 155 |
-
"type": "any",
|
| 156 |
-
"ratio": 3,
|
| 157 |
-
"point_one": 53,
|
| 158 |
-
"point_two": 54
|
| 159 |
-
},
|
| 160 |
-
{
|
| 161 |
-
"id": 6,
|
| 162 |
-
"type": "any",
|
| 163 |
-
"ratio": 3,
|
| 164 |
-
"point_one": 37,
|
| 165 |
-
"point_two": 38
|
| 166 |
-
},
|
| 167 |
-
{
|
| 168 |
-
"id": 7,
|
| 169 |
-
"type": "ore",
|
| 170 |
-
"ratio": 2,
|
| 171 |
-
"point_one": 26,
|
| 172 |
-
"point_two": 16
|
| 173 |
-
},
|
| 174 |
-
{
|
| 175 |
-
"id": 8,
|
| 176 |
-
"type": "brick",
|
| 177 |
-
"ratio": 2,
|
| 178 |
-
"point_one": 7,
|
| 179 |
-
"point_two": 6
|
| 180 |
-
},
|
| 181 |
-
{
|
| 182 |
-
"id": 9,
|
| 183 |
-
"type": "wheat",
|
| 184 |
-
"ratio": 2,
|
| 185 |
-
"point_one": 3,
|
| 186 |
-
"point_two": 2
|
| 187 |
-
}
|
| 188 |
-
],
|
| 189 |
-
"players": [
|
| 190 |
-
{
|
| 191 |
-
"id": 0,
|
| 192 |
-
"name": "a",
|
| 193 |
-
"victory_points": 0,
|
| 194 |
-
"total_cards": 0,
|
| 195 |
-
"cards_list": [],
|
| 196 |
-
"dev_cards_list": [],
|
| 197 |
-
"settlements": 0,
|
| 198 |
-
"cities": 0,
|
| 199 |
-
"roads": 0,
|
| 200 |
-
"longest_road": 0,
|
| 201 |
-
"has_longest_road": false,
|
| 202 |
-
"knights": 0,
|
| 203 |
-
"knights_played": 0,
|
| 204 |
-
"has_largest_army": false
|
| 205 |
-
},
|
| 206 |
-
{
|
| 207 |
-
"id": 1,
|
| 208 |
-
"name": "b",
|
| 209 |
-
"victory_points": 0,
|
| 210 |
-
"total_cards": 0,
|
| 211 |
-
"cards_list": [],
|
| 212 |
-
"dev_cards_list": [],
|
| 213 |
-
"settlements": 0,
|
| 214 |
-
"cities": 0,
|
| 215 |
-
"roads": 0,
|
| 216 |
-
"longest_road": 0,
|
| 217 |
-
"has_longest_road": false,
|
| 218 |
-
"knights": 0,
|
| 219 |
-
"knights_played": 0,
|
| 220 |
-
"has_largest_army": false
|
| 221 |
-
},
|
| 222 |
-
{
|
| 223 |
-
"id": 2,
|
| 224 |
-
"name": "c",
|
| 225 |
-
"victory_points": 0,
|
| 226 |
-
"total_cards": 0,
|
| 227 |
-
"cards_list": [],
|
| 228 |
-
"dev_cards_list": [],
|
| 229 |
-
"settlements": 0,
|
| 230 |
-
"cities": 0,
|
| 231 |
-
"roads": 0,
|
| 232 |
-
"longest_road": 0,
|
| 233 |
-
"has_longest_road": false,
|
| 234 |
-
"knights": 0,
|
| 235 |
-
"knights_played": 0,
|
| 236 |
-
"has_largest_army": false
|
| 237 |
-
}
|
| 238 |
-
],
|
| 239 |
-
"current_player": 0,
|
| 240 |
-
"current_phase": "SETUP_FIRST_ROUND",
|
| 241 |
-
"robber_position": [
|
| 242 |
-
2,
|
| 243 |
-
2
|
| 244 |
-
],
|
| 245 |
-
"dice_result": null,
|
| 246 |
-
"points": [
|
| 247 |
-
{
|
| 248 |
-
"point_id": 1,
|
| 249 |
-
"adjacent_points": [
|
| 250 |
-
2,
|
| 251 |
-
9
|
| 252 |
-
],
|
| 253 |
-
"adjacent_hexes": [
|
| 254 |
-
1
|
| 255 |
-
]
|
| 256 |
-
},
|
| 257 |
-
{
|
| 258 |
-
"point_id": 2,
|
| 259 |
-
"adjacent_points": [
|
| 260 |
-
1,
|
| 261 |
-
3
|
| 262 |
-
],
|
| 263 |
-
"adjacent_hexes": [
|
| 264 |
-
1
|
| 265 |
-
]
|
| 266 |
-
},
|
| 267 |
-
{
|
| 268 |
-
"point_id": 3,
|
| 269 |
-
"adjacent_points": [
|
| 270 |
-
2,
|
| 271 |
-
4,
|
| 272 |
-
11
|
| 273 |
-
],
|
| 274 |
-
"adjacent_hexes": [
|
| 275 |
-
2,
|
| 276 |
-
1
|
| 277 |
-
]
|
| 278 |
-
},
|
| 279 |
-
{
|
| 280 |
-
"point_id": 4,
|
| 281 |
-
"adjacent_points": [
|
| 282 |
-
3,
|
| 283 |
-
5
|
| 284 |
-
],
|
| 285 |
-
"adjacent_hexes": [
|
| 286 |
-
2
|
| 287 |
-
]
|
| 288 |
-
},
|
| 289 |
-
{
|
| 290 |
-
"point_id": 5,
|
| 291 |
-
"adjacent_points": [
|
| 292 |
-
4,
|
| 293 |
-
6,
|
| 294 |
-
13
|
| 295 |
-
],
|
| 296 |
-
"adjacent_hexes": [
|
| 297 |
-
3,
|
| 298 |
-
2
|
| 299 |
-
]
|
| 300 |
-
},
|
| 301 |
-
{
|
| 302 |
-
"point_id": 6,
|
| 303 |
-
"adjacent_points": [
|
| 304 |
-
5,
|
| 305 |
-
7
|
| 306 |
-
],
|
| 307 |
-
"adjacent_hexes": [
|
| 308 |
-
3
|
| 309 |
-
]
|
| 310 |
-
},
|
| 311 |
-
{
|
| 312 |
-
"point_id": 7,
|
| 313 |
-
"adjacent_points": [
|
| 314 |
-
6,
|
| 315 |
-
15
|
| 316 |
-
],
|
| 317 |
-
"adjacent_hexes": [
|
| 318 |
-
3
|
| 319 |
-
]
|
| 320 |
-
},
|
| 321 |
-
{
|
| 322 |
-
"point_id": 8,
|
| 323 |
-
"adjacent_points": [
|
| 324 |
-
9,
|
| 325 |
-
18
|
| 326 |
-
],
|
| 327 |
-
"adjacent_hexes": [
|
| 328 |
-
4
|
| 329 |
-
]
|
| 330 |
-
},
|
| 331 |
-
{
|
| 332 |
-
"point_id": 9,
|
| 333 |
-
"adjacent_points": [
|
| 334 |
-
8,
|
| 335 |
-
10,
|
| 336 |
-
1
|
| 337 |
-
],
|
| 338 |
-
"adjacent_hexes": [
|
| 339 |
-
4,
|
| 340 |
-
1
|
| 341 |
-
]
|
| 342 |
-
},
|
| 343 |
-
{
|
| 344 |
-
"point_id": 10,
|
| 345 |
-
"adjacent_points": [
|
| 346 |
-
9,
|
| 347 |
-
11,
|
| 348 |
-
20
|
| 349 |
-
],
|
| 350 |
-
"adjacent_hexes": [
|
| 351 |
-
5,
|
| 352 |
-
4,
|
| 353 |
-
1
|
| 354 |
-
]
|
| 355 |
-
},
|
| 356 |
-
{
|
| 357 |
-
"point_id": 11,
|
| 358 |
-
"adjacent_points": [
|
| 359 |
-
10,
|
| 360 |
-
12,
|
| 361 |
-
3
|
| 362 |
-
],
|
| 363 |
-
"adjacent_hexes": [
|
| 364 |
-
5,
|
| 365 |
-
2,
|
| 366 |
-
1
|
| 367 |
-
]
|
| 368 |
-
},
|
| 369 |
-
{
|
| 370 |
-
"point_id": 12,
|
| 371 |
-
"adjacent_points": [
|
| 372 |
-
11,
|
| 373 |
-
13,
|
| 374 |
-
22
|
| 375 |
-
],
|
| 376 |
-
"adjacent_hexes": [
|
| 377 |
-
6,
|
| 378 |
-
5,
|
| 379 |
-
2
|
| 380 |
-
]
|
| 381 |
-
},
|
| 382 |
-
{
|
| 383 |
-
"point_id": 13,
|
| 384 |
-
"adjacent_points": [
|
| 385 |
-
12,
|
| 386 |
-
14,
|
| 387 |
-
5
|
| 388 |
-
],
|
| 389 |
-
"adjacent_hexes": [
|
| 390 |
-
6,
|
| 391 |
-
3,
|
| 392 |
-
2
|
| 393 |
-
]
|
| 394 |
-
},
|
| 395 |
-
{
|
| 396 |
-
"point_id": 14,
|
| 397 |
-
"adjacent_points": [
|
| 398 |
-
13,
|
| 399 |
-
15,
|
| 400 |
-
24
|
| 401 |
-
],
|
| 402 |
-
"adjacent_hexes": [
|
| 403 |
-
7,
|
| 404 |
-
6,
|
| 405 |
-
3
|
| 406 |
-
]
|
| 407 |
-
},
|
| 408 |
-
{
|
| 409 |
-
"point_id": 15,
|
| 410 |
-
"adjacent_points": [
|
| 411 |
-
14,
|
| 412 |
-
16,
|
| 413 |
-
7
|
| 414 |
-
],
|
| 415 |
-
"adjacent_hexes": [
|
| 416 |
-
7,
|
| 417 |
-
3
|
| 418 |
-
]
|
| 419 |
-
},
|
| 420 |
-
{
|
| 421 |
-
"point_id": 16,
|
| 422 |
-
"adjacent_points": [
|
| 423 |
-
15,
|
| 424 |
-
26
|
| 425 |
-
],
|
| 426 |
-
"adjacent_hexes": [
|
| 427 |
-
7
|
| 428 |
-
]
|
| 429 |
-
},
|
| 430 |
-
{
|
| 431 |
-
"point_id": 17,
|
| 432 |
-
"adjacent_points": [
|
| 433 |
-
18,
|
| 434 |
-
28
|
| 435 |
-
],
|
| 436 |
-
"adjacent_hexes": [
|
| 437 |
-
8
|
| 438 |
-
]
|
| 439 |
-
},
|
| 440 |
-
{
|
| 441 |
-
"point_id": 18,
|
| 442 |
-
"adjacent_points": [
|
| 443 |
-
17,
|
| 444 |
-
19,
|
| 445 |
-
8
|
| 446 |
-
],
|
| 447 |
-
"adjacent_hexes": [
|
| 448 |
-
8,
|
| 449 |
-
4
|
| 450 |
-
]
|
| 451 |
-
},
|
| 452 |
-
{
|
| 453 |
-
"point_id": 19,
|
| 454 |
-
"adjacent_points": [
|
| 455 |
-
18,
|
| 456 |
-
20,
|
| 457 |
-
30
|
| 458 |
-
],
|
| 459 |
-
"adjacent_hexes": [
|
| 460 |
-
9,
|
| 461 |
-
8,
|
| 462 |
-
4
|
| 463 |
-
]
|
| 464 |
-
},
|
| 465 |
-
{
|
| 466 |
-
"point_id": 20,
|
| 467 |
-
"adjacent_points": [
|
| 468 |
-
19,
|
| 469 |
-
21,
|
| 470 |
-
10
|
| 471 |
-
],
|
| 472 |
-
"adjacent_hexes": [
|
| 473 |
-
9,
|
| 474 |
-
5,
|
| 475 |
-
4
|
| 476 |
-
]
|
| 477 |
-
},
|
| 478 |
-
{
|
| 479 |
-
"point_id": 21,
|
| 480 |
-
"adjacent_points": [
|
| 481 |
-
20,
|
| 482 |
-
22,
|
| 483 |
-
32
|
| 484 |
-
],
|
| 485 |
-
"adjacent_hexes": [
|
| 486 |
-
10,
|
| 487 |
-
9,
|
| 488 |
-
5
|
| 489 |
-
]
|
| 490 |
-
},
|
| 491 |
-
{
|
| 492 |
-
"point_id": 22,
|
| 493 |
-
"adjacent_points": [
|
| 494 |
-
21,
|
| 495 |
-
23,
|
| 496 |
-
12
|
| 497 |
-
],
|
| 498 |
-
"adjacent_hexes": [
|
| 499 |
-
10,
|
| 500 |
-
6,
|
| 501 |
-
5
|
| 502 |
-
]
|
| 503 |
-
},
|
| 504 |
-
{
|
| 505 |
-
"point_id": 23,
|
| 506 |
-
"adjacent_points": [
|
| 507 |
-
22,
|
| 508 |
-
24,
|
| 509 |
-
34
|
| 510 |
-
],
|
| 511 |
-
"adjacent_hexes": [
|
| 512 |
-
11,
|
| 513 |
-
10,
|
| 514 |
-
6
|
| 515 |
-
]
|
| 516 |
-
},
|
| 517 |
-
{
|
| 518 |
-
"point_id": 24,
|
| 519 |
-
"adjacent_points": [
|
| 520 |
-
23,
|
| 521 |
-
25,
|
| 522 |
-
14
|
| 523 |
-
],
|
| 524 |
-
"adjacent_hexes": [
|
| 525 |
-
11,
|
| 526 |
-
7,
|
| 527 |
-
6
|
| 528 |
-
]
|
| 529 |
-
},
|
| 530 |
-
{
|
| 531 |
-
"point_id": 25,
|
| 532 |
-
"adjacent_points": [
|
| 533 |
-
24,
|
| 534 |
-
26,
|
| 535 |
-
36
|
| 536 |
-
],
|
| 537 |
-
"adjacent_hexes": [
|
| 538 |
-
12,
|
| 539 |
-
11,
|
| 540 |
-
7
|
| 541 |
-
]
|
| 542 |
-
},
|
| 543 |
-
{
|
| 544 |
-
"point_id": 26,
|
| 545 |
-
"adjacent_points": [
|
| 546 |
-
25,
|
| 547 |
-
27,
|
| 548 |
-
16
|
| 549 |
-
],
|
| 550 |
-
"adjacent_hexes": [
|
| 551 |
-
12,
|
| 552 |
-
7
|
| 553 |
-
]
|
| 554 |
-
},
|
| 555 |
-
{
|
| 556 |
-
"point_id": 27,
|
| 557 |
-
"adjacent_points": [
|
| 558 |
-
26,
|
| 559 |
-
38
|
| 560 |
-
],
|
| 561 |
-
"adjacent_hexes": [
|
| 562 |
-
12
|
| 563 |
-
]
|
| 564 |
-
},
|
| 565 |
-
{
|
| 566 |
-
"point_id": 28,
|
| 567 |
-
"adjacent_points": [
|
| 568 |
-
29,
|
| 569 |
-
17
|
| 570 |
-
],
|
| 571 |
-
"adjacent_hexes": [
|
| 572 |
-
8
|
| 573 |
-
]
|
| 574 |
-
},
|
| 575 |
-
{
|
| 576 |
-
"point_id": 29,
|
| 577 |
-
"adjacent_points": [
|
| 578 |
-
28,
|
| 579 |
-
30,
|
| 580 |
-
39
|
| 581 |
-
],
|
| 582 |
-
"adjacent_hexes": [
|
| 583 |
-
13,
|
| 584 |
-
8
|
| 585 |
-
]
|
| 586 |
-
},
|
| 587 |
-
{
|
| 588 |
-
"point_id": 30,
|
| 589 |
-
"adjacent_points": [
|
| 590 |
-
29,
|
| 591 |
-
31,
|
| 592 |
-
19
|
| 593 |
-
],
|
| 594 |
-
"adjacent_hexes": [
|
| 595 |
-
13,
|
| 596 |
-
9,
|
| 597 |
-
8
|
| 598 |
-
]
|
| 599 |
-
},
|
| 600 |
-
{
|
| 601 |
-
"point_id": 31,
|
| 602 |
-
"adjacent_points": [
|
| 603 |
-
30,
|
| 604 |
-
32,
|
| 605 |
-
41
|
| 606 |
-
],
|
| 607 |
-
"adjacent_hexes": [
|
| 608 |
-
14,
|
| 609 |
-
13,
|
| 610 |
-
9
|
| 611 |
-
]
|
| 612 |
-
},
|
| 613 |
-
{
|
| 614 |
-
"point_id": 32,
|
| 615 |
-
"adjacent_points": [
|
| 616 |
-
31,
|
| 617 |
-
33,
|
| 618 |
-
21
|
| 619 |
-
],
|
| 620 |
-
"adjacent_hexes": [
|
| 621 |
-
14,
|
| 622 |
-
10,
|
| 623 |
-
9
|
| 624 |
-
]
|
| 625 |
-
},
|
| 626 |
-
{
|
| 627 |
-
"point_id": 33,
|
| 628 |
-
"adjacent_points": [
|
| 629 |
-
32,
|
| 630 |
-
34,
|
| 631 |
-
43
|
| 632 |
-
],
|
| 633 |
-
"adjacent_hexes": [
|
| 634 |
-
15,
|
| 635 |
-
14,
|
| 636 |
-
10
|
| 637 |
-
]
|
| 638 |
-
},
|
| 639 |
-
{
|
| 640 |
-
"point_id": 34,
|
| 641 |
-
"adjacent_points": [
|
| 642 |
-
33,
|
| 643 |
-
35,
|
| 644 |
-
23
|
| 645 |
-
],
|
| 646 |
-
"adjacent_hexes": [
|
| 647 |
-
15,
|
| 648 |
-
11,
|
| 649 |
-
10
|
| 650 |
-
]
|
| 651 |
-
},
|
| 652 |
-
{
|
| 653 |
-
"point_id": 35,
|
| 654 |
-
"adjacent_points": [
|
| 655 |
-
34,
|
| 656 |
-
36,
|
| 657 |
-
45
|
| 658 |
-
],
|
| 659 |
-
"adjacent_hexes": [
|
| 660 |
-
16,
|
| 661 |
-
15,
|
| 662 |
-
11
|
| 663 |
-
]
|
| 664 |
-
},
|
| 665 |
-
{
|
| 666 |
-
"point_id": 36,
|
| 667 |
-
"adjacent_points": [
|
| 668 |
-
35,
|
| 669 |
-
37,
|
| 670 |
-
25
|
| 671 |
-
],
|
| 672 |
-
"adjacent_hexes": [
|
| 673 |
-
16,
|
| 674 |
-
12,
|
| 675 |
-
11
|
| 676 |
-
]
|
| 677 |
-
},
|
| 678 |
-
{
|
| 679 |
-
"point_id": 37,
|
| 680 |
-
"adjacent_points": [
|
| 681 |
-
36,
|
| 682 |
-
38,
|
| 683 |
-
47
|
| 684 |
-
],
|
| 685 |
-
"adjacent_hexes": [
|
| 686 |
-
16,
|
| 687 |
-
12
|
| 688 |
-
]
|
| 689 |
-
},
|
| 690 |
-
{
|
| 691 |
-
"point_id": 38,
|
| 692 |
-
"adjacent_points": [
|
| 693 |
-
37,
|
| 694 |
-
27
|
| 695 |
-
],
|
| 696 |
-
"adjacent_hexes": [
|
| 697 |
-
12
|
| 698 |
-
]
|
| 699 |
-
},
|
| 700 |
-
{
|
| 701 |
-
"point_id": 39,
|
| 702 |
-
"adjacent_points": [
|
| 703 |
-
40,
|
| 704 |
-
29
|
| 705 |
-
],
|
| 706 |
-
"adjacent_hexes": [
|
| 707 |
-
13
|
| 708 |
-
]
|
| 709 |
-
},
|
| 710 |
-
{
|
| 711 |
-
"point_id": 40,
|
| 712 |
-
"adjacent_points": [
|
| 713 |
-
39,
|
| 714 |
-
41,
|
| 715 |
-
48
|
| 716 |
-
],
|
| 717 |
-
"adjacent_hexes": [
|
| 718 |
-
17,
|
| 719 |
-
13
|
| 720 |
-
]
|
| 721 |
-
},
|
| 722 |
-
{
|
| 723 |
-
"point_id": 41,
|
| 724 |
-
"adjacent_points": [
|
| 725 |
-
40,
|
| 726 |
-
42,
|
| 727 |
-
31
|
| 728 |
-
],
|
| 729 |
-
"adjacent_hexes": [
|
| 730 |
-
17,
|
| 731 |
-
14,
|
| 732 |
-
13
|
| 733 |
-
]
|
| 734 |
-
},
|
| 735 |
-
{
|
| 736 |
-
"point_id": 42,
|
| 737 |
-
"adjacent_points": [
|
| 738 |
-
41,
|
| 739 |
-
43,
|
| 740 |
-
50
|
| 741 |
-
],
|
| 742 |
-
"adjacent_hexes": [
|
| 743 |
-
18,
|
| 744 |
-
17,
|
| 745 |
-
14
|
| 746 |
-
]
|
| 747 |
-
},
|
| 748 |
-
{
|
| 749 |
-
"point_id": 43,
|
| 750 |
-
"adjacent_points": [
|
| 751 |
-
42,
|
| 752 |
-
44,
|
| 753 |
-
33
|
| 754 |
-
],
|
| 755 |
-
"adjacent_hexes": [
|
| 756 |
-
18,
|
| 757 |
-
15,
|
| 758 |
-
14
|
| 759 |
-
]
|
| 760 |
-
},
|
| 761 |
-
{
|
| 762 |
-
"point_id": 44,
|
| 763 |
-
"adjacent_points": [
|
| 764 |
-
43,
|
| 765 |
-
45,
|
| 766 |
-
52
|
| 767 |
-
],
|
| 768 |
-
"adjacent_hexes": [
|
| 769 |
-
19,
|
| 770 |
-
18,
|
| 771 |
-
15
|
| 772 |
-
]
|
| 773 |
-
},
|
| 774 |
-
{
|
| 775 |
-
"point_id": 45,
|
| 776 |
-
"adjacent_points": [
|
| 777 |
-
44,
|
| 778 |
-
46,
|
| 779 |
-
35
|
| 780 |
-
],
|
| 781 |
-
"adjacent_hexes": [
|
| 782 |
-
19,
|
| 783 |
-
16,
|
| 784 |
-
15
|
| 785 |
-
]
|
| 786 |
-
},
|
| 787 |
-
{
|
| 788 |
-
"point_id": 46,
|
| 789 |
-
"adjacent_points": [
|
| 790 |
-
45,
|
| 791 |
-
47,
|
| 792 |
-
54
|
| 793 |
-
],
|
| 794 |
-
"adjacent_hexes": [
|
| 795 |
-
19,
|
| 796 |
-
16
|
| 797 |
-
]
|
| 798 |
-
},
|
| 799 |
-
{
|
| 800 |
-
"point_id": 47,
|
| 801 |
-
"adjacent_points": [
|
| 802 |
-
46,
|
| 803 |
-
37
|
| 804 |
-
],
|
| 805 |
-
"adjacent_hexes": [
|
| 806 |
-
16
|
| 807 |
-
]
|
| 808 |
-
},
|
| 809 |
-
{
|
| 810 |
-
"point_id": 48,
|
| 811 |
-
"adjacent_points": [
|
| 812 |
-
49,
|
| 813 |
-
40
|
| 814 |
-
],
|
| 815 |
-
"adjacent_hexes": [
|
| 816 |
-
17
|
| 817 |
-
]
|
| 818 |
-
},
|
| 819 |
-
{
|
| 820 |
-
"point_id": 49,
|
| 821 |
-
"adjacent_points": [
|
| 822 |
-
48,
|
| 823 |
-
50
|
| 824 |
-
],
|
| 825 |
-
"adjacent_hexes": [
|
| 826 |
-
17
|
| 827 |
-
]
|
| 828 |
-
},
|
| 829 |
-
{
|
| 830 |
-
"point_id": 50,
|
| 831 |
-
"adjacent_points": [
|
| 832 |
-
49,
|
| 833 |
-
51,
|
| 834 |
-
42
|
| 835 |
-
],
|
| 836 |
-
"adjacent_hexes": [
|
| 837 |
-
18,
|
| 838 |
-
17
|
| 839 |
-
]
|
| 840 |
-
},
|
| 841 |
-
{
|
| 842 |
-
"point_id": 51,
|
| 843 |
-
"adjacent_points": [
|
| 844 |
-
50,
|
| 845 |
-
52
|
| 846 |
-
],
|
| 847 |
-
"adjacent_hexes": [
|
| 848 |
-
18
|
| 849 |
-
]
|
| 850 |
-
},
|
| 851 |
-
{
|
| 852 |
-
"point_id": 52,
|
| 853 |
-
"adjacent_points": [
|
| 854 |
-
51,
|
| 855 |
-
53,
|
| 856 |
-
44
|
| 857 |
-
],
|
| 858 |
-
"adjacent_hexes": [
|
| 859 |
-
19,
|
| 860 |
-
18
|
| 861 |
-
]
|
| 862 |
-
},
|
| 863 |
-
{
|
| 864 |
-
"point_id": 53,
|
| 865 |
-
"adjacent_points": [
|
| 866 |
-
52,
|
| 867 |
-
54
|
| 868 |
-
],
|
| 869 |
-
"adjacent_hexes": [
|
| 870 |
-
19
|
| 871 |
-
]
|
| 872 |
-
},
|
| 873 |
-
{
|
| 874 |
-
"point_id": 54,
|
| 875 |
-
"adjacent_points": [
|
| 876 |
-
53,
|
| 877 |
-
46
|
| 878 |
-
],
|
| 879 |
-
"adjacent_hexes": [
|
| 880 |
-
19
|
| 881 |
-
]
|
| 882 |
-
}
|
| 883 |
-
]
|
| 884 |
-
}
|
| 885 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/ai_testing/my_games/test_prompt_output.json
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"meta_data": {
|
| 3 |
+
"agent_name": "a",
|
| 4 |
+
"my_color": "Blue",
|
| 5 |
+
"role": "You are a strategic Catan player."
|
| 6 |
+
},
|
| 7 |
+
"task_context": {
|
| 8 |
+
"what_just_happened": "Game started. It's your turn to place your first settlement.",
|
| 9 |
+
"instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. Only one action is currently available."
|
| 10 |
+
},
|
| 11 |
+
"game_state_legend": "OPTIMIZED STATE FORMAT GUIDE:\n \n1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" → Hex 1 is Wood with number 12\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [[Neighbors], [HexIDs], Port?]\n To find yield: Check N[node_id], get HexIDs, look up in H array\n\n2. RESOURCE CODES:\n W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert\n Ports: ?3=Any(3:1), X2=Specific(2:1) where X is resource\n\n3. GAME STATE:\n \"bld\": [NodeID, Owner, Type] where Type: S=Settlement, C=City\n \"rds\": [[From, To], Owner]\n\n4. PLAYERS:\n \"res\": {ResourceCode: Count}\n \"dev\": {\"h\": [HiddenCards], \"r\": [RevealedCards]}\n \"stat\": [\"LR\"=Longest Road, \"LA\"=Largest Army]\n\n5. META:\n \"robber\": HexID where robber is located (blocks that hex)\n \"phase\": Current game phase\n \"curr\": Current player's turn\n",
|
| 12 |
+
"game_state": {
|
| 13 |
+
"my_private_info": {
|
| 14 |
+
"resources": {},
|
| 15 |
+
"development_cards": {
|
| 16 |
+
"hidden": [],
|
| 17 |
+
"revealed": []
|
| 18 |
+
},
|
| 19 |
+
"victory_points": 1,
|
| 20 |
+
"has_longest_road": false,
|
| 21 |
+
"has_largest_army": false
|
| 22 |
+
},
|
| 23 |
+
"board_state": {
|
| 24 |
+
"H": [
|
| 25 |
+
"",
|
| 26 |
+
"W12",
|
| 27 |
+
"S5",
|
| 28 |
+
"W4",
|
| 29 |
+
"S8",
|
| 30 |
+
"B6",
|
| 31 |
+
"W3",
|
| 32 |
+
"Wh8",
|
| 33 |
+
"B10",
|
| 34 |
+
"W11",
|
| 35 |
+
"D",
|
| 36 |
+
"O3",
|
| 37 |
+
"S4",
|
| 38 |
+
"B10",
|
| 39 |
+
"Wh9",
|
| 40 |
+
"Wh6",
|
| 41 |
+
"S11",
|
| 42 |
+
"O5",
|
| 43 |
+
"Wh9",
|
| 44 |
+
"O2"
|
| 45 |
+
],
|
| 46 |
+
"N": [
|
| 47 |
+
null,
|
| 48 |
+
[
|
| 49 |
+
[
|
| 50 |
+
2,
|
| 51 |
+
9
|
| 52 |
+
],
|
| 53 |
+
[
|
| 54 |
+
1
|
| 55 |
+
]
|
| 56 |
+
],
|
| 57 |
+
[
|
| 58 |
+
[
|
| 59 |
+
1,
|
| 60 |
+
3
|
| 61 |
+
],
|
| 62 |
+
[
|
| 63 |
+
1
|
| 64 |
+
],
|
| 65 |
+
"Wh2"
|
| 66 |
+
],
|
| 67 |
+
[
|
| 68 |
+
[
|
| 69 |
+
2,
|
| 70 |
+
4,
|
| 71 |
+
11
|
| 72 |
+
],
|
| 73 |
+
[
|
| 74 |
+
2,
|
| 75 |
+
1
|
| 76 |
+
],
|
| 77 |
+
"Wh2"
|
| 78 |
+
],
|
| 79 |
+
[
|
| 80 |
+
[
|
| 81 |
+
3,
|
| 82 |
+
5
|
| 83 |
+
],
|
| 84 |
+
[
|
| 85 |
+
2
|
| 86 |
+
]
|
| 87 |
+
],
|
| 88 |
+
[
|
| 89 |
+
[
|
| 90 |
+
4,
|
| 91 |
+
6,
|
| 92 |
+
13
|
| 93 |
+
],
|
| 94 |
+
[
|
| 95 |
+
3,
|
| 96 |
+
2
|
| 97 |
+
]
|
| 98 |
+
],
|
| 99 |
+
[
|
| 100 |
+
[
|
| 101 |
+
5,
|
| 102 |
+
7
|
| 103 |
+
],
|
| 104 |
+
[
|
| 105 |
+
3
|
| 106 |
+
],
|
| 107 |
+
"B2"
|
| 108 |
+
],
|
| 109 |
+
[
|
| 110 |
+
[
|
| 111 |
+
6,
|
| 112 |
+
15
|
| 113 |
+
],
|
| 114 |
+
[
|
| 115 |
+
3
|
| 116 |
+
],
|
| 117 |
+
"B2"
|
| 118 |
+
],
|
| 119 |
+
[
|
| 120 |
+
[
|
| 121 |
+
9,
|
| 122 |
+
18
|
| 123 |
+
],
|
| 124 |
+
[
|
| 125 |
+
4
|
| 126 |
+
],
|
| 127 |
+
"?3"
|
| 128 |
+
],
|
| 129 |
+
[
|
| 130 |
+
[
|
| 131 |
+
8,
|
| 132 |
+
10,
|
| 133 |
+
1
|
| 134 |
+
],
|
| 135 |
+
[
|
| 136 |
+
4,
|
| 137 |
+
1
|
| 138 |
+
],
|
| 139 |
+
"?3"
|
| 140 |
+
],
|
| 141 |
+
[
|
| 142 |
+
[
|
| 143 |
+
9,
|
| 144 |
+
11,
|
| 145 |
+
20
|
| 146 |
+
],
|
| 147 |
+
[
|
| 148 |
+
5,
|
| 149 |
+
4,
|
| 150 |
+
1
|
| 151 |
+
]
|
| 152 |
+
],
|
| 153 |
+
[
|
| 154 |
+
[
|
| 155 |
+
10,
|
| 156 |
+
12,
|
| 157 |
+
3
|
| 158 |
+
],
|
| 159 |
+
[
|
| 160 |
+
5,
|
| 161 |
+
2,
|
| 162 |
+
1
|
| 163 |
+
]
|
| 164 |
+
],
|
| 165 |
+
[
|
| 166 |
+
[
|
| 167 |
+
11,
|
| 168 |
+
13,
|
| 169 |
+
22
|
| 170 |
+
],
|
| 171 |
+
[
|
| 172 |
+
6,
|
| 173 |
+
5,
|
| 174 |
+
2
|
| 175 |
+
]
|
| 176 |
+
],
|
| 177 |
+
[
|
| 178 |
+
[
|
| 179 |
+
12,
|
| 180 |
+
14,
|
| 181 |
+
5
|
| 182 |
+
],
|
| 183 |
+
[
|
| 184 |
+
6,
|
| 185 |
+
3,
|
| 186 |
+
2
|
| 187 |
+
]
|
| 188 |
+
],
|
| 189 |
+
[
|
| 190 |
+
[
|
| 191 |
+
13,
|
| 192 |
+
15,
|
| 193 |
+
24
|
| 194 |
+
],
|
| 195 |
+
[
|
| 196 |
+
7,
|
| 197 |
+
6,
|
| 198 |
+
3
|
| 199 |
+
]
|
| 200 |
+
],
|
| 201 |
+
[
|
| 202 |
+
[
|
| 203 |
+
14,
|
| 204 |
+
16,
|
| 205 |
+
7
|
| 206 |
+
],
|
| 207 |
+
[
|
| 208 |
+
7,
|
| 209 |
+
3
|
| 210 |
+
]
|
| 211 |
+
],
|
| 212 |
+
[
|
| 213 |
+
[
|
| 214 |
+
15,
|
| 215 |
+
26
|
| 216 |
+
],
|
| 217 |
+
[
|
| 218 |
+
7
|
| 219 |
+
],
|
| 220 |
+
"O2"
|
| 221 |
+
],
|
| 222 |
+
[
|
| 223 |
+
[
|
| 224 |
+
18,
|
| 225 |
+
28
|
| 226 |
+
],
|
| 227 |
+
[
|
| 228 |
+
8
|
| 229 |
+
],
|
| 230 |
+
"S2"
|
| 231 |
+
],
|
| 232 |
+
[
|
| 233 |
+
[
|
| 234 |
+
17,
|
| 235 |
+
19,
|
| 236 |
+
8
|
| 237 |
+
],
|
| 238 |
+
[
|
| 239 |
+
8,
|
| 240 |
+
4
|
| 241 |
+
]
|
| 242 |
+
],
|
| 243 |
+
[
|
| 244 |
+
[
|
| 245 |
+
18,
|
| 246 |
+
20,
|
| 247 |
+
30
|
| 248 |
+
],
|
| 249 |
+
[
|
| 250 |
+
9,
|
| 251 |
+
8,
|
| 252 |
+
4
|
| 253 |
+
]
|
| 254 |
+
],
|
| 255 |
+
[
|
| 256 |
+
[
|
| 257 |
+
19,
|
| 258 |
+
21,
|
| 259 |
+
10
|
| 260 |
+
],
|
| 261 |
+
[
|
| 262 |
+
9,
|
| 263 |
+
5,
|
| 264 |
+
4
|
| 265 |
+
]
|
| 266 |
+
],
|
| 267 |
+
[
|
| 268 |
+
[
|
| 269 |
+
20,
|
| 270 |
+
22,
|
| 271 |
+
32
|
| 272 |
+
],
|
| 273 |
+
[
|
| 274 |
+
10,
|
| 275 |
+
9,
|
| 276 |
+
5
|
| 277 |
+
]
|
| 278 |
+
],
|
| 279 |
+
[
|
| 280 |
+
[
|
| 281 |
+
21,
|
| 282 |
+
23,
|
| 283 |
+
12
|
| 284 |
+
],
|
| 285 |
+
[
|
| 286 |
+
10,
|
| 287 |
+
6,
|
| 288 |
+
5
|
| 289 |
+
]
|
| 290 |
+
],
|
| 291 |
+
[
|
| 292 |
+
[
|
| 293 |
+
22,
|
| 294 |
+
24,
|
| 295 |
+
34
|
| 296 |
+
],
|
| 297 |
+
[
|
| 298 |
+
11,
|
| 299 |
+
10,
|
| 300 |
+
6
|
| 301 |
+
]
|
| 302 |
+
],
|
| 303 |
+
[
|
| 304 |
+
[
|
| 305 |
+
23,
|
| 306 |
+
25,
|
| 307 |
+
14
|
| 308 |
+
],
|
| 309 |
+
[
|
| 310 |
+
11,
|
| 311 |
+
7,
|
| 312 |
+
6
|
| 313 |
+
]
|
| 314 |
+
],
|
| 315 |
+
[
|
| 316 |
+
[
|
| 317 |
+
24,
|
| 318 |
+
26,
|
| 319 |
+
36
|
| 320 |
+
],
|
| 321 |
+
[
|
| 322 |
+
12,
|
| 323 |
+
11,
|
| 324 |
+
7
|
| 325 |
+
]
|
| 326 |
+
],
|
| 327 |
+
[
|
| 328 |
+
[
|
| 329 |
+
25,
|
| 330 |
+
27,
|
| 331 |
+
16
|
| 332 |
+
],
|
| 333 |
+
[
|
| 334 |
+
12,
|
| 335 |
+
7
|
| 336 |
+
],
|
| 337 |
+
"O2"
|
| 338 |
+
],
|
| 339 |
+
[
|
| 340 |
+
[
|
| 341 |
+
26,
|
| 342 |
+
38
|
| 343 |
+
],
|
| 344 |
+
[
|
| 345 |
+
12
|
| 346 |
+
]
|
| 347 |
+
],
|
| 348 |
+
[
|
| 349 |
+
[
|
| 350 |
+
29,
|
| 351 |
+
17
|
| 352 |
+
],
|
| 353 |
+
[
|
| 354 |
+
8
|
| 355 |
+
],
|
| 356 |
+
"S2"
|
| 357 |
+
],
|
| 358 |
+
[
|
| 359 |
+
[
|
| 360 |
+
28,
|
| 361 |
+
30,
|
| 362 |
+
39
|
| 363 |
+
],
|
| 364 |
+
[
|
| 365 |
+
13,
|
| 366 |
+
8
|
| 367 |
+
]
|
| 368 |
+
],
|
| 369 |
+
[
|
| 370 |
+
[
|
| 371 |
+
29,
|
| 372 |
+
31,
|
| 373 |
+
19
|
| 374 |
+
],
|
| 375 |
+
[
|
| 376 |
+
13,
|
| 377 |
+
9,
|
| 378 |
+
8
|
| 379 |
+
]
|
| 380 |
+
],
|
| 381 |
+
[
|
| 382 |
+
[
|
| 383 |
+
30,
|
| 384 |
+
32,
|
| 385 |
+
41
|
| 386 |
+
],
|
| 387 |
+
[
|
| 388 |
+
14,
|
| 389 |
+
13,
|
| 390 |
+
9
|
| 391 |
+
]
|
| 392 |
+
],
|
| 393 |
+
[
|
| 394 |
+
[
|
| 395 |
+
31,
|
| 396 |
+
33,
|
| 397 |
+
21
|
| 398 |
+
],
|
| 399 |
+
[
|
| 400 |
+
14,
|
| 401 |
+
10,
|
| 402 |
+
9
|
| 403 |
+
]
|
| 404 |
+
],
|
| 405 |
+
[
|
| 406 |
+
[
|
| 407 |
+
32,
|
| 408 |
+
34,
|
| 409 |
+
43
|
| 410 |
+
],
|
| 411 |
+
[
|
| 412 |
+
15,
|
| 413 |
+
14,
|
| 414 |
+
10
|
| 415 |
+
]
|
| 416 |
+
],
|
| 417 |
+
[
|
| 418 |
+
[
|
| 419 |
+
33,
|
| 420 |
+
35,
|
| 421 |
+
23
|
| 422 |
+
],
|
| 423 |
+
[
|
| 424 |
+
15,
|
| 425 |
+
11,
|
| 426 |
+
10
|
| 427 |
+
]
|
| 428 |
+
],
|
| 429 |
+
[
|
| 430 |
+
[
|
| 431 |
+
34,
|
| 432 |
+
36,
|
| 433 |
+
45
|
| 434 |
+
],
|
| 435 |
+
[
|
| 436 |
+
16,
|
| 437 |
+
15,
|
| 438 |
+
11
|
| 439 |
+
]
|
| 440 |
+
],
|
| 441 |
+
[
|
| 442 |
+
[
|
| 443 |
+
35,
|
| 444 |
+
37,
|
| 445 |
+
25
|
| 446 |
+
],
|
| 447 |
+
[
|
| 448 |
+
16,
|
| 449 |
+
12,
|
| 450 |
+
11
|
| 451 |
+
]
|
| 452 |
+
],
|
| 453 |
+
[
|
| 454 |
+
[
|
| 455 |
+
36,
|
| 456 |
+
38,
|
| 457 |
+
47
|
| 458 |
+
],
|
| 459 |
+
[
|
| 460 |
+
16,
|
| 461 |
+
12
|
| 462 |
+
],
|
| 463 |
+
"?3"
|
| 464 |
+
],
|
| 465 |
+
[
|
| 466 |
+
[
|
| 467 |
+
37,
|
| 468 |
+
27
|
| 469 |
+
],
|
| 470 |
+
[
|
| 471 |
+
12
|
| 472 |
+
],
|
| 473 |
+
"?3"
|
| 474 |
+
],
|
| 475 |
+
[
|
| 476 |
+
[
|
| 477 |
+
40,
|
| 478 |
+
29
|
| 479 |
+
],
|
| 480 |
+
[
|
| 481 |
+
13
|
| 482 |
+
]
|
| 483 |
+
],
|
| 484 |
+
[
|
| 485 |
+
[
|
| 486 |
+
39,
|
| 487 |
+
41,
|
| 488 |
+
48
|
| 489 |
+
],
|
| 490 |
+
[
|
| 491 |
+
17,
|
| 492 |
+
13
|
| 493 |
+
],
|
| 494 |
+
"W2"
|
| 495 |
+
],
|
| 496 |
+
[
|
| 497 |
+
[
|
| 498 |
+
40,
|
| 499 |
+
42,
|
| 500 |
+
31
|
| 501 |
+
],
|
| 502 |
+
[
|
| 503 |
+
17,
|
| 504 |
+
14,
|
| 505 |
+
13
|
| 506 |
+
]
|
| 507 |
+
],
|
| 508 |
+
[
|
| 509 |
+
[
|
| 510 |
+
41,
|
| 511 |
+
43,
|
| 512 |
+
50
|
| 513 |
+
],
|
| 514 |
+
[
|
| 515 |
+
18,
|
| 516 |
+
17,
|
| 517 |
+
14
|
| 518 |
+
]
|
| 519 |
+
],
|
| 520 |
+
[
|
| 521 |
+
[
|
| 522 |
+
42,
|
| 523 |
+
44,
|
| 524 |
+
33
|
| 525 |
+
],
|
| 526 |
+
[
|
| 527 |
+
18,
|
| 528 |
+
15,
|
| 529 |
+
14
|
| 530 |
+
]
|
| 531 |
+
],
|
| 532 |
+
[
|
| 533 |
+
[
|
| 534 |
+
43,
|
| 535 |
+
45,
|
| 536 |
+
52
|
| 537 |
+
],
|
| 538 |
+
[
|
| 539 |
+
19,
|
| 540 |
+
18,
|
| 541 |
+
15
|
| 542 |
+
]
|
| 543 |
+
],
|
| 544 |
+
[
|
| 545 |
+
[
|
| 546 |
+
44,
|
| 547 |
+
46,
|
| 548 |
+
35
|
| 549 |
+
],
|
| 550 |
+
[
|
| 551 |
+
19,
|
| 552 |
+
16,
|
| 553 |
+
15
|
| 554 |
+
]
|
| 555 |
+
],
|
| 556 |
+
[
|
| 557 |
+
[
|
| 558 |
+
45,
|
| 559 |
+
47,
|
| 560 |
+
54
|
| 561 |
+
],
|
| 562 |
+
[
|
| 563 |
+
19,
|
| 564 |
+
16
|
| 565 |
+
]
|
| 566 |
+
],
|
| 567 |
+
[
|
| 568 |
+
[
|
| 569 |
+
46,
|
| 570 |
+
37
|
| 571 |
+
],
|
| 572 |
+
[
|
| 573 |
+
16
|
| 574 |
+
]
|
| 575 |
+
],
|
| 576 |
+
[
|
| 577 |
+
[
|
| 578 |
+
49,
|
| 579 |
+
40
|
| 580 |
+
],
|
| 581 |
+
[
|
| 582 |
+
17
|
| 583 |
+
],
|
| 584 |
+
"W2"
|
| 585 |
+
],
|
| 586 |
+
[
|
| 587 |
+
[
|
| 588 |
+
48,
|
| 589 |
+
50
|
| 590 |
+
],
|
| 591 |
+
[
|
| 592 |
+
17
|
| 593 |
+
]
|
| 594 |
+
],
|
| 595 |
+
[
|
| 596 |
+
[
|
| 597 |
+
49,
|
| 598 |
+
51,
|
| 599 |
+
42
|
| 600 |
+
],
|
| 601 |
+
[
|
| 602 |
+
18,
|
| 603 |
+
17
|
| 604 |
+
],
|
| 605 |
+
"?3"
|
| 606 |
+
],
|
| 607 |
+
[
|
| 608 |
+
[
|
| 609 |
+
50,
|
| 610 |
+
52
|
| 611 |
+
],
|
| 612 |
+
[
|
| 613 |
+
18
|
| 614 |
+
],
|
| 615 |
+
"?3"
|
| 616 |
+
],
|
| 617 |
+
[
|
| 618 |
+
[
|
| 619 |
+
51,
|
| 620 |
+
53,
|
| 621 |
+
44
|
| 622 |
+
],
|
| 623 |
+
[
|
| 624 |
+
19,
|
| 625 |
+
18
|
| 626 |
+
]
|
| 627 |
+
],
|
| 628 |
+
[
|
| 629 |
+
[
|
| 630 |
+
52,
|
| 631 |
+
54
|
| 632 |
+
],
|
| 633 |
+
[
|
| 634 |
+
19
|
| 635 |
+
],
|
| 636 |
+
"?3"
|
| 637 |
+
],
|
| 638 |
+
[
|
| 639 |
+
[
|
| 640 |
+
53,
|
| 641 |
+
46
|
| 642 |
+
],
|
| 643 |
+
[
|
| 644 |
+
19
|
| 645 |
+
],
|
| 646 |
+
"?3"
|
| 647 |
+
]
|
| 648 |
+
],
|
| 649 |
+
"buildings": [
|
| 650 |
+
{
|
| 651 |
+
"node": 20,
|
| 652 |
+
"owner": "me",
|
| 653 |
+
"type": "settlement"
|
| 654 |
+
}
|
| 655 |
+
],
|
| 656 |
+
"roads": [
|
| 657 |
+
{
|
| 658 |
+
"from": 20,
|
| 659 |
+
"to": 10,
|
| 660 |
+
"owner": "me"
|
| 661 |
+
}
|
| 662 |
+
],
|
| 663 |
+
"robber_hex": 10,
|
| 664 |
+
"current_phase": "SETUP_FIRST_ROUND",
|
| 665 |
+
"dice_result": null
|
| 666 |
+
},
|
| 667 |
+
"other_players": [
|
| 668 |
+
{
|
| 669 |
+
"name": "b",
|
| 670 |
+
"victory_points": 0,
|
| 671 |
+
"resource_count": 0,
|
| 672 |
+
"development_card_count": 0,
|
| 673 |
+
"knights_played": 0,
|
| 674 |
+
"has_longest_road": false,
|
| 675 |
+
"has_largest_army": false
|
| 676 |
+
},
|
| 677 |
+
{
|
| 678 |
+
"name": "c",
|
| 679 |
+
"victory_points": 0,
|
| 680 |
+
"resource_count": 0,
|
| 681 |
+
"development_card_count": 0,
|
| 682 |
+
"knights_played": 0,
|
| 683 |
+
"has_longest_road": false,
|
| 684 |
+
"has_largest_army": false
|
| 685 |
+
}
|
| 686 |
+
],
|
| 687 |
+
"strategic_context": {
|
| 688 |
+
"hex_analysis": [
|
| 689 |
+
{
|
| 690 |
+
"hex_id": 1,
|
| 691 |
+
"resource": "W",
|
| 692 |
+
"number": 12,
|
| 693 |
+
"probability": "2.8%"
|
| 694 |
+
},
|
| 695 |
+
{
|
| 696 |
+
"hex_id": 2,
|
| 697 |
+
"resource": "S",
|
| 698 |
+
"number": 5,
|
| 699 |
+
"probability": "11.1%"
|
| 700 |
+
},
|
| 701 |
+
{
|
| 702 |
+
"hex_id": 3,
|
| 703 |
+
"resource": "W",
|
| 704 |
+
"number": 4,
|
| 705 |
+
"probability": "8.3%"
|
| 706 |
+
},
|
| 707 |
+
{
|
| 708 |
+
"hex_id": 4,
|
| 709 |
+
"resource": "S",
|
| 710 |
+
"number": 8,
|
| 711 |
+
"probability": "13.9%"
|
| 712 |
+
},
|
| 713 |
+
{
|
| 714 |
+
"hex_id": 5,
|
| 715 |
+
"resource": "B",
|
| 716 |
+
"number": 6,
|
| 717 |
+
"probability": "13.9%"
|
| 718 |
+
},
|
| 719 |
+
{
|
| 720 |
+
"hex_id": 6,
|
| 721 |
+
"resource": "W",
|
| 722 |
+
"number": 3,
|
| 723 |
+
"probability": "5.6%"
|
| 724 |
+
},
|
| 725 |
+
{
|
| 726 |
+
"hex_id": 8,
|
| 727 |
+
"resource": "B",
|
| 728 |
+
"number": 10,
|
| 729 |
+
"probability": "8.3%"
|
| 730 |
+
},
|
| 731 |
+
{
|
| 732 |
+
"hex_id": 9,
|
| 733 |
+
"resource": "W",
|
| 734 |
+
"number": 11,
|
| 735 |
+
"probability": "5.6%"
|
| 736 |
+
},
|
| 737 |
+
{
|
| 738 |
+
"hex_id": 11,
|
| 739 |
+
"resource": "O",
|
| 740 |
+
"number": 3,
|
| 741 |
+
"probability": "5.6%"
|
| 742 |
+
},
|
| 743 |
+
{
|
| 744 |
+
"hex_id": 12,
|
| 745 |
+
"resource": "S",
|
| 746 |
+
"number": 4,
|
| 747 |
+
"probability": "8.3%"
|
| 748 |
+
},
|
| 749 |
+
{
|
| 750 |
+
"hex_id": 13,
|
| 751 |
+
"resource": "B",
|
| 752 |
+
"number": 10,
|
| 753 |
+
"probability": "8.3%"
|
| 754 |
+
},
|
| 755 |
+
{
|
| 756 |
+
"hex_id": 16,
|
| 757 |
+
"resource": "S",
|
| 758 |
+
"number": 11,
|
| 759 |
+
"probability": "5.6%"
|
| 760 |
+
},
|
| 761 |
+
{
|
| 762 |
+
"hex_id": 17,
|
| 763 |
+
"resource": "O",
|
| 764 |
+
"number": 5,
|
| 765 |
+
"probability": "11.1%"
|
| 766 |
+
},
|
| 767 |
+
{
|
| 768 |
+
"hex_id": 19,
|
| 769 |
+
"resource": "O",
|
| 770 |
+
"number": 2,
|
| 771 |
+
"probability": "2.8%"
|
| 772 |
+
}
|
| 773 |
+
],
|
| 774 |
+
"leading_player": "a",
|
| 775 |
+
"current_player": "b",
|
| 776 |
+
"game_phase": "SETUP_FIRST_ROUND",
|
| 777 |
+
"robber_location": 10
|
| 778 |
+
}
|
| 779 |
+
},
|
| 780 |
+
"constraints": {
|
| 781 |
+
"usage_instructions": "Choose one action type from the list below. Populate the 'parameters' field in your response strictly according to the 'example_parameters' structure provided.",
|
| 782 |
+
"allowed_actions": [
|
| 783 |
+
{
|
| 784 |
+
"action": "place_settlement",
|
| 785 |
+
"description": "Place your starting settlement",
|
| 786 |
+
"example_parameters": {
|
| 787 |
+
"node_id": 20
|
| 788 |
+
}
|
| 789 |
+
}
|
| 790 |
+
]
|
| 791 |
+
}
|
| 792 |
+
}
|
examples/ai_testing/play_and_capture.py
CHANGED
|
@@ -24,6 +24,163 @@ original_update_state = None
|
|
| 24 |
# Output directory for states
|
| 25 |
OUTPUT_DIR = Path('examples/ai_testing/my_games')
|
| 26 |
CURRENT_STATE_FILE = OUTPUT_DIR / 'current_state.json'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
|
| 29 |
def save_current_state(state):
|
|
@@ -31,12 +188,24 @@ def save_current_state(state):
|
|
| 31 |
try:
|
| 32 |
OUTPUT_DIR.mkdir(exist_ok=True)
|
| 33 |
cleaned = clean_state_for_llm(state)
|
|
|
|
|
|
|
| 34 |
with open(CURRENT_STATE_FILE, 'w', encoding='utf-8') as f:
|
| 35 |
json.dump({
|
| 36 |
'timestamp': datetime.now().isoformat(),
|
| 37 |
'state_number': len(captured_states),
|
| 38 |
'state': cleaned
|
| 39 |
}, f, indent=2, ensure_ascii=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
except Exception as e:
|
| 41 |
print(f"[⚠️ Failed to save current state: {e}]", flush=True)
|
| 42 |
|
|
@@ -123,21 +292,56 @@ def save_all_states():
|
|
| 123 |
with open(final_file, 'w', encoding='utf-8') as f:
|
| 124 |
json.dump(cleaned_states[-1], f, indent=2, ensure_ascii=False)
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
# Also save to standard location (cleaned)
|
| 127 |
sample_file = Path('examples/ai_testing/sample_states/captured_game.json')
|
| 128 |
sample_file.parent.mkdir(exist_ok=True)
|
| 129 |
with open(sample_file, 'w', encoding='utf-8') as f:
|
| 130 |
json.dump(cleaned_states[-1], f, indent=2, ensure_ascii=False)
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
print("\n" + "="*80)
|
| 133 |
print("✅ GAME SAVED SUCCESSFULLY!")
|
| 134 |
print("="*80)
|
| 135 |
print(f"\nTotal states captured: {len(captured_states)}")
|
| 136 |
print(f"\nSaved to:")
|
| 137 |
-
print(f" 📁 Full history:
|
| 138 |
-
print(f"
|
| 139 |
-
print(f"
|
| 140 |
-
print(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
print("\n" + "="*80)
|
| 142 |
|
| 143 |
# Print summary
|
|
|
|
| 24 |
# Output directory for states
|
| 25 |
OUTPUT_DIR = Path('examples/ai_testing/my_games')
|
| 26 |
CURRENT_STATE_FILE = OUTPUT_DIR / 'current_state.json'
|
| 27 |
+
OPTIMIZED_STATE_FILE = OUTPUT_DIR / 'current_state_optimized.txt'
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def optimize_state_for_ai(input_data):
|
| 31 |
+
"""
|
| 32 |
+
ממיר את מצב המשחק למבנה אופטימלי עבור AI.
|
| 33 |
+
מדחס את המידע ומסיר דופליקציות.
|
| 34 |
+
"""
|
| 35 |
+
# טיפול בעטיפה אם קיימת
|
| 36 |
+
data = input_data['state'] if 'state' in input_data else input_data
|
| 37 |
+
|
| 38 |
+
# מילוני קיצור
|
| 39 |
+
RES_MAP = {"wood": "W", "brick": "B", "sheep": "S", "wheat": "Wh", "ore": "O", "desert": "D"}
|
| 40 |
+
TYPE_MAP = {"settlement": "S", "city": "C"}
|
| 41 |
+
|
| 42 |
+
# 1. יצירת מערך הקסים (H)
|
| 43 |
+
max_hex_id = max([h['id'] for h in data.get('hexes', [])], default=0)
|
| 44 |
+
hex_array = [""] * (max_hex_id + 1)
|
| 45 |
+
|
| 46 |
+
robber_hex = None
|
| 47 |
+
for h in data.get('hexes', []):
|
| 48 |
+
if h.get('has_robber'):
|
| 49 |
+
robber_hex = h['id']
|
| 50 |
+
t = RES_MAP.get(h['type'], "?")
|
| 51 |
+
# אם יש מספר מוסיפים אותו, אחרת (מדבר) רק את הסוג
|
| 52 |
+
val = f"{t}{h['number']}" if h['number'] else t
|
| 53 |
+
hex_array[h['id']] = val
|
| 54 |
+
|
| 55 |
+
# 2. מיפוי נמלים
|
| 56 |
+
port_map = {}
|
| 57 |
+
for p in data.get('harbors', []):
|
| 58 |
+
t = RES_MAP.get(p['type'], "Any") if p['type'] != "any" else "?"
|
| 59 |
+
code = f"{t}{p['ratio']}"
|
| 60 |
+
port_map[p['point_one']] = code
|
| 61 |
+
port_map[p['point_two']] = code
|
| 62 |
+
|
| 63 |
+
# 3. יצירת מערך צמתים (N)
|
| 64 |
+
max_point_id = max([p['point_id'] for p in data.get('points', [])], default=0)
|
| 65 |
+
nodes_array = [None] * (max_point_id + 1)
|
| 66 |
+
|
| 67 |
+
for p in data.get('points', []):
|
| 68 |
+
# המבנה: [ [שכנים], [הקסים], נמל? ]
|
| 69 |
+
val = [p['adjacent_points'], p['adjacent_hexes']]
|
| 70 |
+
if p['point_id'] in port_map:
|
| 71 |
+
val.append(port_map[p['point_id']])
|
| 72 |
+
nodes_array[p['point_id']] = val
|
| 73 |
+
|
| 74 |
+
# 4. עיבוד שחקנים
|
| 75 |
+
players = {}
|
| 76 |
+
pid_to_name = {}
|
| 77 |
+
for pl in data.get('players', []):
|
| 78 |
+
name = pl['name']
|
| 79 |
+
pid_to_name[pl['id']] = name
|
| 80 |
+
|
| 81 |
+
# ספירת משאבים
|
| 82 |
+
res_list = pl.get('cards_list', [])
|
| 83 |
+
res_compact = {}
|
| 84 |
+
if res_list:
|
| 85 |
+
for r in set(res_list):
|
| 86 |
+
r_key = RES_MAP.get(r.lower(), r)
|
| 87 |
+
res_compact[r_key] = res_list.count(r)
|
| 88 |
+
|
| 89 |
+
p_obj = {"vp": pl['victory_points'], "res": res_compact}
|
| 90 |
+
|
| 91 |
+
# קלפי פיתוח
|
| 92 |
+
knights = pl.get('knights_played', 0)
|
| 93 |
+
hidden = pl.get('dev_cards_list', [])
|
| 94 |
+
if knights > 0 or hidden:
|
| 95 |
+
p_obj["dev"] = {}
|
| 96 |
+
if hidden:
|
| 97 |
+
p_obj["dev"]["h"] = hidden
|
| 98 |
+
if knights:
|
| 99 |
+
p_obj["dev"]["r"] = ["K"] * knights
|
| 100 |
+
|
| 101 |
+
# דגלים מיוחדים (LR / LA)
|
| 102 |
+
flags = []
|
| 103 |
+
if pl.get('has_longest_road'):
|
| 104 |
+
flags.append("LR") # Longest Road
|
| 105 |
+
if pl.get('has_largest_army'):
|
| 106 |
+
flags.append("LA") # Largest Army
|
| 107 |
+
|
| 108 |
+
if flags:
|
| 109 |
+
p_obj["stat"] = flags
|
| 110 |
+
|
| 111 |
+
players[name] = p_obj
|
| 112 |
+
|
| 113 |
+
# 5. מצב הלוח (בניינים ודרכים)
|
| 114 |
+
bld = []
|
| 115 |
+
for b in data.get('settlements', []):
|
| 116 |
+
owner_id = b.get('player', 1) - 1 # המרה מ-1-based ל-0-based
|
| 117 |
+
owner = pid_to_name.get(owner_id, "?")
|
| 118 |
+
bld.append([b['vertex'], owner, "S"])
|
| 119 |
+
|
| 120 |
+
for b in data.get('cities', []):
|
| 121 |
+
owner_id = b.get('player', 1) - 1 # המרה מ-1-based ל-0-based
|
| 122 |
+
owner = pid_to_name.get(owner_id, "?")
|
| 123 |
+
bld.append([b['vertex'], owner, "C"])
|
| 124 |
+
|
| 125 |
+
rds = []
|
| 126 |
+
for r in data.get('roads', []):
|
| 127 |
+
owner_id = r.get('player', 1) - 1 # המרה מ-1-based ל-0-based
|
| 128 |
+
owner = pid_to_name.get(owner_id, "?")
|
| 129 |
+
rds.append([[r['from'], r['to']], owner])
|
| 130 |
+
|
| 131 |
+
# המרת ID של השחקן הנוכחי לשם
|
| 132 |
+
curr_id = data.get('current_player')
|
| 133 |
+
curr_name = pid_to_name.get(curr_id, str(curr_id))
|
| 134 |
+
|
| 135 |
+
# החזרת המילון המעובד
|
| 136 |
+
return {
|
| 137 |
+
"meta": {
|
| 138 |
+
"curr": curr_name,
|
| 139 |
+
"phase": data.get('current_phase'),
|
| 140 |
+
"robber": robber_hex,
|
| 141 |
+
"dice": data.get('dice_result')
|
| 142 |
+
},
|
| 143 |
+
"H": hex_array,
|
| 144 |
+
"N": nodes_array,
|
| 145 |
+
"state": {"bld": bld, "rds": rds},
|
| 146 |
+
"players": players
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def format_hybrid_json(data):
|
| 151 |
+
"""יצירת מחרוזת JSON היברידית עם מקרא למעלה"""
|
| 152 |
+
|
| 153 |
+
# המקרא
|
| 154 |
+
legend = """1. LOOKUP TABLES:
|
| 155 |
+
• "H" (Hexes): Array where Index = HexID. Value = Resource+Num.
|
| 156 |
+
Example: H[1]="W12" -> Hex 1 is Wood 12.
|
| 157 |
+
• "N" (Nodes): Array where Index = NodeID.
|
| 158 |
+
Format: [ [Neighbors], [HexIDs], Port? ]
|
| 159 |
+
Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].
|
| 160 |
+
|
| 161 |
+
2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.
|
| 162 |
+
?3=Any 3:1 port, X2=Specific Resource 2:1 port.
|
| 163 |
+
|
| 164 |
+
3. STATE: "bld"=[NodeID, Owner, Type], "rds"=[[From,To], Owner].
|
| 165 |
+
|
| 166 |
+
4. PLAYERS: "res"={Resource:Count}, "dev"={"h":[Hidden Cards], "r":[Revealed] (K=Knight)},
|
| 167 |
+
"stat"=["LR" (Longest Road), "LA" (Largest Army)].
|
| 168 |
+
|
| 169 |
+
5. ROBBER: Located at HexID specified in "meta.robber". H[id] is blocked.
|
| 170 |
+
|
| 171 |
+
JSON:
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
sections = [
|
| 175 |
+
f'"meta":{json.dumps(data["meta"], separators=(",", ":"))}',
|
| 176 |
+
f'"H":{json.dumps(data["H"], separators=(",", ":"))}',
|
| 177 |
+
f'"N":{json.dumps(data["N"], separators=(",", ":"))}',
|
| 178 |
+
f'"state":{json.dumps(data["state"], separators=(",", ":"))}',
|
| 179 |
+
f'"players":{json.dumps(data["players"], separators=(",", ":"))}'
|
| 180 |
+
]
|
| 181 |
+
json_content = "{\n " + ",\n ".join(sections) + "\n}"
|
| 182 |
+
|
| 183 |
+
return legend + json_content
|
| 184 |
|
| 185 |
|
| 186 |
def save_current_state(state):
|
|
|
|
| 188 |
try:
|
| 189 |
OUTPUT_DIR.mkdir(exist_ok=True)
|
| 190 |
cleaned = clean_state_for_llm(state)
|
| 191 |
+
|
| 192 |
+
# שמירת הגרסה המקורית
|
| 193 |
with open(CURRENT_STATE_FILE, 'w', encoding='utf-8') as f:
|
| 194 |
json.dump({
|
| 195 |
'timestamp': datetime.now().isoformat(),
|
| 196 |
'state_number': len(captured_states),
|
| 197 |
'state': cleaned
|
| 198 |
}, f, indent=2, ensure_ascii=False)
|
| 199 |
+
|
| 200 |
+
# שמירת הגרסה האופטימלית
|
| 201 |
+
try:
|
| 202 |
+
optimized = optimize_state_for_ai(cleaned)
|
| 203 |
+
optimized_output = format_hybrid_json(optimized)
|
| 204 |
+
with open(OPTIMIZED_STATE_FILE, 'w', encoding='utf-8') as f:
|
| 205 |
+
f.write(optimized_output)
|
| 206 |
+
except Exception as opt_err:
|
| 207 |
+
print(f"[⚠️ Failed to save optimized state: {opt_err}]", flush=True)
|
| 208 |
+
|
| 209 |
except Exception as e:
|
| 210 |
print(f"[⚠️ Failed to save current state: {e}]", flush=True)
|
| 211 |
|
|
|
|
| 292 |
with open(final_file, 'w', encoding='utf-8') as f:
|
| 293 |
json.dump(cleaned_states[-1], f, indent=2, ensure_ascii=False)
|
| 294 |
|
| 295 |
+
# Save optimized final state
|
| 296 |
+
optimized_final_file = output_dir / f'game_{timestamp}_final_optimized.txt'
|
| 297 |
+
try:
|
| 298 |
+
optimized_final = optimize_state_for_ai(cleaned_states[-1])
|
| 299 |
+
with open(optimized_final_file, 'w', encoding='utf-8') as f:
|
| 300 |
+
f.write(format_hybrid_json(optimized_final))
|
| 301 |
+
except Exception as e:
|
| 302 |
+
print(f"[⚠️ Failed to save optimized final state: {e}]")
|
| 303 |
+
|
| 304 |
+
# Save optimized full history
|
| 305 |
+
optimized_history_file = output_dir / f'game_{timestamp}_full_optimized.txt'
|
| 306 |
+
try:
|
| 307 |
+
optimized_states = [optimize_state_for_ai(state) for state in cleaned_states]
|
| 308 |
+
with open(optimized_history_file, 'w', encoding='utf-8') as f:
|
| 309 |
+
json.dump({
|
| 310 |
+
'total_states': len(optimized_states),
|
| 311 |
+
'timestamp': timestamp,
|
| 312 |
+
'states': optimized_states
|
| 313 |
+
}, f, indent=2, ensure_ascii=False)
|
| 314 |
+
except Exception as e:
|
| 315 |
+
print(f"[⚠️ Failed to save optimized history: {e}]")
|
| 316 |
+
|
| 317 |
# Also save to standard location (cleaned)
|
| 318 |
sample_file = Path('examples/ai_testing/sample_states/captured_game.json')
|
| 319 |
sample_file.parent.mkdir(exist_ok=True)
|
| 320 |
with open(sample_file, 'w', encoding='utf-8') as f:
|
| 321 |
json.dump(cleaned_states[-1], f, indent=2, ensure_ascii=False)
|
| 322 |
|
| 323 |
+
# Save optimized to standard location
|
| 324 |
+
sample_optimized_file = Path('examples/ai_testing/sample_states/captured_game_optimized.txt')
|
| 325 |
+
try:
|
| 326 |
+
optimized_sample = optimize_state_for_ai(cleaned_states[-1])
|
| 327 |
+
with open(sample_optimized_file, 'w', encoding='utf-8') as f:
|
| 328 |
+
f.write(format_hybrid_json(optimized_sample))
|
| 329 |
+
except Exception as e:
|
| 330 |
+
print(f"[⚠️ Failed to save optimized sample: {e}]")
|
| 331 |
+
|
| 332 |
print("\n" + "="*80)
|
| 333 |
print("✅ GAME SAVED SUCCESSFULLY!")
|
| 334 |
print("="*80)
|
| 335 |
print(f"\nTotal states captured: {len(captured_states)}")
|
| 336 |
print(f"\nSaved to:")
|
| 337 |
+
print(f" 📁 Full history: {history_file}")
|
| 338 |
+
print(f" 📁 Full history (opt): {optimized_history_file}")
|
| 339 |
+
print(f" 📄 Final state: {final_file}")
|
| 340 |
+
print(f" 📄 Final state (opt): {optimized_final_file}")
|
| 341 |
+
print(f" 📌 Quick access: {sample_file}")
|
| 342 |
+
print(f" 📌 Quick access (opt): {sample_optimized_file}")
|
| 343 |
+
print(f" 🔄 Real-time: {CURRENT_STATE_FILE}")
|
| 344 |
+
print(f" 🔄 Real-time (opt): {OPTIMIZED_STATE_FILE}")
|
| 345 |
print("\n" + "="*80)
|
| 346 |
|
| 347 |
# Print summary
|
examples/ai_testing/sample_states/captured_game.json
CHANGED
|
@@ -117,72 +117,17 @@
|
|
| 117 |
],
|
| 118 |
"settlements": [
|
| 119 |
{
|
| 120 |
-
"id": "
|
| 121 |
-
"vertex":
|
| 122 |
"player": 1
|
| 123 |
-
},
|
| 124 |
-
{
|
| 125 |
-
"id": "b_14",
|
| 126 |
-
"vertex": 14,
|
| 127 |
-
"player": 2
|
| 128 |
-
},
|
| 129 |
-
{
|
| 130 |
-
"id": "b_31",
|
| 131 |
-
"vertex": 31,
|
| 132 |
-
"player": 3
|
| 133 |
-
},
|
| 134 |
-
{
|
| 135 |
-
"id": "b_36",
|
| 136 |
-
"vertex": 36,
|
| 137 |
-
"player": 2
|
| 138 |
-
},
|
| 139 |
-
{
|
| 140 |
-
"id": "b_40",
|
| 141 |
-
"vertex": 40,
|
| 142 |
-
"player": 1
|
| 143 |
-
},
|
| 144 |
-
{
|
| 145 |
-
"id": "b_44",
|
| 146 |
-
"vertex": 44,
|
| 147 |
-
"player": 3
|
| 148 |
}
|
| 149 |
],
|
| 150 |
"cities": [],
|
| 151 |
"roads": [
|
| 152 |
{
|
| 153 |
"id": 1,
|
| 154 |
-
"from":
|
| 155 |
-
"to":
|
| 156 |
-
"player": 1
|
| 157 |
-
},
|
| 158 |
-
{
|
| 159 |
-
"id": 2,
|
| 160 |
-
"from": 14,
|
| 161 |
-
"to": 13,
|
| 162 |
-
"player": 2
|
| 163 |
-
},
|
| 164 |
-
{
|
| 165 |
-
"id": 3,
|
| 166 |
-
"from": 31,
|
| 167 |
-
"to": 30,
|
| 168 |
-
"player": 3
|
| 169 |
-
},
|
| 170 |
-
{
|
| 171 |
-
"id": 4,
|
| 172 |
-
"from": 44,
|
| 173 |
-
"to": 43,
|
| 174 |
-
"player": 3
|
| 175 |
-
},
|
| 176 |
-
{
|
| 177 |
-
"id": 5,
|
| 178 |
-
"from": 36,
|
| 179 |
-
"to": 35,
|
| 180 |
-
"player": 2
|
| 181 |
-
},
|
| 182 |
-
{
|
| 183 |
-
"id": 6,
|
| 184 |
-
"from": 40,
|
| 185 |
-
"to": 41,
|
| 186 |
"player": 1
|
| 187 |
}
|
| 188 |
],
|
|
@@ -255,17 +200,13 @@
|
|
| 255 |
{
|
| 256 |
"id": 0,
|
| 257 |
"name": "a",
|
| 258 |
-
"victory_points":
|
| 259 |
-
"total_cards":
|
| 260 |
-
"cards_list": [
|
| 261 |
-
"ore",
|
| 262 |
-
"brick",
|
| 263 |
-
"sheep"
|
| 264 |
-
],
|
| 265 |
"dev_cards_list": [],
|
| 266 |
-
"settlements":
|
| 267 |
"cities": 0,
|
| 268 |
-
"roads":
|
| 269 |
"longest_road": 1,
|
| 270 |
"has_longest_road": false,
|
| 271 |
"knights": 0,
|
|
@@ -275,19 +216,14 @@
|
|
| 275 |
{
|
| 276 |
"id": 1,
|
| 277 |
"name": "b",
|
| 278 |
-
"victory_points":
|
| 279 |
-
"total_cards":
|
| 280 |
-
"cards_list": [
|
| 281 |
-
"sheep",
|
| 282 |
-
"sheep",
|
| 283 |
-
"ore",
|
| 284 |
-
"wheat"
|
| 285 |
-
],
|
| 286 |
"dev_cards_list": [],
|
| 287 |
-
"settlements":
|
| 288 |
"cities": 0,
|
| 289 |
-
"roads":
|
| 290 |
-
"longest_road":
|
| 291 |
"has_longest_road": false,
|
| 292 |
"knights": 0,
|
| 293 |
"knights_played": 0,
|
|
@@ -296,18 +232,14 @@
|
|
| 296 |
{
|
| 297 |
"id": 2,
|
| 298 |
"name": "c",
|
| 299 |
-
"victory_points":
|
| 300 |
-
"total_cards":
|
| 301 |
-
"cards_list": [
|
| 302 |
-
"ore",
|
| 303 |
-
"wheat",
|
| 304 |
-
"wheat"
|
| 305 |
-
],
|
| 306 |
"dev_cards_list": [],
|
| 307 |
-
"settlements":
|
| 308 |
"cities": 0,
|
| 309 |
-
"roads":
|
| 310 |
-
"longest_road":
|
| 311 |
"has_longest_road": false,
|
| 312 |
"knights": 0,
|
| 313 |
"knights_played": 0,
|
|
@@ -315,7 +247,7 @@
|
|
| 315 |
}
|
| 316 |
],
|
| 317 |
"current_player": 1,
|
| 318 |
-
"current_phase": "
|
| 319 |
"robber_position": [
|
| 320 |
2,
|
| 321 |
2
|
|
|
|
| 117 |
],
|
| 118 |
"settlements": [
|
| 119 |
{
|
| 120 |
+
"id": "b_20",
|
| 121 |
+
"vertex": 20,
|
| 122 |
"player": 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
],
|
| 125 |
"cities": [],
|
| 126 |
"roads": [
|
| 127 |
{
|
| 128 |
"id": 1,
|
| 129 |
+
"from": 20,
|
| 130 |
+
"to": 10,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
"player": 1
|
| 132 |
}
|
| 133 |
],
|
|
|
|
| 200 |
{
|
| 201 |
"id": 0,
|
| 202 |
"name": "a",
|
| 203 |
+
"victory_points": 1,
|
| 204 |
+
"total_cards": 0,
|
| 205 |
+
"cards_list": [],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
"dev_cards_list": [],
|
| 207 |
+
"settlements": 1,
|
| 208 |
"cities": 0,
|
| 209 |
+
"roads": 1,
|
| 210 |
"longest_road": 1,
|
| 211 |
"has_longest_road": false,
|
| 212 |
"knights": 0,
|
|
|
|
| 216 |
{
|
| 217 |
"id": 1,
|
| 218 |
"name": "b",
|
| 219 |
+
"victory_points": 0,
|
| 220 |
+
"total_cards": 0,
|
| 221 |
+
"cards_list": [],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
"dev_cards_list": [],
|
| 223 |
+
"settlements": 0,
|
| 224 |
"cities": 0,
|
| 225 |
+
"roads": 0,
|
| 226 |
+
"longest_road": 0,
|
| 227 |
"has_longest_road": false,
|
| 228 |
"knights": 0,
|
| 229 |
"knights_played": 0,
|
|
|
|
| 232 |
{
|
| 233 |
"id": 2,
|
| 234 |
"name": "c",
|
| 235 |
+
"victory_points": 0,
|
| 236 |
+
"total_cards": 0,
|
| 237 |
+
"cards_list": [],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
"dev_cards_list": [],
|
| 239 |
+
"settlements": 0,
|
| 240 |
"cities": 0,
|
| 241 |
+
"roads": 0,
|
| 242 |
+
"longest_road": 0,
|
| 243 |
"has_longest_road": false,
|
| 244 |
"knights": 0,
|
| 245 |
"knights_played": 0,
|
|
|
|
| 247 |
}
|
| 248 |
],
|
| 249 |
"current_player": 1,
|
| 250 |
+
"current_phase": "SETUP_FIRST_ROUND",
|
| 251 |
"robber_position": [
|
| 252 |
2,
|
| 253 |
2
|
examples/ai_testing/sample_states/captured_game_optimized.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"meta":{"curr":"b","phase":"SETUP_FIRST_ROUND","robber":10,"dice":null},
|
| 3 |
+
"H":["","W12","S5","W4","S8","B6","W3","Wh8","B10","W11","D","O3","S4","B10","Wh9","Wh6","S11","O5","Wh9","O2"],
|
| 4 |
+
"N":[null,[[2,9],[1]],[[1,3],[1],"Wh2"],[[2,4,11],[2,1],"Wh2"],[[3,5],[2]],[[4,6,13],[3,2]],[[5,7],[3],"B2"],[[6,15],[3],"B2"],[[9,18],[4],"?3"],[[8,10,1],[4,1],"?3"],[[9,11,20],[5,4,1]],[[10,12,3],[5,2,1]],[[11,13,22],[6,5,2]],[[12,14,5],[6,3,2]],[[13,15,24],[7,6,3]],[[14,16,7],[7,3]],[[15,26],[7],"O2"],[[18,28],[8],"S2"],[[17,19,8],[8,4]],[[18,20,30],[9,8,4]],[[19,21,10],[9,5,4]],[[20,22,32],[10,9,5]],[[21,23,12],[10,6,5]],[[22,24,34],[11,10,6]],[[23,25,14],[11,7,6]],[[24,26,36],[12,11,7]],[[25,27,16],[12,7],"O2"],[[26,38],[12]],[[29,17],[8],"S2"],[[28,30,39],[13,8]],[[29,31,19],[13,9,8]],[[30,32,41],[14,13,9]],[[31,33,21],[14,10,9]],[[32,34,43],[15,14,10]],[[33,35,23],[15,11,10]],[[34,36,45],[16,15,11]],[[35,37,25],[16,12,11]],[[36,38,47],[16,12],"?3"],[[37,27],[12],"?3"],[[40,29],[13]],[[39,41,48],[17,13],"W2"],[[40,42,31],[17,14,13]],[[41,43,50],[18,17,14]],[[42,44,33],[18,15,14]],[[43,45,52],[19,18,15]],[[44,46,35],[19,16,15]],[[45,47,54],[19,16]],[[46,37],[16]],[[49,40],[17],"W2"],[[48,50],[17]],[[49,51,42],[18,17],"?3"],[[50,52],[18],"?3"],[[51,53,44],[19,18]],[[52,54],[19],"?3"],[[53,46],[19],"?3"]],
|
| 5 |
+
"state":{"bld":[[20,"a","S"]],"rds":[[[20,21],"a"]]},
|
| 6 |
+
"players":{"a":{"vp":1,"res":{}},"b":{"vp":0,"res":{}},"c":{"vp":0,"res":{}}}
|
| 7 |
+
}
|
examples/ai_testing/sample_states/captured_game_optimized.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
1. LOOKUP TABLES:
|
| 2 |
+
• "H" (Hexes): Array where Index = HexID. Value = Resource+Num.
|
| 3 |
+
Example: H[1]="W12" -> Hex 1 is Wood 12.
|
| 4 |
+
• "N" (Nodes): Array where Index = NodeID.
|
| 5 |
+
Format: [ [Neighbors], [HexIDs], Port? ]
|
| 6 |
+
Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].
|
| 7 |
+
|
| 8 |
+
2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.
|
| 9 |
+
?3=Any 3:1 port, X2=Specific Resource 2:1 port.
|
| 10 |
+
|
| 11 |
+
3. STATE: "bld"=[NodeID, Owner, Type], "rds"=[[From,To], Owner].
|
| 12 |
+
|
| 13 |
+
4. PLAYERS: "res"={Resource:Count}, "dev"={"h":[Hidden Cards], "r":[Revealed] (K=Knight)},
|
| 14 |
+
"stat"=["LR" (Longest Road), "LA" (Largest Army)].
|
| 15 |
+
|
| 16 |
+
5. ROBBER: Located at HexID specified in "meta.robber". H[id] is blocked.
|
| 17 |
+
|
| 18 |
+
JSON:
|
| 19 |
+
{
|
| 20 |
+
"meta":{"curr":"b","phase":"SETUP_FIRST_ROUND","robber":10,"dice":null},
|
| 21 |
+
"H":["","W12","S5","W4","S8","B6","W3","Wh8","B10","W11","D","O3","S4","B10","Wh9","Wh6","S11","O5","Wh9","O2"],
|
| 22 |
+
"N":[null,[[2,9],[1]],[[1,3],[1],"Wh2"],[[2,4,11],[2,1],"Wh2"],[[3,5],[2]],[[4,6,13],[3,2]],[[5,7],[3],"B2"],[[6,15],[3],"B2"],[[9,18],[4],"?3"],[[8,10,1],[4,1],"?3"],[[9,11,20],[5,4,1]],[[10,12,3],[5,2,1]],[[11,13,22],[6,5,2]],[[12,14,5],[6,3,2]],[[13,15,24],[7,6,3]],[[14,16,7],[7,3]],[[15,26],[7],"O2"],[[18,28],[8],"S2"],[[17,19,8],[8,4]],[[18,20,30],[9,8,4]],[[19,21,10],[9,5,4]],[[20,22,32],[10,9,5]],[[21,23,12],[10,6,5]],[[22,24,34],[11,10,6]],[[23,25,14],[11,7,6]],[[24,26,36],[12,11,7]],[[25,27,16],[12,7],"O2"],[[26,38],[12]],[[29,17],[8],"S2"],[[28,30,39],[13,8]],[[29,31,19],[13,9,8]],[[30,32,41],[14,13,9]],[[31,33,21],[14,10,9]],[[32,34,43],[15,14,10]],[[33,35,23],[15,11,10]],[[34,36,45],[16,15,11]],[[35,37,25],[16,12,11]],[[36,38,47],[16,12],"?3"],[[37,27],[12],"?3"],[[40,29],[13]],[[39,41,48],[17,13],"W2"],[[40,42,31],[17,14,13]],[[41,43,50],[18,17,14]],[[42,44,33],[18,15,14]],[[43,45,52],[19,18,15]],[[44,46,35],[19,16,15]],[[45,47,54],[19,16]],[[46,37],[16]],[[49,40],[17],"W2"],[[48,50],[17]],[[49,51,42],[18,17],"?3"],[[50,52],[18],"?3"],[[51,53,44],[19,18]],[[52,54],[19],"?3"],[[53,46],[19],"?3"]],
|
| 23 |
+
"state":{"bld":[[20,"a","S"]],"rds":[[[20,10],"a"]]},
|
| 24 |
+
"players":{"a":{"vp":1,"res":{}},"b":{"vp":0,"res":{}},"c":{"vp":0,"res":{}}}
|
| 25 |
+
}
|
examples/ai_testing/test_optimized_prompts.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test Script for Optimized Prompt System
|
| 3 |
+
-----------------------------------------
|
| 4 |
+
Verifies that the updated prompt system works with the new optimized state format.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# Add parent directory to path
|
| 12 |
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 13 |
+
|
| 14 |
+
from pycatan.ai.state_filter import StateFilter, PlayerPerspective
|
| 15 |
+
from pycatan.ai.prompt_manager import PromptManager
|
| 16 |
+
|
| 17 |
+
def load_optimized_state():
|
| 18 |
+
"""Load the optimized state from file."""
|
| 19 |
+
state_file = Path('examples/ai_testing/my_games/current_state_optimized.txt')
|
| 20 |
+
|
| 21 |
+
if not state_file.exists():
|
| 22 |
+
# Try sample states folder
|
| 23 |
+
state_file = Path('examples/ai_testing/sample_states/captured_game_optimized.txt')
|
| 24 |
+
|
| 25 |
+
if not state_file.exists():
|
| 26 |
+
print("❌ Optimized state file not found!")
|
| 27 |
+
print(f" Expected: {state_file}")
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
with open(state_file, 'r', encoding='utf-8') as f:
|
| 31 |
+
content = f.read()
|
| 32 |
+
|
| 33 |
+
# Find "JSON:" marker and extract JSON after it
|
| 34 |
+
json_marker = content.find('JSON:')
|
| 35 |
+
if json_marker != -1:
|
| 36 |
+
json_start = content.find('{', json_marker)
|
| 37 |
+
else:
|
| 38 |
+
# No marker, just find first {
|
| 39 |
+
json_start = content.find('{')
|
| 40 |
+
|
| 41 |
+
if json_start == -1:
|
| 42 |
+
print("❌ Could not find JSON in file!")
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
json_str = content[json_start:].strip()
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
return json.loads(json_str)
|
| 49 |
+
except json.JSONDecodeError as e:
|
| 50 |
+
print(f"❌ JSON parsing error: {e}")
|
| 51 |
+
print(f" First 200 chars: {json_str[:200]}")
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_state_filter():
|
| 56 |
+
"""Test the state filter with optimized format."""
|
| 57 |
+
print("\n" + "="*80)
|
| 58 |
+
print("TEST 1: State Filter with Optimized Format")
|
| 59 |
+
print("="*80)
|
| 60 |
+
|
| 61 |
+
# Load optimized state
|
| 62 |
+
raw_state = load_optimized_state()
|
| 63 |
+
if not raw_state:
|
| 64 |
+
return False
|
| 65 |
+
|
| 66 |
+
print("\n✓ Loaded optimized state")
|
| 67 |
+
print(f" Players: {list(raw_state.get('players', {}).keys())}")
|
| 68 |
+
print(f" Phase: {raw_state.get('meta', {}).get('phase')}")
|
| 69 |
+
|
| 70 |
+
# Create filter for player 'a'
|
| 71 |
+
perspective = PlayerPerspective(
|
| 72 |
+
player_num=0,
|
| 73 |
+
player_name="a",
|
| 74 |
+
player_color="Blue"
|
| 75 |
+
)
|
| 76 |
+
state_filter = StateFilter(perspective)
|
| 77 |
+
|
| 78 |
+
print("\n✓ Created state filter for player 'a'")
|
| 79 |
+
|
| 80 |
+
# Filter the state
|
| 81 |
+
try:
|
| 82 |
+
filtered = state_filter.filter_game_state(raw_state)
|
| 83 |
+
print("\n✓ Successfully filtered state!")
|
| 84 |
+
|
| 85 |
+
# Show what we got
|
| 86 |
+
print(f"\nFiltered sections:")
|
| 87 |
+
for key in filtered.keys():
|
| 88 |
+
print(f" - {key}")
|
| 89 |
+
|
| 90 |
+
# Show my info
|
| 91 |
+
my_info = filtered.get("my_private_info", {})
|
| 92 |
+
print(f"\nMy private info:")
|
| 93 |
+
print(f" Victory Points: {my_info.get('victory_points')}")
|
| 94 |
+
print(f" Resources: {my_info.get('resources')}")
|
| 95 |
+
print(f" Has Longest Road: {my_info.get('has_longest_road')}")
|
| 96 |
+
|
| 97 |
+
# Show other players
|
| 98 |
+
others = filtered.get("other_players", [])
|
| 99 |
+
print(f"\nOther players: {len(others)}")
|
| 100 |
+
for player in others:
|
| 101 |
+
print(f" - {player.get('name')}: {player.get('victory_points')} VP, {player.get('resource_count')} cards")
|
| 102 |
+
|
| 103 |
+
return True
|
| 104 |
+
|
| 105 |
+
except Exception as e:
|
| 106 |
+
print(f"\n❌ Error filtering state: {e}")
|
| 107 |
+
import traceback
|
| 108 |
+
traceback.print_exc()
|
| 109 |
+
return False
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def test_prompt_generation():
|
| 113 |
+
"""Test full prompt generation."""
|
| 114 |
+
print("\n" + "="*80)
|
| 115 |
+
print("TEST 2: Full Prompt Generation")
|
| 116 |
+
print("="*80)
|
| 117 |
+
|
| 118 |
+
# Load optimized state
|
| 119 |
+
raw_state = load_optimized_state()
|
| 120 |
+
if not raw_state:
|
| 121 |
+
return False
|
| 122 |
+
|
| 123 |
+
# Create prompt manager
|
| 124 |
+
prompt_manager = PromptManager()
|
| 125 |
+
|
| 126 |
+
print("\n✓ Created prompt manager")
|
| 127 |
+
|
| 128 |
+
# Generate prompt
|
| 129 |
+
try:
|
| 130 |
+
prompt = prompt_manager.create_prompt(
|
| 131 |
+
player_num=0,
|
| 132 |
+
player_name="a",
|
| 133 |
+
player_color="Blue",
|
| 134 |
+
game_state=raw_state,
|
| 135 |
+
what_happened="Game started. It's your turn to place your first settlement.",
|
| 136 |
+
available_actions=[
|
| 137 |
+
{
|
| 138 |
+
"action": "place_settlement",
|
| 139 |
+
"description": "Place your starting settlement",
|
| 140 |
+
"example_parameters": {"node_id": 20}
|
| 141 |
+
}
|
| 142 |
+
],
|
| 143 |
+
custom_instructions="You are a strategic Catan player."
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
print("\n✓ Successfully generated prompt!")
|
| 147 |
+
|
| 148 |
+
# Show prompt structure
|
| 149 |
+
print(f"\nPrompt sections:")
|
| 150 |
+
for key in prompt.keys():
|
| 151 |
+
print(f" - {key}")
|
| 152 |
+
|
| 153 |
+
# Show legend
|
| 154 |
+
if "game_state_legend" in prompt:
|
| 155 |
+
print("\n✓ Legend included in prompt")
|
| 156 |
+
legend = prompt["game_state_legend"]
|
| 157 |
+
print(f" Legend length: {len(legend)} characters")
|
| 158 |
+
|
| 159 |
+
# Show game state keys
|
| 160 |
+
game_state = prompt.get("game_state", {})
|
| 161 |
+
print(f"\nGame state sections:")
|
| 162 |
+
for key in game_state.keys():
|
| 163 |
+
print(f" - {key}")
|
| 164 |
+
|
| 165 |
+
# Check board state
|
| 166 |
+
board = game_state.get("board_state", {})
|
| 167 |
+
print(f"\nBoard state:")
|
| 168 |
+
print(f" H array length: {len(board.get('H', []))}")
|
| 169 |
+
print(f" N array length: {len(board.get('N', []))}")
|
| 170 |
+
print(f" Buildings: {len(board.get('buildings', []))}")
|
| 171 |
+
print(f" Roads: {len(board.get('roads', []))}")
|
| 172 |
+
|
| 173 |
+
# Save prompt for inspection
|
| 174 |
+
output_file = Path('examples/ai_testing/my_games/test_prompt_output.json')
|
| 175 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
| 176 |
+
json.dump(prompt, f, indent=2, ensure_ascii=False)
|
| 177 |
+
|
| 178 |
+
print(f"\n✓ Saved full prompt to: {output_file}")
|
| 179 |
+
|
| 180 |
+
return True
|
| 181 |
+
|
| 182 |
+
except Exception as e:
|
| 183 |
+
print(f"\n❌ Error generating prompt: {e}")
|
| 184 |
+
import traceback
|
| 185 |
+
traceback.print_exc()
|
| 186 |
+
return False
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def main():
|
| 190 |
+
"""Run all tests."""
|
| 191 |
+
print("\n" + "="*80)
|
| 192 |
+
print("TESTING OPTIMIZED PROMPT SYSTEM")
|
| 193 |
+
print("="*80)
|
| 194 |
+
|
| 195 |
+
# Run tests
|
| 196 |
+
test1_passed = test_state_filter()
|
| 197 |
+
test2_passed = test_prompt_generation()
|
| 198 |
+
|
| 199 |
+
# Summary
|
| 200 |
+
print("\n" + "="*80)
|
| 201 |
+
print("TEST SUMMARY")
|
| 202 |
+
print("="*80)
|
| 203 |
+
print(f"State Filter: {'✓ PASS' if test1_passed else '❌ FAIL'}")
|
| 204 |
+
print(f"Prompt Generation: {'✓ PASS' if test2_passed else '❌ FAIL'}")
|
| 205 |
+
|
| 206 |
+
if test1_passed and test2_passed:
|
| 207 |
+
print("\n🎉 ALL TESTS PASSED! The optimized prompt system is working!")
|
| 208 |
+
else:
|
| 209 |
+
print("\n⚠️ Some tests failed. Check the output above for details.")
|
| 210 |
+
|
| 211 |
+
print("="*80 + "\n")
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
if __name__ == '__main__':
|
| 215 |
+
main()
|
examples/test_ai_config.py
CHANGED
|
@@ -48,9 +48,9 @@ def test_save_and_load():
|
|
| 48 |
# Create custom config
|
| 49 |
config = AIConfig()
|
| 50 |
config.agent_name = "Test Agent"
|
| 51 |
-
config.agent.
|
| 52 |
-
config.agent.risk_tolerance = 0.8
|
| 53 |
config.llm.temperature = 0.9
|
|
|
|
| 54 |
|
| 55 |
# Save to file
|
| 56 |
test_file = "test_config.yaml"
|
|
@@ -63,9 +63,9 @@ def test_save_and_load():
|
|
| 63 |
|
| 64 |
# Verify values
|
| 65 |
assert loaded_config.agent_name == "Test Agent", "Agent name mismatch"
|
| 66 |
-
assert loaded_config.agent.
|
| 67 |
-
assert loaded_config.agent.risk_tolerance == 0.8, "Risk tolerance mismatch"
|
| 68 |
assert loaded_config.llm.temperature == 0.9, "Temperature mismatch"
|
|
|
|
| 69 |
|
| 70 |
print("✓ All values match correctly")
|
| 71 |
|
|
@@ -77,50 +77,27 @@ def test_save_and_load():
|
|
| 77 |
|
| 78 |
|
| 79 |
def test_personalities():
|
| 80 |
-
"""Test creating configs with different
|
| 81 |
print("\n" + "="*80)
|
| 82 |
-
print("TEST 3: Different
|
| 83 |
print("="*80)
|
| 84 |
|
| 85 |
-
|
| 86 |
-
"aggressive":
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
"focus_on_settlements": 0.8
|
| 91 |
-
},
|
| 92 |
-
"defensive": {
|
| 93 |
-
"personality": "defensive",
|
| 94 |
-
"risk_tolerance": 0.3,
|
| 95 |
-
"trade_willingness": 0.3,
|
| 96 |
-
"focus_on_cities": 0.9
|
| 97 |
-
},
|
| 98 |
-
"balanced": {
|
| 99 |
-
"personality": "balanced",
|
| 100 |
-
"risk_tolerance": 0.5,
|
| 101 |
-
"trade_willingness": 0.5
|
| 102 |
-
},
|
| 103 |
-
"trading": {
|
| 104 |
-
"personality": "trading",
|
| 105 |
-
"risk_tolerance": 0.6,
|
| 106 |
-
"trade_willingness": 0.9,
|
| 107 |
-
"chat_frequency": 0.7
|
| 108 |
-
}
|
| 109 |
}
|
| 110 |
|
| 111 |
-
for name,
|
| 112 |
config = AIConfig()
|
| 113 |
config.agent_name = f"{name.capitalize()} Agent"
|
| 114 |
-
|
| 115 |
-
# Apply settings
|
| 116 |
-
for key, value in settings.items():
|
| 117 |
-
setattr(config.agent, key, value)
|
| 118 |
|
| 119 |
# Validate
|
| 120 |
config.validate()
|
| 121 |
-
print(f"✓ Created and validated '{name}'
|
| 122 |
-
print(f" -
|
| 123 |
-
print(f" - Trade willingness: {config.agent.trade_willingness}")
|
| 124 |
|
| 125 |
return True
|
| 126 |
|
|
@@ -142,16 +119,16 @@ def test_validation():
|
|
| 142 |
except ValueError as e:
|
| 143 |
print(f"✓ Correctly caught invalid temperature: {e}")
|
| 144 |
|
| 145 |
-
# Test invalid
|
| 146 |
config = AIConfig()
|
| 147 |
-
config.
|
| 148 |
|
| 149 |
try:
|
| 150 |
config.validate()
|
| 151 |
-
print("✗ Should have caught invalid
|
| 152 |
return False
|
| 153 |
except ValueError as e:
|
| 154 |
-
print(f"✓ Correctly caught invalid
|
| 155 |
|
| 156 |
# Test valid config
|
| 157 |
config = AIConfig()
|
|
@@ -170,8 +147,9 @@ def test_to_dict_and_back():
|
|
| 170 |
# Create config
|
| 171 |
config = AIConfig()
|
| 172 |
config.agent_name = "Dict Test Agent"
|
| 173 |
-
config.agent.
|
| 174 |
config.llm.temperature = 0.8
|
|
|
|
| 175 |
|
| 176 |
# Convert to dict
|
| 177 |
config_dict = config.to_dict()
|
|
@@ -183,8 +161,9 @@ def test_to_dict_and_back():
|
|
| 183 |
|
| 184 |
# Verify values
|
| 185 |
assert new_config.agent_name == config.agent_name
|
| 186 |
-
assert new_config.agent.
|
| 187 |
assert new_config.llm.temperature == config.llm.temperature
|
|
|
|
| 188 |
print("✓ All values preserved correctly")
|
| 189 |
|
| 190 |
return True
|
|
@@ -199,7 +178,7 @@ def main():
|
|
| 199 |
tests = [
|
| 200 |
("Default Configuration", test_default_config),
|
| 201 |
("Save and Load", test_save_and_load),
|
| 202 |
-
("Different
|
| 203 |
("Validation", test_validation),
|
| 204 |
("Dictionary Conversion", test_to_dict_and_back)
|
| 205 |
]
|
|
|
|
| 48 |
# Create custom config
|
| 49 |
config = AIConfig()
|
| 50 |
config.agent_name = "Test Agent"
|
| 51 |
+
config.agent.custom_instructions = "Focus on settlements"
|
|
|
|
| 52 |
config.llm.temperature = 0.9
|
| 53 |
+
config.memory.short_term_turns = 3
|
| 54 |
|
| 55 |
# Save to file
|
| 56 |
test_file = "test_config.yaml"
|
|
|
|
| 63 |
|
| 64 |
# Verify values
|
| 65 |
assert loaded_config.agent_name == "Test Agent", "Agent name mismatch"
|
| 66 |
+
assert loaded_config.agent.custom_instructions == "Focus on settlements", "Custom instructions mismatch"
|
|
|
|
| 67 |
assert loaded_config.llm.temperature == 0.9, "Temperature mismatch"
|
| 68 |
+
assert loaded_config.memory.short_term_turns == 3, "Memory setting mismatch"
|
| 69 |
|
| 70 |
print("✓ All values match correctly")
|
| 71 |
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
def test_personalities():
|
| 80 |
+
"""Test creating configs with different custom instructions."""
|
| 81 |
print("\n" + "="*80)
|
| 82 |
+
print("TEST 3: Different Agent Configurations")
|
| 83 |
print("="*80)
|
| 84 |
|
| 85 |
+
agents = {
|
| 86 |
+
"aggressive": "Play aggressively and take risks. Expand quickly.",
|
| 87 |
+
"defensive": "Play defensively and focus on building cities.",
|
| 88 |
+
"balanced": "Play a balanced strategy.",
|
| 89 |
+
"trading": "Focus on trading and negotiations."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
+
for name, instructions in agents.items():
|
| 93 |
config = AIConfig()
|
| 94 |
config.agent_name = f"{name.capitalize()} Agent"
|
| 95 |
+
config.agent.custom_instructions = instructions
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
# Validate
|
| 98 |
config.validate()
|
| 99 |
+
print(f"✓ Created and validated '{name}' agent")
|
| 100 |
+
print(f" - Instructions: {instructions[:50]}...")
|
|
|
|
| 101 |
|
| 102 |
return True
|
| 103 |
|
|
|
|
| 119 |
except ValueError as e:
|
| 120 |
print(f"✓ Correctly caught invalid temperature: {e}")
|
| 121 |
|
| 122 |
+
# Test invalid max_tokens
|
| 123 |
config = AIConfig()
|
| 124 |
+
config.llm.max_tokens = 50 # Invalid (< 100)
|
| 125 |
|
| 126 |
try:
|
| 127 |
config.validate()
|
| 128 |
+
print("✗ Should have caught invalid max_tokens")
|
| 129 |
return False
|
| 130 |
except ValueError as e:
|
| 131 |
+
print(f"✓ Correctly caught invalid max_tokens: {e}")
|
| 132 |
|
| 133 |
# Test valid config
|
| 134 |
config = AIConfig()
|
|
|
|
| 147 |
# Create config
|
| 148 |
config = AIConfig()
|
| 149 |
config.agent_name = "Dict Test Agent"
|
| 150 |
+
config.agent.custom_instructions = "Test instructions"
|
| 151 |
config.llm.temperature = 0.8
|
| 152 |
+
config.memory.short_term_turns = 7
|
| 153 |
|
| 154 |
# Convert to dict
|
| 155 |
config_dict = config.to_dict()
|
|
|
|
| 161 |
|
| 162 |
# Verify values
|
| 163 |
assert new_config.agent_name == config.agent_name
|
| 164 |
+
assert new_config.agent.custom_instructions == config.agent.custom_instructions
|
| 165 |
assert new_config.llm.temperature == config.llm.temperature
|
| 166 |
+
assert new_config.memory.short_term_turns == config.memory.short_term_turns
|
| 167 |
print("✓ All values preserved correctly")
|
| 168 |
|
| 169 |
return True
|
|
|
|
| 178 |
tests = [
|
| 179 |
("Default Configuration", test_default_config),
|
| 180 |
("Save and Load", test_save_and_load),
|
| 181 |
+
("Different Agent Configurations", test_personalities),
|
| 182 |
("Validation", test_validation),
|
| 183 |
("Dictionary Conversion", test_to_dict_and_back)
|
| 184 |
]
|
examples/test_prompt_manager.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test Prompt Management System
|
| 3 |
+
|
| 4 |
+
This script tests the prompt generation pipeline:
|
| 5 |
+
1. Load a sample game state
|
| 6 |
+
2. Create a prompt manager
|
| 7 |
+
3. Generate prompts for different scenarios
|
| 8 |
+
4. Verify the output structure
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import sys
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from pprint import pprint
|
| 15 |
+
|
| 16 |
+
# Add project root to path
|
| 17 |
+
project_root = Path(__file__).parent.parent
|
| 18 |
+
sys.path.insert(0, str(project_root))
|
| 19 |
+
|
| 20 |
+
from pycatan.ai.config import AIConfig
|
| 21 |
+
from pycatan.ai.prompt_manager import PromptManager
|
| 22 |
+
from pycatan.ai.prompt_templates import ActionTemplates
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def load_sample_game_state():
|
| 26 |
+
"""Load the sample captured game state."""
|
| 27 |
+
sample_path = project_root / "examples" / "ai_testing" / "sample_states" / "captured_game.json"
|
| 28 |
+
|
| 29 |
+
if not sample_path.exists():
|
| 30 |
+
print(f"✗ Sample game state not found: {sample_path}")
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
with open(sample_path, 'r') as f:
|
| 34 |
+
return json.load(f)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def test_basic_prompt_generation():
|
| 38 |
+
"""Test 1: Generate a basic prompt."""
|
| 39 |
+
print("\n" + "="*80)
|
| 40 |
+
print("TEST 1: Basic Prompt Generation")
|
| 41 |
+
print("="*80)
|
| 42 |
+
|
| 43 |
+
# Create prompt manager
|
| 44 |
+
config = AIConfig()
|
| 45 |
+
manager = PromptManager(config)
|
| 46 |
+
|
| 47 |
+
# Load game state
|
| 48 |
+
game_state = load_sample_game_state()
|
| 49 |
+
if not game_state:
|
| 50 |
+
return False
|
| 51 |
+
|
| 52 |
+
print(f"✓ Loaded game state with {len(game_state.get('hexes', []))} hexes")
|
| 53 |
+
|
| 54 |
+
# Create a simple prompt
|
| 55 |
+
prompt = manager.create_prompt(
|
| 56 |
+
player_num=1,
|
| 57 |
+
player_name="Test Agent",
|
| 58 |
+
player_color="Blue",
|
| 59 |
+
game_state=game_state,
|
| 60 |
+
what_happened="The game has started. Place your starting settlement.",
|
| 61 |
+
available_actions=ActionTemplates.get_actions_for_phase("setup")
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
print("\n✓ Generated prompt with sections:")
|
| 65 |
+
for section in prompt.keys():
|
| 66 |
+
print(f" - {section}")
|
| 67 |
+
|
| 68 |
+
# Verify structure
|
| 69 |
+
assert "meta_data" in prompt, "Missing meta_data section"
|
| 70 |
+
assert "task_context" in prompt, "Missing task_context section"
|
| 71 |
+
assert "game_state" in prompt, "Missing game_state section"
|
| 72 |
+
assert "constraints" in prompt, "Missing constraints section"
|
| 73 |
+
|
| 74 |
+
print("\n✓ Prompt structure is valid")
|
| 75 |
+
|
| 76 |
+
return True
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def test_filtered_game_state():
|
| 80 |
+
"""Test 2: Verify game state filtering."""
|
| 81 |
+
print("\n" + "="*80)
|
| 82 |
+
print("TEST 2: Game State Filtering")
|
| 83 |
+
print("="*80)
|
| 84 |
+
|
| 85 |
+
config = AIConfig()
|
| 86 |
+
manager = PromptManager(config)
|
| 87 |
+
|
| 88 |
+
game_state = load_sample_game_state()
|
| 89 |
+
if not game_state:
|
| 90 |
+
return False
|
| 91 |
+
|
| 92 |
+
# Generate prompt
|
| 93 |
+
prompt = manager.create_prompt(
|
| 94 |
+
player_num=1,
|
| 95 |
+
player_name="Test Agent",
|
| 96 |
+
player_color="Blue",
|
| 97 |
+
game_state=game_state,
|
| 98 |
+
what_happened="Your turn to play."
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Check filtered state structure
|
| 102 |
+
filtered_state = prompt["game_state"]
|
| 103 |
+
|
| 104 |
+
print("\n✓ Filtered game state contains:")
|
| 105 |
+
for key in filtered_state.keys():
|
| 106 |
+
print(f" - {key}")
|
| 107 |
+
|
| 108 |
+
# Verify key sections exist
|
| 109 |
+
assert "my_private_info" in filtered_state, "Missing my_private_info"
|
| 110 |
+
assert "board_state" in filtered_state, "Missing board_state"
|
| 111 |
+
assert "other_players" in filtered_state, "Missing other_players"
|
| 112 |
+
assert "strategic_context" in filtered_state, "Missing strategic_context"
|
| 113 |
+
|
| 114 |
+
print("\n✓ My private info:")
|
| 115 |
+
pprint(filtered_state["my_private_info"], width=100, compact=True)
|
| 116 |
+
|
| 117 |
+
print("\n✓ Strategic context:")
|
| 118 |
+
pprint(filtered_state["strategic_context"], width=100, compact=True)
|
| 119 |
+
|
| 120 |
+
return True
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def test_action_filtering():
|
| 124 |
+
"""Test 3: Filter actions by resources."""
|
| 125 |
+
print("\n" + "="*80)
|
| 126 |
+
print("TEST 3: Action Filtering by Resources")
|
| 127 |
+
print("="*80)
|
| 128 |
+
|
| 129 |
+
manager = PromptManager()
|
| 130 |
+
|
| 131 |
+
# Get all actions
|
| 132 |
+
all_actions = ActionTemplates.get_all_actions()
|
| 133 |
+
print(f"\n✓ Total available actions: {len(all_actions)}")
|
| 134 |
+
|
| 135 |
+
# Test with different resource scenarios
|
| 136 |
+
scenarios = [
|
| 137 |
+
{
|
| 138 |
+
"name": "Poor (no resources)",
|
| 139 |
+
"resources": {"wood": 0, "brick": 0, "sheep": 0, "wheat": 0, "ore": 0}
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"name": "Can build road",
|
| 143 |
+
"resources": {"wood": 1, "brick": 1, "sheep": 0, "wheat": 0, "ore": 0}
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"name": "Can build settlement",
|
| 147 |
+
"resources": {"wood": 1, "brick": 1, "sheep": 1, "wheat": 1, "ore": 0}
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"name": "Rich (can do anything)",
|
| 151 |
+
"resources": {"wood": 5, "brick": 5, "sheep": 5, "wheat": 5, "ore": 5}
|
| 152 |
+
}
|
| 153 |
+
]
|
| 154 |
+
|
| 155 |
+
for scenario in scenarios:
|
| 156 |
+
affordable = manager.filter_actions_by_resources(all_actions, scenario["resources"])
|
| 157 |
+
print(f"\n{scenario['name']}: {len(affordable)} affordable actions")
|
| 158 |
+
for action in affordable:
|
| 159 |
+
if action["type"] in ["BUILD_ROAD", "BUILD_SETTLEMENT", "BUILD_CITY", "BUY_DEV_CARD"]:
|
| 160 |
+
print(f" - {action['type']}")
|
| 161 |
+
|
| 162 |
+
return True
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def test_chat_context():
|
| 166 |
+
"""Test 4: Include chat history in prompt."""
|
| 167 |
+
print("\n" + "="*80)
|
| 168 |
+
print("TEST 4: Chat History Integration")
|
| 169 |
+
print("="*80)
|
| 170 |
+
|
| 171 |
+
manager = PromptManager()
|
| 172 |
+
game_state = load_sample_game_state()
|
| 173 |
+
|
| 174 |
+
if not game_state:
|
| 175 |
+
return False
|
| 176 |
+
|
| 177 |
+
# Add chat history
|
| 178 |
+
chat_history = [
|
| 179 |
+
{"sender": "Red", "content": "I need wood desperately!"},
|
| 180 |
+
{"sender": "Green", "content": "I'll trade you wood for ore."},
|
| 181 |
+
{"sender": "Red", "content": "Deal! I'll give you 2 ore for 2 wood."}
|
| 182 |
+
]
|
| 183 |
+
|
| 184 |
+
prompt = manager.create_prompt(
|
| 185 |
+
player_num=1,
|
| 186 |
+
player_name="Test Agent",
|
| 187 |
+
player_color="Blue",
|
| 188 |
+
game_state=game_state,
|
| 189 |
+
what_happened="Red and Green are negotiating a trade.",
|
| 190 |
+
chat_history=chat_history
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
# Check social context
|
| 194 |
+
assert "social_context" in prompt, "Missing social_context"
|
| 195 |
+
|
| 196 |
+
print("\n✓ Social context included:")
|
| 197 |
+
pprint(prompt["social_context"], width=100)
|
| 198 |
+
|
| 199 |
+
return True
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def test_custom_instructions():
|
| 203 |
+
"""Test 5: Custom instructions per agent."""
|
| 204 |
+
print("\n" + "="*80)
|
| 205 |
+
print("TEST 5: Custom Agent Instructions")
|
| 206 |
+
print("="*80)
|
| 207 |
+
|
| 208 |
+
config = AIConfig()
|
| 209 |
+
config.agent.custom_instructions = "You are an aggressive trader. Always seek to maximize trades."
|
| 210 |
+
|
| 211 |
+
manager = PromptManager(config)
|
| 212 |
+
game_state = load_sample_game_state()
|
| 213 |
+
|
| 214 |
+
if not game_state:
|
| 215 |
+
return False
|
| 216 |
+
|
| 217 |
+
prompt = manager.create_prompt(
|
| 218 |
+
player_num=1,
|
| 219 |
+
player_name="Aggressive Trader",
|
| 220 |
+
player_color="Blue",
|
| 221 |
+
game_state=game_state,
|
| 222 |
+
what_happened="Your turn."
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
print("\n✓ Meta data with custom instructions:")
|
| 226 |
+
pprint(prompt["meta_data"], width=100)
|
| 227 |
+
|
| 228 |
+
assert "aggressive trader" in prompt["meta_data"]["role"].lower()
|
| 229 |
+
print("\n✓ Custom instructions applied successfully")
|
| 230 |
+
|
| 231 |
+
return True
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def test_prompt_json_serialization():
|
| 235 |
+
"""Test 6: Ensure prompt can be serialized to JSON."""
|
| 236 |
+
print("\n" + "="*80)
|
| 237 |
+
print("TEST 6: JSON Serialization")
|
| 238 |
+
print("="*80)
|
| 239 |
+
|
| 240 |
+
manager = PromptManager()
|
| 241 |
+
game_state = load_sample_game_state()
|
| 242 |
+
|
| 243 |
+
if not game_state:
|
| 244 |
+
return False
|
| 245 |
+
|
| 246 |
+
prompt = manager.create_prompt(
|
| 247 |
+
player_num=1,
|
| 248 |
+
player_name="Test Agent",
|
| 249 |
+
player_color="Blue",
|
| 250 |
+
game_state=game_state,
|
| 251 |
+
what_happened="Test event.",
|
| 252 |
+
available_actions=ActionTemplates.get_all_actions()
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
try:
|
| 256 |
+
# Try to serialize to JSON
|
| 257 |
+
json_str = json.dumps(prompt, indent=2)
|
| 258 |
+
print(f"\n✓ Prompt serialized successfully ({len(json_str)} characters)")
|
| 259 |
+
|
| 260 |
+
# Try to deserialize
|
| 261 |
+
reconstructed = json.loads(json_str)
|
| 262 |
+
print("✓ Prompt deserialized successfully")
|
| 263 |
+
|
| 264 |
+
# Save to file for inspection
|
| 265 |
+
output_path = project_root / "logs" / "sample_prompt.json"
|
| 266 |
+
output_path.parent.mkdir(exist_ok=True)
|
| 267 |
+
|
| 268 |
+
with open(output_path, 'w') as f:
|
| 269 |
+
f.write(json_str)
|
| 270 |
+
|
| 271 |
+
print(f"✓ Saved sample prompt to: {output_path}")
|
| 272 |
+
|
| 273 |
+
except Exception as e:
|
| 274 |
+
print(f"✗ Serialization failed: {e}")
|
| 275 |
+
return False
|
| 276 |
+
|
| 277 |
+
return True
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def main():
|
| 281 |
+
"""Run all tests."""
|
| 282 |
+
print("\n" + "="*80)
|
| 283 |
+
print("🧪 PROMPT MANAGEMENT SYSTEM - TEST SUITE")
|
| 284 |
+
print("="*80)
|
| 285 |
+
|
| 286 |
+
tests = [
|
| 287 |
+
("Basic Prompt Generation", test_basic_prompt_generation),
|
| 288 |
+
("Game State Filtering", test_filtered_game_state),
|
| 289 |
+
("Action Filtering", test_action_filtering),
|
| 290 |
+
("Chat History Integration", test_chat_context),
|
| 291 |
+
("Custom Instructions", test_custom_instructions),
|
| 292 |
+
("JSON Serialization", test_prompt_json_serialization)
|
| 293 |
+
]
|
| 294 |
+
|
| 295 |
+
results = []
|
| 296 |
+
for name, test_func in tests:
|
| 297 |
+
try:
|
| 298 |
+
result = test_func()
|
| 299 |
+
results.append((name, result))
|
| 300 |
+
except Exception as e:
|
| 301 |
+
print(f"\n✗ Test '{name}' failed with exception: {e}")
|
| 302 |
+
import traceback
|
| 303 |
+
traceback.print_exc()
|
| 304 |
+
results.append((name, False))
|
| 305 |
+
|
| 306 |
+
# Summary
|
| 307 |
+
print("\n" + "="*80)
|
| 308 |
+
print("TEST SUMMARY")
|
| 309 |
+
print("="*80)
|
| 310 |
+
|
| 311 |
+
passed = sum(1 for _, result in results if result)
|
| 312 |
+
total = len(results)
|
| 313 |
+
|
| 314 |
+
for name, result in results:
|
| 315 |
+
status = "✓ PASS" if result else "✗ FAIL"
|
| 316 |
+
print(f"{status}: {name}")
|
| 317 |
+
|
| 318 |
+
print(f"\nTotal: {passed}/{total} tests passed")
|
| 319 |
+
|
| 320 |
+
if passed == total:
|
| 321 |
+
print("\n🎉 All tests passed!")
|
| 322 |
+
print("\n📋 Next Steps:")
|
| 323 |
+
print(" - Check logs/sample_prompt.json to see generated prompt")
|
| 324 |
+
print(" - Ready to proceed to Response Parser (Phase 1.3)")
|
| 325 |
+
return 0
|
| 326 |
+
else:
|
| 327 |
+
print(f"\n❌ {total - passed} test(s) failed")
|
| 328 |
+
return 1
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
if __name__ == "__main__":
|
| 332 |
+
sys.exit(main())
|
pycatan/ai/__init__.py
CHANGED
|
@@ -7,9 +7,11 @@ that can play Settlers of Catan autonomously.
|
|
| 7 |
Components:
|
| 8 |
- config: Configuration management for AI agents
|
| 9 |
- prompt_manager: Prompt construction and game state filtering
|
| 10 |
-
-
|
| 11 |
-
-
|
| 12 |
-
-
|
|
|
|
|
|
|
| 13 |
|
| 14 |
Architecture Overview:
|
| 15 |
┌─────────────────────────────────────────────────────────┐
|
|
@@ -21,14 +23,17 @@ Architecture Overview:
|
|
| 21 |
│ │ Config │ │ Prompt │ │ Response │ │
|
| 22 |
│ │ Management │ │ Manager │ │ Parser │ │
|
| 23 |
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
|
|
| 24 |
│ │
|
| 25 |
│ ┌──────────────┐ ┌──────────────┐ │
|
| 26 |
│ │ Memory │ │ LLM Client │ │
|
| 27 |
│ │ System │ │ (Multi-API) │ │
|
| 28 |
│ └──────────────┘ └──────────────┘ │
|
| 29 |
-
│
|
| 30 |
└─────────────────────────────────────────────────────────┘
|
|
|
|
|
|
|
| 31 |
"""
|
| 32 |
|
| 33 |
__version__ = "0.1.0"
|
| 34 |
-
__all__ = ["config"]
|
|
|
|
| 7 |
Components:
|
| 8 |
- config: Configuration management for AI agents
|
| 9 |
- prompt_manager: Prompt construction and game state filtering
|
| 10 |
+
- state_filter: Game state filtering and perspective transformation
|
| 11 |
+
- prompt_templates: Prompt structure and action templates
|
| 12 |
+
- response_parser: LLM response parsing and validation (TODO)
|
| 13 |
+
- memory: Agent memory and learning systems (TODO)
|
| 14 |
+
- llm_client: LLM API abstraction and client (TODO)
|
| 15 |
|
| 16 |
Architecture Overview:
|
| 17 |
┌─────────────────────────────────────────────────────────┐
|
|
|
|
| 23 |
│ │ Config │ │ Prompt │ │ Response │ │
|
| 24 |
│ │ Management │ │ Manager │ │ Parser │ │
|
| 25 |
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
| 26 |
+
│ ✅ ✅ 🚧 │
|
| 27 |
│ │
|
| 28 |
│ ┌──────────────┐ ┌──────────────┐ │
|
| 29 |
│ │ Memory │ │ LLM Client │ │
|
| 30 |
│ │ System │ │ (Multi-API) │ │
|
| 31 |
│ └──────────────┘ └──────────────┘ │
|
| 32 |
+
│ 🚧 🚧 │
|
| 33 |
└─────────────────────────────────────────────────────────┘
|
| 34 |
+
|
| 35 |
+
Status: ✅ Complete | 🚧 In Development | ❌ Not Started
|
| 36 |
"""
|
| 37 |
|
| 38 |
__version__ = "0.1.0"
|
| 39 |
+
__all__ = ["config", "prompt_manager", "state_filter", "prompt_templates"]
|
pycatan/ai/config.py
CHANGED
|
@@ -63,30 +63,9 @@ class LLMConfig:
|
|
| 63 |
|
| 64 |
@dataclass
|
| 65 |
class AgentConfig:
|
| 66 |
-
"""
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
personality: str = "balanced" # "aggressive", "defensive", "balanced", "trading"
|
| 70 |
-
risk_tolerance: float = 0.5 # 0.0 (conservative) to 1.0 (risky)
|
| 71 |
-
|
| 72 |
-
# Strategic preferences
|
| 73 |
-
focus_on_settlements: float = 0.6 # Relative priority
|
| 74 |
-
focus_on_cities: float = 0.7
|
| 75 |
-
focus_on_roads: float = 0.5
|
| 76 |
-
focus_on_dev_cards: float = 0.6
|
| 77 |
-
|
| 78 |
-
# Trading behavior
|
| 79 |
-
trade_willingness: float = 0.5 # 0.0 (never trades) to 1.0 (trades often)
|
| 80 |
-
trade_fairness: float = 0.7 # How fair are trade offers
|
| 81 |
-
|
| 82 |
-
# Social behavior
|
| 83 |
-
chat_frequency: float = 0.3 # How often to send chat messages
|
| 84 |
-
use_emojis: bool = True
|
| 85 |
-
chattiness: str = "medium" # "quiet", "medium", "chatty"
|
| 86 |
-
|
| 87 |
-
# Custom instructions
|
| 88 |
-
custom_instructions: Optional[str] = None # Additional instructions for this agent
|
| 89 |
-
|
| 90 |
|
| 91 |
@dataclass
|
| 92 |
class MemoryConfig:
|
|
@@ -291,10 +270,6 @@ class AIConfig:
|
|
| 291 |
if self.llm.max_tokens < 100:
|
| 292 |
raise ValueError(f"max_tokens must be at least 100, got {self.llm.max_tokens}")
|
| 293 |
|
| 294 |
-
# Validate risk tolerance
|
| 295 |
-
if not 0.0 <= self.agent.risk_tolerance <= 1.0:
|
| 296 |
-
raise ValueError(f"risk_tolerance must be between 0.0 and 1.0, got {self.agent.risk_tolerance}")
|
| 297 |
-
|
| 298 |
# Validate timeouts
|
| 299 |
if self.llm.timeout_seconds < 1:
|
| 300 |
raise ValueError(f"timeout_seconds must be at least 1, got {self.llm.timeout_seconds}")
|
|
@@ -318,7 +293,6 @@ class AIConfig:
|
|
| 318 |
f" agent_name='{self.agent_name}',\n"
|
| 319 |
f" provider='{self.llm.provider}',\n"
|
| 320 |
f" model='{self.llm.model_name}',\n"
|
| 321 |
-
f" personality='{self.agent.personality}',\n"
|
| 322 |
f" debug={self.debug.debug_mode}\n"
|
| 323 |
f")"
|
| 324 |
)
|
|
|
|
| 63 |
|
| 64 |
@dataclass
|
| 65 |
class AgentConfig:
|
| 66 |
+
"""Agent configuration - reserved for future use."""
|
| 67 |
+
# Custom instructions for the agent (optional)
|
| 68 |
+
custom_instructions: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
@dataclass
|
| 71 |
class MemoryConfig:
|
|
|
|
| 270 |
if self.llm.max_tokens < 100:
|
| 271 |
raise ValueError(f"max_tokens must be at least 100, got {self.llm.max_tokens}")
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
# Validate timeouts
|
| 274 |
if self.llm.timeout_seconds < 1:
|
| 275 |
raise ValueError(f"timeout_seconds must be at least 1, got {self.llm.timeout_seconds}")
|
|
|
|
| 293 |
f" agent_name='{self.agent_name}',\n"
|
| 294 |
f" provider='{self.llm.provider}',\n"
|
| 295 |
f" model='{self.llm.model_name}',\n"
|
|
|
|
| 296 |
f" debug={self.debug.debug_mode}\n"
|
| 297 |
f")"
|
| 298 |
)
|
pycatan/ai/prompt_manager.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Prompt Management Layer
|
| 3 |
+
|
| 4 |
+
This module orchestrates the entire prompt processing pipeline:
|
| 5 |
+
1. Receives raw game state from GameManager
|
| 6 |
+
2. Filters state for specific agent's perspective
|
| 7 |
+
3. Builds structured prompt with all context
|
| 8 |
+
4. Returns prompt ready for LLM
|
| 9 |
+
|
| 10 |
+
This is the main interface between the game and the AI agents.
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
from pycatan.ai.prompt_manager import PromptManager
|
| 14 |
+
|
| 15 |
+
manager = PromptManager(config)
|
| 16 |
+
prompt = manager.create_prompt(
|
| 17 |
+
player_num=1,
|
| 18 |
+
game_state=raw_state,
|
| 19 |
+
what_happened="Player Red rolled a 6",
|
| 20 |
+
available_actions=actions
|
| 21 |
+
)
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from typing import Dict, Any, List, Optional
|
| 25 |
+
from pycatan.ai.config import AIConfig
|
| 26 |
+
from pycatan.ai.state_filter import StateFilter, PlayerPerspective
|
| 27 |
+
from pycatan.ai.prompt_templates import PromptBuilder, ActionTemplates
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class PromptManager:
|
| 31 |
+
"""
|
| 32 |
+
Main prompt management orchestrator.
|
| 33 |
+
|
| 34 |
+
Coordinates filtering, template application, and prompt generation
|
| 35 |
+
for AI agents. Ensures each agent gets appropriate context in the
|
| 36 |
+
right format.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
def __init__(self, config: Optional[AIConfig] = None):
|
| 40 |
+
"""
|
| 41 |
+
Initialize prompt manager.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
config: AI configuration (uses default if None)
|
| 45 |
+
"""
|
| 46 |
+
self.config = config or AIConfig()
|
| 47 |
+
self.prompt_builder = PromptBuilder()
|
| 48 |
+
|
| 49 |
+
# Cache filters for each player to avoid recreation
|
| 50 |
+
self._filter_cache: Dict[int, StateFilter] = {}
|
| 51 |
+
|
| 52 |
+
def create_prompt(
|
| 53 |
+
self,
|
| 54 |
+
player_num: int,
|
| 55 |
+
player_name: str,
|
| 56 |
+
player_color: str,
|
| 57 |
+
game_state: Dict[str, Any],
|
| 58 |
+
what_happened: str,
|
| 59 |
+
available_actions: Optional[List[Dict[str, Any]]] = None,
|
| 60 |
+
chat_history: Optional[List[Dict[str, str]]] = None,
|
| 61 |
+
agent_memory: Optional[Dict[str, Any]] = None,
|
| 62 |
+
custom_instructions: Optional[str] = None
|
| 63 |
+
) -> Dict[str, Any]:
|
| 64 |
+
"""
|
| 65 |
+
Create a complete prompt for an AI agent.
|
| 66 |
+
|
| 67 |
+
This is the main entry point for prompt creation.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
player_num: Player number (1-4)
|
| 71 |
+
player_name: Player's name
|
| 72 |
+
player_color: Player's color
|
| 73 |
+
game_state: Raw game state from game engine
|
| 74 |
+
what_happened: Description of what just occurred
|
| 75 |
+
available_actions: Actions agent can take (optional)
|
| 76 |
+
chat_history: Recent chat messages (optional)
|
| 77 |
+
agent_memory: Agent's memory/notes (optional)
|
| 78 |
+
custom_instructions: Custom instructions for this agent
|
| 79 |
+
|
| 80 |
+
Returns:
|
| 81 |
+
Complete structured prompt ready for LLM
|
| 82 |
+
"""
|
| 83 |
+
# Get or create state filter for this player
|
| 84 |
+
state_filter = self._get_filter(player_num, player_name, player_color)
|
| 85 |
+
|
| 86 |
+
# Filter game state for this agent's perspective
|
| 87 |
+
filtered_state = state_filter.filter_game_state(game_state)
|
| 88 |
+
|
| 89 |
+
# Build meta data section
|
| 90 |
+
meta_data = {
|
| 91 |
+
"agent_name": player_name,
|
| 92 |
+
"my_color": player_color,
|
| 93 |
+
"role": custom_instructions or self.config.agent.custom_instructions
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
# Build task context section
|
| 97 |
+
task_context = {
|
| 98 |
+
"what_just_happened": what_happened,
|
| 99 |
+
"instructions": self._get_instructions(available_actions)
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
# Build social context section
|
| 103 |
+
social_context = None
|
| 104 |
+
if chat_history:
|
| 105 |
+
social_context = {
|
| 106 |
+
"recent_chat": chat_history[-self.config.memory.chat_history_size:]
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
# Build memory section
|
| 110 |
+
memory = agent_memory if agent_memory else None
|
| 111 |
+
|
| 112 |
+
# Build constraints section (available actions)
|
| 113 |
+
constraints = None
|
| 114 |
+
if available_actions:
|
| 115 |
+
constraints = {
|
| 116 |
+
"usage_instructions": (
|
| 117 |
+
"Choose one action type from the list below. "
|
| 118 |
+
"Populate the 'parameters' field in your response strictly "
|
| 119 |
+
"according to the 'example_parameters' structure provided."
|
| 120 |
+
),
|
| 121 |
+
"allowed_actions": available_actions
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
# Build complete prompt
|
| 125 |
+
prompt = self.prompt_builder.build_prompt(
|
| 126 |
+
meta_data=meta_data,
|
| 127 |
+
task_context=task_context,
|
| 128 |
+
game_state=filtered_state,
|
| 129 |
+
social_context=social_context,
|
| 130 |
+
memory=memory,
|
| 131 |
+
constraints=constraints,
|
| 132 |
+
custom_instructions=custom_instructions
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
return prompt
|
| 136 |
+
|
| 137 |
+
def create_action_prompt(
|
| 138 |
+
self,
|
| 139 |
+
player_num: int,
|
| 140 |
+
player_name: str,
|
| 141 |
+
player_color: str,
|
| 142 |
+
game_state: Dict[str, Any],
|
| 143 |
+
action_type: str,
|
| 144 |
+
context: Optional[str] = None
|
| 145 |
+
) -> Dict[str, Any]:
|
| 146 |
+
"""
|
| 147 |
+
Create a prompt for a specific action type.
|
| 148 |
+
|
| 149 |
+
This is used when GameManager asks "What settlement do you want to build?"
|
| 150 |
+
rather than "What do you want to do?"
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
player_num: Player number
|
| 154 |
+
player_name: Player name
|
| 155 |
+
player_color: Player color
|
| 156 |
+
game_state: Current game state
|
| 157 |
+
action_type: Specific action being requested (e.g., "BUILD_SETTLEMENT")
|
| 158 |
+
context: Additional context about this action
|
| 159 |
+
|
| 160 |
+
Returns:
|
| 161 |
+
Structured prompt for this specific action
|
| 162 |
+
"""
|
| 163 |
+
# Get state filter
|
| 164 |
+
state_filter = self._get_filter(player_num, player_name, player_color)
|
| 165 |
+
filtered_state = state_filter.filter_game_state(game_state)
|
| 166 |
+
|
| 167 |
+
# Find the specific action template
|
| 168 |
+
all_actions = ActionTemplates.get_all_actions()
|
| 169 |
+
action_template = next(
|
| 170 |
+
(a for a in all_actions if a["type"] == action_type),
|
| 171 |
+
None
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
# Build task context
|
| 175 |
+
what_happened = context or f"You need to make a decision: {action_type}"
|
| 176 |
+
task_context = {
|
| 177 |
+
"what_just_happened": what_happened,
|
| 178 |
+
"instructions": f"Decide on the best {action_type} action based on the current game state."
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
# Build meta data
|
| 182 |
+
meta_data = {
|
| 183 |
+
"agent_name": player_name,
|
| 184 |
+
"my_color": player_color,
|
| 185 |
+
"role": self.config.agent.custom_instructions or "You are a Catan player."
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
# Constraints with just this action
|
| 189 |
+
constraints = None
|
| 190 |
+
if action_template:
|
| 191 |
+
constraints = {
|
| 192 |
+
"usage_instructions": f"Perform the {action_type} action.",
|
| 193 |
+
"allowed_actions": [action_template]
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
# Build prompt
|
| 197 |
+
return self.prompt_builder.build_prompt(
|
| 198 |
+
meta_data=meta_data,
|
| 199 |
+
task_context=task_context,
|
| 200 |
+
game_state=filtered_state,
|
| 201 |
+
constraints=constraints
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
def filter_actions_by_resources(
|
| 205 |
+
self,
|
| 206 |
+
actions: List[Dict[str, Any]],
|
| 207 |
+
player_resources: Dict[str, int]
|
| 208 |
+
) -> List[Dict[str, Any]]:
|
| 209 |
+
"""
|
| 210 |
+
Filter available actions based on player's resources.
|
| 211 |
+
|
| 212 |
+
Args:
|
| 213 |
+
actions: List of all possible actions
|
| 214 |
+
player_resources: Player's current resources
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
Actions the player can actually afford
|
| 218 |
+
"""
|
| 219 |
+
return ActionTemplates.filter_by_resources(actions, player_resources)
|
| 220 |
+
|
| 221 |
+
def get_actions_for_phase(self, phase: str) -> List[Dict[str, Any]]:
|
| 222 |
+
"""
|
| 223 |
+
Get available actions for a specific game phase.
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
phase: Game phase name
|
| 227 |
+
|
| 228 |
+
Returns:
|
| 229 |
+
Actions available in this phase
|
| 230 |
+
"""
|
| 231 |
+
return ActionTemplates.get_actions_for_phase(phase)
|
| 232 |
+
|
| 233 |
+
def _get_filter(
|
| 234 |
+
self,
|
| 235 |
+
player_num: int,
|
| 236 |
+
player_name: str,
|
| 237 |
+
player_color: str
|
| 238 |
+
) -> StateFilter:
|
| 239 |
+
"""
|
| 240 |
+
Get or create state filter for a player.
|
| 241 |
+
|
| 242 |
+
Uses caching to avoid recreating filters.
|
| 243 |
+
|
| 244 |
+
Args:
|
| 245 |
+
player_num: Player number
|
| 246 |
+
player_name: Player name
|
| 247 |
+
player_color: Player color
|
| 248 |
+
|
| 249 |
+
Returns:
|
| 250 |
+
StateFilter for this player
|
| 251 |
+
"""
|
| 252 |
+
if player_num not in self._filter_cache:
|
| 253 |
+
perspective = PlayerPerspective(
|
| 254 |
+
player_num=player_num,
|
| 255 |
+
player_name=player_name,
|
| 256 |
+
player_color=player_color
|
| 257 |
+
)
|
| 258 |
+
self._filter_cache[player_num] = StateFilter(perspective)
|
| 259 |
+
|
| 260 |
+
return self._filter_cache[player_num]
|
| 261 |
+
|
| 262 |
+
def _get_instructions(self, available_actions: Optional[List[Dict]]) -> str:
|
| 263 |
+
"""
|
| 264 |
+
Generate instructions based on available actions.
|
| 265 |
+
|
| 266 |
+
Args:
|
| 267 |
+
available_actions: Actions available to agent
|
| 268 |
+
|
| 269 |
+
Returns:
|
| 270 |
+
Instruction text
|
| 271 |
+
"""
|
| 272 |
+
base_instructions = (
|
| 273 |
+
"Analyze the game state and select the optimal move from 'allowed_actions'. "
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
if available_actions:
|
| 277 |
+
num_actions = len(available_actions)
|
| 278 |
+
if num_actions == 1:
|
| 279 |
+
return base_instructions + "Only one action is currently available."
|
| 280 |
+
else:
|
| 281 |
+
return base_instructions + f"You have {num_actions} possible actions to choose from."
|
| 282 |
+
|
| 283 |
+
return base_instructions + "Consider all strategic implications before deciding."
|
| 284 |
+
|
| 285 |
+
def clear_cache(self):
|
| 286 |
+
"""Clear the filter cache. Useful when starting a new game."""
|
| 287 |
+
self._filter_cache.clear()
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
# Convenience function
|
| 291 |
+
def create_prompt_manager(config: Optional[AIConfig] = None) -> PromptManager:
|
| 292 |
+
"""
|
| 293 |
+
Create a prompt manager with optional configuration.
|
| 294 |
+
|
| 295 |
+
Args:
|
| 296 |
+
config: AI configuration (uses default if None)
|
| 297 |
+
|
| 298 |
+
Returns:
|
| 299 |
+
Configured PromptManager instance
|
| 300 |
+
"""
|
| 301 |
+
return PromptManager(config)
|
pycatan/ai/prompt_templates.py
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Prompt Templates for AI Agents
|
| 3 |
+
|
| 4 |
+
This module defines the structure and templates for prompts sent to LLM agents.
|
| 5 |
+
Based on the format defined in promt_format.text, prompts consist of:
|
| 6 |
+
|
| 7 |
+
1. Meta Data - Agent identity and role
|
| 8 |
+
2. Task Context - Current situation and instructions
|
| 9 |
+
3. Game State - World information from agent's perspective
|
| 10 |
+
4. Social Context - Chat messages and relationships
|
| 11 |
+
5. Memory - Agent's notes and observations
|
| 12 |
+
6. Constraints - Available actions and usage rules
|
| 13 |
+
|
| 14 |
+
The templates are flexible and can be customized per agent.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from typing import Dict, Any, List, Optional
|
| 18 |
+
from dataclasses import dataclass, field
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class PromptTemplate:
|
| 23 |
+
"""
|
| 24 |
+
Base template structure for agent prompts.
|
| 25 |
+
|
| 26 |
+
Each section can be customized and sections can be optionally included.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
# Section templates
|
| 30 |
+
meta_data_template: str = ""
|
| 31 |
+
task_context_template: str = ""
|
| 32 |
+
game_state_template: str = ""
|
| 33 |
+
social_context_template: str = ""
|
| 34 |
+
memory_template: str = ""
|
| 35 |
+
constraints_template: str = ""
|
| 36 |
+
|
| 37 |
+
# Which sections to include
|
| 38 |
+
include_meta_data: bool = True
|
| 39 |
+
include_task_context: bool = True
|
| 40 |
+
include_game_state: bool = True
|
| 41 |
+
include_social_context: bool = True
|
| 42 |
+
include_memory: bool = True
|
| 43 |
+
include_constraints: bool = True
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class PromptBuilder:
|
| 47 |
+
"""
|
| 48 |
+
Builds structured prompts for AI agents.
|
| 49 |
+
|
| 50 |
+
Takes filtered game state and context, applies templates,
|
| 51 |
+
and produces a complete prompt ready for LLM.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def __init__(self, template: Optional[PromptTemplate] = None):
|
| 55 |
+
"""
|
| 56 |
+
Initialize prompt builder with a template.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
template: Custom template (uses default if None)
|
| 60 |
+
"""
|
| 61 |
+
self.template = template or self._create_default_template()
|
| 62 |
+
|
| 63 |
+
def _create_default_template(self) -> PromptTemplate:
|
| 64 |
+
"""Create the default prompt template."""
|
| 65 |
+
return PromptTemplate()
|
| 66 |
+
|
| 67 |
+
def build_prompt(
|
| 68 |
+
self,
|
| 69 |
+
meta_data: Dict[str, Any],
|
| 70 |
+
task_context: Dict[str, Any],
|
| 71 |
+
game_state: Dict[str, Any],
|
| 72 |
+
social_context: Optional[Dict[str, Any]] = None,
|
| 73 |
+
memory: Optional[Dict[str, Any]] = None,
|
| 74 |
+
constraints: Optional[Dict[str, Any]] = None,
|
| 75 |
+
custom_instructions: Optional[str] = None
|
| 76 |
+
) -> Dict[str, Any]:
|
| 77 |
+
"""
|
| 78 |
+
Build a complete structured prompt with optimized game state.
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
meta_data: Agent identity and role
|
| 82 |
+
task_context: What just happened and what to do
|
| 83 |
+
game_state: Filtered game state (optimized format)
|
| 84 |
+
social_context: Chat and relationships (optional)
|
| 85 |
+
memory: Agent's notes (optional)
|
| 86 |
+
constraints: Available actions (optional)
|
| 87 |
+
custom_instructions: Additional instructions for this agent
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
Complete structured prompt as dictionary
|
| 91 |
+
"""
|
| 92 |
+
prompt = {}
|
| 93 |
+
|
| 94 |
+
# Build each section
|
| 95 |
+
if self.template.include_meta_data:
|
| 96 |
+
prompt["meta_data"] = self._build_meta_data(meta_data, custom_instructions)
|
| 97 |
+
|
| 98 |
+
if self.template.include_task_context:
|
| 99 |
+
prompt["task_context"] = self._build_task_context(task_context)
|
| 100 |
+
|
| 101 |
+
if self.template.include_game_state:
|
| 102 |
+
# Add legend before game state for optimized format
|
| 103 |
+
prompt["game_state_legend"] = self._build_legend()
|
| 104 |
+
prompt["game_state"] = game_state
|
| 105 |
+
|
| 106 |
+
if self.template.include_social_context and social_context:
|
| 107 |
+
prompt["social_context"] = self._build_social_context(social_context)
|
| 108 |
+
|
| 109 |
+
if self.template.include_memory and memory:
|
| 110 |
+
prompt["memory"] = self._build_memory(memory)
|
| 111 |
+
|
| 112 |
+
if self.template.include_constraints and constraints:
|
| 113 |
+
prompt["constraints"] = self._build_constraints(constraints)
|
| 114 |
+
|
| 115 |
+
return prompt
|
| 116 |
+
|
| 117 |
+
def _build_legend(self) -> str:
|
| 118 |
+
"""
|
| 119 |
+
Build legend for optimized game state format.
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
Legend explaining the compact format
|
| 123 |
+
"""
|
| 124 |
+
return """OPTIMIZED STATE FORMAT GUIDE:
|
| 125 |
+
|
| 126 |
+
1. LOOKUP TABLES:
|
| 127 |
+
• "H" (Hexes): Array where Index = HexID. Value = Resource+Num.
|
| 128 |
+
Example: H[1]="W12" → Hex 1 is Wood with number 12
|
| 129 |
+
• "N" (Nodes): Array where Index = NodeID.
|
| 130 |
+
Format: [[Neighbors], [HexIDs], Port?]
|
| 131 |
+
To find yield: Check N[node_id], get HexIDs, look up in H array
|
| 132 |
+
|
| 133 |
+
2. RESOURCE CODES:
|
| 134 |
+
W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert
|
| 135 |
+
Ports: ?3=Any(3:1), X2=Specific(2:1) where X is resource
|
| 136 |
+
|
| 137 |
+
3. GAME STATE:
|
| 138 |
+
"bld": [NodeID, Owner, Type] where Type: S=Settlement, C=City
|
| 139 |
+
"rds": [[From, To], Owner]
|
| 140 |
+
|
| 141 |
+
4. PLAYERS:
|
| 142 |
+
"res": {ResourceCode: Count}
|
| 143 |
+
"dev": {"h": [HiddenCards], "r": [RevealedCards]}
|
| 144 |
+
"stat": ["LR"=Longest Road, "LA"=Largest Army]
|
| 145 |
+
|
| 146 |
+
5. META:
|
| 147 |
+
"robber": HexID where robber is located (blocks that hex)
|
| 148 |
+
"phase": Current game phase
|
| 149 |
+
"curr": Current player's turn
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
def _build_meta_data(
|
| 153 |
+
self,
|
| 154 |
+
meta_data: Dict[str, Any],
|
| 155 |
+
custom_instructions: Optional[str] = None
|
| 156 |
+
) -> Dict[str, Any]:
|
| 157 |
+
"""
|
| 158 |
+
Build meta data section.
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
meta_data: Basic agent info
|
| 162 |
+
custom_instructions: Custom role/instructions
|
| 163 |
+
|
| 164 |
+
Returns:
|
| 165 |
+
Formatted meta data
|
| 166 |
+
"""
|
| 167 |
+
result = {
|
| 168 |
+
"agent_name": meta_data.get("agent_name", "AI Agent"),
|
| 169 |
+
"my_color": meta_data.get("my_color", "Unknown"),
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
# Add role/instructions
|
| 173 |
+
if custom_instructions:
|
| 174 |
+
result["role"] = custom_instructions
|
| 175 |
+
else:
|
| 176 |
+
result["role"] = meta_data.get("role", "You are a Catan player.")
|
| 177 |
+
|
| 178 |
+
return result
|
| 179 |
+
|
| 180 |
+
def _build_task_context(self, task_context: Dict[str, Any]) -> Dict[str, Any]:
|
| 181 |
+
"""
|
| 182 |
+
Build task context section.
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
task_context: What happened and what to do
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
Formatted task context
|
| 189 |
+
"""
|
| 190 |
+
return {
|
| 191 |
+
"what_just_happened": task_context.get("what_just_happened", ""),
|
| 192 |
+
"instructions": task_context.get(
|
| 193 |
+
"instructions",
|
| 194 |
+
"Analyze the game state and select the optimal move from 'allowed_actions'."
|
| 195 |
+
)
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
def _build_social_context(self, social_context: Dict[str, Any]) -> Dict[str, Any]:
|
| 199 |
+
"""
|
| 200 |
+
Build social context section with chat history.
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
social_context: Chat messages and summaries
|
| 204 |
+
|
| 205 |
+
Returns:
|
| 206 |
+
Formatted social context
|
| 207 |
+
"""
|
| 208 |
+
result = {}
|
| 209 |
+
|
| 210 |
+
# Recent chat messages
|
| 211 |
+
if "recent_chat" in social_context:
|
| 212 |
+
result["recent_chat"] = social_context["recent_chat"]
|
| 213 |
+
|
| 214 |
+
# Chat summaries (if using summarization)
|
| 215 |
+
if "last_summaries" in social_context:
|
| 216 |
+
result["last_summaries"] = social_context["last_summaries"]
|
| 217 |
+
|
| 218 |
+
# Trade offers/negotiations
|
| 219 |
+
if "pending_trades" in social_context:
|
| 220 |
+
result["pending_trades"] = social_context["pending_trades"]
|
| 221 |
+
|
| 222 |
+
return result
|
| 223 |
+
|
| 224 |
+
def _build_memory(self, memory: Dict[str, Any]) -> Dict[str, Any]:
|
| 225 |
+
"""
|
| 226 |
+
Build memory section with agent's notes.
|
| 227 |
+
|
| 228 |
+
Args:
|
| 229 |
+
memory: Agent's observations and plans
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
Formatted memory
|
| 233 |
+
"""
|
| 234 |
+
return {
|
| 235 |
+
"notes_for_myself": memory.get("notes", []),
|
| 236 |
+
"strategic_observations": memory.get("observations", []),
|
| 237 |
+
"player_tracking": memory.get("player_tracking", {})
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
def _build_constraints(self, constraints: Dict[str, Any]) -> Dict[str, Any]:
|
| 241 |
+
"""
|
| 242 |
+
Build constraints section with available actions.
|
| 243 |
+
|
| 244 |
+
Args:
|
| 245 |
+
constraints: Available actions and rules
|
| 246 |
+
|
| 247 |
+
Returns:
|
| 248 |
+
Formatted constraints
|
| 249 |
+
"""
|
| 250 |
+
return {
|
| 251 |
+
"usage_instructions": constraints.get(
|
| 252 |
+
"usage_instructions",
|
| 253 |
+
"Choose one action type from the list below. "
|
| 254 |
+
"Populate the 'parameters' field according to the example structure."
|
| 255 |
+
),
|
| 256 |
+
"allowed_actions": constraints.get("allowed_actions", [])
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
class ActionTemplates:
|
| 261 |
+
"""
|
| 262 |
+
Templates for different game actions.
|
| 263 |
+
|
| 264 |
+
Provides structured definitions of all possible actions
|
| 265 |
+
an agent can take, with examples and parameter structures.
|
| 266 |
+
"""
|
| 267 |
+
|
| 268 |
+
@staticmethod
|
| 269 |
+
def get_all_actions() -> List[Dict[str, Any]]:
|
| 270 |
+
"""
|
| 271 |
+
Get all possible action templates.
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
List of action definitions with examples
|
| 275 |
+
"""
|
| 276 |
+
return [
|
| 277 |
+
{
|
| 278 |
+
"type": "BUILD_ROAD",
|
| 279 |
+
"description": "Build a road on a specific edge ID.",
|
| 280 |
+
"example_parameters": {"edge_id": 12}
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"type": "BUILD_SETTLEMENT",
|
| 284 |
+
"description": "Build a settlement on a specific node ID.",
|
| 285 |
+
"example_parameters": {"node_id": 45}
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"type": "BUILD_CITY",
|
| 289 |
+
"description": "Upgrade an existing settlement to a city.",
|
| 290 |
+
"example_parameters": {"node_id": 45}
|
| 291 |
+
},
|
| 292 |
+
{
|
| 293 |
+
"type": "BUY_DEV_CARD",
|
| 294 |
+
"description": "Purchase a development card.",
|
| 295 |
+
"example_parameters": {}
|
| 296 |
+
},
|
| 297 |
+
{
|
| 298 |
+
"type": "OFFER_TRADE",
|
| 299 |
+
"description": "Propose a trade to all players or a specific one.",
|
| 300 |
+
"example_parameters": {
|
| 301 |
+
"give": {"wood": 1, "sheep": 1},
|
| 302 |
+
"receive": {"ore": 1},
|
| 303 |
+
"target_player_color": "Red" # Optional
|
| 304 |
+
}
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"type": "ACCEPT_TRADE",
|
| 308 |
+
"description": "Accept a trade offer from another player.",
|
| 309 |
+
"example_parameters": {"trade_id": "trade_123"}
|
| 310 |
+
},
|
| 311 |
+
{
|
| 312 |
+
"type": "DECLINE_TRADE",
|
| 313 |
+
"description": "Decline a trade offer.",
|
| 314 |
+
"example_parameters": {"trade_id": "trade_123"}
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
"type": "PLAY_DEV_CARD",
|
| 318 |
+
"description": "Play a development card. Specific params depend on the card.",
|
| 319 |
+
"example_parameters": [
|
| 320 |
+
{"card": "KNIGHT", "robber_hex": 7, "target_victim": "Red"},
|
| 321 |
+
{"card": "MONOPOLY", "resource": "sheep"},
|
| 322 |
+
{"card": "YEAR_OF_PLENTY", "resources": ["wood", "brick"]},
|
| 323 |
+
{"card": "ROAD_BUILDING", "edges": [12, 13]}
|
| 324 |
+
]
|
| 325 |
+
},
|
| 326 |
+
{
|
| 327 |
+
"type": "SEND_CHAT",
|
| 328 |
+
"description": "Send a message to all players.",
|
| 329 |
+
"example_parameters": {"message": "Anyone want to trade sheep for wheat?"}
|
| 330 |
+
},
|
| 331 |
+
{
|
| 332 |
+
"type": "WAIT_FOR_RESPONSE",
|
| 333 |
+
"description": "Do nothing on the board, just wait or chat.",
|
| 334 |
+
"example_parameters": {}
|
| 335 |
+
},
|
| 336 |
+
{
|
| 337 |
+
"type": "END_TURN",
|
| 338 |
+
"description": "Pass the dice to the next player.",
|
| 339 |
+
"example_parameters": {}
|
| 340 |
+
}
|
| 341 |
+
]
|
| 342 |
+
|
| 343 |
+
@staticmethod
|
| 344 |
+
def get_actions_for_phase(phase: str) -> List[Dict[str, Any]]:
|
| 345 |
+
"""
|
| 346 |
+
Get actions available in a specific game phase.
|
| 347 |
+
|
| 348 |
+
Args:
|
| 349 |
+
phase: Game phase (e.g., "setup", "main_turn", "robber")
|
| 350 |
+
|
| 351 |
+
Returns:
|
| 352 |
+
List of actions available in this phase
|
| 353 |
+
"""
|
| 354 |
+
all_actions = ActionTemplates.get_all_actions()
|
| 355 |
+
|
| 356 |
+
if phase == "setup":
|
| 357 |
+
# During setup, only build settlements and roads
|
| 358 |
+
return [a for a in all_actions if a["type"] in ["BUILD_SETTLEMENT", "BUILD_ROAD"]]
|
| 359 |
+
|
| 360 |
+
elif phase == "robber":
|
| 361 |
+
# When 7 is rolled and robber must be moved
|
| 362 |
+
return [a for a in all_actions if a["type"] == "PLAY_DEV_CARD"]
|
| 363 |
+
|
| 364 |
+
elif phase == "main_turn":
|
| 365 |
+
# Normal turn - all actions except setup-specific
|
| 366 |
+
return all_actions
|
| 367 |
+
|
| 368 |
+
else:
|
| 369 |
+
return all_actions
|
| 370 |
+
|
| 371 |
+
@staticmethod
|
| 372 |
+
def filter_by_resources(
|
| 373 |
+
actions: List[Dict[str, Any]],
|
| 374 |
+
available_resources: Dict[str, int]
|
| 375 |
+
) -> List[Dict[str, Any]]:
|
| 376 |
+
"""
|
| 377 |
+
Filter actions based on available resources.
|
| 378 |
+
|
| 379 |
+
Args:
|
| 380 |
+
actions: List of possible actions
|
| 381 |
+
available_resources: Agent's current resources
|
| 382 |
+
|
| 383 |
+
Returns:
|
| 384 |
+
Actions the agent can afford
|
| 385 |
+
"""
|
| 386 |
+
# Resource costs
|
| 387 |
+
costs = {
|
| 388 |
+
"BUILD_ROAD": {"wood": 1, "brick": 1},
|
| 389 |
+
"BUILD_SETTLEMENT": {"wood": 1, "brick": 1, "sheep": 1, "wheat": 1},
|
| 390 |
+
"BUILD_CITY": {"wheat": 2, "ore": 3},
|
| 391 |
+
"BUY_DEV_CARD": {"sheep": 1, "wheat": 1, "ore": 1}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
affordable = []
|
| 395 |
+
for action in actions:
|
| 396 |
+
action_type = action["type"]
|
| 397 |
+
|
| 398 |
+
# Actions that don't require resources
|
| 399 |
+
if action_type not in costs:
|
| 400 |
+
affordable.append(action)
|
| 401 |
+
continue
|
| 402 |
+
|
| 403 |
+
# Check if agent can afford this action
|
| 404 |
+
required = costs[action_type]
|
| 405 |
+
can_afford = all(
|
| 406 |
+
available_resources.get(resource, 0) >= amount
|
| 407 |
+
for resource, amount in required.items()
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
if can_afford:
|
| 411 |
+
affordable.append(action)
|
| 412 |
+
|
| 413 |
+
return affordable
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def create_default_prompt_builder() -> PromptBuilder:
|
| 417 |
+
"""
|
| 418 |
+
Create a prompt builder with default settings.
|
| 419 |
+
|
| 420 |
+
Returns:
|
| 421 |
+
Configured PromptBuilder instance
|
| 422 |
+
"""
|
| 423 |
+
return PromptBuilder()
|
pycatan/ai/state_filter.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Game State Filtering and Perspective Transformation
|
| 3 |
+
|
| 4 |
+
This module handles filtering and transforming raw game state data
|
| 5 |
+
into an agent-specific view. It ensures that:
|
| 6 |
+
1. Agents only see information they should know
|
| 7 |
+
2. Information is presented from the agent's perspective
|
| 8 |
+
3. Complex game state is simplified for LLM consumption
|
| 9 |
+
|
| 10 |
+
Key principles:
|
| 11 |
+
- Hide opponent's private information (cards, exact resources)
|
| 12 |
+
- Transform "Player X did Y" → "You did Y" / "Red did Y"
|
| 13 |
+
- Add helpful computed data (probabilities, scarcity)
|
| 14 |
+
- Keep prompts concise but complete
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from typing import Dict, Any, List, Optional
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class PlayerPerspective:
|
| 23 |
+
"""Represents which player's perspective we're filtering for."""
|
| 24 |
+
player_num: int # Player number (1, 2, 3, 4)
|
| 25 |
+
player_name: str # Player's display name
|
| 26 |
+
player_color: str # Player's color (Blue, Red, Green, Orange)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class StateFilter:
|
| 30 |
+
"""
|
| 31 |
+
Filters and transforms game state for a specific agent's perspective.
|
| 32 |
+
|
| 33 |
+
This class takes raw game state and produces a filtered, agent-centric
|
| 34 |
+
view that:
|
| 35 |
+
- Hides information the agent shouldn't know
|
| 36 |
+
- Presents data from the agent's viewpoint
|
| 37 |
+
- Adds computed context (probabilities, strategic info)
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
def __init__(self, perspective: PlayerPerspective):
|
| 41 |
+
"""
|
| 42 |
+
Initialize state filter for a specific player.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
perspective: Which player's perspective to use
|
| 46 |
+
"""
|
| 47 |
+
self.perspective = perspective
|
| 48 |
+
|
| 49 |
+
def filter_game_state(self, raw_state: Dict[str, Any]) -> Dict[str, Any]:
|
| 50 |
+
"""
|
| 51 |
+
Main filtering method - converts raw game state to agent view.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
raw_state: Complete game state from the game engine
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
Filtered game state from agent's perspective
|
| 58 |
+
"""
|
| 59 |
+
filtered = {}
|
| 60 |
+
|
| 61 |
+
# 1. Extract and format my private information
|
| 62 |
+
filtered["my_private_info"] = self._extract_my_info(raw_state)
|
| 63 |
+
|
| 64 |
+
# 2. Get visible board state
|
| 65 |
+
filtered["board_state"] = self._filter_board_state(raw_state)
|
| 66 |
+
|
| 67 |
+
# 3. Get other players' public information
|
| 68 |
+
filtered["other_players"] = self._filter_other_players(raw_state)
|
| 69 |
+
|
| 70 |
+
# 4. Add computed/helpful context
|
| 71 |
+
filtered["strategic_context"] = self._add_strategic_context(raw_state)
|
| 72 |
+
|
| 73 |
+
return filtered
|
| 74 |
+
|
| 75 |
+
def _extract_my_info(self, raw_state: Dict[str, Any]) -> Dict[str, Any]:
|
| 76 |
+
"""
|
| 77 |
+
Extract this agent's private information.
|
| 78 |
+
Works with optimized state format where players is a dict keyed by name.
|
| 79 |
+
|
| 80 |
+
Returns:
|
| 81 |
+
Dictionary with agent's resources, cards, and points
|
| 82 |
+
"""
|
| 83 |
+
players = raw_state.get("players", {})
|
| 84 |
+
|
| 85 |
+
# In optimized format, players is a dict with player names as keys
|
| 86 |
+
my_player = players.get(self.perspective.player_name)
|
| 87 |
+
|
| 88 |
+
if not my_player:
|
| 89 |
+
return {
|
| 90 |
+
"resources": {},
|
| 91 |
+
"development_cards": {"hidden": [], "revealed": []},
|
| 92 |
+
"victory_points": 0,
|
| 93 |
+
"has_longest_road": False,
|
| 94 |
+
"has_largest_army": False,
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
# Extract dev cards
|
| 98 |
+
dev = my_player.get("dev", {})
|
| 99 |
+
|
| 100 |
+
return {
|
| 101 |
+
"resources": my_player.get("res", {}),
|
| 102 |
+
"development_cards": {
|
| 103 |
+
"hidden": dev.get("h", []),
|
| 104 |
+
"revealed": dev.get("r", [])
|
| 105 |
+
},
|
| 106 |
+
"victory_points": my_player.get("vp", 0),
|
| 107 |
+
"has_longest_road": "LR" in my_player.get("stat", []),
|
| 108 |
+
"has_largest_army": "LA" in my_player.get("stat", []),
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
def _filter_board_state(self, raw_state: Dict[str, Any]) -> Dict[str, Any]:
|
| 112 |
+
"""
|
| 113 |
+
Filter board information using optimized format.
|
| 114 |
+
Returns the compact H/N arrays plus current state.
|
| 115 |
+
"""
|
| 116 |
+
meta = raw_state.get("meta", {})
|
| 117 |
+
state = raw_state.get("state", {})
|
| 118 |
+
|
| 119 |
+
return {
|
| 120 |
+
"H": raw_state.get("H", []), # Hex lookup table
|
| 121 |
+
"N": raw_state.get("N", []), # Node lookup table
|
| 122 |
+
"buildings": self._annotate_buildings(state.get("bld", [])),
|
| 123 |
+
"roads": self._annotate_roads(state.get("rds", [])),
|
| 124 |
+
"robber_hex": meta.get("robber"),
|
| 125 |
+
"current_phase": meta.get("phase"),
|
| 126 |
+
"dice_result": meta.get("dice")
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
def _annotate_buildings(self, buildings: List) -> List[Dict]:
|
| 130 |
+
"""
|
| 131 |
+
Annotate buildings with ownership info (mine vs others).
|
| 132 |
+
Optimized format: [NodeID, Owner, Type]
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
buildings: List of building data [node, owner, type]
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
Annotated building list
|
| 139 |
+
"""
|
| 140 |
+
annotated = []
|
| 141 |
+
for bld in buildings:
|
| 142 |
+
if len(bld) < 3:
|
| 143 |
+
continue
|
| 144 |
+
|
| 145 |
+
node_id, owner, bld_type = bld[0], bld[1], bld[2]
|
| 146 |
+
|
| 147 |
+
annotated.append({
|
| 148 |
+
"node": node_id,
|
| 149 |
+
"owner": "me" if owner == self.perspective.player_name else owner,
|
| 150 |
+
"type": "settlement" if bld_type == "S" else "city"
|
| 151 |
+
})
|
| 152 |
+
|
| 153 |
+
return annotated
|
| 154 |
+
|
| 155 |
+
def _annotate_roads(self, roads: List) -> List[Dict]:
|
| 156 |
+
"""
|
| 157 |
+
Annotate roads with ownership info.
|
| 158 |
+
Optimized format: [[From, To], Owner]
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
roads: List of road data [[from, to], owner]
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
Annotated road list
|
| 165 |
+
"""
|
| 166 |
+
annotated = []
|
| 167 |
+
for road in roads:
|
| 168 |
+
if len(road) < 2:
|
| 169 |
+
continue
|
| 170 |
+
|
| 171 |
+
nodes, owner = road[0], road[1]
|
| 172 |
+
|
| 173 |
+
annotated.append({
|
| 174 |
+
"from": nodes[0],
|
| 175 |
+
"to": nodes[1],
|
| 176 |
+
"owner": "me" if owner == self.perspective.player_name else owner
|
| 177 |
+
})
|
| 178 |
+
|
| 179 |
+
return annotated
|
| 180 |
+
|
| 181 |
+
def _filter_other_players(self, raw_state: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 182 |
+
"""
|
| 183 |
+
Get public information about other players.
|
| 184 |
+
Works with optimized format where players is a dict.
|
| 185 |
+
|
| 186 |
+
This hides:
|
| 187 |
+
- Exact resource counts (only show total)
|
| 188 |
+
- Hidden development cards
|
| 189 |
+
- Other private information
|
| 190 |
+
"""
|
| 191 |
+
players = raw_state.get("players", {})
|
| 192 |
+
other_players = []
|
| 193 |
+
|
| 194 |
+
for player_name, player_data in players.items():
|
| 195 |
+
# Skip myself
|
| 196 |
+
if player_name == self.perspective.player_name:
|
| 197 |
+
continue
|
| 198 |
+
|
| 199 |
+
# Calculate totals from optimized format
|
| 200 |
+
resources = player_data.get("res", {})
|
| 201 |
+
dev_cards = player_data.get("dev", {})
|
| 202 |
+
stats = player_data.get("stat", [])
|
| 203 |
+
|
| 204 |
+
# Public information only
|
| 205 |
+
other_players.append({
|
| 206 |
+
"name": player_name,
|
| 207 |
+
"victory_points": player_data.get("vp", 0),
|
| 208 |
+
"resource_count": sum(resources.values()), # Total only
|
| 209 |
+
"development_card_count": len(dev_cards.get("h", [])) + len(dev_cards.get("r", [])),
|
| 210 |
+
"knights_played": len([c for c in dev_cards.get("r", []) if c == "K"]),
|
| 211 |
+
"has_longest_road": "LR" in stats,
|
| 212 |
+
"has_largest_army": "LA" in stats
|
| 213 |
+
})
|
| 214 |
+
|
| 215 |
+
return other_players
|
| 216 |
+
|
| 217 |
+
def _add_strategic_context(self, raw_state: Dict[str, Any]) -> Dict[str, Any]:
|
| 218 |
+
"""
|
| 219 |
+
Add computed strategic information using optimized format.
|
| 220 |
+
|
| 221 |
+
This includes:
|
| 222 |
+
- Dice roll probabilities for each hex (from H array)
|
| 223 |
+
- Leading player information
|
| 224 |
+
- Current turn info
|
| 225 |
+
"""
|
| 226 |
+
H = raw_state.get("H", [])
|
| 227 |
+
players = raw_state.get("players", {})
|
| 228 |
+
meta = raw_state.get("meta", {})
|
| 229 |
+
|
| 230 |
+
# Dice probabilities
|
| 231 |
+
dice_probs = {
|
| 232 |
+
2: "2.8%", 3: "5.6%", 4: "8.3%", 5: "11.1%", 6: "13.9%",
|
| 233 |
+
8: "13.9%", 9: "11.1%", 10: "8.3%", 11: "5.6%", 12: "2.8%"
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
# Parse hex array for strategic info
|
| 237 |
+
hex_analysis = []
|
| 238 |
+
for hex_id, hex_str in enumerate(H):
|
| 239 |
+
if not hex_str or hex_id == 0:
|
| 240 |
+
continue
|
| 241 |
+
|
| 242 |
+
# Parse format like "W12", "B6", "D"
|
| 243 |
+
if hex_str == "D":
|
| 244 |
+
continue # Skip desert
|
| 245 |
+
|
| 246 |
+
# Extract resource and number
|
| 247 |
+
resource = hex_str[0] if len(hex_str) > 0 else ""
|
| 248 |
+
if len(hex_str) > 1:
|
| 249 |
+
try:
|
| 250 |
+
number = int(hex_str[1:])
|
| 251 |
+
hex_analysis.append({
|
| 252 |
+
"hex_id": hex_id,
|
| 253 |
+
"resource": resource,
|
| 254 |
+
"number": number,
|
| 255 |
+
"probability": dice_probs.get(number, "0%")
|
| 256 |
+
})
|
| 257 |
+
except:
|
| 258 |
+
pass
|
| 259 |
+
|
| 260 |
+
# Find who's winning
|
| 261 |
+
leading_player = None
|
| 262 |
+
max_points = 0
|
| 263 |
+
for name, data in players.items():
|
| 264 |
+
vp = data.get("vp", 0)
|
| 265 |
+
if vp > max_points:
|
| 266 |
+
max_points = vp
|
| 267 |
+
leading_player = name
|
| 268 |
+
|
| 269 |
+
return {
|
| 270 |
+
"hex_analysis": hex_analysis,
|
| 271 |
+
"leading_player": leading_player,
|
| 272 |
+
"current_player": meta.get("curr"),
|
| 273 |
+
"game_phase": meta.get("phase"),
|
| 274 |
+
"robber_location": meta.get("robber")
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
def hide_private_info(self, game_state: Dict[str, Any]) -> Dict[str, Any]:
|
| 278 |
+
"""
|
| 279 |
+
Remove all private information from game state.
|
| 280 |
+
Works with optimized format.
|
| 281 |
+
|
| 282 |
+
Args:
|
| 283 |
+
game_state: Full game state
|
| 284 |
+
|
| 285 |
+
Returns:
|
| 286 |
+
Game state with private info removed
|
| 287 |
+
"""
|
| 288 |
+
filtered = game_state.copy()
|
| 289 |
+
|
| 290 |
+
# Remove opponent development cards and exact resources
|
| 291 |
+
if "players" in filtered:
|
| 292 |
+
players_copy = {}
|
| 293 |
+
for name, data in filtered["players"].items():
|
| 294 |
+
if name != self.perspective.player_name:
|
| 295 |
+
# Hide exact resources - only show count
|
| 296 |
+
res = data.get("res", {})
|
| 297 |
+
total_res = sum(res.values())
|
| 298 |
+
|
| 299 |
+
# Hide hidden dev cards
|
| 300 |
+
dev = data.get("dev", {})
|
| 301 |
+
hidden_count = len(dev.get("h", []))
|
| 302 |
+
revealed = dev.get("r", [])
|
| 303 |
+
|
| 304 |
+
players_copy[name] = {
|
| 305 |
+
"vp": data.get("vp", 0),
|
| 306 |
+
"res": {"total": total_res}, # Only total
|
| 307 |
+
"dev": {"hidden_count": hidden_count, "r": revealed}, # Hide specific cards
|
| 308 |
+
"stat": data.get("stat", [])
|
| 309 |
+
}
|
| 310 |
+
else:
|
| 311 |
+
# Keep my full info
|
| 312 |
+
players_copy[name] = data.copy()
|
| 313 |
+
|
| 314 |
+
filtered["players"] = players_copy
|
| 315 |
+
|
| 316 |
+
return filtered
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def create_filter_for_player(player_num: int, player_name: str, player_color: str) -> StateFilter:
|
| 320 |
+
"""
|
| 321 |
+
Convenience function to create a state filter for a specific player.
|
| 322 |
+
|
| 323 |
+
Args:
|
| 324 |
+
player_num: Player number (1-4)
|
| 325 |
+
player_name: Player's name
|
| 326 |
+
player_color: Player's color
|
| 327 |
+
|
| 328 |
+
Returns:
|
| 329 |
+
Configured StateFilter instance
|
| 330 |
+
"""
|
| 331 |
+
perspective = PlayerPerspective(
|
| 332 |
+
player_num=player_num,
|
| 333 |
+
player_name=player_name,
|
| 334 |
+
player_color=player_color
|
| 335 |
+
)
|
| 336 |
+
return StateFilter(perspective)
|
pycatan/management/game_manager.py
CHANGED
|
@@ -1734,6 +1734,10 @@ class GameManager:
|
|
| 1734 |
player_id = self._current_game_state.current_player
|
| 1735 |
player_name = self.users[player_id].name if hasattr(self.users[player_id], 'name') else f"Player {player_id + 1}"
|
| 1736 |
self.visualization_manager.display_turn_start(player_name, self._current_game_state.turn_number)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1737 |
|
| 1738 |
def _handle_game_end(self) -> None:
|
| 1739 |
"""
|
|
|
|
| 1734 |
player_id = self._current_game_state.current_player
|
| 1735 |
player_name = self.users[player_id].name if hasattr(self.users[player_id], 'name') else f"Player {player_id + 1}"
|
| 1736 |
self.visualization_manager.display_turn_start(player_name, self._current_game_state.turn_number)
|
| 1737 |
+
|
| 1738 |
+
# Display full game state at start of turn (CRITICAL FOR AI!)
|
| 1739 |
+
current_state = self.get_full_state()
|
| 1740 |
+
self.visualization_manager.display_game_state(current_state)
|
| 1741 |
|
| 1742 |
def _handle_game_end(self) -> None:
|
| 1743 |
"""
|
temp_viz_console.py
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
# -*- coding: utf-8 -*-
|
| 3 |
-
import sys
|
| 4 |
-
import time
|
| 5 |
-
import os
|
| 6 |
-
|
| 7 |
-
log_file = r"c:\GIT_new\PyCatan_AI\logs\game_viz.log"
|
| 8 |
-
|
| 9 |
-
print("PyCatan - Game Visualization Console")
|
| 10 |
-
print("=" * 50)
|
| 11 |
-
print("This window shows real-time game state updates.")
|
| 12 |
-
print("Keep this window open while playing!")
|
| 13 |
-
print("=" * 50)
|
| 14 |
-
print(f"Reading from: {log_file}")
|
| 15 |
-
|
| 16 |
-
# Wait for file to exist
|
| 17 |
-
while not os.path.exists(log_file):
|
| 18 |
-
time.sleep(0.1)
|
| 19 |
-
|
| 20 |
-
# Tail the file
|
| 21 |
-
with open(log_file, 'r', encoding='utf-8') as f:
|
| 22 |
-
# Go to the end of file
|
| 23 |
-
# f.seek(0, 2)
|
| 24 |
-
# Actually start from beginning since we just created it
|
| 25 |
-
|
| 26 |
-
while True:
|
| 27 |
-
line = f.readline()
|
| 28 |
-
if line:
|
| 29 |
-
print(line, end='')
|
| 30 |
-
else:
|
| 31 |
-
time.sleep(0.1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|