EZTIME2025 commited on
Commit
21fb2c3
·
1 Parent(s): 9e94e66

update response parser

Browse files
.github/instructions/WORK_PLAN.md CHANGED
@@ -1,8 +1,8 @@
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
 
@@ -90,16 +90,24 @@ Build a fully functional LLM-based AI agent that can play Settlers of Catan auto
90
 
91
  ---
92
 
93
- #### 1.3 Response Parser
94
- - [ ] Define structured response format (JSON schema)
95
- - [ ] Build response parser and validator
96
- - [ ] Implement error handling for malformed responses
97
- - [ ] Create fallback mechanisms for parsing failures
98
- - [ ] Add response logging for debugging
99
 
100
- **Files to create:**
101
- - `pycatan/ai/response_parser.py` - Parse and validate LLM responses
102
- - `pycatan/ai/schemas.py` - JSON schemas for requests/responses
 
 
 
 
 
 
 
 
103
 
104
  ---
105
 
 
1
  # 🗺️ AI Agent Development Work Plan
2
 
3
  **Date:** January 3, 2026
4
+ **Status:** ✅ Phase 1 - Foundation & Infrastructure (100% Complete)
5
+ **Current Task:** Phase 3 - Core AI Agent (3.1) - **NEXT**
6
 
7
  ## 🎯 Project Goal
8
 
 
90
 
91
  ---
92
 
93
+ #### 1.3 Response Parser ✅ **COMPLETED**
94
+ - [x] Define structured response format (JSON schema)
95
+ - [x] Build response parser and validator
96
+ - [x] Implement error handling for malformed responses
97
+ - [x] Create fallback mechanisms for parsing failures
98
+ - [x] Add response logging for debugging
99
 
100
+ **Files created:**
101
+ - `pycatan/ai/response_parser.py` - Parse and validate LLM responses
102
+ - `pycatan/ai/schemas.py` - JSON schemas for requests/responses
103
+
104
+ **Key features:**
105
+ - 🎯 Dual schema support: Active turn (with action) & Observing (no action)
106
+ - 🛡️ Error handling: Invalid JSON, missing fields, type validation
107
+ - 🔧 Fallback mechanisms: JSON repair, structure repair, default values
108
+ - 📊 Parse statistics tracking
109
+ - 🔍 Flexible parsing: Handles markdown code blocks, extra text
110
+ - ✅ Action parameter validation against expected schemas
111
 
112
  ---
113
 
examples/ai_testing/my_games/current_state.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
- "timestamp": "2026-01-03T22:25:16.620950",
3
- "state_number": 2,
4
  "state": {
5
  "hexes": [
6
  {
@@ -118,7 +118,13 @@
118
  "has_robber": false
119
  }
120
  ],
121
- "settlements": [],
 
 
 
 
 
 
122
  "cities": [],
123
  "roads": [],
124
  "harbors": [
@@ -190,11 +196,11 @@
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,
@@ -244,7 +250,7 @@
244
  ],
245
  "dice_result": null,
246
  "allowed_actions": [
247
- "PLACE_STARTING_SETTLEMENT"
248
  ],
249
  "points": [
250
  {
 
1
  {
2
+ "timestamp": "2026-01-03T22:34:02.690022",
3
+ "state_number": 3,
4
  "state": {
5
  "hexes": [
6
  {
 
118
  "has_robber": false
119
  }
120
  ],
121
+ "settlements": [
122
+ {
123
+ "id": "b_20",
124
+ "vertex": 20,
125
+ "player": 1
126
+ }
127
+ ],
128
  "cities": [],
129
  "roads": [],
130
  "harbors": [
 
196
  {
197
  "id": 0,
198
  "name": "a",
199
+ "victory_points": 1,
200
  "total_cards": 0,
201
  "cards_list": [],
202
  "dev_cards_list": [],
203
+ "settlements": 1,
204
  "cities": 0,
205
  "roads": 0,
206
  "longest_road": 0,
 
250
  ],
251
  "dice_result": null,
252
  "allowed_actions": [
253
+ "PLACE_STARTING_ROAD"
254
  ],
255
  "points": [
256
  {
examples/ai_testing/my_games/current_state_optimized.txt CHANGED
@@ -20,6 +20,6 @@ JSON:
20
  "meta":{"curr":"a","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":[],"rds":[]},
24
- "players":{"a":{"vp":0,"res":{}},"b":{"vp":0,"res":{}},"c":{"vp":0,"res":{}}}
25
  }
 
20
  "meta":{"curr":"a","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":[]},
24
+ "players":{"a":{"vp":1,"res":{}},"b":{"vp":0,"res":{}},"c":{"vp":0,"res":{}}}
25
  }
examples/ai_testing/my_games/prompts/prompt_player_a.json CHANGED
@@ -76,18 +76,19 @@
76
  "role": "You are player 'a'. Play strategically to win."
77
  },
78
  "task_context": {
79
- "what_just_happened": "It's your turn in the setup phase. Place your first settlement on a strategic location.",
80
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. You have 2 possible actions. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
81
  },
82
- "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[],\"rds\":[]},\"players\":{\"a\":{\"vp\":0,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}",
83
  "constraints": {
84
  "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.",
85
  "allowed_actions": [
86
  {
87
- "action": "place_settlement",
88
- "description": "Place your starting settlement on an available node",
89
  "example_parameters": {
90
- "location": "20"
 
91
  }
92
  },
93
  {
 
76
  "role": "You are player 'a'. Play strategically to win."
77
  },
78
  "task_context": {
79
+ "what_just_happened": "You placed your first settlement at node 20. Now place a road connecting to it.",
80
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. You have 2 possible actions. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
81
  },
82
+ "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[[20,\"a\",\"S\"]],\"rds\":[]},\"players\":{\"a\":{\"vp\":1,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}",
83
  "constraints": {
84
  "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.",
85
  "allowed_actions": [
86
  {
87
+ "action": "build_road",
88
+ "description": "Place your starting road connecting to your settlement",
89
  "example_parameters": {
90
+ "from": "20",
91
+ "to": "21"
92
  }
93
  },
94
  {
examples/ai_testing/my_games/prompts/prompt_player_a.txt CHANGED
@@ -1,6 +1,6 @@
1
  ================================================================================
2
  AI AGENT PROMPT - PLAYER A
3
- Generated: 2026-01-03 22:25:23
4
  ================================================================================
5
 
6
  📋 RESPONSE SCHEMA (Expected LLM Output Format)
@@ -86,18 +86,19 @@ Generated: 2026-01-03 22:25:23
86
  "role": "You are player 'a'. Play strategically to win."
87
  },
88
  "task_context": {
89
- "what_just_happened": "It's your turn in the setup phase. Place your first settlement on a strategic location.",
90
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. You have 2 possible actions. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
91
  },
92
- "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[],\"rds\":[]},\"players\":{\"a\":{\"vp\":0,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}",
93
  "constraints": {
94
  "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.",
95
  "allowed_actions": [
96
  {
97
- "action": "place_settlement",
98
- "description": "Place your starting settlement on an available node",
99
  "example_parameters": {
100
- "location": "20"
 
101
  }
102
  },
103
  {
 
1
  ================================================================================
2
  AI AGENT PROMPT - PLAYER A
3
+ Generated: 2026-01-03 22:34:03
4
  ================================================================================
5
 
6
  📋 RESPONSE SCHEMA (Expected LLM Output Format)
 
86
  "role": "You are player 'a'. Play strategically to win."
87
  },
88
  "task_context": {
89
+ "what_just_happened": "You placed your first settlement at node 20. Now place a road connecting to it.",
90
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. You have 2 possible actions. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
91
  },
92
+ "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[[20,\"a\",\"S\"]],\"rds\":[]},\"players\":{\"a\":{\"vp\":1,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}",
93
  "constraints": {
94
  "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.",
95
  "allowed_actions": [
96
  {
97
+ "action": "build_road",
98
+ "description": "Place your starting road connecting to your settlement",
99
  "example_parameters": {
100
+ "from": "20",
101
+ "to": "21"
102
  }
103
  },
104
  {
examples/ai_testing/my_games/prompts/prompt_player_b.json CHANGED
@@ -34,9 +34,9 @@
34
  "role": "You are player 'b'. Play strategically to win."
35
  },
36
  "task_context": {
37
- "what_just_happened": "Player a is placing their first settlement.",
38
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
39
  },
40
- "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[],\"rds\":[]},\"players\":{\"a\":{\"vp\":0,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}"
41
  }
42
  }
 
34
  "role": "You are player 'b'. Play strategically to win."
35
  },
36
  "task_context": {
37
+ "what_just_happened": "Player a placed their first settlement at node 20 and is now choosing where to place their starting road.",
38
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
39
  },
40
+ "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[[20,\"a\",\"S\"]],\"rds\":[]},\"players\":{\"a\":{\"vp\":1,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}"
41
  }
42
  }
examples/ai_testing/my_games/prompts/prompt_player_b.txt CHANGED
@@ -1,6 +1,6 @@
1
  ================================================================================
2
  AI AGENT PROMPT - PLAYER B
3
- Generated: 2026-01-03 22:25:23
4
  ================================================================================
5
 
6
  📋 RESPONSE SCHEMA (Expected LLM Output Format)
@@ -44,10 +44,10 @@ Generated: 2026-01-03 22:25:23
44
  "role": "You are player 'b'. Play strategically to win."
45
  },
46
  "task_context": {
47
- "what_just_happened": "Player a is placing their first settlement.",
48
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
49
  },
50
- "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[],\"rds\":[]},\"players\":{\"a\":{\"vp\":0,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}"
51
  }
52
 
53
  ================================================================================
 
1
  ================================================================================
2
  AI AGENT PROMPT - PLAYER B
3
+ Generated: 2026-01-03 22:34:03
4
  ================================================================================
5
 
6
  📋 RESPONSE SCHEMA (Expected LLM Output Format)
 
44
  "role": "You are player 'b'. Play strategically to win."
45
  },
46
  "task_context": {
47
+ "what_just_happened": "Player a placed their first settlement at node 20 and is now choosing where to place their starting road.",
48
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
49
  },
50
+ "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[[20,\"a\",\"S\"]],\"rds\":[]},\"players\":{\"a\":{\"vp\":1,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}"
51
  }
52
 
53
  ================================================================================
examples/ai_testing/my_games/prompts/prompt_player_c.json CHANGED
@@ -34,9 +34,9 @@
34
  "role": "You are player 'c'. Play strategically to win."
35
  },
36
  "task_context": {
37
- "what_just_happened": "Player a is placing their first settlement.",
38
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
39
  },
40
- "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[],\"rds\":[]},\"players\":{\"a\":{\"vp\":0,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}"
41
  }
42
  }
 
34
  "role": "You are player 'c'. Play strategically to win."
35
  },
36
  "task_context": {
37
+ "what_just_happened": "Player a placed their first settlement at node 20 and is now choosing where to place their starting road.",
38
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
39
  },
40
+ "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[[20,\"a\",\"S\"]],\"rds\":[]},\"players\":{\"a\":{\"vp\":1,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}"
41
  }
42
  }
examples/ai_testing/my_games/prompts/prompt_player_c.txt CHANGED
@@ -1,6 +1,6 @@
1
  ================================================================================
2
  AI AGENT PROMPT - PLAYER C
3
- Generated: 2026-01-03 22:25:23
4
  ================================================================================
5
 
6
  📋 RESPONSE SCHEMA (Expected LLM Output Format)
@@ -44,10 +44,10 @@ Generated: 2026-01-03 22:25:23
44
  "role": "You are player 'c'. Play strategically to win."
45
  },
46
  "task_context": {
47
- "what_just_happened": "Player a is placing their first settlement.",
48
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
49
  },
50
- "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[],\"rds\":[]},\"players\":{\"a\":{\"vp\":0,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}"
51
  }
52
 
53
  ================================================================================
 
1
  ================================================================================
2
  AI AGENT PROMPT - PLAYER C
3
+ Generated: 2026-01-03 22:34:03
4
  ================================================================================
5
 
6
  📋 RESPONSE SCHEMA (Expected LLM Output Format)
 
44
  "role": "You are player 'c'. Play strategically to win."
45
  },
46
  "task_context": {
47
+ "what_just_happened": "Player a placed their first settlement at node 20 and is now choosing where to place their starting road.",
48
  "instructions": "Analyze the game state and select the optimal move from 'allowed_actions'. If you wish to negotiate or wait for other players, select the 'wait_for_response' action."
49
  },
50
+ "game_state": "\n 1. LOOKUP TABLES:\n • \"H\" (Hexes): Array where Index = HexID. Value = Resource+Num.\n Example: H[1]=\"W12\" -> Hex 1 is Wood 12.\n • \"N\" (Nodes): Array where Index = NodeID.\n Format: [ [Neighbors], [HexIDs], Port? ]\n Logic: To find yield of Node 10, check N[10]. Get HexIDs (e.g. [1,5]). Look up H[1] and H[5].\n\n2. CODES: W=Wood, B=Brick, S=Sheep, Wh=Wheat, O=Ore, D=Desert.\n ?3=Any 3:1 port, X2=Specific Resource 2:1 port.\n\n3. STATE: \"bld\"=[NodeID, Owner, Type], \"rds\"=[[From,To], Owner].\n\n4. PLAYERS: \"res\"={Resource:Count}, \"dev\"={\"h\":[Hidden Cards], \"r\":[Revealed] (K=Knight)}, \n \"stat\"=[\"LR\" (Longest Road), \"LA\" (Largest Army)].\n\n5. ROBBER: Located at HexID specified in \"meta.robber\". H[id] is blocked.\n\nJSON:\n{\"meta\":{\"curr\":\"a\",\"phase\":\"SETUP_FIRST_ROUND\",\"robber\":10,\"dice\":null},\"H\":[\"\",\"W12\",\"S5\",\"W4\",\"S8\",\"B6\",\"W3\",\"Wh8\",\"B10\",\"W11\",\"D\",\"O3\",\"S4\",\"B10\",\"Wh9\",\"Wh6\",\"S11\",\"O5\",\"Wh9\",\"O2\"],\"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\"]],\"state\":{\"bld\":[[20,\"a\",\"S\"]],\"rds\":[]},\"players\":{\"a\":{\"vp\":1,\"res\":{}},\"b\":{\"vp\":0,\"res\":{}},\"c\":{\"vp\":0,\"res\":{}}}}"
51
  }
52
 
53
  ================================================================================
pycatan/ai/response_parser.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Response parser for AI agent LLM responses.
3
+
4
+ This module handles:
5
+ 1. Parsing JSON responses from LLM
6
+ 2. Validating response structure against schemas
7
+ 3. Error handling and recovery
8
+ 4. Fallback mechanisms for malformed responses
9
+ 5. Logging all parsing attempts
10
+ """
11
+
12
+ import json
13
+ import logging
14
+ import re
15
+ from typing import Dict, Any, Optional, Tuple
16
+ from dataclasses import dataclass
17
+ from enum import Enum
18
+
19
+ from pycatan.ai.schemas import (
20
+ ResponseType,
21
+ get_schema_for_response_type,
22
+ validate_action_parameters,
23
+ ACTIVE_TURN_RESPONSE_SCHEMA,
24
+ OBSERVING_RESPONSE_SCHEMA
25
+ )
26
+
27
+
28
+ # Set up logging
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class ParseError(Enum):
33
+ """Types of parsing errors."""
34
+ INVALID_JSON = "invalid_json"
35
+ MISSING_REQUIRED_FIELD = "missing_required_field"
36
+ INVALID_FIELD_TYPE = "invalid_field_type"
37
+ INVALID_ACTION = "invalid_action"
38
+ VALIDATION_ERROR = "validation_error"
39
+
40
+
41
+ @dataclass
42
+ class ParseResult:
43
+ """Result of parsing an LLM response."""
44
+ success: bool
45
+ data: Optional[Dict[str, Any]] = None
46
+ error_type: Optional[ParseError] = None
47
+ error_message: Optional[str] = None
48
+ raw_response: Optional[str] = None
49
+ fallback_used: bool = False
50
+
51
+
52
+ class ResponseParser:
53
+ """
54
+ Parser for AI agent LLM responses with error handling and fallback mechanisms.
55
+ """
56
+
57
+ def __init__(self, enable_fallbacks: bool = True, strict_mode: bool = False):
58
+ """
59
+ Initialize the response parser.
60
+
61
+ Args:
62
+ enable_fallbacks: Whether to use fallback mechanisms for parsing errors
63
+ strict_mode: If True, fail on any validation error. If False, be lenient.
64
+ """
65
+ self.enable_fallbacks = enable_fallbacks
66
+ self.strict_mode = strict_mode
67
+ self.parse_attempts = 0
68
+ self.successful_parses = 0
69
+ self.failed_parses = 0
70
+
71
+ def parse(self,
72
+ raw_response: str,
73
+ response_type: ResponseType,
74
+ allowed_actions: Optional[list] = None) -> ParseResult:
75
+ """
76
+ Parse and validate an LLM response.
77
+
78
+ Args:
79
+ raw_response: Raw string response from LLM
80
+ response_type: Expected response type (active turn or observing)
81
+ allowed_actions: List of allowed action types (for validation)
82
+
83
+ Returns:
84
+ ParseResult with success status and parsed data or error info
85
+ """
86
+ self.parse_attempts += 1
87
+
88
+ logger.info(f"Parsing response (attempt #{self.parse_attempts})")
89
+ logger.debug(f"Raw response: {raw_response[:200]}...")
90
+
91
+ # Step 1: Extract JSON from response
92
+ json_str = self._extract_json(raw_response)
93
+ if json_str is None:
94
+ self.failed_parses += 1
95
+ return ParseResult(
96
+ success=False,
97
+ error_type=ParseError.INVALID_JSON,
98
+ error_message="Could not find valid JSON in response",
99
+ raw_response=raw_response
100
+ )
101
+
102
+ # Step 2: Parse JSON
103
+ try:
104
+ data = json.loads(json_str)
105
+ except json.JSONDecodeError as e:
106
+ logger.error(f"JSON decode error: {e}")
107
+
108
+ if self.enable_fallbacks:
109
+ # Try to fix common JSON errors
110
+ fixed_data = self._try_fix_json(json_str)
111
+ if fixed_data is not None:
112
+ logger.warning("Used fallback JSON repair mechanism")
113
+ data = fixed_data
114
+ else:
115
+ self.failed_parses += 1
116
+ return ParseResult(
117
+ success=False,
118
+ error_type=ParseError.INVALID_JSON,
119
+ error_message=f"JSON parse error: {str(e)}",
120
+ raw_response=raw_response
121
+ )
122
+ else:
123
+ self.failed_parses += 1
124
+ return ParseResult(
125
+ success=False,
126
+ error_type=ParseError.INVALID_JSON,
127
+ error_message=f"JSON parse error: {str(e)}",
128
+ raw_response=raw_response
129
+ )
130
+
131
+ # Step 3: Validate structure
132
+ validation_result = self._validate_structure(data, response_type)
133
+ if not validation_result[0]:
134
+ if self.enable_fallbacks and not self.strict_mode:
135
+ # Try to repair structure
136
+ data = self._try_repair_structure(data, response_type)
137
+ if data is None:
138
+ self.failed_parses += 1
139
+ return ParseResult(
140
+ success=False,
141
+ error_type=ParseError.VALIDATION_ERROR,
142
+ error_message=validation_result[1],
143
+ raw_response=raw_response,
144
+ data=data
145
+ )
146
+ logger.warning("Used fallback structure repair mechanism")
147
+ else:
148
+ self.failed_parses += 1
149
+ return ParseResult(
150
+ success=False,
151
+ error_type=ParseError.VALIDATION_ERROR,
152
+ error_message=validation_result[1],
153
+ raw_response=raw_response,
154
+ data=data
155
+ )
156
+
157
+ # Step 4: Validate action if present
158
+ if response_type == ResponseType.ACTIVE_TURN and "action" in data:
159
+ action_validation = self._validate_action(data["action"], allowed_actions)
160
+ if not action_validation[0]:
161
+ if self.strict_mode:
162
+ self.failed_parses += 1
163
+ return ParseResult(
164
+ success=False,
165
+ error_type=ParseError.INVALID_ACTION,
166
+ error_message=action_validation[1],
167
+ raw_response=raw_response,
168
+ data=data
169
+ )
170
+ else:
171
+ logger.warning(f"Action validation warning: {action_validation[1]}")
172
+
173
+ # Success!
174
+ self.successful_parses += 1
175
+ logger.info("Successfully parsed and validated response")
176
+
177
+ return ParseResult(
178
+ success=True,
179
+ data=data,
180
+ raw_response=raw_response
181
+ )
182
+
183
+ def _extract_json(self, text: str) -> Optional[str]:
184
+ """
185
+ Extract JSON from text that may contain additional content.
186
+
187
+ Handles cases where LLM returns JSON wrapped in markdown code blocks
188
+ or with additional text before/after.
189
+ """
190
+ # Try to find JSON in code blocks first
191
+ code_block_pattern = r'```(?:json)?\s*(\{.*?\})\s*```'
192
+ matches = re.findall(code_block_pattern, text, re.DOTALL)
193
+ if matches:
194
+ return matches[0]
195
+
196
+ # Try to find raw JSON object
197
+ json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
198
+ matches = re.findall(json_pattern, text, re.DOTALL)
199
+ if matches:
200
+ # Return the longest match (most likely to be complete)
201
+ return max(matches, key=len)
202
+
203
+ # If text looks like pure JSON, return as is
204
+ stripped = text.strip()
205
+ if stripped.startswith('{') and stripped.endswith('}'):
206
+ return stripped
207
+
208
+ return None
209
+
210
+ def _try_fix_json(self, json_str: str) -> Optional[Dict[str, Any]]:
211
+ """
212
+ Attempt to fix common JSON errors.
213
+
214
+ Common issues:
215
+ - Missing closing quotes
216
+ - Trailing commas
217
+ - Single quotes instead of double quotes
218
+ """
219
+ fixes = [
220
+ # Replace single quotes with double quotes (careful with apostrophes)
221
+ lambda s: s.replace("'", '"'),
222
+ # Remove trailing commas
223
+ lambda s: re.sub(r',\s*}', '}', s),
224
+ lambda s: re.sub(r',\s*]', ']', s),
225
+ ]
226
+
227
+ for fix in fixes:
228
+ try:
229
+ fixed = fix(json_str)
230
+ return json.loads(fixed)
231
+ except (json.JSONDecodeError, Exception):
232
+ continue
233
+
234
+ return None
235
+
236
+ def _validate_structure(self, data: Dict[str, Any], response_type: ResponseType) -> Tuple[bool, Optional[str]]:
237
+ """
238
+ Validate response structure against schema.
239
+
240
+ Returns:
241
+ Tuple of (is_valid, error_message)
242
+ """
243
+ schema = get_schema_for_response_type(response_type)
244
+
245
+ # Check required fields
246
+ for field in schema.get("required", []):
247
+ if field not in data:
248
+ return False, f"Missing required field: '{field}'"
249
+
250
+ # Validate field types and constraints
251
+ for field, value in data.items():
252
+ if field in schema["properties"]:
253
+ field_schema = schema["properties"][field]
254
+
255
+ # Check type
256
+ expected_type = field_schema.get("type")
257
+ if expected_type == "string" and not isinstance(value, str):
258
+ return False, f"Field '{field}' must be a string"
259
+ elif expected_type == "object" and not isinstance(value, dict):
260
+ return False, f"Field '{field}' must be an object"
261
+
262
+ # Check string constraints
263
+ if isinstance(value, str):
264
+ min_length = field_schema.get("minLength")
265
+ max_length = field_schema.get("maxLength")
266
+ if min_length and len(value) < min_length:
267
+ return False, f"Field '{field}' must be at least {min_length} characters"
268
+ if max_length and len(value) > max_length:
269
+ return False, f"Field '{field}' must be at most {max_length} characters"
270
+
271
+ return True, None
272
+
273
+ def _validate_action(self, action: Dict[str, Any], allowed_actions: Optional[list]) -> Tuple[bool, Optional[str]]:
274
+ """
275
+ Validate action structure and parameters.
276
+
277
+ Returns:
278
+ Tuple of (is_valid, error_message)
279
+ """
280
+ if not isinstance(action, dict):
281
+ return False, "Action must be an object"
282
+
283
+ if "type" not in action:
284
+ return False, "Action missing 'type' field"
285
+
286
+ if "parameters" not in action:
287
+ return False, "Action missing 'parameters' field"
288
+
289
+ action_type = action["type"]
290
+
291
+ # Check if action is in allowed list
292
+ if allowed_actions:
293
+ if action_type not in allowed_actions:
294
+ return False, f"Action type '{action_type}' not in allowed actions: {allowed_actions}"
295
+
296
+ # Validate parameters
297
+ parameters = action["parameters"]
298
+ if not isinstance(parameters, dict):
299
+ return False, "Action parameters must be an object"
300
+
301
+ # Validate parameter schema
302
+ param_valid, param_error = validate_action_parameters(action_type, parameters)
303
+ if not param_valid:
304
+ return False, param_error
305
+
306
+ return True, None
307
+
308
+ def _try_repair_structure(self, data: Dict[str, Any], response_type: ResponseType) -> Optional[Dict[str, Any]]:
309
+ """
310
+ Attempt to repair missing or invalid fields.
311
+
312
+ Strategies:
313
+ - Add default values for missing optional fields
314
+ - Convert types if possible
315
+ - Use empty objects/strings as fallbacks
316
+ """
317
+ schema = get_schema_for_response_type(response_type)
318
+ repaired = data.copy()
319
+
320
+ # Add missing required fields with defaults
321
+ for field in schema.get("required", []):
322
+ if field not in repaired:
323
+ if field == "internal_thinking":
324
+ repaired[field] = "[No reasoning provided]"
325
+ elif field == "action":
326
+ repaired[field] = {"type": "wait_for_response", "parameters": {}}
327
+ else:
328
+ return None # Can't repair
329
+
330
+ # Try to fix internal_thinking if too short
331
+ if "internal_thinking" in repaired:
332
+ min_length = schema["properties"]["internal_thinking"].get("minLength", 0)
333
+ if len(repaired["internal_thinking"]) < min_length:
334
+ repaired["internal_thinking"] += " [Response was too brief]"
335
+
336
+ return repaired
337
+
338
+ def get_statistics(self) -> Dict[str, Any]:
339
+ """Get parser statistics."""
340
+ return {
341
+ "total_attempts": self.parse_attempts,
342
+ "successful": self.successful_parses,
343
+ "failed": self.failed_parses,
344
+ "success_rate": (
345
+ self.successful_parses / self.parse_attempts
346
+ if self.parse_attempts > 0
347
+ else 0.0
348
+ )
349
+ }
pycatan/ai/schemas.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ JSON Schema definitions for AI agent requests and responses.
3
+
4
+ This module contains all schema definitions used for:
5
+ 1. Validating LLM responses
6
+ 2. Generating prompts with schema documentation
7
+ 3. Type checking and validation
8
+ """
9
+
10
+ from typing import Dict, Any, List, Optional
11
+ from enum import Enum
12
+
13
+
14
+ class ResponseType(Enum):
15
+ """Types of responses expected from AI agents."""
16
+ ACTIVE_TURN = "active_turn" # When it's the agent's turn to act
17
+ OBSERVING = "observing" # When agent is observing other players
18
+
19
+
20
+ # Schema for when it's the agent's turn (must take action)
21
+ ACTIVE_TURN_RESPONSE_SCHEMA = {
22
+ "type": "object",
23
+ "required": ["internal_thinking", "action"],
24
+ "properties": {
25
+ "internal_thinking": {
26
+ "type": "string",
27
+ "description": "Private strategy. What's your plan and why?",
28
+ "minLength": 50
29
+ },
30
+ "note_to_self": {
31
+ "type": "string",
32
+ "description": "Important facts for when it's your turn. Use only if essential for clarity or direct user query. Omit otherwise.",
33
+ "maxLength": 100
34
+ },
35
+ "say_outloud": {
36
+ "type": "string",
37
+ "description": "A short message to other players (max 100 chars). Use for negotiation, threats, or table talk. Keep in mind you pay for speak outload.",
38
+ "maxLength": 100
39
+ },
40
+ "action": {
41
+ "type": "object",
42
+ "required": ["type", "parameters"],
43
+ "properties": {
44
+ "type": {
45
+ "type": "string",
46
+ "description": "The action type (must match one from allowed_actions in constraints)"
47
+ },
48
+ "parameters": {
49
+ "type": "object",
50
+ "description": "Action-specific parameters. If no parameters are needed, provide an empty object.",
51
+ "additionalProperties": True # Allow flexible parameters based on action
52
+ }
53
+ }
54
+ }
55
+ },
56
+ "propertyOrdering": [
57
+ "internal_thinking",
58
+ "note_to_self",
59
+ "say_outloud",
60
+ "action"
61
+ ]
62
+ }
63
+
64
+
65
+ # Schema for when agent is observing (not their turn)
66
+ OBSERVING_RESPONSE_SCHEMA = {
67
+ "type": "object",
68
+ "required": ["internal_thinking"],
69
+ "properties": {
70
+ "internal_thinking": {
71
+ "type": "string",
72
+ "description": "Track what's happening. What are opponents doing? What's your strategy for next turn?",
73
+ "minLength": 30
74
+ },
75
+ "note_to_self": {
76
+ "type": "string",
77
+ "description": "Important facts for when it's your turn. Use only if essential for clarity or direct user query. Omit otherwise.",
78
+ "maxLength": 100
79
+ },
80
+ "say_outloud": {
81
+ "type": "string",
82
+ "description": "Optional message to other players (max 100 chars). Propose trades or negotiate. You pay for this.",
83
+ "maxLength": 100
84
+ }
85
+ },
86
+ "propertyOrdering": [
87
+ "internal_thinking",
88
+ "note_to_self",
89
+ "say_outloud"
90
+ ]
91
+ }
92
+
93
+
94
+ def get_schema_for_response_type(response_type: ResponseType) -> Dict[str, Any]:
95
+ """
96
+ Get the appropriate schema based on response type.
97
+
98
+ Args:
99
+ response_type: Type of response expected (active turn or observing)
100
+
101
+ Returns:
102
+ JSON schema dictionary
103
+ """
104
+ if response_type == ResponseType.ACTIVE_TURN:
105
+ return ACTIVE_TURN_RESPONSE_SCHEMA
106
+ elif response_type == ResponseType.OBSERVING:
107
+ return OBSERVING_RESPONSE_SCHEMA
108
+ else:
109
+ raise ValueError(f"Unknown response type: {response_type}")
110
+
111
+
112
+ def get_schema_description(response_type: ResponseType) -> str:
113
+ """
114
+ Get a human-readable description of what the schema expects.
115
+
116
+ Args:
117
+ response_type: Type of response expected
118
+
119
+ Returns:
120
+ Description string
121
+ """
122
+ if response_type == ResponseType.ACTIVE_TURN:
123
+ return (
124
+ "Response must include:\n"
125
+ "- internal_thinking: Your strategy (min 50 chars)\n"
126
+ "- action: {type: action_name, parameters: {...}}\n"
127
+ "Optional:\n"
128
+ "- note_to_self: Important reminder (max 100 chars)\n"
129
+ "- say_outloud: Message to other players (max 100 chars)"
130
+ )
131
+ elif response_type == ResponseType.OBSERVING:
132
+ return (
133
+ "Response must include:\n"
134
+ "- internal_thinking: What you observed (min 30 chars)\n"
135
+ "Optional:\n"
136
+ "- note_to_self: Important reminder (max 100 chars)\n"
137
+ "- say_outloud: Message to other players (max 100 chars)"
138
+ )
139
+ else:
140
+ return "Unknown response type"
141
+
142
+
143
+ # Common action parameter schemas for validation
144
+ ACTION_PARAMETER_SCHEMAS = {
145
+ "build_road": {
146
+ "required": ["from", "to"],
147
+ "properties": {
148
+ "from": {"type": "string", "description": "Starting node ID"},
149
+ "to": {"type": "string", "description": "Ending node ID"}
150
+ }
151
+ },
152
+ "build_settlement": {
153
+ "required": ["node"],
154
+ "properties": {
155
+ "node": {"type": "string", "description": "Node ID where to build"}
156
+ }
157
+ },
158
+ "build_city": {
159
+ "required": ["node"],
160
+ "properties": {
161
+ "node": {"type": "string", "description": "Node ID where settlement exists"}
162
+ }
163
+ },
164
+ "buy_dev_card": {
165
+ "required": [],
166
+ "properties": {}
167
+ },
168
+ "play_knight": {
169
+ "required": ["hex", "victim"],
170
+ "properties": {
171
+ "hex": {"type": "string", "description": "Hex ID to move robber"},
172
+ "victim": {"type": "string", "description": "Player to steal from (or 'none')"}
173
+ }
174
+ },
175
+ "trade_with_bank": {
176
+ "required": ["give", "receive"],
177
+ "properties": {
178
+ "give": {"type": "string", "description": "Resource to give"},
179
+ "receive": {"type": "string", "description": "Resource to receive"}
180
+ }
181
+ },
182
+ "propose_trade": {
183
+ "required": ["offer", "request"],
184
+ "properties": {
185
+ "offer": {"type": "object", "description": "Resources offered"},
186
+ "request": {"type": "object", "description": "Resources requested"}
187
+ }
188
+ },
189
+ "wait_for_response": {
190
+ "required": [],
191
+ "properties": {}
192
+ },
193
+ "end_turn": {
194
+ "required": [],
195
+ "properties": {}
196
+ }
197
+ }
198
+
199
+
200
+ def validate_action_parameters(action_type: str, parameters: Dict[str, Any]) -> tuple[bool, Optional[str]]:
201
+ """
202
+ Validate that action parameters match expected schema.
203
+
204
+ Args:
205
+ action_type: Type of action (e.g., "build_road")
206
+ parameters: Parameters provided by the agent
207
+
208
+ Returns:
209
+ Tuple of (is_valid, error_message)
210
+ """
211
+ if action_type not in ACTION_PARAMETER_SCHEMAS:
212
+ # Unknown action type - let game manager handle validation
213
+ return True, None
214
+
215
+ schema = ACTION_PARAMETER_SCHEMAS[action_type]
216
+
217
+ # Check required parameters
218
+ for required_param in schema["required"]:
219
+ if required_param not in parameters:
220
+ return False, f"Missing required parameter '{required_param}' for action '{action_type}'"
221
+
222
+ # Check parameter types if defined
223
+ for param_name, param_value in parameters.items():
224
+ if param_name in schema["properties"]:
225
+ expected_type = schema["properties"][param_name].get("type")
226
+ if expected_type == "string" and not isinstance(param_value, str):
227
+ return False, f"Parameter '{param_name}' should be a string"
228
+ elif expected_type == "number" and not isinstance(param_value, (int, float)):
229
+ return False, f"Parameter '{param_name}' should be a number"
230
+ elif expected_type == "object" and not isinstance(param_value, dict):
231
+ return False, f"Parameter '{param_name}' should be an object"
232
+
233
+ return True, None