EZTIME2025 commited on
Commit
6016237
·
1 Parent(s): 61ad06f

make state smallet

Browse files
.github/instructions/WORK_PLAN.md CHANGED
@@ -1,7 +1,8 @@
1
  # 🗺️ AI Agent Development Work Plan
2
 
3
  **Date:** January 3, 2026
4
- **Status:** 📋 Planning Phase
 
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
- - [ ] Create centralized configuration system
22
- - [ ] LLM settings (model, temperature, max_tokens, etc.)
23
- - [ ] API credentials management
24
- - [ ] Agent parameters (personality, risk tolerance, etc.)
25
- - [ ] Performance settings (timeouts, retries, caching)
26
- - [ ] Create config file format (YAML/JSON)
27
- - [ ] Build configuration loader and validator
28
- - [ ] Add environment variable support for sensitive data
29
-
30
- **Files to create:**
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
- - [ ] Design prompt processing pipeline
38
- - [ ] Implement game state filtering
39
- - [ ] Hide opponent's private information
40
- - [ ] Filter development cards
41
- - [ ] Remove non-visible game elements
42
- - [ ] Build perspective transformation
43
- - [ ] Convert game state to agent's viewpoint
44
- - [ ] Format resources and points
45
- - [ ] Present relative positioning
46
- - [ ] Create prompt template system
47
- - [ ] Meta data section
48
- - [ ] Task context section
49
- - [ ] Game state section
50
- - [ ] Social context section
51
- - [ ] Memory section
52
- - [ ] Constraints section
53
- - [ ] Build custom instruction injection per agent
 
 
 
 
 
54
 
55
- **Files to create:**
56
- - `pycatan/ai/prompt_manager.py` - Main prompt processing
57
- - `pycatan/ai/state_filter.py` - Game state filtering logic
58
- - `pycatan/ai/prompt_templates.py` - Template definitions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: {history_file}")
138
- print(f" 📄 Final state: {final_file}")
139
- print(f" 📌 Quick access: {sample_file}")
140
- print(f" 🔄 Real-time: {CURRENT_STATE_FILE} (updated during game)")
 
 
 
 
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": "b_10",
121
- "vertex": 10,
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": 10,
155
- "to": 11,
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": 2,
259
- "total_cards": 3,
260
- "cards_list": [
261
- "ore",
262
- "brick",
263
- "sheep"
264
- ],
265
  "dev_cards_list": [],
266
- "settlements": 2,
267
  "cities": 0,
268
- "roads": 2,
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": 2,
279
- "total_cards": 4,
280
- "cards_list": [
281
- "sheep",
282
- "sheep",
283
- "ore",
284
- "wheat"
285
- ],
286
  "dev_cards_list": [],
287
- "settlements": 2,
288
  "cities": 0,
289
- "roads": 2,
290
- "longest_road": 1,
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": 2,
300
- "total_cards": 3,
301
- "cards_list": [
302
- "ore",
303
- "wheat",
304
- "wheat"
305
- ],
306
  "dev_cards_list": [],
307
- "settlements": 2,
308
  "cities": 0,
309
- "roads": 2,
310
- "longest_road": 1,
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": "NORMAL_PLAY",
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.personality = "aggressive"
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.personality == "aggressive", "Personality mismatch"
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 personalities."""
81
  print("\n" + "="*80)
82
- print("TEST 3: Different Personalities")
83
  print("="*80)
84
 
85
- personalities = {
86
- "aggressive": {
87
- "personality": "aggressive",
88
- "risk_tolerance": 0.8,
89
- "trade_willingness": 0.7,
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, settings in personalities.items():
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}' personality")
122
- print(f" - Risk tolerance: {config.agent.risk_tolerance}")
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 risk tolerance
146
  config = AIConfig()
147
- config.agent.risk_tolerance = 1.5 # Invalid (> 1.0)
148
 
149
  try:
150
  config.validate()
151
- print("✗ Should have caught invalid risk_tolerance")
152
  return False
153
  except ValueError as e:
154
- print(f"✓ Correctly caught invalid risk_tolerance: {e}")
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.personality = "trading"
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.personality == config.agent.personality
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 Personalities", test_personalities),
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
- - response_parser: LLM response parsing and validation
11
- - memory: Agent memory and learning systems
12
- - llm_client: LLM API abstraction and client
 
 
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
- """Configuration for agent personality and behavior."""
67
-
68
- # Agent identity
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)