EZTIME2025 commited on
Commit
c903325
·
1 Parent(s): 74d81e4

from_old_account

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +14 -0
  2. .github/copilot-instructions.md +84 -0
  3. .github/instructions/ARCHITECTURE.md +146 -0
  4. .github/instructions/BUILD_PLAN.md +179 -0
  5. .github/instructions/STEP_BY_STEP_GUIDE.md +4 -0
  6. .github/instructions/WEB_VISUALIZATION_GUIDE.md +336 -0
  7. .gitignore +65 -0
  8. CHANGES.txt +1 -0
  9. LICENSE.txt +19 -0
  10. MANIFEST.in +1 -0
  11. Pipfile +6 -0
  12. Pipfile.lock +45 -0
  13. board_definition.json +1435 -0
  14. demo_point_system.py +157 -0
  15. dist/pycatan-0.1.tar.gz +0 -0
  16. examples/board_renderer.py +193 -0
  17. game_viz.log +442 -0
  18. play_catan.py +33 -0
  19. print_game_logic.py +40 -0
  20. pycatan/__init__.py +33 -0
  21. pycatan/actions.py +253 -0
  22. pycatan/board.py +219 -0
  23. pycatan/board_definition.py +556 -0
  24. pycatan/building.py +33 -0
  25. pycatan/card.py +21 -0
  26. pycatan/console_visualization.py +645 -0
  27. pycatan/default_board.py +230 -0
  28. pycatan/game.py +508 -0
  29. pycatan/game_manager.py +1563 -0
  30. pycatan/game_moves.txt +19 -0
  31. pycatan/harbor.py +70 -0
  32. pycatan/human_user.py +667 -0
  33. pycatan/player.py +385 -0
  34. pycatan/point.py +8 -0
  35. pycatan/point_mapping.py +202 -0
  36. pycatan/real_game.py +372 -0
  37. pycatan/starting_board.json +64 -0
  38. pycatan/static/css/style.css +774 -0
  39. pycatan/static/images/Desert.png +0 -0
  40. pycatan/static/images/Fields.png +0 -0
  41. pycatan/static/images/Forest.png +0 -0
  42. pycatan/static/images/Hills.png +0 -0
  43. pycatan/static/images/Mountains.png +0 -0
  44. pycatan/static/images/Pasture.png +0 -0
  45. pycatan/static/js/board.js +763 -0
  46. pycatan/static/js/gameData.js +154 -0
  47. pycatan/static/js/main.js +414 -0
  48. pycatan/static/js/manual_mapping.js +240 -0
  49. pycatan/statuses.py +33 -0
  50. pycatan/templates/index.html +107 -0
.gitattributes CHANGED
@@ -1,4 +1,18 @@
1
  # Auto detect text files and perform LF normalization
2
  * text=auto
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  examples/ai_testing/my_games/session_20260515_220558/session_summary.json filter=lfs diff=lfs merge=lfs -text
4
  *.wav filter=lfs diff=lfs merge=lfs -text
 
1
  # Auto detect text files and perform LF normalization
2
  * text=auto
3
+
4
+ # Custom for Visual Studio
5
+ *.cs diff=csharp
6
+ # Standard to msysgit
7
+ *.doc diff=astextplain
8
+ *.DOC diff=astextplain
9
+ *.docx diff=astextplain
10
+ *.DOCX diff=astextplain
11
+ *.dot diff=astextplain
12
+ *.DOT diff=astextplain
13
+ *.pdf diff=astextplain
14
+ *.PDF diff=astextplain
15
+ *.rtf diff=astextplain
16
+ *.RTF diff=astextplain
17
  examples/ai_testing/my_games/session_20260515_220558/session_summary.json filter=lfs diff=lfs merge=lfs -text
18
  *.wav filter=lfs diff=lfs merge=lfs -text
.github/copilot-instructions.md ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PyCatan AI Coding Instructions
2
+
3
+ ## Project Overview
4
+ PyCatan is a Python library for simulating Settlers of Catan games.
5
+
6
+ **🚀 NEW: Active Development Phase**
7
+ This project is being actively expanded with a complete simulation framework including GameManager, AI players, and multiple visualization interfaces.
8
+
9
+ **📚 Additional Documentation:**
10
+ - **[Architecture Overview](instructions/ARCHITECTURE.md)** - Project vision, architecture design, and component responsibilities
11
+ - **[Build Plan](instructions/BUILD_PLAN.md)** - Development roadmap, tasks, and progress tracking
12
+ - **[API Reference](instructions/STEP_BY_STEP_GUIDE.md)** - הסבר לאיך לתקשר עם המשתמש שאתה עובד איתו
13
+ ## Legacy Note
14
+ The original core game logic is stable and functional. New development focuses on building a complete simulation layer on top of the existing foundation.
15
+
16
+ ## Core Architecture
17
+
18
+ ### Game Flow Model
19
+ - **Game** (`pycatan/game.py`) orchestrates everything - manages players, board, development cards, and win conditions
20
+ - **Board** (`pycatan/board.py`) is abstract base class; **DefaultBoard** (`pycatan/default_board.py`) implements hexagonal tile layout
21
+ - **Player** (`pycatan/player.py`) manages individual state: cards, buildings, victory points, longest road calculation
22
+ - **Point** and **Tile** objects form the geometric foundation with bidirectional relationships
23
+
24
+ ### Key Patterns
25
+
26
+ #### Coordinate System
27
+ - Board uses `[row, index]` coordinates throughout (not x,y)
28
+ - Points are intersections where settlements/cities go; tiles are hexes that produce resources
29
+ - Example: `game.add_settlement(player=0, point=board.points[0][0], is_starting=True)`
30
+
31
+ #### Status-Based Error Handling
32
+ All game actions return `Statuses` enum values instead of throwing exceptions:
33
+ ```python
34
+ from pycatan.statuses import Statuses
35
+ result = game.add_settlement(player, point)
36
+ if result == Statuses.ERR_BLOCKED:
37
+ # Handle blocked building location
38
+ ```
39
+
40
+ #### Starting vs Normal Phase
41
+ Most building actions have `is_starting` parameter - starting phase bypasses card costs and connectivity rules:
42
+ ```python
43
+ game.add_settlement(player, point, is_starting=True) # Free during setup
44
+ game.add_road(player, start, end, is_starting=True) # No cards required
45
+ ```
46
+
47
+ ## Development Workflows
48
+
49
+ ### Testing
50
+ - Use pytest: `python -m pytest tests/` (not the bash script on Windows)
51
+ - Tests in `tests/` follow class-based pattern: `class TestGame:` with `test_*` methods
52
+ - Mock game state by directly manipulating player cards: `player.add_cards([ResCard.Wood, ResCard.Brick])`
53
+
54
+ ### Building/Distribution
55
+ - Package managed via setuptools (`setup.py`)
56
+ - Published to PyPI as `pycatan` package
57
+ - Version number in `setup.py` (currently 0.13)
58
+
59
+ ## Critical Implementation Details
60
+
61
+ ### Longest Road Calculation
62
+ Complex recursive algorithm in `Player.get_longest_road()` - avoid modifying without understanding the connected road traversal logic.
63
+
64
+ ### Card Management
65
+ - Resources use `ResCard` enum, development cards use `DevCard` enum
66
+ - Player card checking with `has_cards()` handles duplicates correctly by creating temporary lists
67
+ - Bank trading supports both 4:1 and harbor-specific rates (2:1 or 3:1)
68
+
69
+ ### Development Card Usage
70
+ Different dev cards require different `args` dictionaries in `use_dev_card()`:
71
+ - Knight: `{'robber_pos': [r, i], 'victim': player_num}`
72
+ - Road Building: `{'road_one': {'start': point, 'end': point}, 'road_two': {...}}`
73
+ - Monopoly: `{'card_type': ResCard.Wood}`
74
+
75
+ ### Import Structure
76
+ Main module exports through `pycatan/__init__.py`:
77
+ - Import as: `from pycatan import Game, Player, ResCard, Statuses`
78
+ - Avoid importing submodules directly unless extending core classes
79
+
80
+ ## Testing Conventions
81
+ - Create game instances with explicit player counts: `Game(num_of_players=4)`
82
+ - Use board coordinate system: `board.points[row][index]` for settlements
83
+ - Test error conditions by checking returned status codes, not exceptions
84
+ - Starting phase tests should verify cards aren't consumed: `assert len(player.cards) == original_count`
.github/instructions/ARCHITECTURE.md ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PyCatan Architecture Overview
2
+
3
+ ## מהות הפרויקט
4
+
5
+ פרויקט PyCatan הוא הרחבה של הספרייה הקיימת למשחק Settlers of Catan, שמוסיפה שכבת סימולציה מלאה עם מנהל משחק, ממשקי משתמש, ו-AI players.
6
+
7
+ ### המטרה
8
+ - יצירת פלטפורמה לסימולציות של משחק Catan
9
+ - תמיכה בשחקנים אנושיים ו-AI בו-זמנית
10
+ - ממשקי ויזואליזציה מרובים (קונסול, ווב)
11
+ - ארכיטקטורה מודולרית וניתנת להרחבה
12
+
13
+ ## הארכיטקטורה
14
+
15
+ ```
16
+ ┌─────────────────────────────────────────────┐
17
+ │ GameManager │ ← מנהל התורות והזרימה
18
+ ├─────────────────────────────────────────────┤
19
+ │ • game_loop() │
20
+ │ • handle_turn_rules() │
21
+ │ • request_input_from_user() │
22
+ │ • coordinate_interactions() │
23
+ └─────────────────┬───────────────────────────┘
24
+
25
+ ┌─────────────┼─────────────┐
26
+ │ │ │
27
+ ▼ ▼ ▼
28
+ ┌─────────┐ ┌─────────┐ ┌─────────────┐
29
+ │ Game │ │ Users │ │Visualizations│
30
+ │(קיים) │ │ (חדש) │ │ (חדש) │
31
+ └─────────┘ └─────────┘ └─────────────┘
32
+ ```
33
+
34
+ ### רכיבי המערכת
35
+
36
+ #### 1. GameManager (חדש)
37
+ **תפקיד:** מנהל את זרימת המשחק, התורות, וחוקי התורות
38
+ - ניהול לולאת המשחק הראשית
39
+ - תיאום בין שחקנים (מסחר, אישורים)
40
+ - אכיפת חוקי תורות (7 = השלכת קלפים, וכו')
41
+ - ניהול state של התור הנוכחי
42
+
43
+ #### 2. Game (קיים - התאמות קלות)
44
+ **תפקיד:** לוגיקת המשחק הבסיסית וחוקי המשחק
45
+ - כל הפונקציונליות הקיימת (add_settlement, trade, וכו')
46
+ - הוספת get_full_state() לויזואליזציה
47
+ - validation של פעולות
48
+
49
+ #### 3. User Hierarchy (חדש)
50
+ ```python
51
+ User (Abstract)
52
+ ├── HumanUser # אינטראקציה דרך טרמינל
53
+ └── AIUser # החלטות אלגוריתמיות
54
+ ```
55
+ **תפקיד:** מספק אינפוט למנהל המשחק
56
+ - get_input() מחזיר פעולות מובנות
57
+ - כל User מנהל את הלוגיקה שלו (UI/AI)
58
+
59
+ #### 4. Visualization (חדש)
60
+ ```python
61
+ Visualization (Base)
62
+ ├── ConsoleVisualization # הצגה בטרמינל
63
+ ├── WebVisualization # ממשק ווב
64
+ └── LogVisualization # תיעוד לקובץ
65
+ ```
66
+ **תפקיד:** הצגת מצב המשחק ופעולות
67
+ - notify_action() - עדכון מיידי
68
+ - update_full_state() - עדכון מלא בסוף תור
69
+
70
+ #### 5. Actions & Data (חדש)
71
+ ```python
72
+ ActionType (Enum) # סוגי פעולות
73
+ Action (DataClass) # מבנה פעולה
74
+ GameState (DataClass) # מצב משחק
75
+ ```
76
+
77
+ ## זרימת המשחק
78
+
79
+ ### 1. אתחול
80
+ ```
81
+ GameManager.initialize()
82
+ ├── יצירת Game instance
83
+ ├── רישום Users
84
+ ├── הגדרת Visualizations
85
+ └── setup_phase()
86
+ ```
87
+
88
+ ### 2. לולאת משחק ראשית
89
+ ```
90
+ while not game.has_ended:
91
+ ├── roll_dice() או request_roll()
92
+ ├── handle_dice_effects() (7 = robber, משאבים)
93
+ ├── player_action_loop()
94
+ │ ├── current_user.get_input()
95
+ │ ├── validate_action()
96
+ │ ├── execute_action()
97
+ │ └── update_visualizations()
98
+ └── end_turn()
99
+ ```
100
+
101
+ ### 3. אינטראקציות בין-שחקנים
102
+ ```
103
+ Player A: propose_trade()
104
+ ├── GameManager validates proposal
105
+ ├── GameManager.request_input(Player B, "trade_response")
106
+ ├── Player B: accept/reject
107
+ └── GameManager executes or cancels
108
+ ```
109
+
110
+ ## עקרונות עיצוב
111
+
112
+ ### הפרדת אחריויות
113
+ - **Game** = מה מותר לעשות (חוקי המשחק)
114
+ - **GameManager** = מתי ואיך לעשות (זרימת התורות)
115
+ - **User** = מה לעשות (החלטות)
116
+ - **Visualization** = איך להציג (ממשק)
117
+
118
+ ### מודולריות
119
+ - כל רכיב עצמאי וניתן להחלפה
120
+ - ממשקים ברורים בין הרכיבים
121
+ - קל להוסיף סוגי Users או Visualizations חדשים
122
+
123
+ ### פשטות
124
+ - זרימה ישירה וברורה
125
+ - בלי abstractions מיותרות
126
+ - debugging וטיפול בשגיא��ת פשוטים
127
+
128
+ ## התאמות לקוד הקיים
129
+
130
+ המטרה היא למזער שינויים בקוד הקיים:
131
+ - Game class נשאר כמעט זהה
132
+ - הוספת מתודות get_state() למחלקות קיימות
133
+ - Player class יישאר ללא שינוי
134
+ - Board/Building/Card classes ללא שינוי
135
+
136
+ ## דוגמת שימוש
137
+
138
+ ```python
139
+ # הגדרת המשחק
140
+ users = [HumanUser("Alice"), AIUser("Bob"), HumanUser("Charlie")]
141
+ visualizations = [ConsoleVisualization(), LogVisualization("game.log")]
142
+ game_manager = GameManager(users, visualizations)
143
+
144
+ # הפעלת המשחק
145
+ game_manager.start_game() # מפעיל את game_loop()
146
+ ```
.github/instructions/BUILD_PLAN.md ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PyCatan - תוכנית בנייה
2
+
3
+ ## סקירה כללית
4
+
5
+ תוכנית הבנייה מחולקת ל-6 שלבים עיקריים, כשכל שלב בונה על הקודם ומוסיף פונקציונליות חדשה.
6
+
7
+ ---
8
+
9
+ ## שלב 1: תשתית בסיסית
10
+ **מטרה:** בניית המבנים הבסיסיים והממשקים
11
+ **סטטוס:** ✅ הושלם במלואו
12
+ **תאריך השלמה:** 10 נובמבר 2025
13
+
14
+ **סיכום השלב:**
15
+ - יצרנו את כל התשתית הבסיסית למערכת ניהול המשחק החדשה
16
+ - 3 קבצי קוד ראשיים + 3 קבצי בדיקות
17
+ - סה"כ 75 בדיקות יחידה - כולן עוברות בהצלחה
18
+ - עדכון __init__.py לנגישות המודולים החדשים
19
+ - המערכת מוכנה לשלב הבא
20
+
21
+ ## שלב 2: ממשק בסיסי
22
+ **מטרה:** יצירת ממשק שימוש בסיסי למשחק
23
+ **סטטוס:** ✅ הושלם במלואו!
24
+ **תאריך השלמה:** 13 נובמבר 2025
25
+
26
+ **סיכום השלב:**
27
+ - בנינו ממשק CLI מלא ומתקדם עם HumanUser class
28
+ - 15+ סוגי פקודות עם פרסור חכם ו-error handling מקיף
29
+ - מערכת התראות לפעולות ואירועי משחק
30
+ - Console Visualization מתקדמת עם צבעים ומידע מפורט
31
+ - Web Visualization מלאה עם real-time updates
32
+ - Game Loop פונקציונלי עם טיפול בשגיאות ומונה errors
33
+ - 36 בדיקות יחידה חדשות + דוגמאות אינטרקטיביות
34
+ - **המערכת מוכנה לחיבור למשחק האמיתי!**
35
+
36
+ ## מצב נוכחי: 🎯 שלב 3 - אינטגרציה ובדיקות - 21 נובמבר 2025
37
+ **סטטוס:** 🟡 בתהליך מתקדם. התשתית מוכנה, מתחילים בדיקות ותיקוני באגים.
38
+
39
+ ### משימה 3.1: Game Class Integration
40
+ **סטטוס:** ✅ הושלם (עם באגים ידועים לתיקון)
41
+
42
+ - [x] הוספת get_full_state() ל-Game
43
+ - [x] הוספת get_state() ל-Player, Board (מומש בתוך get_full_state)
44
+ - [x] יצירת ActionResult objects
45
+ - [x] mapping בין Actions לפונקציות Game (בסיסי)
46
+ - [x] error handling משופר (בסיסי)
47
+
48
+ **הערות:**
49
+ - פונקציית `add_city` ב-Game מכילה באג (משתנים לא מוגדרים)
50
+ - מיפוי פעולות מסחר וערים ב-GameManager עדיין לא מומש מלא
51
+
52
+ ### משימה 3.2: Validation & Error Handling
53
+ **סטטוס:** 🟡 בתהליך
54
+
55
+ - [x] validation בסיסי של פעולות ב-GameManager
56
+ - [x] טיפול בשגיאות ברמת GameManager (try/catch blocks)
57
+ - [x] הודעות שגיאה ברורות למשתמש
58
+ - [ ] rollback mechanisms אם נדרש
59
+ - [ ] תיקון באגים לוגיים ב-Game class
60
+
61
+ ### משימה 3.3: End-to-End Testing
62
+ **סטטוס:** 🚀 מוכן להתחלה
63
+ **זהו הפוקוס הנוכחי!**
64
+
65
+ - [ ] הרצת משחק מלא עם HumanUser
66
+ - [ ] בדיקת כל סוגי הפעולות (בנייה, גלגול קוביות)
67
+ - [ ] זיהוי ותיקון באגים בזמן אמת
68
+ - [ ] וידוא סנכרון בין ה-Visualizations למצב המשחק
69
+
70
+ ---
71
+
72
+ ## שלב 4: חוקי תורות
73
+ **מטרה:** הוספת כל חוקי התורות של Catan
74
+
75
+ ### משימה 4.1: Dice Rolling & Resource Distribution
76
+ **סטטוס:** ⭕ לא התחיל
77
+ **זמן משוער:** 2-3 שעות
78
+
79
+ - [ ] מנגנון גלגול קוביות
80
+ - [ ] חלוקת משאבים אוטומטית
81
+ - [ ] טיפול במקרים מיוחדים
82
+ - [ ] logging ו-visualization של גלגולים
83
+
84
+ ### משימה 4.2: Rule of 7 (Robber & Discard)
85
+ **סטטוס:** ⭕ לא התחיל
86
+ **זמן משוער:** 3-4 שעות
87
+
88
+ - [ ] השלכת קלפים עבור שחקנים עם 7+ קלפים
89
+ - [ ] העברת הרובר
90
+ - [ ] גנבת קלף אקראי
91
+ - [ ] תיאום בין מספר שחקנים
92
+
93
+ ### משימה 4.3: Turn Phases & Flow Control
94
+ **סטטוס:** ⭕ לא התחיל
95
+ **זמן משוער:** 2-3 שעות
96
+
97
+ - [ ] שלבי תור: ROLL -> ACTIONS -> END
98
+ - [ ] מגבלות על פעולות בכל שלב
99
+ - [ ] מעבר אוטומטי בין שלבים
100
+ - [ ] תזמונים ו-timeouts אופציונליים
101
+
102
+ ---
103
+
104
+ ## שלב 5: אינטראקציות בין-שחקנים
105
+ **מטרה:** טיפול במסחר ואינטראקציות מורכבות
106
+
107
+ ### משימה 5.1: Trading System
108
+ **סטטוס:** ⭕ לא התחיל
109
+ **זמן משוער:** 4-5 שעות
110
+
111
+ - [ ] הצעות מסחר בין שחקנים
112
+ - [ ] מנגנון אישור/דחייה
113
+ - [ ] counter-offers
114
+ - [ ] timeout למסחר
115
+ - [ ] mסחר עם הבנק (4:1, harbors)
116
+
117
+ ### משימה 5.2: Robber Interactions
118
+ **סטטוס:** ⭕ לא התחיל
119
+ **זמן משוער:** 2-3 שעות
120
+
121
+ - [ ] בחירת מיקום רובר
122
+ - [ ] בחירת שחקן לגניבה
123
+ - [ ] Knight card interactions
124
+ - [ ] ויזואליזציה של רובר
125
+
126
+ ### משימה 5.3: Development Cards & Special Actions
127
+ **סטטוס:** ⭕ לא התחיל
128
+ **זמן משוער:** 3-4 שעות
129
+
130
+ - [ ] שימוש בקלפי פיתוח
131
+ - [ ] Road Building (שני כבישים)
132
+ - [ ] Monopoly (איסוף קלפים)
133
+ - [ ] Year of Plenty (קבלת משאבים)
134
+ - [ ] Victory Point cards
135
+
136
+ ---
137
+
138
+ ## שלב 6: AI ו-Visualizations מתקדמות
139
+ **מטרה:** השלמת המערכת עם AI ו-UI משופר
140
+ **סטטוס:** 🟡 בתהליך (1/3 משימות הושלמה)
141
+
142
+ ### משימה 6.1: WebVisualization Implementation
143
+ **סטטוס:** ✅ הושלם במלואו
144
+ **תאריך השלמה:** 11 נובמבר 2025
145
+
146
+ - [x] יצירת `web_visualization.py`
147
+ - [x] Flask server מלא עם API endpoints
148
+ - [x] Server-Sent Events לעדכונים בזמן אמת
149
+ - [x] ממשק לכל הmethods הדרושים מ-Visualization base class
150
+ - [x] המרה מדויקת של GameState לפורמט web
151
+ - [x] ניהול SSE clients ו-broadcast של עדכונים
152
+ - [x] Context manager support ו-lifecycle management
153
+ - [x] העתקה ואיילפוץ של תשתית הווב הקיימת
154
+ - [x] HTML template עם game info panel ו-action log
155
+ - [x] CSS עם תמיכה בrealtime updates
156
+ - [x] JavaScript עם חיבור לFlask endpoints ו-SSE
157
+ - [x] CatanBoard class מתקדמת עם אינטראקטיביות מלאה
158
+ - [x] בדיקות יחידה מקיפות (14 בדיקות - כולן עוברות)
159
+ - [x] דוגמה אינטרקטיבית מלאה עם אפשרויות השוואה
160
+ - [x] אינטגרציה מלאה עם מערכת הVisualization הקיימת
161
+
162
+ **קבצים שנוצרו:**
163
+ - `pycatan/web_visualization.py`
164
+ - `pycatan/templates/index.html`
165
+ - `pycatan/static/css/style.css`
166
+ - `pycatan/static/js/main.js`
167
+ - `pycatan/static/js/board.js`
168
+ - `pycatan/static/js/gameData.js`
169
+ - `tests/test_web_visualization.py`
170
+ - `examples/demo_web_visualization.py`
171
+
172
+ **פיצ'רים מתקדמים:**
173
+ - 🌐 Real-time board visualization בדפדפן
174
+ - 📡 Server-Sent Events לעדכונים מיידיים
175
+ - 🎮 לוח אינטרקטיבי עם zoom, pan, ו-vertex display
176
+ - 📊 Panel מידע שחקנים עם נקודות זכייה וקלפים
177
+ - 📜 Action log בזמן אמת עם הבחנה בין הצלחה לכישלון
178
+ - 🔄 תמיכה בהתחברות מרובת clients
179
+ - 🚀 Auto-browser opening ו-graceful shutdown
.github/instructions/STEP_BY_STEP_GUIDE.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ הוראה חשובה!
2
+ אחרי שאתה מסיים לבנות חלק מסויים עצור וודא שהמשתמש שמתקשר איתך מבין מה אתה עושה, קח בחשבון שהמשתמש שמתקשר איתך מבין פייתון אבל לא מאסטר בפייתון ולכן חשוב שתשקף מה אתה עושה ולמה
3
+
4
+ תמצא את האיזון הנכון, בין לשקף מה אתה עושה, ללפתח
.github/instructions/WEB_VISUALIZATION_GUIDE.md ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # מדריך ה-Web Visualization של PyCatan
2
+
3
+ ## סקירה כללית
4
+
5
+ ה-Web Visualization של PyCatan הוא מערכת visualization מתקדמת שמאפשרת צפייה במשחקי Catan בדפדפן בזמן אמת. המערכת בנויה על ארכיטקטורה client-server עם עדכונים מיידיים ואינטראקטיביות מלאה.
6
+
7
+ ## 🏗️ ארכיטקטורה כללית
8
+
9
+ ```
10
+ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
11
+ │ │ HTTP │ │ SSE │ │
12
+ │ Browser │◄────────┤ Flask Server │────────►│ Game Data │
13
+ │ (Client) │ │ (Backend) │ │ (PyCatan) │
14
+ │ │ │ │ │ │
15
+ │ - HTML/CSS │ │ - Web Routes │ │ - GameState │
16
+ │ - JavaScript │ │ - SSE Events │ │ - Player Data │
17
+ │ - Board Display │ │ - Data Conversion │ │ - Board Data │
18
+ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘
19
+ ```
20
+
21
+ ## 🖥️ הרכיבים העיקריים
22
+
23
+ ### 1. Flask Server (Backend)
24
+ **קובץ:** `pycatan/web_visualization.py`
25
+
26
+ **תפקידים:**
27
+ - 🌍 **Web Server:** מפעיל שרת Flask על `http://localhost:5001`
28
+ - 📁 **Static Files:** מגיש קבצי HTML, CSS, JavaScript
29
+ - 📡 **API Endpoints:** מספק נתונים דרך HTTP
30
+ - 🔄 **Real-time Updates:** שולח עדכונים בזמן אמת דרך SSE
31
+
32
+ **המסלולים (Routes) העיקריים:**
33
+ ```python
34
+ @self.app.route('/') # דף הבית - index.html
35
+ @self.app.route('/api/game-state') # מצב המשחק הנוכחי
36
+ @self.app.route('/api/events') # עדכונים בזמן אמת (SSE)
37
+ @self.app.route('/api/point_mapping') # מיפוי נקודות הלוח
38
+ ```
39
+
40
+ ### 2. Frontend JavaScript (Client)
41
+ **מיקום:** `pycatan/static/js/`
42
+
43
+ #### **קבצי JavaScript עיקריים:**
44
+
45
+ **`main.js` - המנהל הראשי:**
46
+ - 🔌 חיבור לשרת Flask
47
+ - 📡 ניהול Server-Sent Events
48
+ - 🎛️ כפתורי בקרה (זום, reset וכו')
49
+ - 🎯 ניהול state המשחק
50
+
51
+ **`board.js` - מנוע הלוח:**
52
+ - 🎲 הלוח האינטרקטיבי של Catan
53
+ - 🔶 הצגת 19 משושי Catan עם צבעים ומספרים
54
+ - 🏘️ הצגת settlements ו-cities של השחקנים
55
+ - 🛣️ הצגת roads בצבעי השחקנים
56
+ - 🔍 זום, גרירה ואינטראקטיביות מלאה
57
+ - 📍 הצגת נקודות לבניית מבנים
58
+
59
+ **`gameData.js` - נתוני דמו:**
60
+ - 💾 נתוני fallback שמוצגים אם אין חיבור לשרת
61
+ - 🎮 מכיל לוח Catan מלא עם שחקנים, מבנים וכבישים
62
+
63
+ ### 3. HTML & CSS
64
+ **מיקום:** `pycatan/templates/` & `pycatan/static/css/`
65
+
66
+ - **`index.html`** - מבנה הדף הראשי
67
+ - **`style.css`** - עיצוב ואנימציות
68
+ - **SVG Graphics** - לוח אינטרקטיבי מבוסס וקטורים
69
+
70
+ ## 📡 Server-Sent Events (SSE) - הטכנולוגיה המרכזית
71
+
72
+ ### מה זה SSE?
73
+ **Server-Sent Events** מאפשר לשרת לשלוח עדכונים לדפדפן **בזמן אמת** ללא צורך בשליחת בקשות חוזרות.
74
+
75
+ ### איך זה עובד?
76
+
77
+ **🔌 בצד הלקוח (JavaScript):**
78
+ ```javascript
79
+ eventSource = new EventSource('/api/events');
80
+
81
+ eventSource.onmessage = function(event) {
82
+ const data = JSON.parse(event.data);
83
+ if (data.type === 'game_update') {
84
+ updateGameState(data.payload); // עדכן את הלוח!
85
+ }
86
+ };
87
+ ```
88
+
89
+ **📡 בצד השרת (Python):**
90
+ ```python
91
+ def _broadcast_to_clients(self, event_data):
92
+ for client_queue in self.sse_clients:
93
+ client_queue.put_nowait(event_data) # שלח לכל הלקוחות!
94
+ ```
95
+
96
+ **🎯 סוגי העדכונים שנשלחים:**
97
+ - **`game_update`** - מצב משחק מלא
98
+ - **`action_executed`** - פעולה בוצעה
99
+ - **`turn_start`** - תור חדש התחיל
100
+ - **`dice_roll`** - קוביות נגרלו
101
+ - **`heartbeat`** - שמירה על החיבור
102
+
103
+ ## 🔄 זרימת הנתונים המלאה
104
+
105
+ ### המסלול הקלאסי:
106
+
107
+ ```
108
+ 1. 🎮 PyCatan Game
109
+ ↓ (קורא ל)
110
+ 2. 🖥️ GameManager.update_visualizations()
111
+ ↓ (מעביר ל)
112
+ 3. 🌐 WebVisualization.update_full_state(game_state)
113
+ ↓ (ממיר ל)
114
+ 4. 📊 _convert_game_state() → web_state
115
+ ↓ (שולח עם)
116
+ 5. 📡 _broadcast_to_clients({'type': 'game_update', 'payload': web_state})
117
+ ↓ (מגיע ל)
118
+ 6. 🌍 Browser: eventSource.onmessage()
119
+ ↓ (מעדכן ל)
120
+ 7. 🎲 CatanBoard.updateFromGameState()
121
+ ↓ (מציג ב)
122
+ 8. 👀 Visual Board Display
123
+ ```
124
+
125
+ ### המרת נתונים PyCatan ↔ Web:
126
+
127
+ **🎯 דוגמה להמרה:**
128
+ ```python
129
+ # PyCatan Format:
130
+ GameState(
131
+ players_state=[PlayerState(name="Alice", cards=["wood", "brick"])],
132
+ board_state=BoardState(tiles=[{"type": "forest", "token": 11}])
133
+ )
134
+
135
+ # ↓ המרה ↓
136
+
137
+ # Web Format:
138
+ {
139
+ 'players': [{'id': 0, 'name': 'Alice', 'total_cards': 2}],
140
+ 'hexes': [{'id': 1, 'type': 'wood', 'number': 11}],
141
+ 'current_player': 0,
142
+ 'settlements': [],
143
+ 'cities': [],
144
+ 'roads': []
145
+ }
146
+ ```
147
+
148
+ ## 🚀 תהליך הטעינה והאתחול
149
+
150
+ ### 1. טעינה ראשונית:
151
+ ```
152
+ 1. 🌍 Browser נטען → index.html
153
+ 2. 📜 main.js נטען
154
+ 3. 🗺️ loadPointMapping() - טוען מיפוי נקודות
155
+ 4. 📊 fetch('/api/game-state') - טוען מצב ראשוני
156
+ 5. 🔌 new EventSource('/api/events') - מתחבר לעדכונים
157
+ 6. 🎲 catanBoard.createBoard() - בונה את הלוח הגרפי
158
+ 7. ✅ מוכן לעדכונים בזמן אמת!
159
+ ```
160
+
161
+ ### 2. החיבור לSSE:
162
+ ```python
163
+ @self.app.route('/api/events')
164
+ def sse_events():
165
+ # יוצר queue עבור הלקוח החדש
166
+ client_queue = Queue()
167
+ self.sse_clients.append(client_queue)
168
+
169
+ # שולח מצב משחק ראשוני
170
+ if self.current_game_state:
171
+ yield f"data: {json.dumps({'type': 'game_update', 'payload': self.current_game_state})}\n\n"
172
+
173
+ # מאזין לעדכונים חדשים
174
+ while True:
175
+ event_data = client_queue.get(timeout=30)
176
+ yield f"data: {json.dumps(event_data)}\n\n"
177
+ ```
178
+
179
+ ## 🎮 פיצ'רים ויכולות
180
+
181
+ ### אינטראקטיביות:
182
+ - **🔍 זום:** גלגל העכבר או כפתורי +/-
183
+ - **🖱️ גרירה:** לחיצה וגרירה להזזת הלוח
184
+ - **📍 נקודות:** הצגה/הסתרה של נקודות בניה
185
+ - **🔄 reset:** חזרה למצב ראשוני
186
+ - **🎯 לחיצות:** על משושים להעברת השודד
187
+
188
+ ### תצוגה:
189
+ - **🎨 צבעים:** כל סוג משאב בצבע שונה
190
+ - **🏘️ מבנים:** settlements (עיגולים) ו-cities (ריבועים)
191
+ - **🛣️ כבישים:** קווים בצבעי השחקנים
192
+ - **🎲 מידע:** פאנל מידע שחקנים ויומן פעולות
193
+ - **📊 Real-time:** עדכונים מיידיים
194
+
195
+ ### רב-משתמש:
196
+ - **🌐 Multiple Clients:** כמה דפדפנים יכולים לצפות באותו משחק
197
+ - **🔄 Sync:** כל הלקוחות רואים אותו דבר בו-זמנית
198
+ - **📡 Broadcast:** עדכון אחד נשלח לכל המחוברים
199
+
200
+ ## 🔧 קבצי המערכת
201
+
202
+ ### Backend (Python):
203
+ ```
204
+ pycatan/web_visualization.py # השרת הראשי
205
+ pycatan/visualization.py # Base class
206
+ pycatan/actions.py # Data structures
207
+ ```
208
+
209
+ ### Frontend (Web):
210
+ ```
211
+ pycatan/templates/index.html # דף ה-HTML הראשי
212
+ pycatan/static/css/style.css # עיצוב CSS
213
+ pycatan/static/js/main.js # JavaScript ראשי
214
+ pycatan/static/js/board.js # לוח אינטרקטיבי
215
+ pycatan/static/js/gameData.js # נתוני דמו
216
+ ```
217
+
218
+ ### Tests & Examples:
219
+ ```
220
+ tests/test_web_visualization.py # בדיקות יחידה
221
+ examples/demo_web_visualization.py # דוגמה אינטרקטיבית
222
+ test_web_visualization_full.py # בדיקה מקיפה
223
+ ```
224
+
225
+ ## 💡 שימושים ודוגמאות
226
+
227
+ ### הפעלה בסיסית:
228
+ ```python
229
+ from pycatan.web_visualization import WebVisualization
230
+ from pycatan.actions import GameState
231
+
232
+ # יצירת visualizer
233
+ web_viz = WebVisualization(port=5001, auto_open=True)
234
+
235
+ # התחלת שרת
236
+ web_viz.start_server()
237
+
238
+ # עדכון מצב משחק
239
+ game_state = create_game_state()
240
+ web_viz.update_full_state(game_state)
241
+
242
+ # הדפדפן ייפתח אוטומטית ב-http://localhost:5001
243
+ ```
244
+
245
+ ### שימוש במערכת המלאה:
246
+ ```python
247
+ from pycatan import GameManager, HumanUser
248
+ from pycatan.web_visualization import WebVisualization
249
+ from pycatan.console_visualization import ConsoleVisualization
250
+
251
+ # יצירת משתמשים
252
+ users = [HumanUser("Alice"), HumanUser("Bob")]
253
+
254
+ # יצירת visualizations
255
+ web_viz = WebVisualization()
256
+ console_viz = ConsoleVisualization()
257
+ visualizations = [web_viz, console_viz]
258
+
259
+ # יצירת מנהל משחק
260
+ game_manager = GameManager(users, visualizations)
261
+
262
+ # הפעלת משחק - הוא יופיע גם בקונסול וגם בדפדפן!
263
+ game_manager.start_game()
264
+ ```
265
+
266
+ ## 🎯 יתרונות המערכת
267
+
268
+ ### טכניים:
269
+ - **🔄 Real-time:** עדכונים מיידיים ללא refresh
270
+ - **📱 Cross-platform:** עובד בכל דפדפן מודרני
271
+ - **🔌 Resilient:** fallback לנתוני דמו אם אין חיבור
272
+ - **🎛️ Interactive:** אינטראקטיביות מלאה עם הלוח
273
+ - **🚀 Performance:** SVG מהיר ויעיל
274
+
275
+ ### מבחינת משתמש:
276
+ - **👀 Visual:** צפייה נוחה ואינטואיטיבית
277
+ - **🎮 Multiple Viewers:** כמה אנשים יכולים לצפות
278
+ - **📊 Rich Info:** מידע מפורט על השחקנים
279
+ - **📜 Action Log:** מעקב אחר כל הפעולות
280
+ - **🔍 Zoom & Pan:** ניווט חופשי בלוח
281
+
282
+ ## 🐛 דיבוג ופתרון בעיות
283
+
284
+ ### בעיות נפוצות:
285
+
286
+ **1. הלוח לא נטען:**
287
+ - בדוק שהשרת פועל על http://localhost:5001
288
+ - בדוק את קונסול הדפדפן לשגיאות JavaScript
289
+ - ודא שקבצי ה-static נגישים
290
+
291
+ **2. אין עדכונים בזמן אמת:**
292
+ - בדוק חיבור SSE בקונסול הדפדפן
293
+ - ודא ש-`_broadcast_to_clients()` נקרא
294
+ - בדוק שהלקוח רשום ב-`self.sse_clients`
295
+
296
+ **3. משושים מוצגים לא נכון:**
297
+ - בדוק את המיפוי ב-`_convert_hexes()`
298
+ - ודא שהנתונים מגיעים בפורמט הנכון
299
+ - בדוק את ה-`tile_type_map`
300
+
301
+ ### כלי דיבוג:
302
+ - **Console Logs:** הרבה הדפסות debug במערכת
303
+ - **Network Tab:** בדוק בקשות HTTP ו-SSE
304
+ - **Elements Inspector:** בדוק את ה-SVG שנוצר
305
+ - **Flask Debug:** הפעל עם `debug=True`
306
+
307
+ ## 🔮 עתיד והרחבות
308
+
309
+ ### אפשרויות הרחבה:
310
+ - **🤖 AI Player Control:** שליטה על שחקני AI מהדפדפן
311
+ - **💬 Chat System:** מערכת צ'אט למשחק מרובה משתתפים
312
+ - **📊 Statistics:** סטטיסטיקות משחק מפורטות
313
+ - **🎵 Sound Effects:** אפקטי קול לפעולות
314
+ - **📱 Mobile Support:** תמיכה משופרת במובייל
315
+ - **🎥 Replay System:** שמירה והשמעה של משחקים
316
+
317
+ ### אינטגרציה עם מערכות אחרות:
318
+ - **🌐 Web Multiplayer:** משחק מרובה משתתפים אמיתי
319
+ - **📡 WebSocket:** עבור אינטראקציה דו-כיוונית
320
+ - **💾 Database:** שמירת משחקים וסטטיסטיקות
321
+ - **🔐 Authentication:** מערכת התחברות משתמשים
322
+
323
+ ---
324
+
325
+ ## 📝 סיכום
326
+
327
+ ה-Web Visualization של PyCatan הוא מערכת visualization מתקדמת ואמינה שמספקת חוויית צפייה עשירה במשחקי Catan. המערכת משלבת טכנולוגיות מודרניות כמו SSE, SVG ו-Flask ליצירת פלטפורמה אינטראקטיבית ומהירה.
328
+
329
+ המערכת מספקת:
330
+ - צפייה בזמן אמת במשחק
331
+ - אינטראקטיביות מלאה עם הלוח
332
+ - תמיכה במספר צופים בו-זמנית
333
+ - fallback מחשבתי לכשלי רשת
334
+ - ארכיטקטורה ניתנת להרחבה
335
+
336
+ **זהו הבסיס המושלם לפיתוח מערכת multiplayer מלאה של Catan!** 🎉
.gitignore ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Windows image file caches
2
+ Thumbs.db
3
+ ehthumbs.db
4
+
5
+ # Folder config file
6
+ Desktop.ini
7
+
8
+ # Recycle Bin used on file shares
9
+ $RECYCLE.BIN/
10
+
11
+ # Windows Installer files
12
+ *.cab
13
+ *.msi
14
+ *.msm
15
+ *.msp
16
+
17
+ # Windows shortcuts
18
+ *.lnk
19
+
20
+ # =========================
21
+ # Operating System Files
22
+ # =========================
23
+
24
+ # OSX
25
+ # =========================
26
+
27
+ .DS_Store
28
+ .AppleDouble
29
+ .LSOverride
30
+
31
+ # Thumbnails
32
+ ._*
33
+
34
+ # Files that might appear in the root of a volume
35
+ .DocumentRevisions-V100
36
+ .fseventsd
37
+ .Spotlight-V100
38
+ .TemporaryItems
39
+ .Trashes
40
+ .VolumeIcon.icns
41
+
42
+ # Directories potentially created on remote AFP share
43
+ .AppleDB
44
+ .AppleDesktop
45
+ Network Trash Folder
46
+ Temporary Items
47
+ .apdisk
48
+
49
+ # Manually added stuff below this ------------------------------------------
50
+
51
+ # the pychache folders
52
+ __pycache__/
53
+
54
+ # other PIP files
55
+ pycatan.egg-info/
56
+
57
+ # the virtualenv cache
58
+ .cache
59
+
60
+ # the virtualenvwrapper folders created when testing
61
+ lib/
62
+ bin/
63
+ include/
64
+ pip-selfcheck.json
65
+ .pytest_cache/
CHANGES.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ v1.0, 2017 -- Initial Release
LICENSE.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2017 Josef Waller
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
MANIFEST.in ADDED
@@ -0,0 +1 @@
 
 
1
+ include readme.md
Pipfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [[source]]
2
+ verify_ssl = true
3
+ url = "https://pypi.python.org/simple"
4
+
5
+ [packages]
6
+ pytest = "*"
Pipfile.lock ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_meta": {
3
+ "hash": {
4
+ "sha256": "8d28e503af2a5207eb2e6fe53e8b11567cbb25cd3a16efe8370dc00cdaea7614"
5
+ },
6
+ "host-environment-markers": {
7
+ "implementation_name": "cpython",
8
+ "implementation_version": "3.5.2",
9
+ "os_name": "posix",
10
+ "platform_machine": "x86_64",
11
+ "platform_python_implementation": "CPython",
12
+ "platform_release": "4.4.0-43-Microsoft",
13
+ "platform_system": "Linux",
14
+ "platform_version": "#1-Microsoft Wed Dec 31 14:42:53 PST 2014",
15
+ "python_full_version": "3.5.2",
16
+ "python_version": "3.5",
17
+ "sys_platform": "linux"
18
+ },
19
+ "pipfile-spec": 3,
20
+ "requires": {},
21
+ "sources": [
22
+ {
23
+ "url": "https://pypi.python.org/simple",
24
+ "verify_ssl": true
25
+ }
26
+ ]
27
+ },
28
+ "default": {
29
+ "py": {
30
+ "hashes": [
31
+ "sha256:2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a",
32
+ "sha256:0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3"
33
+ ],
34
+ "version": "==1.4.34"
35
+ },
36
+ "pytest": {
37
+ "hashes": [
38
+ "sha256:b84f554f8ddc23add65c411bf112b2d88e2489fd45f753b1cae5936358bdf314",
39
+ "sha256:f46e49e0340a532764991c498244a60e3a37d7424a532b3ff1a6a7653f1a403a"
40
+ ],
41
+ "version": "==3.2.2"
42
+ }
43
+ },
44
+ "develop": {}
45
+ }
board_definition.json ADDED
@@ -0,0 +1,1435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "hexes": {
3
+ "1": {
4
+ "hex_id": 1,
5
+ "game_coords": [
6
+ 0,
7
+ 0
8
+ ],
9
+ "axial_coords": [
10
+ 0,
11
+ -2
12
+ ],
13
+ "adjacent_points": [
14
+ 1,
15
+ 2,
16
+ 3,
17
+ 9,
18
+ 10,
19
+ 11
20
+ ]
21
+ },
22
+ "2": {
23
+ "hex_id": 2,
24
+ "game_coords": [
25
+ 0,
26
+ 1
27
+ ],
28
+ "axial_coords": [
29
+ 1,
30
+ -2
31
+ ],
32
+ "adjacent_points": [
33
+ 3,
34
+ 4,
35
+ 5,
36
+ 11,
37
+ 12,
38
+ 13
39
+ ]
40
+ },
41
+ "3": {
42
+ "hex_id": 3,
43
+ "game_coords": [
44
+ 0,
45
+ 2
46
+ ],
47
+ "axial_coords": [
48
+ 2,
49
+ -2
50
+ ],
51
+ "adjacent_points": [
52
+ 5,
53
+ 6,
54
+ 7,
55
+ 13,
56
+ 14,
57
+ 15
58
+ ]
59
+ },
60
+ "4": {
61
+ "hex_id": 4,
62
+ "game_coords": [
63
+ 1,
64
+ 0
65
+ ],
66
+ "axial_coords": [
67
+ -1,
68
+ -1
69
+ ],
70
+ "adjacent_points": [
71
+ 8,
72
+ 9,
73
+ 10,
74
+ 18,
75
+ 19,
76
+ 20
77
+ ]
78
+ },
79
+ "5": {
80
+ "hex_id": 5,
81
+ "game_coords": [
82
+ 1,
83
+ 1
84
+ ],
85
+ "axial_coords": [
86
+ 0,
87
+ -1
88
+ ],
89
+ "adjacent_points": [
90
+ 10,
91
+ 11,
92
+ 12,
93
+ 20,
94
+ 21,
95
+ 22
96
+ ]
97
+ },
98
+ "6": {
99
+ "hex_id": 6,
100
+ "game_coords": [
101
+ 1,
102
+ 2
103
+ ],
104
+ "axial_coords": [
105
+ 1,
106
+ -1
107
+ ],
108
+ "adjacent_points": [
109
+ 12,
110
+ 13,
111
+ 14,
112
+ 22,
113
+ 23,
114
+ 24
115
+ ]
116
+ },
117
+ "7": {
118
+ "hex_id": 7,
119
+ "game_coords": [
120
+ 1,
121
+ 3
122
+ ],
123
+ "axial_coords": [
124
+ 2,
125
+ -1
126
+ ],
127
+ "adjacent_points": [
128
+ 14,
129
+ 15,
130
+ 16,
131
+ 24,
132
+ 25,
133
+ 26
134
+ ]
135
+ },
136
+ "8": {
137
+ "hex_id": 8,
138
+ "game_coords": [
139
+ 2,
140
+ 0
141
+ ],
142
+ "axial_coords": [
143
+ -2,
144
+ 0
145
+ ],
146
+ "adjacent_points": [
147
+ 17,
148
+ 18,
149
+ 19,
150
+ 28,
151
+ 29,
152
+ 30
153
+ ]
154
+ },
155
+ "9": {
156
+ "hex_id": 9,
157
+ "game_coords": [
158
+ 2,
159
+ 1
160
+ ],
161
+ "axial_coords": [
162
+ -1,
163
+ 0
164
+ ],
165
+ "adjacent_points": [
166
+ 19,
167
+ 20,
168
+ 21,
169
+ 30,
170
+ 31,
171
+ 32
172
+ ]
173
+ },
174
+ "10": {
175
+ "hex_id": 10,
176
+ "game_coords": [
177
+ 2,
178
+ 2
179
+ ],
180
+ "axial_coords": [
181
+ 0,
182
+ 0
183
+ ],
184
+ "adjacent_points": [
185
+ 21,
186
+ 22,
187
+ 23,
188
+ 32,
189
+ 33,
190
+ 34
191
+ ]
192
+ },
193
+ "11": {
194
+ "hex_id": 11,
195
+ "game_coords": [
196
+ 2,
197
+ 3
198
+ ],
199
+ "axial_coords": [
200
+ 1,
201
+ 0
202
+ ],
203
+ "adjacent_points": [
204
+ 23,
205
+ 24,
206
+ 25,
207
+ 34,
208
+ 35,
209
+ 36
210
+ ]
211
+ },
212
+ "12": {
213
+ "hex_id": 12,
214
+ "game_coords": [
215
+ 2,
216
+ 4
217
+ ],
218
+ "axial_coords": [
219
+ 2,
220
+ 0
221
+ ],
222
+ "adjacent_points": [
223
+ 25,
224
+ 26,
225
+ 27,
226
+ 36,
227
+ 37,
228
+ 38
229
+ ]
230
+ },
231
+ "13": {
232
+ "hex_id": 13,
233
+ "game_coords": [
234
+ 3,
235
+ 0
236
+ ],
237
+ "axial_coords": [
238
+ -2,
239
+ 1
240
+ ],
241
+ "adjacent_points": [
242
+ 29,
243
+ 30,
244
+ 31,
245
+ 39,
246
+ 40,
247
+ 41
248
+ ]
249
+ },
250
+ "14": {
251
+ "hex_id": 14,
252
+ "game_coords": [
253
+ 3,
254
+ 1
255
+ ],
256
+ "axial_coords": [
257
+ -1,
258
+ 1
259
+ ],
260
+ "adjacent_points": [
261
+ 31,
262
+ 32,
263
+ 33,
264
+ 41,
265
+ 42,
266
+ 43
267
+ ]
268
+ },
269
+ "15": {
270
+ "hex_id": 15,
271
+ "game_coords": [
272
+ 3,
273
+ 2
274
+ ],
275
+ "axial_coords": [
276
+ 0,
277
+ 1
278
+ ],
279
+ "adjacent_points": [
280
+ 33,
281
+ 34,
282
+ 35,
283
+ 43,
284
+ 44,
285
+ 45
286
+ ]
287
+ },
288
+ "16": {
289
+ "hex_id": 16,
290
+ "game_coords": [
291
+ 3,
292
+ 3
293
+ ],
294
+ "axial_coords": [
295
+ 1,
296
+ 1
297
+ ],
298
+ "adjacent_points": [
299
+ 35,
300
+ 36,
301
+ 37,
302
+ 45,
303
+ 46,
304
+ 47
305
+ ]
306
+ },
307
+ "17": {
308
+ "hex_id": 17,
309
+ "game_coords": [
310
+ 4,
311
+ 0
312
+ ],
313
+ "axial_coords": [
314
+ -2,
315
+ 2
316
+ ],
317
+ "adjacent_points": [
318
+ 40,
319
+ 41,
320
+ 42,
321
+ 48,
322
+ 49,
323
+ 50
324
+ ]
325
+ },
326
+ "18": {
327
+ "hex_id": 18,
328
+ "game_coords": [
329
+ 4,
330
+ 1
331
+ ],
332
+ "axial_coords": [
333
+ -1,
334
+ 2
335
+ ],
336
+ "adjacent_points": [
337
+ 42,
338
+ 43,
339
+ 44,
340
+ 50,
341
+ 51,
342
+ 52
343
+ ]
344
+ },
345
+ "19": {
346
+ "hex_id": 19,
347
+ "game_coords": [
348
+ 4,
349
+ 2
350
+ ],
351
+ "axial_coords": [
352
+ 0,
353
+ 2
354
+ ],
355
+ "adjacent_points": [
356
+ 44,
357
+ 45,
358
+ 46,
359
+ 52,
360
+ 53,
361
+ 54
362
+ ]
363
+ }
364
+ },
365
+ "points": {
366
+ "1": {
367
+ "point_id": 1,
368
+ "game_coords": [
369
+ 0,
370
+ 0
371
+ ],
372
+ "pixel_coords": [
373
+ 377.5,
374
+ 105.14428414850131
375
+ ],
376
+ "adjacent_points": [
377
+ 2,
378
+ 9
379
+ ],
380
+ "adjacent_hexes": [
381
+ 1
382
+ ]
383
+ },
384
+ "2": {
385
+ "point_id": 2,
386
+ "game_coords": [
387
+ 0,
388
+ 1
389
+ ],
390
+ "pixel_coords": [
391
+ 422.5,
392
+ 105.14428414850128
393
+ ],
394
+ "adjacent_points": [
395
+ 1,
396
+ 3
397
+ ],
398
+ "adjacent_hexes": [
399
+ 1
400
+ ]
401
+ },
402
+ "3": {
403
+ "point_id": 3,
404
+ "game_coords": [
405
+ 0,
406
+ 2
407
+ ],
408
+ "pixel_coords": [
409
+ 445,
410
+ 144.11542731880104
411
+ ],
412
+ "adjacent_points": [
413
+ 2,
414
+ 4,
415
+ 11
416
+ ],
417
+ "adjacent_hexes": [
418
+ 2,
419
+ 1
420
+ ]
421
+ },
422
+ "4": {
423
+ "point_id": 4,
424
+ "game_coords": [
425
+ 0,
426
+ 3
427
+ ],
428
+ "pixel_coords": [
429
+ 490,
430
+ 144.11542731880104
431
+ ],
432
+ "adjacent_points": [
433
+ 3,
434
+ 5
435
+ ],
436
+ "adjacent_hexes": [
437
+ 2
438
+ ]
439
+ },
440
+ "5": {
441
+ "point_id": 5,
442
+ "game_coords": [
443
+ 0,
444
+ 4
445
+ ],
446
+ "pixel_coords": [
447
+ 512.5,
448
+ 183.08657048910078
449
+ ],
450
+ "adjacent_points": [
451
+ 4,
452
+ 6,
453
+ 13
454
+ ],
455
+ "adjacent_hexes": [
456
+ 3,
457
+ 2
458
+ ]
459
+ },
460
+ "6": {
461
+ "point_id": 6,
462
+ "game_coords": [
463
+ 0,
464
+ 5
465
+ ],
466
+ "pixel_coords": [
467
+ 557.5,
468
+ 183.08657048910078
469
+ ],
470
+ "adjacent_points": [
471
+ 5,
472
+ 7
473
+ ],
474
+ "adjacent_hexes": [
475
+ 3
476
+ ]
477
+ },
478
+ "7": {
479
+ "point_id": 7,
480
+ "game_coords": [
481
+ 0,
482
+ 6
483
+ ],
484
+ "pixel_coords": [
485
+ 580,
486
+ 222.05771365940052
487
+ ],
488
+ "adjacent_points": [
489
+ 6,
490
+ 15
491
+ ],
492
+ "adjacent_hexes": [
493
+ 3
494
+ ]
495
+ },
496
+ "8": {
497
+ "point_id": 8,
498
+ "game_coords": [
499
+ 1,
500
+ 0
501
+ ],
502
+ "pixel_coords": [
503
+ 310,
504
+ 144.11542731880104
505
+ ],
506
+ "adjacent_points": [
507
+ 9,
508
+ 18
509
+ ],
510
+ "adjacent_hexes": [
511
+ 4
512
+ ]
513
+ },
514
+ "9": {
515
+ "point_id": 9,
516
+ "game_coords": [
517
+ 1,
518
+ 1
519
+ ],
520
+ "pixel_coords": [
521
+ 355,
522
+ 144.11542731880104
523
+ ],
524
+ "adjacent_points": [
525
+ 8,
526
+ 10,
527
+ 1
528
+ ],
529
+ "adjacent_hexes": [
530
+ 4,
531
+ 1
532
+ ]
533
+ },
534
+ "10": {
535
+ "point_id": 10,
536
+ "game_coords": [
537
+ 1,
538
+ 2
539
+ ],
540
+ "pixel_coords": [
541
+ 377.5,
542
+ 183.08657048910078
543
+ ],
544
+ "adjacent_points": [
545
+ 9,
546
+ 11,
547
+ 20
548
+ ],
549
+ "adjacent_hexes": [
550
+ 5,
551
+ 4,
552
+ 1
553
+ ]
554
+ },
555
+ "11": {
556
+ "point_id": 11,
557
+ "game_coords": [
558
+ 1,
559
+ 3
560
+ ],
561
+ "pixel_coords": [
562
+ 422.5,
563
+ 183.08657048910078
564
+ ],
565
+ "adjacent_points": [
566
+ 10,
567
+ 12,
568
+ 3
569
+ ],
570
+ "adjacent_hexes": [
571
+ 5,
572
+ 2,
573
+ 1
574
+ ]
575
+ },
576
+ "12": {
577
+ "point_id": 12,
578
+ "game_coords": [
579
+ 1,
580
+ 4
581
+ ],
582
+ "pixel_coords": [
583
+ 445,
584
+ 222.05771365940052
585
+ ],
586
+ "adjacent_points": [
587
+ 11,
588
+ 13,
589
+ 22
590
+ ],
591
+ "adjacent_hexes": [
592
+ 6,
593
+ 5,
594
+ 2
595
+ ]
596
+ },
597
+ "13": {
598
+ "point_id": 13,
599
+ "game_coords": [
600
+ 1,
601
+ 5
602
+ ],
603
+ "pixel_coords": [
604
+ 490,
605
+ 222.05771365940052
606
+ ],
607
+ "adjacent_points": [
608
+ 12,
609
+ 14,
610
+ 5
611
+ ],
612
+ "adjacent_hexes": [
613
+ 6,
614
+ 3,
615
+ 2
616
+ ]
617
+ },
618
+ "14": {
619
+ "point_id": 14,
620
+ "game_coords": [
621
+ 1,
622
+ 6
623
+ ],
624
+ "pixel_coords": [
625
+ 512.5,
626
+ 261.02885682970026
627
+ ],
628
+ "adjacent_points": [
629
+ 13,
630
+ 15,
631
+ 24
632
+ ],
633
+ "adjacent_hexes": [
634
+ 7,
635
+ 6,
636
+ 3
637
+ ]
638
+ },
639
+ "15": {
640
+ "point_id": 15,
641
+ "game_coords": [
642
+ 1,
643
+ 7
644
+ ],
645
+ "pixel_coords": [
646
+ 557.5,
647
+ 261.02885682970026
648
+ ],
649
+ "adjacent_points": [
650
+ 14,
651
+ 16,
652
+ 7
653
+ ],
654
+ "adjacent_hexes": [
655
+ 7,
656
+ 3
657
+ ]
658
+ },
659
+ "16": {
660
+ "point_id": 16,
661
+ "game_coords": [
662
+ 1,
663
+ 8
664
+ ],
665
+ "pixel_coords": [
666
+ 580,
667
+ 300
668
+ ],
669
+ "adjacent_points": [
670
+ 15,
671
+ 26
672
+ ],
673
+ "adjacent_hexes": [
674
+ 7
675
+ ]
676
+ },
677
+ "17": {
678
+ "point_id": 17,
679
+ "game_coords": [
680
+ 2,
681
+ 0
682
+ ],
683
+ "pixel_coords": [
684
+ 242.49999999999997,
685
+ 183.08657048910078
686
+ ],
687
+ "adjacent_points": [
688
+ 18,
689
+ 28
690
+ ],
691
+ "adjacent_hexes": [
692
+ 8
693
+ ]
694
+ },
695
+ "18": {
696
+ "point_id": 18,
697
+ "game_coords": [
698
+ 2,
699
+ 1
700
+ ],
701
+ "pixel_coords": [
702
+ 287.5,
703
+ 183.08657048910078
704
+ ],
705
+ "adjacent_points": [
706
+ 17,
707
+ 19,
708
+ 8
709
+ ],
710
+ "adjacent_hexes": [
711
+ 8,
712
+ 4
713
+ ]
714
+ },
715
+ "19": {
716
+ "point_id": 19,
717
+ "game_coords": [
718
+ 2,
719
+ 2
720
+ ],
721
+ "pixel_coords": [
722
+ 310,
723
+ 222.05771365940052
724
+ ],
725
+ "adjacent_points": [
726
+ 18,
727
+ 20,
728
+ 30
729
+ ],
730
+ "adjacent_hexes": [
731
+ 9,
732
+ 8,
733
+ 4
734
+ ]
735
+ },
736
+ "20": {
737
+ "point_id": 20,
738
+ "game_coords": [
739
+ 2,
740
+ 3
741
+ ],
742
+ "pixel_coords": [
743
+ 355,
744
+ 222.05771365940052
745
+ ],
746
+ "adjacent_points": [
747
+ 19,
748
+ 21,
749
+ 10
750
+ ],
751
+ "adjacent_hexes": [
752
+ 9,
753
+ 5,
754
+ 4
755
+ ]
756
+ },
757
+ "21": {
758
+ "point_id": 21,
759
+ "game_coords": [
760
+ 2,
761
+ 4
762
+ ],
763
+ "pixel_coords": [
764
+ 377.5,
765
+ 261.02885682970026
766
+ ],
767
+ "adjacent_points": [
768
+ 20,
769
+ 22,
770
+ 32
771
+ ],
772
+ "adjacent_hexes": [
773
+ 10,
774
+ 9,
775
+ 5
776
+ ]
777
+ },
778
+ "22": {
779
+ "point_id": 22,
780
+ "game_coords": [
781
+ 2,
782
+ 5
783
+ ],
784
+ "pixel_coords": [
785
+ 422.5,
786
+ 261.02885682970026
787
+ ],
788
+ "adjacent_points": [
789
+ 21,
790
+ 23,
791
+ 12
792
+ ],
793
+ "adjacent_hexes": [
794
+ 10,
795
+ 6,
796
+ 5
797
+ ]
798
+ },
799
+ "23": {
800
+ "point_id": 23,
801
+ "game_coords": [
802
+ 2,
803
+ 6
804
+ ],
805
+ "pixel_coords": [
806
+ 445,
807
+ 300
808
+ ],
809
+ "adjacent_points": [
810
+ 22,
811
+ 24,
812
+ 34
813
+ ],
814
+ "adjacent_hexes": [
815
+ 11,
816
+ 10,
817
+ 6
818
+ ]
819
+ },
820
+ "24": {
821
+ "point_id": 24,
822
+ "game_coords": [
823
+ 2,
824
+ 7
825
+ ],
826
+ "pixel_coords": [
827
+ 490,
828
+ 300
829
+ ],
830
+ "adjacent_points": [
831
+ 23,
832
+ 25,
833
+ 14
834
+ ],
835
+ "adjacent_hexes": [
836
+ 11,
837
+ 7,
838
+ 6
839
+ ]
840
+ },
841
+ "25": {
842
+ "point_id": 25,
843
+ "game_coords": [
844
+ 2,
845
+ 8
846
+ ],
847
+ "pixel_coords": [
848
+ 512.5,
849
+ 338.97114317029974
850
+ ],
851
+ "adjacent_points": [
852
+ 24,
853
+ 26,
854
+ 36
855
+ ],
856
+ "adjacent_hexes": [
857
+ 12,
858
+ 11,
859
+ 7
860
+ ]
861
+ },
862
+ "26": {
863
+ "point_id": 26,
864
+ "game_coords": [
865
+ 2,
866
+ 9
867
+ ],
868
+ "pixel_coords": [
869
+ 557.5,
870
+ 338.97114317029974
871
+ ],
872
+ "adjacent_points": [
873
+ 25,
874
+ 27,
875
+ 16
876
+ ],
877
+ "adjacent_hexes": [
878
+ 12,
879
+ 7
880
+ ]
881
+ },
882
+ "27": {
883
+ "point_id": 27,
884
+ "game_coords": [
885
+ 2,
886
+ 10
887
+ ],
888
+ "pixel_coords": [
889
+ 580,
890
+ 377.9422863405995
891
+ ],
892
+ "adjacent_points": [
893
+ 26,
894
+ 38
895
+ ],
896
+ "adjacent_hexes": [
897
+ 12
898
+ ]
899
+ },
900
+ "28": {
901
+ "point_id": 28,
902
+ "game_coords": [
903
+ 3,
904
+ 0
905
+ ],
906
+ "pixel_coords": [
907
+ 220,
908
+ 222.05771365940052
909
+ ],
910
+ "adjacent_points": [
911
+ 29,
912
+ 17
913
+ ],
914
+ "adjacent_hexes": [
915
+ 8
916
+ ]
917
+ },
918
+ "29": {
919
+ "point_id": 29,
920
+ "game_coords": [
921
+ 3,
922
+ 1
923
+ ],
924
+ "pixel_coords": [
925
+ 242.5,
926
+ 261.02885682970026
927
+ ],
928
+ "adjacent_points": [
929
+ 28,
930
+ 30,
931
+ 39
932
+ ],
933
+ "adjacent_hexes": [
934
+ 13,
935
+ 8
936
+ ]
937
+ },
938
+ "30": {
939
+ "point_id": 30,
940
+ "game_coords": [
941
+ 3,
942
+ 2
943
+ ],
944
+ "pixel_coords": [
945
+ 287.5,
946
+ 261.02885682970026
947
+ ],
948
+ "adjacent_points": [
949
+ 29,
950
+ 31,
951
+ 19
952
+ ],
953
+ "adjacent_hexes": [
954
+ 13,
955
+ 9,
956
+ 8
957
+ ]
958
+ },
959
+ "31": {
960
+ "point_id": 31,
961
+ "game_coords": [
962
+ 3,
963
+ 3
964
+ ],
965
+ "pixel_coords": [
966
+ 310,
967
+ 300
968
+ ],
969
+ "adjacent_points": [
970
+ 30,
971
+ 32,
972
+ 41
973
+ ],
974
+ "adjacent_hexes": [
975
+ 14,
976
+ 13,
977
+ 9
978
+ ]
979
+ },
980
+ "32": {
981
+ "point_id": 32,
982
+ "game_coords": [
983
+ 3,
984
+ 4
985
+ ],
986
+ "pixel_coords": [
987
+ 355,
988
+ 300
989
+ ],
990
+ "adjacent_points": [
991
+ 31,
992
+ 33,
993
+ 21
994
+ ],
995
+ "adjacent_hexes": [
996
+ 14,
997
+ 10,
998
+ 9
999
+ ]
1000
+ },
1001
+ "33": {
1002
+ "point_id": 33,
1003
+ "game_coords": [
1004
+ 3,
1005
+ 5
1006
+ ],
1007
+ "pixel_coords": [
1008
+ 377.5,
1009
+ 338.97114317029974
1010
+ ],
1011
+ "adjacent_points": [
1012
+ 32,
1013
+ 34,
1014
+ 43
1015
+ ],
1016
+ "adjacent_hexes": [
1017
+ 15,
1018
+ 14,
1019
+ 10
1020
+ ]
1021
+ },
1022
+ "34": {
1023
+ "point_id": 34,
1024
+ "game_coords": [
1025
+ 3,
1026
+ 6
1027
+ ],
1028
+ "pixel_coords": [
1029
+ 422.5,
1030
+ 338.97114317029974
1031
+ ],
1032
+ "adjacent_points": [
1033
+ 33,
1034
+ 35,
1035
+ 23
1036
+ ],
1037
+ "adjacent_hexes": [
1038
+ 15,
1039
+ 11,
1040
+ 10
1041
+ ]
1042
+ },
1043
+ "35": {
1044
+ "point_id": 35,
1045
+ "game_coords": [
1046
+ 3,
1047
+ 7
1048
+ ],
1049
+ "pixel_coords": [
1050
+ 445,
1051
+ 377.9422863405995
1052
+ ],
1053
+ "adjacent_points": [
1054
+ 34,
1055
+ 36,
1056
+ 45
1057
+ ],
1058
+ "adjacent_hexes": [
1059
+ 16,
1060
+ 15,
1061
+ 11
1062
+ ]
1063
+ },
1064
+ "36": {
1065
+ "point_id": 36,
1066
+ "game_coords": [
1067
+ 3,
1068
+ 8
1069
+ ],
1070
+ "pixel_coords": [
1071
+ 490,
1072
+ 377.9422863405995
1073
+ ],
1074
+ "adjacent_points": [
1075
+ 35,
1076
+ 37,
1077
+ 25
1078
+ ],
1079
+ "adjacent_hexes": [
1080
+ 16,
1081
+ 12,
1082
+ 11
1083
+ ]
1084
+ },
1085
+ "37": {
1086
+ "point_id": 37,
1087
+ "game_coords": [
1088
+ 3,
1089
+ 9
1090
+ ],
1091
+ "pixel_coords": [
1092
+ 512.5,
1093
+ 416.9134295108992
1094
+ ],
1095
+ "adjacent_points": [
1096
+ 36,
1097
+ 38,
1098
+ 47
1099
+ ],
1100
+ "adjacent_hexes": [
1101
+ 16,
1102
+ 12
1103
+ ]
1104
+ },
1105
+ "38": {
1106
+ "point_id": 38,
1107
+ "game_coords": [
1108
+ 3,
1109
+ 10
1110
+ ],
1111
+ "pixel_coords": [
1112
+ 557.5,
1113
+ 416.9134295108992
1114
+ ],
1115
+ "adjacent_points": [
1116
+ 37,
1117
+ 27
1118
+ ],
1119
+ "adjacent_hexes": [
1120
+ 12
1121
+ ]
1122
+ },
1123
+ "39": {
1124
+ "point_id": 39,
1125
+ "game_coords": [
1126
+ 4,
1127
+ 0
1128
+ ],
1129
+ "pixel_coords": [
1130
+ 220,
1131
+ 300
1132
+ ],
1133
+ "adjacent_points": [
1134
+ 40,
1135
+ 29
1136
+ ],
1137
+ "adjacent_hexes": [
1138
+ 13
1139
+ ]
1140
+ },
1141
+ "40": {
1142
+ "point_id": 40,
1143
+ "game_coords": [
1144
+ 4,
1145
+ 1
1146
+ ],
1147
+ "pixel_coords": [
1148
+ 242.5,
1149
+ 338.97114317029974
1150
+ ],
1151
+ "adjacent_points": [
1152
+ 39,
1153
+ 41,
1154
+ 48
1155
+ ],
1156
+ "adjacent_hexes": [
1157
+ 17,
1158
+ 13
1159
+ ]
1160
+ },
1161
+ "41": {
1162
+ "point_id": 41,
1163
+ "game_coords": [
1164
+ 4,
1165
+ 2
1166
+ ],
1167
+ "pixel_coords": [
1168
+ 287.5,
1169
+ 338.97114317029974
1170
+ ],
1171
+ "adjacent_points": [
1172
+ 40,
1173
+ 42,
1174
+ 31
1175
+ ],
1176
+ "adjacent_hexes": [
1177
+ 17,
1178
+ 14,
1179
+ 13
1180
+ ]
1181
+ },
1182
+ "42": {
1183
+ "point_id": 42,
1184
+ "game_coords": [
1185
+ 4,
1186
+ 3
1187
+ ],
1188
+ "pixel_coords": [
1189
+ 310,
1190
+ 377.9422863405995
1191
+ ],
1192
+ "adjacent_points": [
1193
+ 41,
1194
+ 43,
1195
+ 50
1196
+ ],
1197
+ "adjacent_hexes": [
1198
+ 18,
1199
+ 17,
1200
+ 14
1201
+ ]
1202
+ },
1203
+ "43": {
1204
+ "point_id": 43,
1205
+ "game_coords": [
1206
+ 4,
1207
+ 4
1208
+ ],
1209
+ "pixel_coords": [
1210
+ 355,
1211
+ 377.9422863405995
1212
+ ],
1213
+ "adjacent_points": [
1214
+ 42,
1215
+ 44,
1216
+ 33
1217
+ ],
1218
+ "adjacent_hexes": [
1219
+ 18,
1220
+ 15,
1221
+ 14
1222
+ ]
1223
+ },
1224
+ "44": {
1225
+ "point_id": 44,
1226
+ "game_coords": [
1227
+ 4,
1228
+ 5
1229
+ ],
1230
+ "pixel_coords": [
1231
+ 377.5,
1232
+ 416.9134295108992
1233
+ ],
1234
+ "adjacent_points": [
1235
+ 43,
1236
+ 45,
1237
+ 52
1238
+ ],
1239
+ "adjacent_hexes": [
1240
+ 19,
1241
+ 18,
1242
+ 15
1243
+ ]
1244
+ },
1245
+ "45": {
1246
+ "point_id": 45,
1247
+ "game_coords": [
1248
+ 4,
1249
+ 6
1250
+ ],
1251
+ "pixel_coords": [
1252
+ 422.5,
1253
+ 416.9134295108992
1254
+ ],
1255
+ "adjacent_points": [
1256
+ 44,
1257
+ 46,
1258
+ 35
1259
+ ],
1260
+ "adjacent_hexes": [
1261
+ 19,
1262
+ 16,
1263
+ 15
1264
+ ]
1265
+ },
1266
+ "46": {
1267
+ "point_id": 46,
1268
+ "game_coords": [
1269
+ 4,
1270
+ 7
1271
+ ],
1272
+ "pixel_coords": [
1273
+ 445,
1274
+ 455.88457268119896
1275
+ ],
1276
+ "adjacent_points": [
1277
+ 45,
1278
+ 47,
1279
+ 54
1280
+ ],
1281
+ "adjacent_hexes": [
1282
+ 19,
1283
+ 16
1284
+ ]
1285
+ },
1286
+ "47": {
1287
+ "point_id": 47,
1288
+ "game_coords": [
1289
+ 4,
1290
+ 8
1291
+ ],
1292
+ "pixel_coords": [
1293
+ 490,
1294
+ 455.88457268119896
1295
+ ],
1296
+ "adjacent_points": [
1297
+ 46,
1298
+ 37
1299
+ ],
1300
+ "adjacent_hexes": [
1301
+ 16
1302
+ ]
1303
+ },
1304
+ "48": {
1305
+ "point_id": 48,
1306
+ "game_coords": [
1307
+ 5,
1308
+ 0
1309
+ ],
1310
+ "pixel_coords": [
1311
+ 220,
1312
+ 377.9422863405995
1313
+ ],
1314
+ "adjacent_points": [
1315
+ 49,
1316
+ 40
1317
+ ],
1318
+ "adjacent_hexes": [
1319
+ 17
1320
+ ]
1321
+ },
1322
+ "49": {
1323
+ "point_id": 49,
1324
+ "game_coords": [
1325
+ 5,
1326
+ 1
1327
+ ],
1328
+ "pixel_coords": [
1329
+ 242.5,
1330
+ 416.9134295108992
1331
+ ],
1332
+ "adjacent_points": [
1333
+ 48,
1334
+ 50
1335
+ ],
1336
+ "adjacent_hexes": [
1337
+ 17
1338
+ ]
1339
+ },
1340
+ "50": {
1341
+ "point_id": 50,
1342
+ "game_coords": [
1343
+ 5,
1344
+ 2
1345
+ ],
1346
+ "pixel_coords": [
1347
+ 287.5,
1348
+ 416.9134295108992
1349
+ ],
1350
+ "adjacent_points": [
1351
+ 49,
1352
+ 51,
1353
+ 42
1354
+ ],
1355
+ "adjacent_hexes": [
1356
+ 18,
1357
+ 17
1358
+ ]
1359
+ },
1360
+ "51": {
1361
+ "point_id": 51,
1362
+ "game_coords": [
1363
+ 5,
1364
+ 3
1365
+ ],
1366
+ "pixel_coords": [
1367
+ 310,
1368
+ 455.88457268119896
1369
+ ],
1370
+ "adjacent_points": [
1371
+ 50,
1372
+ 52
1373
+ ],
1374
+ "adjacent_hexes": [
1375
+ 18
1376
+ ]
1377
+ },
1378
+ "52": {
1379
+ "point_id": 52,
1380
+ "game_coords": [
1381
+ 5,
1382
+ 4
1383
+ ],
1384
+ "pixel_coords": [
1385
+ 355,
1386
+ 455.88457268119896
1387
+ ],
1388
+ "adjacent_points": [
1389
+ 51,
1390
+ 53,
1391
+ 44
1392
+ ],
1393
+ "adjacent_hexes": [
1394
+ 19,
1395
+ 18
1396
+ ]
1397
+ },
1398
+ "53": {
1399
+ "point_id": 53,
1400
+ "game_coords": [
1401
+ 5,
1402
+ 5
1403
+ ],
1404
+ "pixel_coords": [
1405
+ 377.5,
1406
+ 494.8557158514987
1407
+ ],
1408
+ "adjacent_points": [
1409
+ 52,
1410
+ 54
1411
+ ],
1412
+ "adjacent_hexes": [
1413
+ 19
1414
+ ]
1415
+ },
1416
+ "54": {
1417
+ "point_id": 54,
1418
+ "game_coords": [
1419
+ 5,
1420
+ 6
1421
+ ],
1422
+ "pixel_coords": [
1423
+ 422.5,
1424
+ 494.8557158514987
1425
+ ],
1426
+ "adjacent_points": [
1427
+ 53,
1428
+ 46
1429
+ ],
1430
+ "adjacent_hexes": [
1431
+ 19
1432
+ ]
1433
+ }
1434
+ }
1435
+ }
demo_point_system.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Interactive demo showing how users can work with point IDs.
3
+
4
+ This demonstrates the new simplified interface where users work
5
+ with point IDs (1-54) instead of complex coordinates.
6
+ """
7
+
8
+ from pycatan import Game, board_definition
9
+ from pycatan.statuses import Statuses
10
+
11
+ def print_board_info():
12
+ """Print basic board information."""
13
+ print("🎮 Catan Board Information")
14
+ print("=" * 40)
15
+ print(f"📍 Points available: 1-{len(board_definition.get_all_point_ids())}")
16
+ print(f"⬢ Hexes on board: {len(board_definition.get_all_hex_ids())}")
17
+ print()
18
+
19
+ def show_point_examples():
20
+ """Show examples of working with points."""
21
+ print("📍 Point Examples:")
22
+ print("-" * 20)
23
+
24
+ # Show some corner and edge points
25
+ example_points = [1, 7, 8, 16, 46, 54] # Corner and edge points
26
+
27
+ for point_id in example_points:
28
+ coords = board_definition.point_id_to_game_coords(point_id)
29
+ adjacent = board_definition.get_adjacent_point_ids(point_id)
30
+ hexes = board_definition.get_adjacent_hex_ids(point_id)
31
+
32
+ print(f"Point {point_id:2d}: connects to points {adjacent}, touches {len(hexes)} hexes")
33
+
34
+ print()
35
+
36
+ def demonstrate_road_building():
37
+ """Demonstrate road building validation."""
38
+ print("🛤️ Road Building Examples:")
39
+ print("-" * 30)
40
+
41
+ # Valid road connections
42
+ valid_roads = [
43
+ (1, 2), # Adjacent corner points
44
+ (10, 11), # Adjacent edge points
45
+ (25, 26), # Adjacent middle points
46
+ (25, 36), # Vertical connection
47
+ ]
48
+
49
+ invalid_roads = [
50
+ (1, 54), # Opposite corners
51
+ (1, 10), # Non-adjacent
52
+ (25, 30), # Non-adjacent
53
+ ]
54
+
55
+ print("Valid roads:")
56
+ for start, end in valid_roads:
57
+ is_valid = board_definition.is_valid_road_placement(start, end)
58
+ print(f" Point {start:2d} -> Point {end:2d}: {'✓' if is_valid else '✗'}")
59
+
60
+ print("\nInvalid roads:")
61
+ for start, end in invalid_roads:
62
+ is_valid = board_definition.is_valid_road_placement(start, end)
63
+ print(f" Point {start:2d} -> Point {end:2d}: {'✓' if is_valid else '✗'}")
64
+
65
+ print()
66
+
67
+ def simulate_game_moves():
68
+ """Simulate some game moves using point IDs."""
69
+ print("🎯 Simulated Game Moves:")
70
+ print("-" * 25)
71
+
72
+ # Create game
73
+ game = Game(num_of_players=2)
74
+
75
+ # Show how a user would build settlements and roads
76
+ print("Player 0 builds settlement at point 1:")
77
+ print(f" Command: game.add_settlement(player=0, point=board.points[0][0], is_starting=True)")
78
+
79
+ # Convert point ID to actual point for game
80
+ point_coords = board_definition.point_id_to_game_coords(1)
81
+ if point_coords:
82
+ point = game.board.points[point_coords[0]][point_coords[1]]
83
+ result = game.add_settlement(player=0, point=point, is_starting=True)
84
+ print(f" Result: {result}")
85
+
86
+ print("\nPlayer 0 builds road from point 1 to point 2:")
87
+ # Build road between adjacent points
88
+ start_coords = board_definition.point_id_to_game_coords(1)
89
+ end_coords = board_definition.point_id_to_game_coords(2)
90
+
91
+ if start_coords and end_coords:
92
+ start_point = game.board.points[start_coords[0]][start_coords[1]]
93
+ end_point = game.board.points[end_coords[0]][end_coords[1]]
94
+ result = game.add_road(player=0, start=start_point, end=end_point, is_starting=True)
95
+ print(f" Result: {result}")
96
+
97
+ print("\nCurrent game state:")
98
+ state = game.get_full_state()
99
+ print(f" Buildings: {len(state.board_state.buildings)}")
100
+ print(f" Roads: {len(state.board_state.roads)}")
101
+
102
+ if state.board_state.buildings:
103
+ for point_id, building_info in state.board_state.buildings.items():
104
+ print(f" {building_info['type'].title()} at point {point_id} (owner: Player {building_info['owner']})")
105
+
106
+ if state.board_state.roads:
107
+ for road_info in state.board_state.roads:
108
+ print(f" Road from point {road_info['start_point_id']} to {road_info['end_point_id']} (owner: Player {road_info['owner']})")
109
+
110
+ print()
111
+
112
+ def show_future_user_interface():
113
+ """Show how the user interface could look."""
114
+ print("🖥️ Future User Interface Example:")
115
+ print("-" * 35)
116
+
117
+ print("User input examples:")
118
+ print(" > build settlement 25")
119
+ print(" > build road 25 26")
120
+ print(" > build city 25")
121
+ print()
122
+
123
+ print("The system would:")
124
+ print(" 1. Validate point IDs (1-54)")
125
+ print(" 2. Check road connectivity")
126
+ print(" 3. Convert to internal coordinates")
127
+ print(" 4. Execute game action")
128
+ print(" 5. Update web visualization")
129
+ print()
130
+
131
+ def main():
132
+ """Run the interactive demo."""
133
+ print("🎮 PyCatan Point ID System Demo")
134
+ print("=" * 50)
135
+ print()
136
+
137
+ print_board_info()
138
+ show_point_examples()
139
+ demonstrate_road_building()
140
+ simulate_game_moves()
141
+ show_future_user_interface()
142
+
143
+ print("✨ Key Benefits:")
144
+ print(" - Simple point IDs (1-54) instead of complex coordinates")
145
+ print(" - Consistent across Game, Web UI, and user input")
146
+ print(" - Automatic validation of road placement")
147
+ print(" - Single source of truth for board layout")
148
+ print(" - Easy for humans to remember and use")
149
+ print()
150
+
151
+ print("🚀 Next steps:")
152
+ print(" - Update HumanUser to accept point IDs")
153
+ print(" - Update GameManager to convert point IDs")
154
+ print(" - Test with web interface")
155
+
156
+ if __name__ == "__main__":
157
+ main()
dist/pycatan-0.1.tar.gz ADDED
Binary file (14.5 kB). View file
 
examples/board_renderer.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pycatan.board import Board
2
+ from pycatan.hex_type import HexType
3
+ from pycatan.game import Game
4
+ from blessings import Terminal
5
+ import math
6
+
7
+ # Render an board object in ascii in the command prompt
8
+ class BoardRenderer:
9
+
10
+ def __init__(self, board, center):
11
+ self.board = board
12
+ self.center = center
13
+ self.terminal = Terminal()
14
+ # Different colors to use for the 4 players
15
+ self.player_colors = [
16
+ self.terminal.red,
17
+ self.terminal.cyan,
18
+ self.terminal.green,
19
+ self.terminal.yellow
20
+ ]
21
+
22
+ def render(self):
23
+ # Clear screen
24
+ print(self.terminal.clear())
25
+ # Render hexes
26
+ for r in self.board.hexes:
27
+ for h in r:
28
+ self.render_hex(h)
29
+ # Render roads
30
+ for r in self.board.roads:
31
+ self.render_road(r)
32
+ # Render points
33
+ for r in self.board.points:
34
+ for p in r:
35
+ self.render_point(p)
36
+ # Reset cursor position
37
+ print(self.terminal.position(0, 0))
38
+
39
+ def render_hex(self, hex_obj):
40
+ # the lines needed to draw each hex
41
+ hex_lines = [
42
+ "___",
43
+ "/%s%s\\" % (BoardRenderer.get_hex_type_string(hex_obj.type), str(hex_obj.token_num).rjust(2) if hex_obj.token_num else " "),
44
+ "\\___/"
45
+ ]
46
+ # Get the x, y coordinates to render the hex
47
+ coords = self.get_render_coords(hex_obj.position[0], hex_obj.position[1])
48
+ # Draw each hex's lines
49
+ for line_index in range(len(hex_lines)):
50
+ # Shift the first line over by 1
51
+ x_offset = 1 if line_index == 0 else 0
52
+ # Get position
53
+ position = self.terminal.move(self.center[1] + line_index + coords[1], x_offset + self.center[0] + coords[0])
54
+ # Print the line
55
+ print(position + hex_lines[line_index])
56
+
57
+ # Draw a point on the hex
58
+ def render_point(self, point_obj):
59
+ # Get the building
60
+ building = point_obj.building
61
+ # Check it exists
62
+ if building != None:
63
+ # Check the point's coordinates
64
+ coords = self.get_point_coords(point_obj.position[0], point_obj.position[1])
65
+ # Draw a dot there
66
+ position = self.terminal.move(self.center[1] + coords[1], self.center[0] + coords[0])
67
+ # Get the owner of the point
68
+ owner = building.owner
69
+ print(self.player_colors[owner] + position + "." + self.terminal.normal)
70
+
71
+ # Render a road onto the board
72
+ def render_road(self, road_obj):
73
+ # Position to draw the road
74
+ pos = [0, 0]
75
+ # String to draw representing the road
76
+ # Should be either "\", "/" or "___"
77
+ road_str = ""
78
+ # Get the points
79
+ point_one_pos = road_obj.point_one
80
+ point_two_pos = road_obj.point_two
81
+ # Get their coordinates
82
+ p_one_coords = self.get_point_coords(point_one_pos[0], point_one_pos[1])
83
+ p_two_coords = self.get_point_coords(point_two_pos[0], point_two_pos[1])
84
+ # If they're on the same line
85
+ if p_one_coords[1] == p_two_coords[1]:
86
+ # Just draw a line between them
87
+ pos = [min(p_one_coords[0], p_two_coords[0]), p_one_coords[1]]
88
+ road_str = "___"
89
+ else:
90
+ if p_one_coords[0] < p_two_coords[0]:
91
+ if p_one_coords[1] < p_two_coords[1]:
92
+ road_str = "\\"
93
+ else:
94
+ road_str = "/"
95
+ else:
96
+ if p_one_coords[1] < p_two_coords[1]:
97
+ road_str = "/"
98
+ else:
99
+ road_str = "\\"
100
+ pos = [min(p_one_coords[0], p_two_coords[0]) + 1, max(p_two_coords[1], p_one_coords[1])]
101
+
102
+ # Get position
103
+ render_pos = self.terminal.move(pos[1] + self.center[1], pos[0] + self.center[0])
104
+ # Print the road
105
+ print(self.player_colors[road_obj.owner] + render_pos + road_str)
106
+
107
+ # Get the x, y coordinates for a hex from a row and index
108
+ def get_render_coords(self, row, index):
109
+ # Initial coords
110
+ x = 0
111
+ y = 0
112
+ # Width/Height of each hex
113
+ # Each row is futher left than the previous, so decrease x based on row
114
+ x -= 4 * row
115
+ # Each row is also half a hex further down than the previous one
116
+ y += 1 * row
117
+ # Each index moves the hex down and to the right half a hex each
118
+ x += 4 * index
119
+ y += 1 * index
120
+ # If the row is in the bottom half, it should move the hex down and to the left
121
+ length = len(self.board.hexes)
122
+ if row > length / 2:
123
+ # Move if one hex to the right for every row between its row and the halfway row
124
+ x += 4 * math.ceil(row - length / 2)
125
+ # Move it one hex down for every row between its row and the halfway row
126
+ y += 1 + math.floor(row - length / 2)
127
+ # Return coords
128
+ return [x, y]
129
+
130
+ # Get the x, y coordinates for a point from a row and index
131
+ def get_point_coords(self, row, index):
132
+ # Initial coords
133
+ x = 1
134
+ y = 0
135
+ # Each row moves the point down
136
+ # Do different positioning if the row is in the top/bottom half of the board
137
+ half_length = math.floor(len(self.board.points) / 2)
138
+ if row < half_length:
139
+ # Each index moves the point over two
140
+ x += 2 * index
141
+ # Each second index moves the point down one
142
+ y += 1 * math.floor(index / 2)
143
+ # Each row moves the point down and to the left
144
+ x -= 4 * row
145
+ y += 1 * row
146
+ # If the row is in the bottom half, the point should be moved down and to the right
147
+ if row >= half_length:
148
+ diff = row - half_length
149
+ # Move the point to the first position in the bottom row
150
+ x -= 4 * half_length - 2
151
+ y += half_length
152
+ # Move down for each row
153
+ y += 2 * diff
154
+ # Move down and to the right for each index
155
+ y += math.ceil(index / 2)
156
+ x += 2 * index
157
+ # Return point
158
+ return [x, y]
159
+
160
+
161
+ # Get a 1 letter long string representation on a certain hex type
162
+ @staticmethod
163
+ def get_hex_type_string(hex_type):
164
+ if hex_type == HexType.HILLS:
165
+ return "H"
166
+ elif hex_type == HexType.MOUNTAINS:
167
+ return "M"
168
+ elif hex_type == HexType.PASTURE:
169
+ return "P"
170
+ elif hex_type == HexType.FOREST:
171
+ return "F"
172
+ elif hex_type == HexType.FIELDS:
173
+ # Since F is already used, use W for "wheat"
174
+ return "W"
175
+ elif hex_type == HexType.DESERT:
176
+ return "D"
177
+ else:
178
+ raise Exception("Unknown HexType %s passed to get_hex_type_string" % hex_type)
179
+
180
+
181
+
182
+ if __name__ == "__main__":
183
+ g = Game()
184
+ br = BoardRenderer(g.board, [50, 10])
185
+ # Add some settlements
186
+ g.add_settlement(player=0, r=0, i=0, is_starting=True)
187
+ g.add_settlement(player=1, r=2, i=3, is_starting=True)
188
+ g.add_settlement(player=2, r=4, i=1, is_starting=True)
189
+ # Add some roads
190
+ g.add_road(player=0, start=[0, 0], end=[0, 1], is_starting=True)
191
+ g.add_road(player=1, start=[2, 3], end=[2, 2], is_starting=True)
192
+ g.add_road(player=2, start=[4, 1], end=[4, 0], is_starting=True)
193
+ br.render()
game_viz.log ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ >>> Turn 0: a's turn
3
+ ✓ a built a settlement
4
+
5
+ ==================================================
6
+  GAME STATE 
7
+ ==================================================
8
+
9
+ Turn: 0
10
+ Current Player: ► a
11
+
12
+ PLAYERS
13
+ -------
14
+
15
+ ► a
16
+ Victory Points: 1
17
+ Resources: None
18
+ Buildings: Settlements: 1, Cities: 0, Roads: 0
19
+
20
+ V
21
+ Victory Points: 0
22
+ Resources: None
23
+ Buildings: Settlements: 0, Cities: 0, Roads: 0
24
+
25
+ BOARD
26
+ -----
27
+ Board Tiles: 19 tiles configured
28
+
29
+ ✓ a built a road
30
+
31
+ ==================================================
32
+  GAME STATE 
33
+ ==================================================
34
+
35
+ Turn: 0
36
+ Current Player: ► a
37
+
38
+ PLAYERS
39
+ -------
40
+
41
+ ► a
42
+ Victory Points: 1
43
+ Resources: None
44
+ Buildings: Settlements: 1, Cities: 0, Roads: 1
45
+
46
+ V
47
+ Victory Points: 0
48
+ Resources: None
49
+ Buildings: Settlements: 0, Cities: 0, Roads: 0
50
+
51
+ BOARD
52
+ -----
53
+ Board Tiles: 19 tiles configured
54
+
55
+
56
+ >>> Turn 1: V's turn
57
+ ✓ V built a settlement
58
+
59
+ ==================================================
60
+  GAME STATE 
61
+ ==================================================
62
+
63
+ Turn: 1
64
+ Current Player: ► V
65
+
66
+ PLAYERS
67
+ -------
68
+
69
+ a
70
+ Victory Points: 1
71
+ Resources: None
72
+ Buildings: Settlements: 1, Cities: 0, Roads: 1
73
+
74
+ ► V
75
+ Victory Points: 1
76
+ Resources: None
77
+ Buildings: Settlements: 1, Cities: 0, Roads: 0
78
+
79
+ BOARD
80
+ -----
81
+ Board Tiles: 19 tiles configured
82
+
83
+ ✓ V built a road
84
+
85
+ ==================================================
86
+  GAME STATE 
87
+ ==================================================
88
+
89
+ Turn: 1
90
+ Current Player: ► V
91
+
92
+ PLAYERS
93
+ -------
94
+
95
+ a
96
+ Victory Points: 1
97
+ Resources: None
98
+ Buildings: Settlements: 1, Cities: 0, Roads: 1
99
+
100
+ ► V
101
+ Victory Points: 1
102
+ Resources: None
103
+ Buildings: Settlements: 1, Cities: 0, Roads: 1
104
+
105
+ BOARD
106
+ -----
107
+ Board Tiles: 19 tiles configured
108
+
109
+
110
+ >>> Turn 2: V's turn
111
+
112
+ 📦 Resources distributed:
113
+ V: Wood, Brick, Sheep
114
+ ✓ V built a settlement
115
+
116
+ ==================================================
117
+  GAME STATE 
118
+ ==================================================
119
+
120
+ Turn: 2
121
+ Current Player: ► V
122
+
123
+ PLAYERS
124
+ -------
125
+
126
+ a
127
+ Victory Points: 1
128
+ Resources: None
129
+ Buildings: Settlements: 1, Cities: 0, Roads: 1
130
+
131
+ ► V
132
+ Victory Points: 2
133
+ Resources: None
134
+ Buildings: Settlements: 2, Cities: 0, Roads: 1
135
+
136
+ BOARD
137
+ -----
138
+ Board Tiles: 19 tiles configured
139
+
140
+ ✓ V built a road
141
+
142
+ ==================================================
143
+  GAME STATE 
144
+ ==================================================
145
+
146
+ Turn: 2
147
+ Current Player: ► V
148
+
149
+ PLAYERS
150
+ -------
151
+
152
+ a
153
+ Victory Points: 1
154
+ Resources: None
155
+ Buildings: Settlements: 1, Cities: 0, Roads: 1
156
+
157
+ ► V
158
+ Victory Points: 2
159
+ Resources: None
160
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
161
+
162
+ BOARD
163
+ -----
164
+ Board Tiles: 19 tiles configured
165
+
166
+
167
+ >>> Turn 3: a's turn
168
+
169
+ 📦 Resources distributed:
170
+ a: Wheat, Brick, Wood
171
+ ✓ a built a settlement
172
+
173
+ ==================================================
174
+  GAME STATE 
175
+ ==================================================
176
+
177
+ Turn: 3
178
+ Current Player: ► a
179
+
180
+ PLAYERS
181
+ -------
182
+
183
+ ► a
184
+ Victory Points: 2
185
+ Resources: None
186
+ Buildings: Settlements: 2, Cities: 0, Roads: 1
187
+
188
+ V
189
+ Victory Points: 2
190
+ Resources: None
191
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
192
+
193
+ BOARD
194
+ -----
195
+ Board Tiles: 19 tiles configured
196
+
197
+ ✓ a built a road
198
+
199
+ ==================================================
200
+  GAME STATE 
201
+ ==================================================
202
+
203
+ Turn: 3
204
+ Current Player: ► a
205
+
206
+ PLAYERS
207
+ -------
208
+
209
+ ► a
210
+ Victory Points: 2
211
+ Resources: None
212
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
213
+
214
+ V
215
+ Victory Points: 2
216
+ Resources: None
217
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
218
+
219
+ BOARD
220
+ -----
221
+ Board Tiles: 19 tiles configured
222
+
223
+
224
+ >>> Turn 4: a's turn
225
+
226
+ 🎲 a rolled: 5 + 3 = 8
227
+ ✓ a rolled the dice
228
+
229
+ ==================================================
230
+  GAME STATE 
231
+ ==================================================
232
+
233
+ Turn: 4
234
+ Current Player: ► a
235
+
236
+ PLAYERS
237
+ -------
238
+
239
+ ► a
240
+ Victory Points: 2
241
+ Resources: None
242
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
243
+
244
+ V
245
+ Victory Points: 2
246
+ Resources: None
247
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
248
+
249
+ BOARD
250
+ -----
251
+ Board Tiles: 19 tiles configured
252
+
253
+ ✗ a proposed a trade
254
+ Error: V doesn't have the required cards
255
+
256
+ ==================================================
257
+  GAME STATE 
258
+ ==================================================
259
+
260
+ Turn: 4
261
+ Current Player: ► a
262
+
263
+ PLAYERS
264
+ -------
265
+
266
+ ► a
267
+ Victory Points: 2
268
+ Resources: None
269
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
270
+
271
+ V
272
+ Victory Points: 2
273
+ Resources: None
274
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
275
+
276
+ BOARD
277
+ -----
278
+ Board Tiles: 19 tiles configured
279
+
280
+ ✓ a proposed a trade
281
+
282
+ ==================================================
283
+  GAME STATE 
284
+ ==================================================
285
+
286
+ Turn: 4
287
+ Current Player: ► a
288
+
289
+ PLAYERS
290
+ -------
291
+
292
+ ► a
293
+ Victory Points: 2
294
+ Resources: None
295
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
296
+
297
+ V
298
+ Victory Points: 2
299
+ Resources: None
300
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
301
+
302
+ BOARD
303
+ -----
304
+ Board Tiles: 19 tiles configured
305
+
306
+ ✓ a proposed a trade
307
+
308
+ ==================================================
309
+  GAME STATE 
310
+ ==================================================
311
+
312
+ Turn: 4
313
+ Current Player: ► a
314
+
315
+ PLAYERS
316
+ -------
317
+
318
+ ► a
319
+ Victory Points: 2
320
+ Resources: None
321
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
322
+
323
+ V
324
+ Victory Points: 2
325
+ Resources: None
326
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
327
+
328
+ BOARD
329
+ -----
330
+ Board Tiles: 19 tiles configured
331
+
332
+ ✗ a proposed a trade
333
+ Error: V rejected your trade offer
334
+
335
+ ==================================================
336
+  GAME STATE 
337
+ ==================================================
338
+
339
+ Turn: 4
340
+ Current Player: ► a
341
+
342
+ PLAYERS
343
+ -------
344
+
345
+ ► a
346
+ Victory Points: 2
347
+ Resources: None
348
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
349
+
350
+ V
351
+ Victory Points: 2
352
+ Resources: None
353
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
354
+
355
+ BOARD
356
+ -----
357
+ Board Tiles: 19 tiles configured
358
+
359
+ ✓ a ended their turn
360
+
361
+ ==================================================
362
+  GAME STATE 
363
+ ==================================================
364
+
365
+ Turn: 4
366
+ Current Player: ► a
367
+
368
+ PLAYERS
369
+ -------
370
+
371
+ ► a
372
+ Victory Points: 2
373
+ Resources: None
374
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
375
+
376
+ V
377
+ Victory Points: 2
378
+ Resources: None
379
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
380
+
381
+ BOARD
382
+ -----
383
+ Board Tiles: 19 tiles configured
384
+
385
+
386
+ >>> Turn 5: V's turn
387
+
388
+ 🎲 V rolled: 6 + 1 = 7
389
+ ✓ V rolled the dice
390
+
391
+ ==================================================
392
+  GAME STATE 
393
+ ==================================================
394
+
395
+ Turn: 5
396
+ Current Player: ► V
397
+
398
+ PLAYERS
399
+ -------
400
+
401
+ a
402
+ Victory Points: 2
403
+ Resources: None
404
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
405
+
406
+ ► V
407
+ Victory Points: 2
408
+ Resources: None
409
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
410
+
411
+ BOARD
412
+ -----
413
+ Board Tiles: 19 tiles configured
414
+
415
+ ✓ V ended their turn
416
+
417
+ ==================================================
418
+  GAME STATE 
419
+ ==================================================
420
+
421
+ Turn: 5
422
+ Current Player: ► V
423
+
424
+ PLAYERS
425
+ -------
426
+
427
+ a
428
+ Victory Points: 2
429
+ Resources: None
430
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
431
+
432
+ ► V
433
+ Victory Points: 2
434
+ Resources: None
435
+ Buildings: Settlements: 2, Cities: 0, Roads: 2
436
+
437
+ BOARD
438
+ -----
439
+ Board Tiles: 19 tiles configured
440
+
441
+
442
+ >>> Turn 6: a's turn
play_catan.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PyCatan Game Launcher
5
+
6
+ Simple script to launch a complete PyCatan game experience.
7
+ Run this file to start playing!
8
+
9
+ Features:
10
+ - Interactive player setup
11
+ - Multiple interface windows
12
+ - Console + Web visualization
13
+ - Real-time game updates
14
+ """
15
+
16
+ from pycatan import RealGame
17
+
18
+ if __name__ == "__main__":
19
+ print("=" * 60)
20
+ print("Welcome to PyCatan!")
21
+ print("=" * 60)
22
+ print("Starting the complete game experience...")
23
+ print()
24
+ print("What will happen:")
25
+ print("• You'll enter player count and names")
26
+ print("• A separate console will open for game visualization")
27
+ print("• Your web browser will open with the game board")
28
+ print("• You'll play in this main console")
29
+ print()
30
+
31
+ # Create and run the game
32
+ game = RealGame()
33
+ game.run()
print_game_logic.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sys
3
+ import os
4
+
5
+ # Add the current directory to the path
6
+ sys.path.append(os.getcwd())
7
+
8
+ from pycatan import Game
9
+ from pycatan.board_definition import board_definition
10
+
11
+ def print_game_expectations():
12
+ print("Initializing Game...")
13
+ game = Game()
14
+ board = game.board
15
+
16
+ print("\n" + "="*60)
17
+ print("GAME LOGIC EXPECTATIONS (What the Python code thinks)")
18
+ print("="*60 + "\n")
19
+
20
+ print("Format: Hex [Row, Col] -> Connected Point Coordinates [Row, Col]")
21
+ print("-" * 60)
22
+
23
+ # Iterate through all tiles in the game board
24
+ for r, row in enumerate(board.tiles):
25
+ for i, tile in enumerate(row):
26
+ # Get connected points according to Game Logic
27
+ game_connected_points = tile.points
28
+
29
+ # Get coordinates of connected points
30
+ point_coords = []
31
+ for p in game_connected_points:
32
+ point_coords.append(list(p.position))
33
+
34
+ # Sort for readability
35
+ point_coords.sort()
36
+
37
+ print(f"Hex [{r}, {i}] connects to Points: {point_coords}")
38
+
39
+ if __name__ == "__main__":
40
+ print_game_expectations()
pycatan/__init__.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pycatan.board import Board
2
+ from pycatan.building import Building
3
+ from pycatan.card import ResCard, DevCard
4
+ from pycatan.game import Game
5
+ from pycatan.harbor import Harbor
6
+ from pycatan.player import Player
7
+ from pycatan.statuses import Statuses
8
+
9
+ # Board definition system
10
+ from pycatan.board_definition import board_definition, point_id_to_coords, coords_to_point_id
11
+
12
+ # New simulation framework components
13
+ from pycatan.actions import (
14
+ Action, ActionType, ActionResult, GameState, PlayerState, BoardState,
15
+ GamePhase, TurnPhase, create_build_settlement_action, create_build_road_action,
16
+ create_trade_action
17
+ )
18
+ from pycatan.user import User, UserInputError, validate_user_list, create_test_user
19
+ from pycatan.human_user import HumanUser
20
+ from pycatan.game_manager import GameManager
21
+ from pycatan.visualization import Visualization, VisualizationManager
22
+ from pycatan.console_visualization import ConsoleVisualization
23
+
24
+ # Optional web visualization (requires Flask)
25
+ try:
26
+ from pycatan.web_visualization import WebVisualization, create_web_visualization
27
+ except ImportError:
28
+ # Flask not available - web visualization disabled
29
+ WebVisualization = None
30
+ create_web_visualization = None
31
+
32
+ # Complete game experience
33
+ from pycatan.real_game import RealGame
pycatan/actions.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Actions & Data Structures for PyCatan Game Management
3
+
4
+ This module defines the core data structures for managing game actions,
5
+ state, and results in the PyCatan simulation framework.
6
+ """
7
+
8
+ from enum import Enum, auto
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Dict, List, Optional, Union
11
+ from datetime import datetime
12
+
13
+
14
+ class ActionType(Enum):
15
+ """Enumeration of all possible actions in a Catan game."""
16
+
17
+ # Building actions
18
+ BUILD_SETTLEMENT = auto()
19
+ BUILD_CITY = auto()
20
+ BUILD_ROAD = auto()
21
+
22
+ # Trading actions
23
+ TRADE_PROPOSE = auto()
24
+ TRADE_ACCEPT = auto()
25
+ TRADE_REJECT = auto()
26
+ TRADE_COUNTER = auto()
27
+ TRADE_BANK = auto()
28
+
29
+ # Development card actions
30
+ USE_DEV_CARD = auto()
31
+ BUY_DEV_CARD = auto()
32
+
33
+ # Turn management
34
+ ROLL_DICE = auto()
35
+ END_TURN = auto()
36
+
37
+ # Special actions
38
+ ROBBER_MOVE = auto()
39
+ DISCARD_CARDS = auto()
40
+ STEAL_CARD = auto()
41
+
42
+ # Setup phase actions
43
+ PLACE_STARTING_SETTLEMENT = auto()
44
+ PLACE_STARTING_ROAD = auto()
45
+
46
+
47
+ class GamePhase(Enum):
48
+ """Enumeration of game phases."""
49
+ SETUP_FIRST_ROUND = auto()
50
+ SETUP_SECOND_ROUND = auto()
51
+ NORMAL_PLAY = auto()
52
+ ENDED = auto()
53
+
54
+
55
+ class TurnPhase(Enum):
56
+ """Enumeration of phases within a single turn."""
57
+ ROLL_DICE = auto()
58
+ HANDLE_DICE_EFFECTS = auto() # Resource distribution, robber on 7
59
+ DISCARD_PHASE = auto() # Players with 7+ cards must discard half
60
+ ROBBER_MOVE = auto() # Current player must move the robber
61
+ ROBBER_STEAL = auto() # Current player must steal from adjacent player
62
+ PLAYER_ACTIONS = auto()
63
+ END_TURN = auto()
64
+
65
+
66
+ @dataclass
67
+ class Action:
68
+ """
69
+ Represents a single action that can be performed in the game.
70
+
71
+ This is the primary interface between Users and the GameManager.
72
+ All player decisions are expressed as Action objects.
73
+ """
74
+ action_type: ActionType
75
+ player_id: int
76
+ parameters: Dict[str, Any] = field(default_factory=dict)
77
+ timestamp: datetime = field(default_factory=datetime.now)
78
+
79
+ def __post_init__(self):
80
+ """Validate action parameters based on action type."""
81
+ self._validate_parameters()
82
+
83
+ def _validate_parameters(self):
84
+ """Basic validation of action parameters."""
85
+ required_params = {
86
+ ActionType.BUILD_SETTLEMENT: ['point_coords'],
87
+ ActionType.BUILD_CITY: ['point_coords'],
88
+ ActionType.BUILD_ROAD: ['start_coords', 'end_coords'],
89
+ ActionType.TRADE_PROPOSE: ['offer', 'request', 'target_player'],
90
+ # TRADE_ACCEPT and TRADE_REJECT don't need trade_id in synchronous mode
91
+ ActionType.TRADE_COUNTER: ['trade_id', 'counter_offer', 'counter_request'],
92
+ ActionType.TRADE_BANK: ['offer', 'request'],
93
+ ActionType.USE_DEV_CARD: ['card_type'],
94
+ ActionType.ROBBER_MOVE: ['tile_coords'],
95
+ ActionType.STEAL_CARD: ['target_player'],
96
+ ActionType.DISCARD_CARDS: ['cards'],
97
+ ActionType.PLACE_STARTING_SETTLEMENT: ['point_coords'],
98
+ ActionType.PLACE_STARTING_ROAD: ['start_coords', 'end_coords'],
99
+ }
100
+
101
+ required = required_params.get(self.action_type, [])
102
+ missing = [param for param in required if param not in self.parameters]
103
+
104
+ if missing:
105
+ raise ValueError(f"Action {self.action_type} missing required parameters: {missing}")
106
+
107
+
108
+ @dataclass
109
+ @dataclass
110
+ class PlayerState:
111
+ """Represents the complete state of a single player."""
112
+ player_id: int
113
+ name: str
114
+ cards: List[str] # Resource cards
115
+ dev_cards: List[str] # Development cards
116
+ settlements: List[tuple] # Coordinates of settlements
117
+ cities: List[tuple] # Coordinates of cities
118
+ roads: List[tuple] # Coordinates of roads (start, end)
119
+ victory_points: int
120
+ longest_road_length: int
121
+ has_longest_road: bool
122
+ has_largest_army: bool
123
+ knights_played: int
124
+
125
+
126
+ @dataclass
127
+ class BoardState:
128
+ """Represents the state of the game board."""
129
+ tiles: List[Dict[str, Any]] # Tile information
130
+ robber_position: tuple # Coordinates of robber
131
+ harbors: List[Dict[str, Any]] # Harbor information
132
+ buildings: Dict[tuple, Dict[str, Any]] # Point -> building info
133
+ roads: List[tuple] # All roads on board
134
+
135
+
136
+ @dataclass
137
+ class GameState:
138
+ """
139
+ Represents the complete state of a Catan game at any point in time.
140
+
141
+ This is used for visualization, AI decision-making, and game persistence.
142
+ """
143
+ # Game metadata
144
+ game_id: str = ""
145
+ turn_number: int = 0
146
+ current_player: int = 0
147
+ game_phase: GamePhase = GamePhase.SETUP_FIRST_ROUND
148
+ turn_phase: TurnPhase = TurnPhase.ROLL_DICE
149
+
150
+ # Game state
151
+ board_state: BoardState = field(default_factory=lambda: BoardState([], (0, 0), [], {}, []))
152
+ players_state: List[PlayerState] = field(default_factory=list)
153
+
154
+ # Game resources
155
+ dev_cards_available: int = 25
156
+ resource_cards_available: Dict[str, int] = field(default_factory=lambda: {
157
+ 'wood': 19, 'brick': 19, 'sheep': 19, 'wheat': 19, 'ore': 19
158
+ })
159
+
160
+ # Current turn state
161
+ dice_rolled: Optional[tuple] = None
162
+ pending_trades: List[Dict[str, Any]] = field(default_factory=list)
163
+ pending_actions: List[str] = field(default_factory=list) # Actions waiting for completion
164
+
165
+ # Robber/Discard state (when 7 is rolled)
166
+ players_must_discard: Dict[int, int] = field(default_factory=dict) # player_id -> cards to discard
167
+ robber_moved: bool = False # Whether robber has been moved this turn
168
+ steal_pending: bool = False # Whether steal action is pending
169
+
170
+ # Game history (for replay/debugging)
171
+ action_history: List[Action] = field(default_factory=list)
172
+
173
+ def get_player_state(self, player_id: int) -> Optional[PlayerState]:
174
+ """Get state for a specific player."""
175
+ for player in self.players_state:
176
+ if player.player_id == player_id:
177
+ return player
178
+ return None
179
+
180
+ def get_current_player_state(self) -> Optional[PlayerState]:
181
+ """Get state for the current player."""
182
+ return self.get_player_state(self.current_player)
183
+
184
+
185
+ @dataclass
186
+ class ActionResult:
187
+ """
188
+ Result of executing an action.
189
+
190
+ Contains information about success/failure, updated game state,
191
+ and any side effects that occurred.
192
+ """
193
+ success: bool
194
+ error_message: Optional[str] = None
195
+ updated_state: Optional[GameState] = None
196
+ affected_players: List[int] = field(default_factory=list)
197
+ side_effects: List[Action] = field(default_factory=list) # Additional actions triggered
198
+ status_code: str = "" # Maps to pycatan.statuses for compatibility
199
+
200
+ @classmethod
201
+ def success_result(cls, updated_state: GameState, affected_players: List[int] = None) -> 'ActionResult':
202
+ """Create a successful action result."""
203
+ return cls(
204
+ success=True,
205
+ updated_state=updated_state,
206
+ affected_players=affected_players or [],
207
+ status_code="ALL_GOOD"
208
+ )
209
+
210
+ @classmethod
211
+ def failure_result(cls, error_message: str, status_code: str = "") -> 'ActionResult':
212
+ """Create a failed action result."""
213
+ return cls(
214
+ success=False,
215
+ error_message=error_message,
216
+ status_code=status_code
217
+ )
218
+
219
+
220
+ # Utility functions for common action creation
221
+ def create_build_settlement_action(player_id: int, point_coords: tuple, is_starting: bool = False) -> Action:
222
+ """Helper function to create a build settlement action."""
223
+ action_type = ActionType.PLACE_STARTING_SETTLEMENT if is_starting else ActionType.BUILD_SETTLEMENT
224
+ return Action(
225
+ action_type=action_type,
226
+ player_id=player_id,
227
+ parameters={'point_coords': point_coords}
228
+ )
229
+
230
+
231
+ def create_build_road_action(player_id: int, start_coords: tuple, end_coords: tuple, is_starting: bool = False) -> Action:
232
+ """Helper function to create a build road action."""
233
+ action_type = ActionType.PLACE_STARTING_ROAD if is_starting else ActionType.BUILD_ROAD
234
+ return Action(
235
+ action_type=action_type,
236
+ player_id=player_id,
237
+ parameters={'start_coords': start_coords, 'end_coords': end_coords}
238
+ )
239
+
240
+
241
+ def create_trade_action(player_id: int, offer: Dict[str, int], request: Dict[str, int],
242
+ target_player: Optional[int] = None) -> Action:
243
+ """Helper function to create a trade action."""
244
+ action_type = ActionType.TRADE_BANK if target_player is None else ActionType.TRADE_PROPOSE
245
+ parameters = {'offer': offer, 'request': request}
246
+ if target_player is not None:
247
+ parameters['target_player'] = target_player
248
+
249
+ return Action(
250
+ action_type=action_type,
251
+ player_id=player_id,
252
+ parameters=parameters
253
+ )
pycatan/board.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pycatan.harbor import Harbor, HarborType
2
+ from pycatan.player import Player
3
+ from pycatan.statuses import Statuses
4
+ from pycatan.building import Building
5
+ from pycatan.tile_type import TileType
6
+ from pycatan.card import ResCard, DevCard
7
+ from pycatan.tile import Tile
8
+ from pycatan.point import Point
9
+
10
+ # used to shuffle the deck of tiles
11
+ import random
12
+
13
+ import abc
14
+
15
+ # used for debugging
16
+ import pprint
17
+
18
+ # Base class for different Catan boards
19
+ # Should not be instantiated, otherwise the board will be empty
20
+ class Board(object):
21
+ __metaclass__ = abc.ABCMeta
22
+
23
+ def __init__(self, game):
24
+ # The game the board is in
25
+ self.game = game
26
+ # The tiles on the board
27
+ # Should be set in a subclass
28
+ self.tiles = ()
29
+ # The points on the board
30
+ # Where the players can place settlements/cities
31
+ # Will be set at the end of __init__
32
+ self.points = ()
33
+ # The roads
34
+ self.roads = []
35
+ # The locations of the harbors
36
+ self.harbors = []
37
+ # The location of the robber
38
+ # going r, i
39
+ self.robber = None
40
+
41
+ # gives the players cards for a certain roll
42
+ def add_yield(self, roll):
43
+
44
+ # Track resources distributed: {player_name: [resource_names]}
45
+ distribution = {}
46
+
47
+ for r in self.points:
48
+ for p in r:
49
+ # Check there is a building on the point
50
+ if p.building != None:
51
+ building = p.building
52
+ tiles = p.tiles
53
+
54
+ # checks if any tiles have the right number
55
+ for current_tile in tiles:
56
+
57
+ # makes sure the robber isn't there
58
+ if self.robber == current_tile.position:
59
+ # skips this tile
60
+ continue
61
+
62
+ if current_tile.token_num == roll:
63
+ # adds the card to the player's inventory
64
+ owner = building.owner
65
+ # gets the card type
66
+ card_type = Board.get_card_from_tile(current_tile.type)
67
+
68
+ if card_type:
69
+ cards_to_add = []
70
+ # adds two if it is a city
71
+ if building.type == Building.BUILDING_CITY:
72
+ cards_to_add = [card_type, card_type]
73
+ else:
74
+ cards_to_add = [card_type]
75
+
76
+ self.game.players[owner].add_cards(cards_to_add)
77
+
78
+ # Record distribution
79
+ player_name = f"Player {owner + 1}"
80
+ if player_name not in distribution:
81
+ distribution[player_name] = []
82
+
83
+ for card in cards_to_add:
84
+ distribution[player_name].append(card.name.split('.')[-1] if hasattr(card, 'name') else str(card))
85
+
86
+ return distribution
87
+
88
+
89
+ # adds a Building object to the board
90
+ def add_building(self, building, point):
91
+ point.building = building
92
+
93
+ # adds a Building object, which must be a road
94
+ # since roads record their own position and are not in self.points
95
+ def add_road(self, road):
96
+ self.roads.append(road)
97
+
98
+ # upgrades an existing settlement to a city
99
+ def upgrade_settlement(self, player, point):
100
+ # Get building at point
101
+ building = point.building
102
+
103
+ # checks there is a settlement at r, i
104
+ if building == None:
105
+ return Statuses.ERR_NOT_EXIST
106
+
107
+ # checks the settlement is controlled by the correct player
108
+ # if no player is specified, uses the current controlling player
109
+ if building.owner != player:
110
+ return Statuses.ERR_BAD_OWNER
111
+
112
+ # checks it is a settlement and not a city
113
+ if building.type != Building.BUILDING_SETTLEMENT:
114
+ return Statuses.ERR_UPGRADE_CITY
115
+
116
+ # checks the player has the cards
117
+ needed_cards = [
118
+ ResCard.Wheat,
119
+ ResCard.Wheat,
120
+ ResCard.Ore,
121
+ ResCard.Ore,
122
+ ResCard.Ore
123
+ ]
124
+ if not self.game.players[player].has_cards(needed_cards):
125
+ return Statuses.ERR_CARDS
126
+
127
+ # removes the cards
128
+ self.game.players[player].remove_cards(needed_cards)
129
+ # changes the settlement to a city
130
+ building.type = Building.BUILDING_CITY
131
+ # adds another victory point
132
+ self.game.players[player].victory_points += 1
133
+
134
+ return Statuses.ALL_GOOD
135
+
136
+ # gets all the buildings on the board
137
+ def get_buildings(self):
138
+
139
+ buildings = []
140
+ for r in self.points:
141
+ for p in r:
142
+ if p.building != None:
143
+ buildings.append(p.building)
144
+
145
+ return buildings
146
+
147
+ # moves the robber to a givne coord
148
+ def move_robber(self, tile_pos):
149
+ self.robber = tile_pos
150
+
151
+ def __repr__(self):
152
+ return ("Board Object")
153
+
154
+ # Get a shuffled deck of the correct number of each type of tile in a board
155
+ @staticmethod
156
+ def get_shuffled_tile_deck():
157
+ deck = []
158
+ # sets up all_tiles
159
+ for i in range(4):
160
+
161
+ # adds four fields, forests and pastures
162
+ deck.append(TileType.Fields)
163
+ deck.append(TileType.Forest)
164
+ deck.append(TileType.Pasture)
165
+ # adds three mountains and hills
166
+ if i < 3:
167
+ deck.append(TileType.Mountains)
168
+ deck.append(TileType.Hills)
169
+
170
+ # adds one desert
171
+ if i == 0:
172
+ deck.append(TileType.Desert)
173
+
174
+ # shuffles the deck
175
+ random.shuffle(deck)
176
+ return deck
177
+
178
+ @staticmethod
179
+ def get_shuffled_tile_nums():
180
+ nums = []
181
+ # Get 2 of each number, most of the time
182
+ for i in range(2):
183
+ # Go through each type
184
+ for x in range(2, 13):
185
+ # Does not add a number token with 7
186
+ if x != 7:
187
+ # Only adds one 2 and one 12
188
+ if x == 2 or x == 12:
189
+ if i == 0:
190
+ nums.append(x)
191
+ # Adds two of everything else
192
+ else:
193
+ nums.append(x)
194
+ random.shuffle(nums)
195
+ return nums
196
+
197
+ # returns the card associated with the tile
198
+ # for example, Brick for Hills, Wood for forests, etc
199
+ @staticmethod
200
+ def get_card_from_tile(tile):
201
+
202
+ # returns the appropriete card
203
+ if tile == TileType.Forest:
204
+ return ResCard.Wood
205
+
206
+ elif tile == TileType.Hills:
207
+ return ResCard.Brick
208
+
209
+ elif tile == TileType.Pasture:
210
+ return ResCard.Sheep
211
+
212
+ elif tile == TileType.Fields:
213
+ return ResCard.Wheat
214
+
215
+ elif tile == TileType.Mountains:
216
+ return ResCard.Ore
217
+
218
+ else:
219
+ return None
pycatan/board_definition.py ADDED
@@ -0,0 +1,556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Central Board Definition for PyCatan
3
+
4
+ This module provides the definitive, canonical mapping of the Catan board,
5
+ including all coordinate systems and conversion functions.
6
+
7
+ ALL other modules should use this as the single source of truth for:
8
+ - Point coordinates and IDs
9
+ - Hex/tile coordinates and IDs
10
+ - Conversion between coordinate systems
11
+ - Board layout and geometry
12
+
13
+ This ensures consistency across Game, WebVisualization, JavaScript, and user input.
14
+ """
15
+
16
+ from typing import Dict, List, Tuple, Optional, NamedTuple
17
+ from dataclasses import dataclass
18
+ import json
19
+
20
+
21
+ @dataclass
22
+ class HexDefinition:
23
+ """Definition of a single hex on the board."""
24
+ hex_id: int # 1-19 for standard Catan
25
+ game_coords: Tuple[int, int] # [row, index] used by Game internally
26
+ axial_coords: Tuple[int, int] # [q, r] for web display
27
+ adjacent_points: List[int] # Point IDs (1-54) that border this hex
28
+
29
+
30
+ @dataclass
31
+ class PointDefinition:
32
+ """Definition of a single point/vertex on the board."""
33
+ point_id: int # 1-54 for standard Catan
34
+ game_coords: Tuple[int, int] # [row, index] used by Game internally
35
+ pixel_coords: Tuple[float, float] # [x, y] for web display
36
+ adjacent_points: List[int] # Connected point IDs for roads
37
+ adjacent_hexes: List[int] # Hex IDs that this point touches
38
+
39
+
40
+ class BoardDefinition:
41
+ """
42
+ Central definition of the Catan board layout.
43
+
44
+ This class provides the single source of truth for all coordinate
45
+ mappings and board geometry. All other systems should use this
46
+ instead of their own coordinate conversion logic.
47
+ """
48
+
49
+ def __init__(self):
50
+ """Initialize the standard Catan board definition."""
51
+ self.hexes: Dict[int, HexDefinition] = {}
52
+ self.points: Dict[int, PointDefinition] = {}
53
+
54
+ # Try to load from file first
55
+ if not self._load_from_file():
56
+ # Fallback to hardcoded initialization
57
+ self._initialize_hexes()
58
+ self._initialize_points()
59
+ self._calculate_adjacencies()
60
+
61
+ def _load_from_file(self, filename: str = 'board_definition.json') -> bool:
62
+ """Load board definition from JSON file."""
63
+ import os
64
+ if not os.path.exists(filename):
65
+ return False
66
+
67
+ try:
68
+ with open(filename, 'r') as f:
69
+ data = json.load(f)
70
+
71
+ # Load Hexes
72
+ for hex_id_str, hex_data in data.get('hexes', {}).items():
73
+ hex_id = int(hex_id_str)
74
+ self.hexes[hex_id] = HexDefinition(
75
+ hex_id=hex_id,
76
+ game_coords=tuple(hex_data['game_coords']),
77
+ axial_coords=tuple(hex_data['axial_coords']),
78
+ adjacent_points=hex_data.get('adjacent_points', [])
79
+ )
80
+
81
+ # Load Points
82
+ for point_id_str, point_data in data.get('points', {}).items():
83
+ point_id = int(point_id_str)
84
+ self.points[point_id] = PointDefinition(
85
+ point_id=point_id,
86
+ game_coords=tuple(point_data['game_coords']),
87
+ pixel_coords=tuple(point_data['pixel_coords']),
88
+ adjacent_points=point_data.get('adjacent_points', []),
89
+ adjacent_hexes=point_data.get('adjacent_hexes', [])
90
+ )
91
+
92
+ print(f"Loaded board definition from {filename}")
93
+ return True
94
+ except Exception as e:
95
+ print(f"Error loading board definition: {e}")
96
+ return False
97
+
98
+ def _initialize_hexes(self):
99
+ """Initialize all 19 hexes with their coordinate mappings."""
100
+ # Standard Catan board: 5 rows with 3,4,5,4,3 hexes
101
+ hex_id = 1
102
+
103
+ # Define the game coordinate to axial coordinate conversion
104
+ # This matches the layout expected by the web visualization
105
+ hex_layouts = [
106
+ # Row 0: 3 hexes -> axial coordinates
107
+ [(0, 0, 0, -2), (0, 1, 1, -2), (0, 2, 2, -2)],
108
+ # Row 1: 4 hexes
109
+ [(1, 0, -1, -1), (1, 1, 0, -1), (1, 2, 1, -1), (1, 3, 2, -1)],
110
+ # Row 2: 5 hexes (middle row)
111
+ [(2, 0, -2, 0), (2, 1, -1, 0), (2, 2, 0, 0), (2, 3, 1, 0), (2, 4, 2, 0)],
112
+ # Row 3: 4 hexes
113
+ [(3, 0, -2, 1), (3, 1, -1, 1), (3, 2, 0, 1), (3, 3, 1, 1)],
114
+ # Row 4: 3 hexes
115
+ [(4, 0, -2, 2), (4, 1, -1, 2), (4, 2, 0, 2)]
116
+ ]
117
+
118
+ for row_layout in hex_layouts:
119
+ for row, col, q, r in row_layout:
120
+ self.hexes[hex_id] = HexDefinition(
121
+ hex_id=hex_id,
122
+ game_coords=(row, col),
123
+ axial_coords=(q, r),
124
+ adjacent_points=[] # Will be calculated later
125
+ )
126
+ hex_id += 1
127
+
128
+ def _initialize_points(self):
129
+ """Initialize all 54 points with their coordinate mappings."""
130
+ # Standard Catan board: 6 rows with 7,9,11,11,9,7 points
131
+ point_id = 1
132
+
133
+ # Define all point coordinates as used by the Game internally
134
+ point_layouts = [
135
+ # Row 0: 7 points
136
+ [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6)],
137
+ # Row 1: 9 points
138
+ [(1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8)],
139
+ # Row 2: 11 points
140
+ [(2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (2, 10)],
141
+ # Row 3: 11 points
142
+ [(3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (3, 10)],
143
+ # Row 4: 9 points
144
+ [(4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8)],
145
+ # Row 5: 7 points
146
+ [(5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6)]
147
+ ]
148
+
149
+ for row_points in point_layouts:
150
+ for row, col in row_points:
151
+ # Calculate pixel coordinates for web display
152
+ pixel_x, pixel_y = self._calculate_pixel_coords(row, col)
153
+
154
+ self.points[point_id] = PointDefinition(
155
+ point_id=point_id,
156
+ game_coords=(row, col),
157
+ pixel_coords=(pixel_x, pixel_y),
158
+ adjacent_points=[], # Will be calculated later
159
+ adjacent_hexes=[] # Will be calculated later
160
+ )
161
+ point_id += 1
162
+
163
+ def _calculate_pixel_coords(self, row: int, col: int) -> Tuple[float, float]:
164
+ """
165
+ Calculate pixel coordinates for a point given its game coordinates.
166
+
167
+ Uses direct mapping of each point to the appropriate hex vertex position.
168
+ This ensures points are positioned exactly on hex corners.
169
+ """
170
+ import math
171
+
172
+ # Base parameters matching JavaScript
173
+ HEX_RADIUS = 45
174
+ CENTER_X = 400
175
+ CENTER_Y = 300
176
+
177
+ # Helper: get pixel coords of hex center from axial coords
178
+ def get_hex_center(q, r):
179
+ x = CENTER_X + HEX_RADIUS * (3/2 * q)
180
+ y = CENTER_Y + HEX_RADIUS * (math.sqrt(3)/2 * q + math.sqrt(3) * r)
181
+ return (x, y)
182
+
183
+ # Helper: get vertex position (0=top, clockwise)
184
+ def get_hex_vertex(q, r, vertex_idx):
185
+ cx, cy = get_hex_center(q, r)
186
+ angle_deg = 60 * vertex_idx - 90 # Start at top (270°), go clockwise
187
+ angle_rad = math.radians(angle_deg)
188
+ x = cx + HEX_RADIUS * math.cos(angle_rad)
189
+ y = cy + HEX_RADIUS * math.sin(angle_rad)
190
+ return (x, y)
191
+
192
+ # Manual mapping: (row, col) -> (q, r, vertex)
193
+ # Based on standard Catan board with axial coordinates
194
+ point_to_hex_vertex = {
195
+ # Row 0 (7 points) - top edge
196
+ (0, 0): (0, -2, 4), (0, 1): (0, -2, 5), (0, 2): (0, -2, 0),
197
+ (0, 3): (1, -2, 5), (0, 4): (1, -2, 0), (0, 5): (2, -2, 5),
198
+ (0, 6): (2, -2, 0),
199
+
200
+ # Row 1 (9 points)
201
+ (1, 0): (-1, -1, 4), (1, 1): (-1, -1, 5), (1, 2): (0, -1, 4),
202
+ (1, 3): (0, -1, 5), (1, 4): (1, -1, 4), (1, 5): (1, -1, 5),
203
+ (1, 6): (2, -1, 4), (1, 7): (2, -1, 5), (1, 8): (2, -1, 0),
204
+
205
+ # Row 2 (11 points) - widest
206
+ (2, 0): (-2, 0, 4), (2, 1): (-2, 0, 5), (2, 2): (-1, 0, 4),
207
+ (2, 3): (-1, 0, 5), (2, 4): (0, 0, 4), (2, 5): (0, 0, 5),
208
+ (2, 6): (1, 0, 4), (2, 7): (1, 0, 5), (2, 8): (2, 0, 4),
209
+ (2, 9): (2, 0, 5), (2, 10): (2, 0, 0),
210
+
211
+ # Row 3 (11 points) - widest
212
+ (3, 0): (-2, 1, 3), (3, 1): (-2, 1, 4), (3, 2): (-1, 1, 3),
213
+ (3, 3): (-1, 1, 4), (3, 4): (0, 1, 3), (3, 5): (0, 1, 4),
214
+ (3, 6): (1, 1, 3), (3, 7): (1, 1, 4), (3, 8): (1, 1, 5),
215
+ (3, 9): (1, 1, 0), (3, 10): (1, 1, 1),
216
+
217
+ # Row 4 (9 points)
218
+ (4, 0): (-2, 2, 2), (4, 1): (-2, 2, 3), (4, 2): (-1, 2, 2),
219
+ (4, 3): (-1, 2, 3), (4, 4): (0, 2, 2), (4, 5): (0, 2, 3),
220
+ (4, 6): (0, 2, 4), (4, 7): (0, 2, 5), (4, 8): (0, 2, 0),
221
+
222
+ # Row 5 (7 points) - bottom edge
223
+ (5, 0): (-2, 2, 1), (5, 1): (-2, 2, 2), (5, 2): (-1, 2, 1),
224
+ (5, 3): (-1, 2, 2), (5, 4): (0, 2, 1), (5, 5): (0, 2, 2),
225
+ (5, 6): (0, 2, 3),
226
+ }
227
+
228
+ # Get the hex and vertex for this point
229
+ if (row, col) in point_to_hex_vertex:
230
+ q, r, vertex = point_to_hex_vertex[(row, col)]
231
+ return get_hex_vertex(q, r, vertex)
232
+ else:
233
+ # Fallback for unmapped points
234
+ print(f"Warning: No mapping for point ({row}, {col})")
235
+ return (CENTER_X, CENTER_Y)
236
+ return (x, y)
237
+
238
+ def _calculate_adjacencies(self):
239
+ """Calculate which points and hexes are adjacent to each other."""
240
+ # For each hex, determine which points border it
241
+ # For each point, determine which other points it connects to and which hexes it touches
242
+
243
+ # This is based on the geometric relationships of the hexagonal board
244
+ # We'll use the existing logic from DefaultBoard.get_tile_indexes_for_point
245
+ # but in a cleaner, centralized way
246
+
247
+ for point_id, point_def in self.points.items():
248
+ row, col = point_def.game_coords
249
+
250
+ # Find adjacent hexes using the existing logic from DefaultBoard
251
+ adjacent_hex_coords = self._get_hex_coords_for_point(row, col)
252
+ for hex_coord in adjacent_hex_coords:
253
+ # Find hex with these coordinates
254
+ for hex_id, hex_def in self.hexes.items():
255
+ if hex_def.game_coords == hex_coord:
256
+ point_def.adjacent_hexes.append(hex_id)
257
+ hex_def.adjacent_points.append(point_id)
258
+
259
+ # Find adjacent points using board connectivity rules
260
+ point_def.adjacent_points = self._get_connected_point_ids(row, col)
261
+
262
+ def _get_hex_coords_for_point(self, row: int, col: int) -> List[Tuple[int, int]]:
263
+ """
264
+ Get hex coordinates that border a given point.
265
+ This is the same logic as DefaultBoard.get_tile_indexes_for_point
266
+ """
267
+ hex_coords = []
268
+
269
+ # The complex logic from DefaultBoard - but cleaner
270
+ if row < 3: # Top half
271
+ # Hexes below the point
272
+ if col < [7, 9, 11][row] - 1:
273
+ hex_coords.append((row, col // 2))
274
+
275
+ if col % 2 == 0 and col > 0:
276
+ hex_coords.append((row, col // 2 - 1))
277
+
278
+ # Hexes above the point
279
+ if row > 0:
280
+ if col > 0 and col < [7, 9, 11][row] - 2:
281
+ hex_coords.append((row - 1, (col - 1) // 2))
282
+
283
+ if col % 2 == 1 and col < [7, 9, 11][row] - 1 and col > 1:
284
+ hex_coords.append((row - 1, (col - 1) // 2 - 1))
285
+
286
+ else: # Bottom half
287
+ # Hexes below
288
+ if row < 5:
289
+ if col < [11, 9, 7][row - 3] - 2 and col > 0:
290
+ hex_coords.append((row, (col - 1) // 2))
291
+
292
+ if col % 2 == 1 and col > 1 and col < [11, 9, 7][row - 3]:
293
+ hex_coords.append((row, (col - 1) // 2 - 1))
294
+
295
+ # Hexes above
296
+ if col < [11, 9, 7][row - 3] - 1:
297
+ hex_coords.append((row - 1, col // 2))
298
+
299
+ if col > 1 and col % 2 == 0:
300
+ hex_coords.append((row - 1, (col - 1) // 2))
301
+
302
+ return hex_coords
303
+
304
+ def _get_connected_point_ids(self, row: int, col: int) -> List[int]:
305
+ """
306
+ Get point IDs that are directly connected to the given point.
307
+ This is based on DefaultBoard.get_connected_points logic.
308
+ """
309
+ connected = []
310
+
311
+ # Left and right connections
312
+ if col > 0:
313
+ left_point_id = self.coords_to_point_id(row, col - 1)
314
+ if left_point_id:
315
+ connected.append(left_point_id)
316
+
317
+ row_widths = [7, 9, 11, 11, 9, 7]
318
+ if col < row_widths[row] - 1:
319
+ right_point_id = self.coords_to_point_id(row, col + 1)
320
+ if right_point_id:
321
+ connected.append(right_point_id)
322
+
323
+ # Up and down connections (more complex due to hexagonal geometry)
324
+ if row == 2 and col % 2 == 0:
325
+ down_point_id = self.coords_to_point_id(row + 1, col)
326
+ if down_point_id:
327
+ connected.append(down_point_id)
328
+ elif row == 3 and col % 2 == 0:
329
+ up_point_id = self.coords_to_point_id(row - 1, col)
330
+ if up_point_id:
331
+ connected.append(up_point_id)
332
+ elif row < 3:
333
+ if col % 2 == 0:
334
+ down_point_id = self.coords_to_point_id(row + 1, col + 1)
335
+ if down_point_id:
336
+ connected.append(down_point_id)
337
+ elif row > 0 and col > 0:
338
+ up_point_id = self.coords_to_point_id(row - 1, col - 1)
339
+ if up_point_id:
340
+ connected.append(up_point_id)
341
+ else:
342
+ if col % 2 == 0:
343
+ up_point_id = self.coords_to_point_id(row - 1, col + 1)
344
+ if up_point_id:
345
+ connected.append(up_point_id)
346
+ elif row < 5 and col > 0:
347
+ down_point_id = self.coords_to_point_id(row + 1, col - 1)
348
+ if down_point_id:
349
+ connected.append(down_point_id)
350
+
351
+ return connected
352
+
353
+ # ===== PUBLIC API FOR COORDINATE CONVERSIONS =====
354
+
355
+ def point_id_to_game_coords(self, point_id: int) -> Optional[Tuple[int, int]]:
356
+ """Convert point ID (1-54) to game coordinates [row, col]."""
357
+ point_def = self.points.get(point_id)
358
+ return point_def.game_coords if point_def else None
359
+
360
+ def game_coords_to_point_id(self, row: int, col: int) -> Optional[int]:
361
+ """Convert game coordinates [row, col] to point ID (1-54)."""
362
+ for point_id, point_def in self.points.items():
363
+ if point_def.game_coords == (row, col):
364
+ return point_id
365
+ return None
366
+
367
+ def coords_to_point_id(self, row: int, col: int) -> Optional[int]:
368
+ """Alias for game_coords_to_point_id for backward compatibility."""
369
+ return self.game_coords_to_point_id(row, col)
370
+
371
+ def point_id_to_pixel_coords(self, point_id: int) -> Optional[Tuple[float, float]]:
372
+ """Convert point ID to pixel coordinates for web display."""
373
+ point_def = self.points.get(point_id)
374
+ return point_def.pixel_coords if point_def else None
375
+
376
+ def hex_id_to_game_coords(self, hex_id: int) -> Optional[Tuple[int, int]]:
377
+ """Convert hex ID (1-19) to game coordinates [row, col]."""
378
+ hex_def = self.hexes.get(hex_id)
379
+ return hex_def.game_coords if hex_def else None
380
+
381
+ def game_coords_to_hex_id(self, row: int, col: int) -> Optional[int]:
382
+ """Convert game coordinates [row, col] to hex ID (1-19)."""
383
+ for hex_id, hex_def in self.hexes.items():
384
+ if hex_def.game_coords == (row, col):
385
+ return hex_id
386
+ return None
387
+
388
+ def hex_id_to_axial_coords(self, hex_id: int) -> Optional[Tuple[int, int]]:
389
+ """Convert hex ID to axial coordinates [q, r] for web display."""
390
+ hex_def = self.hexes.get(hex_id)
391
+ return hex_def.axial_coords if hex_def else None
392
+
393
+ def get_adjacent_point_ids(self, point_id: int) -> List[int]:
394
+ """Get all point IDs directly connected to the given point."""
395
+ point_def = self.points.get(point_id)
396
+ return point_def.adjacent_points.copy() if point_def else []
397
+
398
+ def get_adjacent_hex_ids(self, point_id: int) -> List[int]:
399
+ """Get all hex IDs that border the given point."""
400
+ point_def = self.points.get(point_id)
401
+ return point_def.adjacent_hexes.copy() if point_def else []
402
+
403
+ def get_hex_border_points(self, hex_id: int) -> List[int]:
404
+ """Get all point IDs that border the given hex."""
405
+ hex_def = self.hexes.get(hex_id)
406
+ return hex_def.adjacent_points.copy() if hex_def else []
407
+
408
+ def is_valid_road_placement(self, point_id_1: int, point_id_2: int) -> bool:
409
+ """Check if a road can be placed between two points."""
410
+ if point_id_1 == point_id_2:
411
+ return False
412
+
413
+ adjacent_points = self.get_adjacent_point_ids(point_id_1)
414
+ return point_id_2 in adjacent_points
415
+
416
+ def get_all_point_ids(self) -> List[int]:
417
+ """Get all valid point IDs (1-54)."""
418
+ return sorted(self.points.keys())
419
+
420
+ def get_all_hex_ids(self) -> List[int]:
421
+ """Get all valid hex IDs (1-19)."""
422
+ return sorted(self.hexes.keys())
423
+
424
+ # ===== EXPORT FUNCTIONS FOR OTHER SYSTEMS =====
425
+
426
+ def export_for_web(self) -> Dict:
427
+ """Export board definition in format expected by web visualization."""
428
+ return {
429
+ 'hexes': [
430
+ {
431
+ 'id': hex_def.hex_id,
432
+ 'q': hex_def.axial_coords[0],
433
+ 'r': hex_def.axial_coords[1],
434
+ 'game_coords': hex_def.game_coords
435
+ }
436
+ for hex_def in self.hexes.values()
437
+ ],
438
+ 'points': [
439
+ {
440
+ 'id': point_def.point_id,
441
+ 'x': point_def.pixel_coords[0],
442
+ 'y': point_def.pixel_coords[1],
443
+ 'game_coords': point_def.game_coords,
444
+ 'adjacent_points': point_def.adjacent_points,
445
+ 'adjacent_hexes': point_def.adjacent_hexes
446
+ }
447
+ for point_def in self.points.values()
448
+ ],
449
+ 'total_points': len(self.points),
450
+ 'total_hexes': len(self.hexes)
451
+ }
452
+
453
+ def export_point_mapping(self) -> Dict:
454
+ """Export point mapping for backward compatibility with point_mapping.py."""
455
+ return {
456
+ 'point_to_coords': {
457
+ point_id: list(point_def.game_coords)
458
+ for point_id, point_def in self.points.items()
459
+ },
460
+ 'coords_to_point': {
461
+ f"{point_def.game_coords[0]},{point_def.game_coords[1]}": point_id
462
+ for point_id, point_def in self.points.items()
463
+ },
464
+ 'total_points': len(self.points)
465
+ }
466
+
467
+ def save_to_file(self, filename: str = 'board_definition.json'):
468
+ """Save board definition to JSON file."""
469
+ data = {
470
+ 'hexes': {
471
+ hex_id: {
472
+ 'hex_id': hex_def.hex_id,
473
+ 'game_coords': hex_def.game_coords,
474
+ 'axial_coords': hex_def.axial_coords,
475
+ 'adjacent_points': hex_def.adjacent_points
476
+ }
477
+ for hex_id, hex_def in self.hexes.items()
478
+ },
479
+ 'points': {
480
+ point_id: {
481
+ 'point_id': point_def.point_id,
482
+ 'game_coords': point_def.game_coords,
483
+ 'pixel_coords': point_def.pixel_coords,
484
+ 'adjacent_points': point_def.adjacent_points,
485
+ 'adjacent_hexes': point_def.adjacent_hexes
486
+ }
487
+ for point_id, point_def in self.points.items()
488
+ }
489
+ }
490
+
491
+ with open(filename, 'w') as f:
492
+ json.dump(data, f, indent=2)
493
+
494
+ print(f"Board definition saved to {filename}")
495
+
496
+
497
+ # Global board definition instance - single source of truth
498
+ board_definition = BoardDefinition()
499
+
500
+
501
+ # Convenience functions for backward compatibility
502
+ def point_id_to_coords(point_id: int) -> Optional[List[int]]:
503
+ """Convert point ID to game coordinates."""
504
+ coords = board_definition.point_id_to_game_coords(point_id)
505
+ return list(coords) if coords else None
506
+
507
+
508
+ def coords_to_point_id(row: int, col: int) -> Optional[int]:
509
+ """Convert game coordinates to point ID."""
510
+ return board_definition.game_coords_to_point_id(row, col)
511
+
512
+
513
+ def get_adjacent_points(point_id: int) -> List[int]:
514
+ """Get adjacent point IDs for the given point."""
515
+ return board_definition.get_adjacent_point_ids(point_id)
516
+
517
+
518
+ def validate_road_placement(point_id_1: int, point_id_2: int) -> bool:
519
+ """Check if a road can be placed between two points."""
520
+ return board_definition.is_valid_road_placement(point_id_1, point_id_2)
521
+
522
+
523
+ if __name__ == "__main__":
524
+ # Test and demonstration
525
+ print("=== PyCatan Board Definition ===")
526
+ print(f"Total hexes: {len(board_definition.hexes)}")
527
+ print(f"Total points: {len(board_definition.points)}")
528
+
529
+ # Test coordinate conversions
530
+ print("\n=== Coordinate Conversion Tests ===")
531
+ test_points = [1, 10, 25, 54]
532
+ for point_id in test_points:
533
+ game_coords = board_definition.point_id_to_game_coords(point_id)
534
+ pixel_coords = board_definition.point_id_to_pixel_coords(point_id)
535
+ back_to_id = board_definition.game_coords_to_point_id(*game_coords) if game_coords else None
536
+
537
+ print(f"Point {point_id}: {game_coords} -> pixels {pixel_coords} -> back to {back_to_id}")
538
+
539
+ # Test adjacency
540
+ print("\n=== Adjacency Tests ===")
541
+ test_point = 25
542
+ adjacent = board_definition.get_adjacent_point_ids(test_point)
543
+ adjacent_hexes = board_definition.get_adjacent_hex_ids(test_point)
544
+ print(f"Point {test_point} adjacent to points: {adjacent}")
545
+ print(f"Point {test_point} adjacent to hexes: {adjacent_hexes}")
546
+
547
+ # Test road validation
548
+ print("\n=== Road Validation Tests ===")
549
+ test_roads = [(1, 2), (1, 8), (25, 26), (1, 54)]
550
+ for p1, p2 in test_roads:
551
+ valid = board_definition.is_valid_road_placement(p1, p2)
552
+ status = "✓" if valid else "✗"
553
+ print(f"Road {p1} -> {p2}: {status}")
554
+
555
+ # Export for web
556
+ board_definition.save_to_file('board_definition.json')
pycatan/building.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # a settlement/city class
2
+
3
+ class Building:
4
+
5
+ BUILDING_SETTLEMENT = 0
6
+ BUILDING_ROAD = 1
7
+ BUILDING_CITY = 2
8
+
9
+ def __init__(self, owner, type, point_one=None, point_two=None):
10
+
11
+ # sets the owner and type
12
+ self.owner = owner
13
+ self.type = type
14
+
15
+ # records where it is if it is a road
16
+ if self.type == Building.BUILDING_ROAD:
17
+
18
+ self.point_one = point_one
19
+ self.point_two = point_two
20
+
21
+ else:
22
+ self.point = point_one
23
+
24
+ def __repr__(self):
25
+
26
+ if self.type == Building.BUILDING_ROAD:
27
+ return "Road, owned by player %s, from %s to %s" % (self.owner, self.point_one.position, self.point_two.position)
28
+
29
+ elif self.type == Building.BUILDING_SETTLEMENT:
30
+ return "Settlement, owned by player %s" % self.owner
31
+
32
+ else:
33
+ return "City, owned by player %s" % self.owner
pycatan/card.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+ # The different types of resource cards
4
+ class ResCard(Enum):
5
+
6
+ # the resource cards
7
+ Wood = 0
8
+ Brick = 1
9
+ Ore = 2
10
+ Sheep = 3
11
+ Wheat = 4
12
+
13
+ # The different types of developement cards
14
+ class DevCard(Enum):
15
+
16
+ # the developement cards
17
+ Road = 0
18
+ VictoryPoint = 1
19
+ Knight = 2
20
+ Monopoly = 3
21
+ YearOfPlenty = 4
pycatan/console_visualization.py ADDED
@@ -0,0 +1,645 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Console-based visualization for PyCatan game.
3
+
4
+ This module provides a text-based console interface for displaying game state,
5
+ actions, and events. It formats the game information in a readable way for
6
+ terminal display.
7
+ """
8
+
9
+ from typing import Dict, Any, List, Optional
10
+ from .visualization import Visualization
11
+ from .actions import Action, ActionResult, ActionType, GameState
12
+ from .card import ResCard, DevCard
13
+
14
+
15
+ class ConsoleVisualization(Visualization):
16
+ """
17
+ Console-based visualization implementation.
18
+
19
+ Displays game information in a formatted text output suitable for
20
+ terminal/console viewing. Uses colors and formatting when available.
21
+ """
22
+
23
+ def __init__(self, use_colors: bool = True, compact_mode: bool = False, output_file: Optional[str] = None):
24
+ """
25
+ Initialize the console visualization.
26
+
27
+ Args:
28
+ use_colors: Whether to use ANSI color codes for formatting
29
+ compact_mode: Whether to use compact display format
30
+ output_file: Optional path to a file to write output to instead of stdout
31
+ """
32
+ super().__init__("Console")
33
+ self.use_colors = use_colors
34
+ self.compact_mode = compact_mode
35
+ self.output_file = output_file
36
+
37
+ # ANSI color codes (if enabled)
38
+ if self.use_colors:
39
+ self.colors = {
40
+ 'reset': '\033[0m',
41
+ 'bold': '\033[1m',
42
+ 'red': '\033[91m',
43
+ 'green': '\033[92m',
44
+ 'yellow': '\033[93m',
45
+ 'blue': '\033[94m',
46
+ 'purple': '\033[95m',
47
+ 'cyan': '\033[96m',
48
+ 'white': '\033[97m'
49
+ }
50
+ else:
51
+ self.colors = {key: '' for key in ['reset', 'bold', 'red', 'green',
52
+ 'yellow', 'blue', 'purple', 'cyan', 'white']}
53
+
54
+ def _print(self, text: str = "", end: str = "\n"):
55
+ """Internal print method to handle output destination."""
56
+ if self.output_file:
57
+ try:
58
+ with open(self.output_file, 'a', encoding='utf-8') as f:
59
+ f.write(str(text) + end)
60
+ except Exception:
61
+ pass # Ignore errors writing to file
62
+ else:
63
+ print(text, end=end)
64
+
65
+ def _format_header(self, text: str) -> str:
66
+ """Format a header with colors and formatting."""
67
+ return f"\n{self.colors['bold']}{self.colors['cyan']}{'='*50}{self.colors['reset']}\n" \
68
+ f"{self.colors['bold']}{self.colors['cyan']}{text.center(50)}{self.colors['reset']}\n" \
69
+ f"{self.colors['bold']}{self.colors['cyan']}{'='*50}{self.colors['reset']}\n"
70
+
71
+ def _format_subheader(self, text: str) -> str:
72
+ """Format a subheader with colors."""
73
+ return f"\n{self.colors['bold']}{self.colors['yellow']}{text}{self.colors['reset']}\n" \
74
+ f"{self.colors['yellow']}{'-' * len(text)}{self.colors['reset']}"
75
+
76
+ def _format_player_name(self, name: str, is_current: bool = False) -> str:
77
+ """Format a player name with highlighting if current."""
78
+ if is_current:
79
+ return f"{self.colors['bold']}{self.colors['green']}► {name}{self.colors['reset']}"
80
+ else:
81
+ return f"{self.colors['white']}{name}{self.colors['reset']}"
82
+
83
+ def _format_resource_card(self, card: ResCard) -> str:
84
+ """Format a resource card with appropriate color."""
85
+ card_colors = {
86
+ ResCard.Wood: 'green',
87
+ ResCard.Brick: 'red',
88
+ ResCard.Wheat: 'yellow',
89
+ ResCard.Sheep: 'white',
90
+ ResCard.Ore: 'purple'
91
+ }
92
+ color = card_colors.get(card, 'white')
93
+ return f"{self.colors[color]}{card.name}{self.colors['reset']}"
94
+
95
+ def _format_building(self, building_type: str, count: int) -> str:
96
+ """Format building information."""
97
+ if count == 0:
98
+ return f"{building_type}: {self.colors['red']}0{self.colors['reset']}"
99
+ else:
100
+ return f"{building_type}: {self.colors['green']}{count}{self.colors['reset']}"
101
+
102
+ def _convert_gamestate_to_dict(self, game_state: Any) -> Dict[str, Any]:
103
+ """Convert GameState object to dictionary format expected by visualization."""
104
+ # If it's already a dict, return it
105
+ if isinstance(game_state, dict):
106
+ return game_state
107
+
108
+ # It's a GameState object
109
+ state_dict = {
110
+ 'turn_number': game_state.turn_number,
111
+ 'current_player_index': game_state.current_player,
112
+ 'players': []
113
+ }
114
+
115
+ # Convert players
116
+ for p in game_state.players_state:
117
+ player_dict = {
118
+ 'name': p.name,
119
+ 'victory_points': p.victory_points,
120
+ 'cards': p.cards,
121
+ 'dev_cards': p.dev_cards,
122
+ 'settlements': len(p.settlements),
123
+ 'cities': len(p.cities),
124
+ 'roads': len(p.roads)
125
+ }
126
+ state_dict['players'].append(player_dict)
127
+
128
+ # Find current player name
129
+ current_player_name = "Unknown"
130
+ for p in game_state.players_state:
131
+ if p.player_id == game_state.current_player:
132
+ current_player_name = p.name
133
+ break
134
+ state_dict['current_player_name'] = current_player_name
135
+
136
+ # Board
137
+ state_dict['board'] = {
138
+ 'robber_position': game_state.board_state.robber_position,
139
+ 'tiles': game_state.board_state.tiles
140
+ }
141
+
142
+ return state_dict
143
+
144
+ def display_game_state(self, game_state: Any) -> None:
145
+ """Display the complete game state."""
146
+ if not self.enabled:
147
+ return
148
+
149
+ # Convert GameState object to dict if needed
150
+ game_state = self._convert_gamestate_to_dict(game_state)
151
+
152
+ self._print(self._format_header("GAME STATE"))
153
+
154
+ # Turn information
155
+ self._print(f"Turn: {self.colors['bold']}{game_state.get('turn_number', 'N/A')}{self.colors['reset']}")
156
+ self._print(f"Current Player: {self._format_player_name(game_state.get('current_player_name', 'N/A'), True)}")
157
+
158
+ # Players information
159
+ players = game_state.get('players', [])
160
+ if players:
161
+ self._print(self._format_subheader("PLAYERS"))
162
+
163
+ for i, player in enumerate(players):
164
+ is_current = i == game_state.get('current_player_index', -1)
165
+ player_name = player.get('name', f'Player {i}')
166
+
167
+ self._print(f"\n{self._format_player_name(player_name, is_current)}")
168
+
169
+ # Victory points
170
+ vp = player.get('victory_points', 0)
171
+ vp_color = 'green' if vp >= 10 else 'white'
172
+ self._print(f" Victory Points: {self.colors[vp_color]}{vp}{self.colors['reset']}")
173
+
174
+ # Resource cards
175
+ cards = player.get('cards', [])
176
+ card_counts = {}
177
+ for card in cards:
178
+ card_counts[card] = card_counts.get(card, 0) + 1
179
+
180
+ if card_counts:
181
+ self._print(f" Resources: ", end="")
182
+ card_strs = []
183
+ for card_type in [ResCard.Wood, ResCard.Brick, ResCard.Wheat, ResCard.Sheep, ResCard.Ore]:
184
+ count = card_counts.get(card_type, 0)
185
+ if count > 0:
186
+ card_strs.append(f"{self._format_resource_card(card_type)}×{count}")
187
+ self._print(", ".join(card_strs) if card_strs else "None")
188
+ else:
189
+ self._print(f" Resources: None")
190
+
191
+ # Development cards
192
+ dev_cards = player.get('dev_cards', [])
193
+ if dev_cards:
194
+ self._print(f" Dev Cards: {len(dev_cards)}")
195
+
196
+ # Buildings
197
+ settlements = player.get('settlements', 0)
198
+ cities = player.get('cities', 0)
199
+ roads = player.get('roads', 0)
200
+ self._print(f" Buildings: {self._format_building('Settlements', settlements)}, " \
201
+ f"{self._format_building('Cities', cities)}, " \
202
+ f"{self._format_building('Roads', roads)}")
203
+
204
+ # Board information (simplified)
205
+ board = game_state.get('board', {})
206
+ if board:
207
+ self._print(self._format_subheader("BOARD"))
208
+
209
+ # Robber position
210
+ robber_pos = game_state.get('robber_position')
211
+ if robber_pos:
212
+ self._print(f"Robber Position: {robber_pos}")
213
+
214
+ # Additional board info could be added here
215
+ if not self.compact_mode:
216
+ tiles = board.get('tiles', [])
217
+ if tiles:
218
+ self._print(f"Board Tiles: {len(tiles)} tiles configured")
219
+
220
+ self._print() # Empty line at end
221
+
222
+ def display_action(self, action: Action, result: ActionResult) -> None:
223
+ """Display a single action and its result."""
224
+ if not self.enabled:
225
+ return
226
+
227
+ # Determine result color
228
+ if result.success:
229
+ result_color = 'green'
230
+ result_symbol = '✓'
231
+ else:
232
+ result_color = 'red'
233
+ result_symbol = '✗'
234
+
235
+ # Format action description
236
+ action_desc = self._get_action_description(action)
237
+
238
+ self._print(f"{self.colors[result_color]}{result_symbol}{self.colors['reset']} {action_desc}")
239
+
240
+ if not result.success and result.error_message:
241
+ self._print(f" {self.colors['red']}Error: {result.error_message}{self.colors['reset']}")
242
+ elif result.success and result.error_message:
243
+ self._print(f" {self.colors['green']}{result.error_message}{self.colors['reset']}")
244
+
245
+ def display_turn_start(self, player_name: str, turn_number: int) -> None:
246
+ """Display turn start notification."""
247
+ if not self.enabled:
248
+ return
249
+
250
+ self._print(f"\n{self.colors['bold']}{self.colors['blue']}>>> Turn {turn_number}: {player_name}'s turn{self.colors['reset']}")
251
+
252
+ def display_dice_roll(self, player_name: str, dice_values: List[int], total: int) -> None:
253
+ """Display dice roll results."""
254
+ if not self.enabled:
255
+ return
256
+
257
+ dice_str = " + ".join(str(d) for d in dice_values)
258
+ total_color = 'red' if total == 7 else 'white'
259
+
260
+ self._print(f"\n{self.colors['bold']}🎲 {player_name} rolled: " \
261
+ f"{dice_str} = {self.colors[total_color]}{total}{self.colors['reset']}")
262
+
263
+ def display_resource_distribution(self, distributions: Dict[str, List[str]]) -> None:
264
+ """Display resource distribution from dice roll."""
265
+ if not self.enabled:
266
+ return
267
+
268
+ if not distributions:
269
+ self._print("No resources were distributed.")
270
+ return
271
+
272
+ self._print("\n📦 Resources distributed:")
273
+ for player_name, resources in distributions.items():
274
+ if resources:
275
+ resource_str = ", ".join(resources)
276
+ self._print(f" {player_name}: {self.colors['green']}{resource_str}{self.colors['reset']}")
277
+
278
+ def display_error(self, message: str) -> None:
279
+ """Display error message."""
280
+ if not self.enabled:
281
+ return
282
+
283
+ self._print(f"{self.colors['red']}❌ Error: {message}{self.colors['reset']}")
284
+
285
+ def display_message(self, message: str) -> None:
286
+ """Display general information message."""
287
+ if not self.enabled:
288
+ return
289
+
290
+ self._print(f"{self.colors['cyan']}ℹ️ {message}{self.colors['reset']}")
291
+
292
+ def _get_action_description(self, action: Action) -> str:
293
+ """Get a human-readable description of an action."""
294
+ player_name = action.parameters.get('player_name', f'Player {action.player_id}')
295
+
296
+ if action.action_type in [ActionType.BUILD_SETTLEMENT, ActionType.PLACE_STARTING_SETTLEMENT]:
297
+ return f"{player_name} built a settlement"
298
+ elif action.action_type == ActionType.BUILD_CITY:
299
+ return f"{player_name} built a city"
300
+ elif action.action_type in [ActionType.BUILD_ROAD, ActionType.PLACE_STARTING_ROAD]:
301
+ return f"{player_name} built a road"
302
+ elif action.action_type == ActionType.TRADE_BANK:
303
+ given = action.parameters.get('give', 'resources')
304
+ received = action.parameters.get('receive', 'resources')
305
+ return f"{player_name} traded {given} for {received} with bank"
306
+ elif action.action_type == ActionType.TRADE_PROPOSE:
307
+ return f"{player_name} proposed a trade"
308
+ elif action.action_type == ActionType.BUY_DEV_CARD:
309
+ return f"{player_name} bought a development card"
310
+ elif action.action_type == ActionType.USE_DEV_CARD:
311
+ card_type = action.parameters.get('card_type', 'development card')
312
+ return f"{player_name} used {card_type}"
313
+ elif action.action_type == ActionType.END_TURN:
314
+ return f"{player_name} ended their turn"
315
+ elif action.action_type == ActionType.ROLL_DICE:
316
+ return f"{player_name} rolled the dice"
317
+ else:
318
+ return f"{player_name} performed {action.action_type.value}"
319
+
320
+ def clear_screen(self) -> None:
321
+ """Clear the console screen (platform dependent)."""
322
+ import os
323
+ os.system('cls' if os.name == 'nt' else 'clear')
324
+
325
+ def set_compact_mode(self, enabled: bool) -> None:
326
+ """Enable or disable compact display mode."""
327
+ self.compact_mode = enabled
328
+
329
+ def set_colors(self, enabled: bool) -> None:
330
+ """Enable or disable color output."""
331
+ self.use_colors = enabled
332
+ if not enabled:
333
+ self.colors = {key: '' for key in self.colors.keys()}
334
+
335
+ def display_board_layout(self, game_state: Dict[str, Any]) -> None:
336
+ """
337
+ Display the complete board layout with tiles, numbers and robber position.
338
+ This shows all the hexagonal tiles, their resource types, and dice numbers.
339
+ """
340
+ if not self.enabled:
341
+ return
342
+
343
+ print(self._format_header("BOARD LAYOUT"))
344
+
345
+ # Handle both Game objects and dict game states
346
+ if hasattr(game_state, 'board'):
347
+ # This is a Game object - use its board directly
348
+ self._display_real_board_tiles(game_state.board.tiles)
349
+
350
+ # Get robber position from game if available
351
+ robber_pos = getattr(game_state, 'robber_pos', None)
352
+ if robber_pos:
353
+ print(f"\n{self.colors['red']}🛡️ Robber Position: {robber_pos}{self.colors['reset']}")
354
+ else:
355
+ # This is a dict game_state - try to extract board info
356
+ board = game_state.get('board_state', {}) or game_state.get('board', {})
357
+ tiles = board.get('tiles', [])
358
+
359
+ if tiles:
360
+ self._display_dict_board_tiles(tiles)
361
+ else:
362
+ print("No board tiles found in game state!")
363
+ print("Note: This display works with full Game objects.")
364
+
365
+ # Show robber position from dict
366
+ robber_pos = game_state.get('robber_position')
367
+ if robber_pos:
368
+ print(f"\n{self.colors['red']}🛡️ Robber Position: {robber_pos}{self.colors['reset']}")
369
+
370
+ print()
371
+
372
+ def _display_real_board_tiles(self, tiles):
373
+ """Display tiles from actual Game.board.tiles structure."""
374
+ print("📋 Board Tiles (by rows):")
375
+ print()
376
+
377
+ # Group tiles by rows
378
+ tiles_by_row = {}
379
+ tile_count = 0
380
+
381
+ for row_index, tile_row in enumerate(tiles):
382
+ if not tile_row: # Skip empty rows
383
+ continue
384
+
385
+ tiles_by_row[row_index] = []
386
+ for col_index, tile in enumerate(tile_row):
387
+ if tile: # Only count non-None tiles
388
+ tile_count += 1
389
+ tiles_by_row[row_index].append((col_index, tile, tile_count))
390
+
391
+ # Display each row
392
+ for row in sorted(tiles_by_row.keys()):
393
+ if not tiles_by_row[row]: # Skip empty rows
394
+ continue
395
+
396
+ print(f"{self.colors['yellow']}Row {row}:{self.colors['reset']}")
397
+
398
+ for col, tile, tile_num in tiles_by_row[row]:
399
+ # Get tile information
400
+ tile_type = self._get_tile_type_name(tile)
401
+ tile_number = self._get_tile_number(tile)
402
+
403
+ # Color coding for tile types
404
+ tile_color = self._get_tile_color(tile_type)
405
+
406
+ # Format tile display
407
+ tile_display = f"{self.colors[tile_color]}{tile_type:<10}{self.colors['reset']}"
408
+ number_display = f"({tile_number:>2})" if tile_number != 'N/A' else " "
409
+
410
+ print(f" [{tile_num:>2}] {tile_display} {number_display} at [{row},{col}]")
411
+ print()
412
+
413
+ print(f"Total: {tile_count} tiles on the board")
414
+
415
+ def _display_dict_board_tiles(self, tiles):
416
+ """Display tiles from dictionary format (fallback)."""
417
+ print("📋 Board Tiles:")
418
+ print()
419
+
420
+ for i, tile in enumerate(tiles, 1):
421
+ # Handle dict format
422
+ if isinstance(tile, dict):
423
+ tile_type = tile.get('type', 'Unknown')
424
+ tile_number = tile.get('number', 'N/A')
425
+ position = tile.get('position', ['?', '?'])
426
+ else:
427
+ tile_type = 'Unknown'
428
+ tile_number = 'N/A'
429
+ position = ['?', '?']
430
+
431
+ # Color coding
432
+ tile_color = self._get_tile_color(tile_type)
433
+
434
+ # Format display
435
+ tile_display = f"{self.colors[tile_color]}{tile_type:<10}{self.colors['reset']}"
436
+ number_display = f"({tile_number:>2})" if tile_number != 'N/A' else " "
437
+
438
+ print(f" [{i:>2}] {tile_display} {number_display} at {position}")
439
+
440
+ def _get_tile_type_name(self, tile):
441
+ """Get the tile type name from a tile object."""
442
+ # Try different attribute names
443
+ if hasattr(tile, 'tile_type'):
444
+ tile_type = tile.tile_type
445
+ elif hasattr(tile, 'type'):
446
+ tile_type = tile.type
447
+ else:
448
+ return 'Unknown'
449
+
450
+ # Handle different formats
451
+ if hasattr(tile_type, 'name'):
452
+ return tile_type.name
453
+ elif hasattr(tile_type, 'value'):
454
+ # Handle enum by value
455
+ type_names = {
456
+ 0: 'Desert', 1: 'Fields', 2: 'Pasture',
457
+ 3: 'Mountains', 4: 'Hills', 5: 'Forest'
458
+ }
459
+ return type_names.get(tile_type.value, 'Unknown')
460
+ else:
461
+ return str(tile_type)
462
+
463
+ def _get_tile_number(self, tile):
464
+ """Get the tile number from a tile object."""
465
+ # Try different attribute names
466
+ if hasattr(tile, 'number') and tile.number is not None:
467
+ return tile.number
468
+ elif hasattr(tile, 'token_num') and tile.token_num is not None:
469
+ return tile.token_num
470
+ else:
471
+ return 'N/A'
472
+
473
+ def _get_tile_color(self, tile_type):
474
+ """Get the appropriate color for a tile type."""
475
+ tile_colors = {
476
+ 'Forest': 'green', 'FOREST': 'green', 'wood': 'green',
477
+ 'Hills': 'red', 'HILLS': 'red', 'brick': 'red',
478
+ 'Fields': 'yellow', 'FIELDS': 'yellow', 'wheat': 'yellow',
479
+ 'Pasture': 'white', 'PASTURE': 'white', 'sheep': 'white',
480
+ 'Mountains': 'purple', 'MOUNTAINS': 'purple', 'ore': 'purple',
481
+ 'Desert': 'cyan', 'DESERT': 'cyan', 'desert': 'cyan'
482
+ }
483
+ return tile_colors.get(tile_type, 'white')
484
+
485
+ def display_points_reference(self) -> None:
486
+ """
487
+ Display the point mapping reference (1-54) organized by board sections.
488
+ This helps players understand which point numbers correspond to which locations.
489
+ """
490
+ if not self.enabled:
491
+ return
492
+
493
+ print(self._format_header("POINTS REFERENCE (1-54)"))
494
+
495
+ print("Points are numbered 1-54 and represent intersections where you can build settlements/cities.")
496
+ print(f"{self.colors['yellow']}Use these point numbers when building!{self.colors['reset']}\n")
497
+
498
+ # Organize points by rows (based on actual Catan board layout - 54 points total)
499
+ points_layout = {
500
+ "Top Row (7 points)": list(range(1, 8)), # Points 1-7
501
+ "Second Row (9 points)": list(range(8, 17)), # Points 8-16
502
+ "Third Row (11 points)": list(range(17, 28)), # Points 17-27
503
+ "Fourth Row (11 points)": list(range(28, 39)), # Points 28-38
504
+ "Fifth Row (9 points)": list(range(39, 48)), # Points 39-47
505
+ "Bottom Row (7 points)": list(range(48, 55)) # Points 48-54
506
+ }
507
+
508
+ for section_name, points in points_layout.items():
509
+ print(f"{self.colors['cyan']}{section_name}:{self.colors['reset']}")
510
+
511
+ # Display points in rows of 6 for better readability
512
+ for i in range(0, len(points), 6):
513
+ row_points = points[i:i+6]
514
+ formatted_points = [f"{self.colors['green']}{p:>2}{self.colors['reset']}" for p in row_points]
515
+ print(f" {' '.join(formatted_points)}")
516
+ print()
517
+
518
+ print(f"{self.colors['yellow']}💡 Tip: Use 'overview' command to see the full board with both tiles and points!{self.colors['reset']}")
519
+ print()
520
+
521
+ def display_robber_info(self, game_state: Dict[str, Any]) -> None:
522
+ """
523
+ Display information about the robber's current position and effects.
524
+ """
525
+ if not self.enabled:
526
+ return
527
+
528
+ print(self._format_header("ROBBER INFORMATION"))
529
+
530
+ robber_pos = game_state.get('robber_position')
531
+
532
+ if robber_pos:
533
+ print(f"🛡️ {self.colors['red']}Robber is currently at position: {robber_pos}{self.colors['reset']}")
534
+
535
+ # Try to find which tile the robber is on
536
+ board = game_state.get('board_state', {}) or game_state.get('board', {})
537
+ tiles = board.get('tiles', [])
538
+
539
+ robber_tile = None
540
+ for i, tile in enumerate(tiles):
541
+ tile_pos = None
542
+ if hasattr(tile, 'position'):
543
+ tile_pos = tile.position
544
+ elif isinstance(tile, dict) and 'position' in tile:
545
+ tile_pos = tile['position']
546
+
547
+ if tile_pos and list(tile_pos) == robber_pos:
548
+ robber_tile = tile
549
+ break
550
+
551
+ if robber_tile:
552
+ # Get tile type
553
+ if hasattr(robber_tile, 'tile_type'):
554
+ tile_type = robber_tile.tile_type.name if hasattr(robber_tile.tile_type, 'name') else str(robber_tile.tile_type)
555
+ elif isinstance(robber_tile, dict) and 'type' in robber_tile:
556
+ tile_type = robber_tile['type']
557
+ else:
558
+ tile_type = 'Unknown'
559
+
560
+ # Get tile number
561
+ if hasattr(robber_tile, 'number'):
562
+ tile_number = robber_tile.number
563
+ elif isinstance(robber_tile, dict) and 'number' in robber_tile:
564
+ tile_number = robber_tile['number']
565
+ else:
566
+ tile_number = 'N/A'
567
+
568
+ print(f"📍 This is a {self.colors['yellow']}{tile_type}{self.colors['reset']} tile with number {self.colors['bold']}{tile_number}{self.colors['reset']}")
569
+ print(f"⚠️ {self.colors['red']}This tile does not produce resources while the robber is there!{self.colors['reset']}")
570
+
571
+ print(f"\n{self.colors['cyan']}Robber Effects:{self.colors['reset']}")
572
+ print(" • Blocks resource production on the occupied tile")
573
+ print(" • Can steal cards from players with settlements/cities on that tile")
574
+ print(" • Moved when a 7 is rolled or when a Knight card is played")
575
+ else:
576
+ print("🤔 Robber position not found in game state.")
577
+
578
+ print()
579
+
580
+ def display_game_overview(self, game_state: Dict[str, Any]) -> None:
581
+ """
582
+ Display a comprehensive overview of the entire game including board, players, and current state.
583
+ This is the "everything at once" view for players who want complete information.
584
+ """
585
+ if not self.enabled:
586
+ return
587
+
588
+ print(self._format_header("COMPLETE GAME OVERVIEW"))
589
+
590
+ # Game status
591
+ turn_num = game_state.get('turn_number', 'N/A')
592
+ current_player = game_state.get('current_player_name', 'N/A')
593
+ print(f"🎲 {self.colors['bold']}Turn {turn_num} - {current_player}'s Turn{self.colors['reset']}")
594
+ print()
595
+
596
+ # Quick board summary
597
+ board = game_state.get('board_state', {}) or game_state.get('board', {})
598
+ tiles = board.get('tiles', [])
599
+
600
+ if tiles:
601
+ # Count tile types
602
+ tile_counts = {}
603
+ for tile in tiles:
604
+ tile_type = 'Unknown'
605
+ if hasattr(tile, 'tile_type'):
606
+ tile_type = tile.tile_type.name if hasattr(tile.tile_type, 'name') else str(tile.tile_type)
607
+ elif isinstance(tile, dict) and 'type' in tile:
608
+ tile_type = tile['type']
609
+
610
+ tile_counts[tile_type] = tile_counts.get(tile_type, 0) + 1
611
+
612
+ print(f"{self.colors['cyan']}📋 Board Summary:{self.colors['reset']}")
613
+ for tile_type, count in tile_counts.items():
614
+ print(f" {tile_type}: {count} tiles")
615
+ print()
616
+
617
+ # Robber info (condensed)
618
+ robber_pos = game_state.get('robber_position')
619
+ if robber_pos:
620
+ print(f"🛡️ {self.colors['red']}Robber at: {robber_pos}{self.colors['reset']}")
621
+
622
+ # Players summary
623
+ players = game_state.get('players', [])
624
+ if players:
625
+ print(f"\n{self.colors['cyan']}👥 Players Status:{self.colors['reset']}")
626
+ for i, player in enumerate(players):
627
+ is_current = i == game_state.get('current_player_index', -1)
628
+ player_name = player.get('name', f'Player {i}')
629
+ vp = player.get('victory_points', 0)
630
+
631
+ indicator = "👑" if is_current else " "
632
+ vp_indicator = "🏆" if vp >= 10 else f"{vp}VP"
633
+
634
+ cards_count = len(player.get('cards', []))
635
+ buildings = f"S:{player.get('settlements', 0)} C:{player.get('cities', 0)} R:{player.get('roads', 0)}"
636
+
637
+ print(f"{indicator} {self.colors['bold'] if is_current else ''}{player_name:<12}{self.colors['reset']} " \
638
+ f"{vp_indicator:<4} {cards_count:>2}cards {buildings}")
639
+
640
+ print(f"\n{self.colors['yellow']}💡 Commands:{self.colors['reset']}")
641
+ print(" 'points' - Show detailed point reference (1-54)")
642
+ print(" 'board' - Show detailed board layout")
643
+ print(" 'robber' - Show robber information")
644
+ print(" 'help' - Show all available commands")
645
+ print()
pycatan/default_board.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pycatan.board import Board
2
+ from pycatan.tile import Tile
3
+ from pycatan.point import Point
4
+ from pycatan.tile_type import TileType
5
+ from pycatan.harbor import Harbor, HarborType
6
+
7
+ import math
8
+ import random
9
+
10
+ # The default, tileagonal board filled with random tiles and tokens
11
+ class DefaultBoard(Board):
12
+
13
+ def __init__(self, game):
14
+ super(DefaultBoard, self).__init__(game)
15
+
16
+ # Set tiles
17
+ tile_deck = Board.get_shuffled_tile_deck()
18
+ token_deck = Board.get_shuffled_tile_nums()
19
+ temp_tiles = []
20
+ for r in range(5):
21
+ temp_tiles.append([])
22
+ for i in range([3, 4, 5, 4, 3][r]):
23
+ # Add a tile
24
+ new_tile = Tile(type=tile_deck.pop(), token_num=None, position=[r, i], points=[])
25
+ temp_tiles[-1].append(new_tile)
26
+ # Remove the token if it is the desert
27
+ if new_tile.type == TileType.Desert:
28
+ self.robber = [r, i]
29
+ else:
30
+ new_tile.token_num = token_deck.pop()
31
+
32
+ self.tiles = tuple(map(lambda x: tuple(x), temp_tiles))
33
+
34
+ # Add points
35
+ temp_points = []
36
+ for r in range(6):
37
+ temp_points.append([])
38
+ for i in range([7, 9, 11, 11, 9, 7][r]):
39
+ point = Point(tiles=[], position=[r, i])
40
+ temp_points[-1].append(point)
41
+ # Set point/tile relations
42
+ for pos in DefaultBoard.get_tile_indexes_for_point(r, i):
43
+ point.tiles.append(self.tiles[pos[0]][pos[1]])
44
+ self.tiles[pos[0]][pos[1]].points.append(point)
45
+
46
+
47
+ self.points = tuple(map(lambda x: tuple(x), temp_points))
48
+ # Set the connected points for each point
49
+ # Must be done after initializing each point so that the point object exists
50
+ for r in self.points:
51
+ for p in r:
52
+ p.connected_points = self.get_connected_points(p.position[0], p.position[1])
53
+ # adds a harbor for each points in the pattern 2 3 2 2 3 2 etc
54
+ outside_points = DefaultBoard.get_outside_points()
55
+ # the pattern of spaces between harbors
56
+ pattern = [1, 2, 1]
57
+ # the current index of pattern
58
+ index = 0
59
+ # the different types of harbors
60
+ harbor_types = [
61
+ HarborType.Wood,
62
+ HarborType.Brick,
63
+ HarborType.Ore,
64
+ HarborType.Wheat,
65
+ HarborType.Sheep,
66
+ HarborType.Any,
67
+ HarborType.Any,
68
+ HarborType.Any,
69
+ HarborType.Any
70
+ ]
71
+ # Shuffles the harbors
72
+ random.shuffle(harbor_types)
73
+ # Run loop until harbor_types is empty
74
+ while harbor_types:
75
+ # Create a new harbor
76
+ p_one = outside_points.pop()
77
+ p_two = outside_points.pop()
78
+ harbor = Harbor(
79
+ point_one = self.points[p_one[0]][p_one[1]],
80
+ point_two = self.points[p_two[0]][p_two[1]],
81
+ type = harbor_types.pop())
82
+ # Add it to harbors
83
+ self.harbors.append(harbor)
84
+ # Remove the unused points from outside_points
85
+ for _ in range(pattern[index % len(pattern)]):
86
+ outside_points.pop()
87
+ # Use next pattern value for number of points inbetween next time
88
+ index += 1
89
+
90
+ # puts the robber on the desert tile to start
91
+ for r in range(len(temp_tiles)):
92
+ # checks if this row has the desert
93
+ if temp_tiles[r].count(TileType.Desert) > 0:
94
+ # places the robber
95
+ self.robber = [r, temp_tiles[r].index(TileType.Desert)]
96
+
97
+ # Returns the indexes of the tiles connected to a certain points
98
+ # on the default, tileagonal Catan board
99
+ @staticmethod
100
+ def get_tile_indexes_for_point(r, i):
101
+ # the indexes of the tiles
102
+ tile_indexes = []
103
+ # Points on a tileagonal board
104
+ points = [
105
+ [None] * 7,
106
+ [None] * 9,
107
+ [None] * 11,
108
+ [None] * 11,
109
+ [None] * 9,
110
+ [None] * 7
111
+ ]
112
+ # gets the adjacent tiles differently depending on whether the point is in the top or the bottom
113
+ if r < len(points) / 2:
114
+ # gets the tiles below the point ------------------
115
+
116
+ # adds the tiles to the right
117
+ if i < len(points[r]) - 1:
118
+ tile_indexes.append([r, math.floor(i / 2)])
119
+
120
+ # if the index is even, the number is between two tiles
121
+ if i % 2 == 0 and i > 0:
122
+ tile_indexes.append([r, math.floor(i / 2) - 1])
123
+
124
+ # gets the tiles above the point ------------------
125
+
126
+ if r > 0:
127
+ # gets the tile to the right
128
+ if i > 0 and i < len(points[r]) - 2:
129
+ tile_indexes.append([r - 1, math.floor((i - 1) / 2)])
130
+
131
+ # gets the tile to the left
132
+ if i % 2 == 1 and i < len(points[r]) - 1 and i > 1:
133
+ tile_indexes.append([r - 1, math.floor((i - 1) / 2) - 1])
134
+
135
+ else:
136
+
137
+ # adds the below -------------
138
+
139
+ if r < len(points) - 1:
140
+ # gets the tile to the right or directly below
141
+ if i < len(points[r]) - 2 and i > 0:
142
+ tile_indexes.append([r, math.floor((i - 1) / 2)])
143
+
144
+ # gets the tile to the left
145
+ if i % 2 == 1 and i > 1 and i < len(points[r]):
146
+ tile_indexes.append([r, math.floor((i - 1) / 2 - 1)])
147
+
148
+ # gets the tiles above ------------
149
+
150
+ # gets the tile above and to the right or directly above
151
+ if i < len(points[r]) - 1:
152
+ tile_indexes.append([r - 1, math.floor(i / 2)])
153
+
154
+ # gets the tile to the left
155
+ if i > 1 and i % 2 == 0:
156
+ tile_indexes.append([r - 1, math.floor((i - 1) / 2)])
157
+
158
+ return tile_indexes
159
+
160
+ # gets the points that are connected to the point given
161
+ def get_connected_points(self, r, i):
162
+ to_return = []
163
+ # Get the point to the left and the right
164
+ if i > 0:
165
+ to_return.append(self.points[r][i - 1])
166
+
167
+ if i < len(self.points[r]) - 1:
168
+ to_return.append(self.points[r][i + 1])
169
+
170
+ # Get the point above and below
171
+ # First, if the point is in the center two rows, the connected point
172
+ # is either directly above/below this point
173
+ if r == 2 and i % 2 == 0:
174
+ to_return.append(self.points[r + 1][i])
175
+ elif r == 3 and i % 2 == 0:
176
+ to_return.append(self.points[r - 1][i])
177
+ # If the point is not in the 2 center rows, the point will have an offset
178
+ elif r < len(self.points) / 2:
179
+ if i % 2 == 0:
180
+ to_return.append(self.points[r + 1][i + 1])
181
+ elif r > 0 and i > 0:
182
+ to_return.append(self.points[r - 1][i - 1])
183
+ else:
184
+ if i % 2 == 0:
185
+ to_return.append(self.points[r - 1][i + 1])
186
+ elif r < len(self.points) - 1 and i > 0:
187
+ to_return.append(self.points[r + 1][i - 1])
188
+ return to_return
189
+
190
+ # Get the points along the outside of the board, in clockwise order
191
+ @staticmethod
192
+ def get_outside_points():
193
+ # The lengths of each row of points on the board
194
+ row_lengths = [
195
+ 7,
196
+ 9,
197
+ 11,
198
+ 11,
199
+ 9,
200
+ 7
201
+ ]
202
+ # The points on the bottom
203
+ bottom = list(map(lambda x: [len(row_lengths) - 1, x], range(row_lengths[-1])))
204
+ # The points on the top
205
+ top = list(map(lambda x: [0, x], range(row_lengths[0])))
206
+ # adds all the points on the right and left
207
+ right = []
208
+ left = []
209
+ for r in range(1, len(row_lengths) - 1):
210
+ # Get the last two and first two points on this row
211
+ last_two = list(map(lambda x: [r, x], range(row_lengths[r])[-2:]))
212
+ first_two = list(map(lambda x: [r, x], reversed(range(2))))
213
+ # If the points are one the bottom half of the board, reverse them
214
+ if r > (len(row_lengths) - 1) / 2:
215
+ last_two = list(reversed(last_two))
216
+ first_two = list(reversed(first_two))
217
+ # Add points to right and left
218
+ right.extend(last_two)
219
+ left.extend(first_two)
220
+
221
+ # Put different sides of points in order
222
+ # bottom and left are reversed since we want to count those points in reverse order
223
+ # to make sure we go in clockwise order
224
+ outside_points = []
225
+ outside_points.extend(top)
226
+ outside_points.extend(right)
227
+ outside_points.extend(reversed(bottom))
228
+ outside_points.extend(reversed(left))
229
+ # Return them
230
+ return outside_points
pycatan/game.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pycatan.default_board import DefaultBoard
2
+ from pycatan.player import Player
3
+ from pycatan.statuses import Statuses
4
+ from pycatan.card import ResCard, DevCard
5
+ from pycatan.building import Building
6
+ from pycatan.harbor import Harbor
7
+ from pycatan.board_definition import board_definition
8
+
9
+ import random
10
+ import math
11
+
12
+ class Game:
13
+
14
+ # initializes the game
15
+ def __init__(self, num_of_players=3, on_win=None, starting_board=False):
16
+ # creates a board
17
+ self.board = DefaultBoard(game=self);
18
+ # creates players
19
+ self.players = []
20
+ for i in range(num_of_players):
21
+ self.players.append(Player(num=i, game=self))
22
+ # Set onWin method
23
+ self.on_win = on_win
24
+ # creates a new Developement deck
25
+ self.dev_deck = []
26
+ for i in range(14):
27
+ # Add 2 Road, Monopoly and Year of Plenty cards
28
+ if i < 2:
29
+ self.dev_deck.append(DevCard.Road)
30
+ self.dev_deck.append(DevCard.Monopoly)
31
+ self.dev_deck.append(DevCard.YearOfPlenty)
32
+ # Add 5 Victory Point cards
33
+ if i < 5:
34
+ self.dev_deck.append(DevCard.VictoryPoint)
35
+ # Add 14 knight cards
36
+ self.dev_deck.append(DevCard.Knight)
37
+ # Shuffle the developement deck
38
+ random.shuffle(self.dev_deck)
39
+ # the longest road owner and largest army owner
40
+ self.longest_road_owner = None
41
+ self.largest_army = None
42
+ # whether the game has finished or not
43
+ self.has_ended = False
44
+
45
+ # creates a new settlement belong to the player at the coodinates
46
+ def add_settlement(self, player, point, is_starting=False):
47
+ # builds the settlement
48
+ status = self.players[player].build_settlement(point=point, is_starting=is_starting)
49
+ # If successful, check if the player has now won
50
+ if status == Statuses.ALL_GOOD:
51
+ if self.players[player].get_VP() >= 10:
52
+ # End the game
53
+ self.has_ended = True
54
+ self.winner = player
55
+
56
+ return status
57
+
58
+ # builds a road going from point start to point end
59
+ def add_road(self, player, start, end, is_starting=False):
60
+ # builds the road
61
+ stat = self.players[player].build_road(start=start, end=end, is_starting=is_starting)
62
+ # checks for a new longest road segment
63
+ self.set_longest_road()
64
+ # returns the status
65
+ return stat
66
+
67
+ # builds a new developement cards for the player
68
+ def build_dev(self, player):
69
+ # makes sure there is still at least one development card left
70
+ if len(self.dev_deck) < 1:
71
+ return Statuses.ERR_DECK
72
+ # makes sure the player has the right cards
73
+ needed_cards = [
74
+ ResCard.Wheat,
75
+ ResCard.Ore,
76
+ ResCard.Sheep
77
+ ]
78
+ if not self.players[player].has_cards(needed_cards):
79
+ return Statuses.ERR_CARDS
80
+ # removes the cards
81
+ self.players[player].remove_cards(needed_cards)
82
+ # gives the player a dev card
83
+ self.players[player].add_dev_card(self.dev_deck[0])
84
+ # removes that dev card from the deck
85
+ del self.dev_deck[0]
86
+
87
+ # gives players the proper cards for a given roll
88
+ def add_yield_for_roll(self, roll):
89
+ return self.board.add_yield(roll)
90
+
91
+ # trades cards (given in an array) between two players
92
+ def trade(self, player_one, player_two, cards_one, cards_two):
93
+ # check if they players have the cards they are trading
94
+ # Needs to do this before deleting because one might have the cards while the other does not
95
+ if not self.players[player_one].has_cards(cards_one):
96
+ return Statuses.ERR_CARDS
97
+
98
+ elif not self.players[player_two].has_cards(cards_two):
99
+ return Statuses.ERR_CARDS
100
+
101
+ else:
102
+ # removes the cards
103
+ self.players[player_one].remove_cards(cards_one)
104
+ self.players[player_two].remove_cards(cards_two)
105
+ # add the new cards
106
+ self.players[player_one].add_cards(cards_two)
107
+ self.players[player_two].add_cards(cards_one)
108
+ return Statuses.ALL_GOOD
109
+
110
+ # moves the robber
111
+ # Note that player is the player moving the robber
112
+ # and victim is the player whose card is being taken
113
+ def move_robber(self, tile, player, victim):
114
+ # checks the player wants to take a card from somebody
115
+ if victim != None:
116
+ # checks the victim has a settlement on the tile
117
+ has_settlement = False
118
+ # Iterate over points and check if there is a settlement/city on any of them
119
+ points = self.board.get_connected_points(tile.position[0], tile.position[1])
120
+ for p in points:
121
+ if p != None and p.building != None:
122
+ # Check the victim owns the settlement/city
123
+ if p.building.owner == victim:
124
+ has_settlement = True
125
+
126
+ if not has_settlement:
127
+ return Statuses.ERR_INPUT
128
+
129
+ # moves the robber
130
+ self.board.move_robber(tile)
131
+ # takes a random card from the victim
132
+ if victim != None:
133
+ # removes a random card from the victim
134
+ index = round(random.random() * (len(self.players[victim].cards) - 1))
135
+ card = self.players[victim].cards[index]
136
+ self.players[victim].remove_cards([card])
137
+ # adds it to the player
138
+ self.players[player].add_cards([card])
139
+
140
+ return Statuses.ALL_GOOD
141
+
142
+ # trades cards from a player to the bank
143
+ # either by 4 for 1 or using a harbor
144
+ def trade_to_bank(self, player, cards, request):
145
+ # makes sure the player has the cards
146
+ if not self.players[player].has_cards(cards):
147
+ return Statuses.ERR_CARDS
148
+ # checks all the cards are the same type
149
+ card_type = cards[0]
150
+ for c in cards[1:]:
151
+ if c != card_type:
152
+ return Statuses.ERR_CARDS
153
+ # if there are not four cards
154
+ if len(cards) != 4:
155
+ # checks if the player has a settlement on the right type of harbor
156
+ has_harbor = False
157
+ harbor_types = self.players[player].get_connected_harbor_types()
158
+ print(harbor_types)
159
+ for h_type in harbor_types:
160
+ if Harbor.get_card_from_harbor_type(h_type) == card_type and len(cards) == 2:
161
+ has_harbor = True
162
+ break
163
+ elif Harbor.get_card_from_harbor_type(h_type) == None and len(cards) == 3:
164
+ has_harbor = True
165
+ break
166
+
167
+ if not has_harbor:
168
+ return Statuses.ERR_HARBOR
169
+
170
+ # removes cards
171
+ self.players[player].remove_cards(cards)
172
+ # adds the new card
173
+ self.players[player].add_cards([request])
174
+
175
+ return Statuses.ALL_GOOD
176
+
177
+ # gives the longest road to the correct player
178
+ def set_longest_road(self):
179
+ # The length of the current longest road segment
180
+ longest = 0
181
+ owner = self.longest_road_owner
182
+
183
+ for p in self.players:
184
+ # longest road needs to be longer than anbody else's
185
+ # and at least 5 road segments long
186
+ if p.longest_road_length > longest and p.longest_road_length >= 5:
187
+ longest = p.longest_road_length
188
+ owner = self.players.index(p)
189
+
190
+ if self.longest_road_owner != owner:
191
+ self.longest_road_owner = owner
192
+ # checks if the player has won now that they has longest road
193
+ if self.players[owner].get_VP() >= 10:
194
+ self.has_ended = True
195
+ self.winner = owner
196
+
197
+ # changes a settlement on the board for a city
198
+ def add_city(self, point, player):
199
+ status = self.board.upgrade_settlement(player, r, i)
200
+
201
+ if status == Statuses.ALL_GOOD:
202
+ # checks if the player won
203
+ if self.players[player].get_VP() >= 10:
204
+ self.winner = player
205
+
206
+ return status
207
+
208
+ # uses a developement card
209
+ # the required args will vary between different dev cards
210
+ def use_dev_card(self, player, card, args):
211
+ # checks the player has the development card
212
+ if not self.players[player].has_dev_cards([card]):
213
+ return Statuses.ERR_CARDS
214
+
215
+ # applies the action
216
+ if card == DevCard.Road:
217
+ # checks the correct arguments are given
218
+ road_names = [
219
+ "road_one",
220
+ "road_two"
221
+ ]
222
+ for r in road_names:
223
+ if not r in args:
224
+ return Statuses.ERR_INPUT
225
+
226
+ else:
227
+ # Check the roads have a start and an end
228
+ if not "start" in args[r] or not "end" in args[r]:
229
+ return Statuses.ERR_INPUT
230
+
231
+ # checks the road location is valid
232
+
233
+ # whether the other road is completely isolated but is connected to this road
234
+ other_road_is_isolated = False
235
+
236
+ for r in road_names:
237
+ location_status = self.players[player].road_location_is_valid(args[r]['start'], args[r]['end'])
238
+
239
+ # if the road location is not OK
240
+ # since the player can build two roads, some
241
+ # locations that would be invalid are valid depending on the other road location
242
+ if not location_status == Statuses.ALL_GOOD:
243
+ # checks if it is isolated, but would be connected to the other road
244
+ if location_status == Statuses.ERR_ISOLATED:
245
+ # if the other road is also isolated, just return an error
246
+ if other_road_is_isolated:
247
+ return location_status
248
+
249
+ # checks if the two roads are connected
250
+ # (since the other one is connected, this road is connected through it)
251
+ road_points = [
252
+ "start",
253
+ "end"
254
+ ]
255
+ roads_are_connected = False
256
+ for p_one in road_points:
257
+ for p_two in road_points:
258
+ if args["road_one"][p_one] == args['road_two'][p_two]:
259
+ other_road_is_isolated = True
260
+ # doesn't return an isolated error
261
+ roads_are_connected = True
262
+
263
+ if not roads_are_connected:
264
+ return location_status
265
+ else:
266
+ return location_status
267
+
268
+ # builds the roads
269
+ for r in road_names:
270
+ self.board.add_road(Building(point_one=args[r]["start"], point_two=args[r]["end"], owner=player, type=Building.BUILDING_ROAD))
271
+
272
+ return Statuses.ALL_GOOD
273
+
274
+ elif card == DevCard.Knight:
275
+ # checks there are the right arguments
276
+ if not ("robber_pos" in args and "victim" in args):
277
+ return Statuses.ERR_INPUT
278
+
279
+ # checks the victim input is valid
280
+ if args["victim"] != None:
281
+ if args["victim"] < 0 or args["victim"] >= len(self.players) or args["victim"] == player:
282
+ return Statuses.ERR_INPUT
283
+
284
+ # moves the robber
285
+ result = self.move_robber(r=args["robber_pos"][0], i=args["robber_pos"][1], player=player, victim=args["victim"])
286
+
287
+ if result != Statuses.ALL_GOOD:
288
+ return result
289
+
290
+ # adds one to the player's knight count
291
+ (self.players[player]).knight_cards += 1
292
+
293
+ # checks for the largest army
294
+ if self.largest_army == None:
295
+ # if nobody has the largest army, the player needs at least 3 cards
296
+ if self.players[player].knight_cards >= 3:
297
+ self.largest_army = player
298
+
299
+ else:
300
+ # the player needs to have more than anybody else
301
+ current_longest = self.players[self.largest_army].knight_cards
302
+
303
+ if self.players[player].knight_cards > current_longest:
304
+ self.largest_army = player
305
+
306
+ elif card == DevCard.Monopoly:
307
+ # gets the type of card
308
+ card_type = args['card_type']
309
+ # for each player, checks if they have the card
310
+ for p in self.players:
311
+ if p.has_cards([card_type]):
312
+ # gets how many this player has
313
+ number_of_cards = p.cards.count(card_type)
314
+ cards_to_give = [card_type] * number_of_cards
315
+ # removes the cards
316
+ p.remove_cards(cards_to_give)
317
+ # adds them to the user's cards
318
+ self.players[player].add_cards(cards_to_give)
319
+
320
+ elif card == DevCard.VictoryPoint:
321
+ # players do not play developement cards, so it returns an error
322
+ return Statuses.ERR_INPUT
323
+
324
+ elif card == DevCard.YearOfPlenty:
325
+ # checks the player gave two development cards
326
+ if not 'card_one' in args and not 'card_two' in args:
327
+ return Statuses.ERR_INPUT
328
+
329
+ # gives the player 2 resource cards of their choice
330
+ self.players[player].add_cards([
331
+ args['card_one'],
332
+ args['card_two']
333
+ ])
334
+
335
+ else:
336
+ # error here
337
+ return Statuses.ERR_INPUT
338
+
339
+ # removes the card
340
+ self.players[player].remove_dev_card(card)
341
+
342
+ return Statuses.ALL_GOOD
343
+
344
+ # simulates 2 dice rolling
345
+ def get_roll(self):
346
+ return round(random.random() * 6) + round(random.random() * 6)
347
+
348
+ def get_full_state(self):
349
+ """
350
+ Get the complete current state of the game.
351
+
352
+ This method extracts all relevant game state information
353
+ and returns it in a GameState object for use by the
354
+ GameManager and visualization systems.
355
+
356
+ Returns:
357
+ GameState: Complete current game state
358
+ """
359
+ from .actions import GameState, PlayerState, BoardState, GamePhase, TurnPhase
360
+
361
+ # Create player states
362
+ players_state = []
363
+ for i, player in enumerate(self.players):
364
+ player_state = PlayerState(
365
+ player_id=i,
366
+ name=f"Player {i}", # Default name - can be enhanced later
367
+ cards=[card.name.lower() for card in player.cards], # Convert enum to string
368
+ dev_cards=[card.name.lower() for card in player.dev_cards],
369
+ settlements=self._get_player_settlements(player),
370
+ cities=self._get_player_cities(player),
371
+ roads=self._get_player_roads(player),
372
+ victory_points=player.get_VP(),
373
+ longest_road_length=player.longest_road_length,
374
+ has_longest_road=(self.longest_road_owner == i),
375
+ has_largest_army=(self.largest_army == i),
376
+ knights_played=player.knight_cards
377
+ )
378
+ players_state.append(player_state)
379
+
380
+ # Create board state
381
+ board_state = BoardState(
382
+ tiles=self._get_tiles_info(),
383
+ robber_position=tuple(self._get_robber_position()),
384
+ harbors=self._get_ports_info(),
385
+ buildings=self._get_all_buildings(),
386
+ roads=self._get_all_roads()
387
+ )
388
+
389
+ # Create and return game state
390
+ return GameState(
391
+ game_id="current_game", # Default ID - can be enhanced
392
+ turn_number=0, # Basic - can be enhanced
393
+ current_player=0, # Basic - managed by GameManager
394
+ game_phase=GamePhase.NORMAL_PLAY, # Default to normal play
395
+ turn_phase=TurnPhase.PLAYER_ACTIONS, # Default to player actions
396
+ players_state=players_state,
397
+ board_state=board_state
398
+ )
399
+
400
+ def _get_player_settlements(self, player):
401
+ """Get list of settlement coordinates for a player."""
402
+ settlements = []
403
+ for point in self.board.points:
404
+ for p in point:
405
+ if p and p.building and p.building.type == 0 and p.building.owner == player.num: # BUILDING_SETTLEMENT = 0
406
+ settlements.append(p.position)
407
+ return settlements
408
+
409
+ def _get_player_cities(self, player):
410
+ """Get list of city coordinates for a player."""
411
+ cities = []
412
+ for point in self.board.points:
413
+ for p in point:
414
+ if p and p.building and p.building.type == 2 and p.building.owner == player.num: # BUILDING_CITY = 2
415
+ cities.append(p.position)
416
+ return cities
417
+
418
+ def _get_player_roads(self, player):
419
+ """Get list of road connections for a player."""
420
+ roads = []
421
+ for road in self.board.roads:
422
+ if road and road.owner == player.num and road.type == 1: # BUILDING_ROAD = 1
423
+ # Roads connect two points
424
+ start_pos = road.point_one.position if road.point_one else [0, 0]
425
+ end_pos = road.point_two.position if road.point_two else [0, 0]
426
+ roads.append(start_pos + end_pos) # [start_row, start_col, end_row, end_col]
427
+ return roads
428
+
429
+ def _get_robber_position(self):
430
+ """Get current robber position."""
431
+ if hasattr(self.board, 'robber') and self.board.robber:
432
+ return self.board.robber
433
+ return [3, 3] # Default center position if not found
434
+
435
+ def _get_tiles_info(self):
436
+ """Get information about all tiles using BoardDefinition."""
437
+ tiles = []
438
+ for i, tile_row in enumerate(self.board.tiles):
439
+ for j, tile in enumerate(tile_row):
440
+ if tile:
441
+ # Get hex ID from BoardDefinition
442
+ hex_id = board_definition.game_coords_to_hex_id(i, j)
443
+ # Get axial coordinates for web display
444
+ axial_coords = board_definition.hex_id_to_axial_coords(hex_id) if hex_id else (i, j)
445
+
446
+ tile_info = {
447
+ 'id': hex_id or (i * 10 + j), # Fallback ID if not found
448
+ 'position': [i, j], # Game coordinates
449
+ 'axial_coords': axial_coords, # Web display coordinates
450
+ 'type': tile.type.name.lower() if hasattr(tile.type, 'name') else 'desert',
451
+ 'token': getattr(tile, 'token_num', None),
452
+ 'has_robber': (self.board.robber == [i, j]) if hasattr(self.board, 'robber') else False
453
+ }
454
+ tiles.append(tile_info)
455
+ return tiles
456
+
457
+ def _get_ports_info(self):
458
+ """Get information about all ports/harbors."""
459
+ ports = []
460
+ for harbor in self.board.harbors:
461
+ if harbor:
462
+ port_info = {
463
+ 'position': getattr(harbor, 'position', [0, 0]),
464
+ 'resource': harbor.type.name.lower() if hasattr(harbor.type, 'name') else 'any',
465
+ 'ratio': getattr(harbor, 'ratio', 3)
466
+ }
467
+ ports.append(port_info)
468
+ return ports
469
+
470
+ def _get_all_buildings(self):
471
+ """Get all buildings on the board using point IDs."""
472
+ buildings = {}
473
+ for point_row in self.board.points:
474
+ for point in point_row:
475
+ if point and point.building:
476
+ # Convert coordinates to point ID using BoardDefinition
477
+ point_id = board_definition.game_coords_to_point_id(point.position[0], point.position[1])
478
+
479
+ if point_id:
480
+ buildings[point_id] = {
481
+ 'type': 'settlement' if point.building.type == Building.BUILDING_SETTLEMENT else 'city',
482
+ 'owner': point.building.owner,
483
+ 'game_coords': point.position # Keep for debugging
484
+ }
485
+ return buildings
486
+
487
+ def _get_all_roads(self):
488
+ """Get all roads on the board using point IDs."""
489
+ roads = []
490
+ for road in self.board.roads:
491
+ if road and road.type == Building.BUILDING_ROAD:
492
+ # Convert coordinates to point IDs using BoardDefinition
493
+ start_coords = road.point_one.position if road.point_one else [0, 0]
494
+ end_coords = road.point_two.position if road.point_two else [0, 0]
495
+
496
+ start_point_id = board_definition.game_coords_to_point_id(start_coords[0], start_coords[1])
497
+ end_point_id = board_definition.game_coords_to_point_id(end_coords[0], end_coords[1])
498
+
499
+ if start_point_id and end_point_id:
500
+ roads.append({
501
+ 'start_point_id': start_point_id,
502
+ 'end_point_id': end_point_id,
503
+ 'owner': road.owner,
504
+ 'start_coords': start_coords, # Keep for debugging
505
+ 'end_coords': end_coords # Keep for debugging
506
+ })
507
+ return roads
508
+
pycatan/game_manager.py ADDED
@@ -0,0 +1,1563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GameManager - Central coordinator for PyCatan game flow
3
+
4
+ This module contains the GameManager class that orchestrates the entire
5
+ game flow, manages turns, coordinates between users and the game state.
6
+ """
7
+
8
+ from typing import List, Optional, Dict, Any
9
+ import uuid
10
+ import random
11
+ from datetime import datetime
12
+
13
+ from pycatan.actions import Action, ActionResult, GameState, GamePhase, TurnPhase, ActionType
14
+ from pycatan.user import User, UserList, validate_user_list, UserInputError
15
+ from pycatan.game import Game
16
+ from pycatan.statuses import Statuses
17
+
18
+
19
+ class GameManager:
20
+ """
21
+ Central coordinator for a Catan game session.
22
+
23
+ The GameManager orchestrates the entire game flow:
24
+ - Manages turn order and game phases
25
+ - Coordinates between Users and the Game logic
26
+ - Maintains the current game state
27
+ - Handles user input and action execution
28
+ - Manages game-wide events and notifications
29
+ """
30
+
31
+ def __init__(self, users: UserList, game_config: Optional[Dict[str, Any]] = None, random_seed: Optional[int] = None):
32
+ """
33
+ Initialize a new GameManager.
34
+
35
+ Args:
36
+ users: List of User objects for this game
37
+ game_config: Optional configuration for the game (board layout, rules, etc.)
38
+ random_seed: Optional seed for random number generator (for reproducible games)
39
+
40
+ Raises:
41
+ ValueError: If users list is invalid
42
+ """
43
+ # Validate users
44
+ validate_user_list(users)
45
+
46
+ # Set random seed if provided (for reproducible games)
47
+ if random_seed is not None:
48
+ random.seed(random_seed)
49
+
50
+ # Store game metadata
51
+ self.game_id = str(uuid.uuid4())
52
+ self.created_at = datetime.now()
53
+ self.users = users
54
+ self.num_players = len(users)
55
+
56
+ # Initialize game configuration
57
+ self.config = game_config or {}
58
+
59
+ # Visualization manager (can be set later)
60
+ self.visualization_manager = None
61
+
62
+ # Create the underlying game instance
63
+ self.game = Game(num_of_players=self.num_players)
64
+
65
+ # Initialize game state
66
+ self._current_game_state = GameState(
67
+ game_id=self.game_id,
68
+ turn_number=0,
69
+ current_player=0,
70
+ game_phase=GamePhase.SETUP_FIRST_ROUND,
71
+ turn_phase=TurnPhase.ROLL_DICE
72
+ )
73
+
74
+ # Game flow control
75
+ self._is_running = False
76
+ self._is_paused = False
77
+
78
+ # Action history and pending operations
79
+ self._action_history: List[Action] = []
80
+ self._pending_actions: List[Action] = []
81
+
82
+ # Error tracking per player to prevent infinite loops
83
+ self._player_error_count = [0] * self.num_players
84
+
85
+ # Setup phase progress tracking
86
+ self._setup_turn_progress = {'settlement': False, 'road': False}
87
+
88
+ @property
89
+ def is_running(self) -> bool:
90
+ """Whether the game is currently running."""
91
+ return self._is_running
92
+
93
+ @property
94
+ def is_paused(self) -> bool:
95
+ """Whether the game is currently paused."""
96
+ return self._is_paused
97
+
98
+ @property
99
+ def current_player_id(self) -> int:
100
+ """ID of the current player."""
101
+ return self._current_game_state.current_player
102
+
103
+ @property
104
+ def current_user(self) -> User:
105
+ """The User object for the current player."""
106
+ return self.users[self.current_player_id]
107
+
108
+ def get_full_state(self) -> GameState:
109
+ """
110
+ Get the complete current state of the game.
111
+
112
+ This method extracts state from the Game object and combines it
113
+ with GameManager state like current player and turn information.
114
+
115
+ Returns:
116
+ GameState: Complete current game state
117
+ """
118
+ # Get the base state from the Game object
119
+ game_state = self.game.get_full_state()
120
+
121
+ # Update player names with user names
122
+ for i, user in enumerate(self.users):
123
+ if i < len(game_state.players_state):
124
+ game_state.players_state[i].name = getattr(user, 'name', f'Player {i + 1}')
125
+
126
+ # Update with GameManager-specific information
127
+ game_state.game_id = self.game_id
128
+ game_state.turn_number = self._current_game_state.turn_number
129
+ game_state.current_player = self._current_game_state.current_player
130
+ game_state.game_phase = self._current_game_state.game_phase
131
+ game_state.turn_phase = self._current_game_state.turn_phase
132
+
133
+ return game_state
134
+
135
+ def get_available_actions(self) -> List[str]:
136
+ """
137
+ Get a list of available action types for the current game state.
138
+
139
+ Returns:
140
+ List[str]: List of allowed ActionType names
141
+ """
142
+ actions = []
143
+ phase = self._current_game_state.game_phase
144
+ turn_phase = self._current_game_state.turn_phase
145
+
146
+ if phase in [GamePhase.SETUP_FIRST_ROUND, GamePhase.SETUP_SECOND_ROUND]:
147
+ # In setup phase: settlement first, then road
148
+ if not self._setup_turn_progress['settlement']:
149
+ # Can only place settlement when none placed yet
150
+ actions.append(ActionType.PLACE_STARTING_SETTLEMENT.name)
151
+ elif not self._setup_turn_progress['road']:
152
+ # Can only place road after settlement is placed
153
+ actions.append(ActionType.PLACE_STARTING_ROAD.name)
154
+ else:
155
+ # Both built, turn should auto-end but allow manual end
156
+ actions.append(ActionType.END_TURN.name)
157
+
158
+ elif phase == GamePhase.NORMAL_PLAY:
159
+ # Check special robber-related phases first
160
+ if turn_phase == TurnPhase.DISCARD_PHASE:
161
+ # Only discard action allowed
162
+ actions.append(ActionType.DISCARD_CARDS.name)
163
+ elif turn_phase == TurnPhase.ROBBER_MOVE:
164
+ # Only robber move allowed
165
+ actions.append(ActionType.ROBBER_MOVE.name)
166
+ elif turn_phase == TurnPhase.ROBBER_STEAL:
167
+ # Only steal action allowed
168
+ actions.append(ActionType.STEAL_CARD.name)
169
+ elif not self._current_game_state.dice_rolled:
170
+ # Normal pre-roll phase
171
+ actions.extend([
172
+ ActionType.ROLL_DICE.name,
173
+ ActionType.USE_DEV_CARD.name
174
+ ])
175
+ else:
176
+ # Normal post-roll phase - player actions
177
+ actions.extend([
178
+ ActionType.BUILD_SETTLEMENT.name,
179
+ ActionType.BUILD_CITY.name,
180
+ ActionType.BUILD_ROAD.name,
181
+ ActionType.TRADE_PROPOSE.name,
182
+ ActionType.TRADE_BANK.name,
183
+ ActionType.BUY_DEV_CARD.name,
184
+ ActionType.USE_DEV_CARD.name,
185
+ ActionType.END_TURN.name
186
+ ])
187
+
188
+ return actions
189
+
190
+ def execute_action(self, action: Action) -> ActionResult:
191
+ """
192
+ Execute an action in the game.
193
+
194
+ This is the main entry point for all game actions.
195
+ Validates the action and delegates to the appropriate handler.
196
+
197
+ Args:
198
+ action: The action to execute
199
+
200
+ Returns:
201
+ ActionResult: Result of the action execution
202
+ """
203
+ # Basic validation
204
+ if not self._is_running:
205
+ return ActionResult.failure_result(
206
+ "Game is not running",
207
+ "GAME_NOT_RUNNING"
208
+ )
209
+
210
+ if action.player_id != self.current_player_id:
211
+ return ActionResult.failure_result(
212
+ f"Not player {action.player_id}'s turn",
213
+ "NOT_YOUR_TURN"
214
+ )
215
+
216
+ # Log the action attempt
217
+ self._action_history.append(action)
218
+
219
+ try:
220
+ # Route to appropriate handler based on action type
221
+ if action.action_type == ActionType.END_TURN:
222
+ return self._handle_end_turn(action)
223
+ elif action.action_type in [ActionType.BUILD_SETTLEMENT, ActionType.BUILD_CITY, ActionType.BUILD_ROAD,
224
+ ActionType.PLACE_STARTING_SETTLEMENT, ActionType.PLACE_STARTING_ROAD]:
225
+ return self._handle_building_action(action)
226
+ elif action.action_type == ActionType.ROLL_DICE:
227
+ return self._handle_roll_dice(action)
228
+ elif action.action_type in [ActionType.TRADE_PROPOSE, ActionType.TRADE_ACCEPT, ActionType.TRADE_REJECT]:
229
+ return self._handle_trade_action(action)
230
+ elif action.action_type == ActionType.DISCARD_CARDS:
231
+ return self._handle_discard_cards(action)
232
+ elif action.action_type == ActionType.ROBBER_MOVE:
233
+ return self._handle_robber_move(action)
234
+ elif action.action_type == ActionType.STEAL_CARD:
235
+ return self._handle_steal_card(action)
236
+ else:
237
+ # For now, return "not implemented" for other actions
238
+ return ActionResult.failure_result(
239
+ f"Action {action.action_type} not yet implemented",
240
+ "NOT_IMPLEMENTED"
241
+ )
242
+
243
+ except Exception as e:
244
+ return ActionResult.failure_result(
245
+ f"Error executing action: {str(e)}",
246
+ "EXECUTION_ERROR"
247
+ )
248
+
249
+ def _handle_end_turn(self, action: Action) -> ActionResult:
250
+ """Handle end turn action."""
251
+ # In the new architecture, this method just validates and returns success.
252
+ # The actual turn advancement happens in _advance_to_next_player()
253
+ # which is called by the game loop when this action returns success.
254
+
255
+ return ActionResult.success_result(
256
+ self.get_full_state(),
257
+ affected_players=[action.player_id]
258
+ )
259
+
260
+ def _handle_building_action(self, action: Action) -> ActionResult:
261
+ """Handle building actions (settlements, cities, roads)."""
262
+ try:
263
+ if action.action_type in [ActionType.BUILD_SETTLEMENT, ActionType.PLACE_STARTING_SETTLEMENT]:
264
+ return self._execute_build_settlement(action)
265
+ elif action.action_type == ActionType.BUILD_CITY:
266
+ return self._execute_build_city(action)
267
+ elif action.action_type in [ActionType.BUILD_ROAD, ActionType.PLACE_STARTING_ROAD]:
268
+ return self._execute_build_road(action)
269
+ else:
270
+ return ActionResult.failure_result(
271
+ f"Unknown building action: {action.action_type}",
272
+ "UNKNOWN_ACTION"
273
+ )
274
+ except Exception as e:
275
+ return ActionResult.failure_result(
276
+ f"Error executing building action: {str(e)}",
277
+ "BUILDING_ERROR"
278
+ )
279
+
280
+ def _distribute_setup_resources(self, player_id: int, point: Any) -> None:
281
+ """Distribute initial resources based on the second settlement."""
282
+ from pycatan.board import Board
283
+
284
+ resources_given = []
285
+
286
+ # Iterate over tiles adjacent to the point
287
+ # Point object has 'tiles' attribute which is a list of Tile objects
288
+ if hasattr(point, 'tiles'):
289
+ for tile in point.tiles:
290
+ # Get resource type from tile type
291
+ card_type = Board.get_card_from_tile(tile.type)
292
+
293
+ # If it's a valid resource (not None/Desert)
294
+ if card_type:
295
+ # Add card to player
296
+ self.game.players[player_id].add_cards([card_type])
297
+ resources_given.append(card_type.name)
298
+
299
+ if resources_given:
300
+ # Create a dummy action for notification purposes
301
+ # We use PLACE_STARTING_SETTLEMENT but need to provide dummy point_coords
302
+ # to satisfy validation, even though they aren't used for the notification
303
+ dummy_action = Action(
304
+ ActionType.PLACE_STARTING_SETTLEMENT,
305
+ player_id,
306
+ {'point_coords': [0, 0]} # Dummy coordinates
307
+ )
308
+
309
+ self._notify_user(
310
+ player_id,
311
+ dummy_action,
312
+ True,
313
+ f"Received starting resources: {', '.join(resources_given)}"
314
+ )
315
+
316
+ # Notify visualization about starting resources
317
+ if self.visualization_manager:
318
+ player_name = self.users[player_id].name if hasattr(self.users[player_id], 'name') else f"Player {player_id + 1}"
319
+ # Create distribution dict format
320
+ distribution = {player_name: resources_given}
321
+ self.visualization_manager.display_resource_distribution(distribution)
322
+
323
+ def _execute_build_settlement(self, action: Action) -> ActionResult:
324
+ """Execute settlement building action."""
325
+ # Extract coordinates from action parameters
326
+ if 'point_coords' not in action.parameters:
327
+ return ActionResult.failure_result(
328
+ "Settlement action missing point_coords parameter",
329
+ "MISSING_COORDS"
330
+ )
331
+
332
+ coords = action.parameters['point_coords']
333
+
334
+ # Determine if this is a starting settlement based on game phase
335
+ # The GameManager is the authority on rules, so we check the phase here
336
+ in_setup_phase = self._current_game_state.game_phase in [GamePhase.SETUP_FIRST_ROUND, GamePhase.SETUP_SECOND_ROUND]
337
+
338
+ # If in setup phase, force is_starting to True (free building)
339
+ # Otherwise respect the action parameter (defaulting to False)
340
+ if in_setup_phase:
341
+ is_starting = True
342
+ else:
343
+ is_starting = action.parameters.get('is_starting', False)
344
+
345
+ # Get the point object from board
346
+ try:
347
+ point = self.game.board.points[coords[0]][coords[1]]
348
+ except (IndexError, TypeError):
349
+ return ActionResult.failure_result(
350
+ f"Invalid coordinates: {coords}",
351
+ "INVALID_COORDS"
352
+ )
353
+
354
+ # Call the actual Game method
355
+ status = self.game.add_settlement(action.player_id, point, is_starting)
356
+
357
+ # Update setup progress if successful and in setup phase
358
+ if status == Statuses.ALL_GOOD and in_setup_phase:
359
+ self._setup_turn_progress['settlement'] = True
360
+
361
+ # Only distribute resources in the second round of setup
362
+ if self._current_game_state.game_phase == GamePhase.SETUP_SECOND_ROUND:
363
+ self._distribute_setup_resources(action.player_id, point)
364
+
365
+ # Convert Status to ActionResult
366
+ return self._convert_status_to_result(status, self.get_full_state(), [action.player_id])
367
+
368
+ def _execute_build_city(self, action: Action) -> ActionResult:
369
+ """Execute city building action."""
370
+ # Extract coordinates from action parameters
371
+ if 'point_coords' not in action.parameters:
372
+ return ActionResult.failure_result(
373
+ "City action missing point_coords parameter",
374
+ "MISSING_COORDS"
375
+ )
376
+
377
+ coords = action.parameters['point_coords']
378
+
379
+ # Get the point object from board
380
+ try:
381
+ point = self.game.board.points[coords[0]][coords[1]]
382
+ except (IndexError, TypeError):
383
+ return ActionResult.failure_result(
384
+ f"Invalid coordinates: {coords}",
385
+ "INVALID_COORDS"
386
+ )
387
+
388
+ # Call the actual Game method (add_city doesn't exist, need to implement via settlement upgrade)
389
+ # For now, return not implemented
390
+ return ActionResult.failure_result(
391
+ "City building not yet implemented in Game class",
392
+ "NOT_IMPLEMENTED"
393
+ )
394
+
395
+ def _execute_build_road(self, action: Action) -> ActionResult:
396
+ """Execute road building action."""
397
+ # Extract coordinates from action parameters
398
+ if 'start_coords' not in action.parameters or 'end_coords' not in action.parameters:
399
+ return ActionResult.failure_result(
400
+ "Road action missing start_coords or end_coords parameters",
401
+ "MISSING_COORDS"
402
+ )
403
+
404
+ start_coords = action.parameters['start_coords']
405
+ end_coords = action.parameters['end_coords']
406
+
407
+ # Determine if this is a starting road based on game phase
408
+ # The GameManager is the authority on rules, so we check the phase here
409
+ in_setup_phase = self._current_game_state.game_phase in [GamePhase.SETUP_FIRST_ROUND, GamePhase.SETUP_SECOND_ROUND]
410
+
411
+ # If in setup phase, force is_starting to True (free building)
412
+ # Otherwise respect the action parameter (defaulting to False)
413
+ if in_setup_phase:
414
+ is_starting = True
415
+ else:
416
+ is_starting = action.parameters.get('is_starting', False)
417
+
418
+ # Get point objects from board
419
+ try:
420
+ start_point = self.game.board.points[start_coords[0]][start_coords[1]]
421
+ end_point = self.game.board.points[end_coords[0]][end_coords[1]]
422
+ except (IndexError, TypeError):
423
+ return ActionResult.failure_result(
424
+ f"Invalid coordinates: start={start_coords}, end={end_coords}",
425
+ "INVALID_COORDS"
426
+ )
427
+
428
+ # Call the actual Game method
429
+ status = self.game.add_road(action.player_id, start_point, end_point, is_starting)
430
+
431
+ # Update setup progress if successful and in setup phase
432
+ if status == Statuses.ALL_GOOD and in_setup_phase:
433
+ self._setup_turn_progress['road'] = True
434
+
435
+ # Convert Status to ActionResult
436
+ return self._convert_status_to_result(status, self.get_full_state(), [action.player_id])
437
+
438
+ def _convert_status_to_result(self, status, game_state, affected_players):
439
+ """Convert Game.Statuses to ActionResult."""
440
+ from pycatan.statuses import Statuses
441
+
442
+ if status == Statuses.ALL_GOOD:
443
+ return ActionResult.success_result(game_state, affected_players)
444
+ elif status == Statuses.ERR_CARDS:
445
+ return ActionResult.failure_result("Not enough cards", "INSUFFICIENT_RESOURCES")
446
+ elif status == Statuses.ERR_BLOCKED:
447
+ return ActionResult.failure_result("Location is blocked", "LOCATION_BLOCKED")
448
+ elif status == Statuses.ERR_INPUT:
449
+ return ActionResult.failure_result("Invalid input", "INVALID_INPUT")
450
+ elif status == Statuses.ERR_NOT_CON:
451
+ return ActionResult.failure_result("Road points are not connected", "NOT_CONNECTED")
452
+ elif status == Statuses.ERR_ISOLATED:
453
+ return ActionResult.failure_result("Not connected to existing buildings", "ISOLATED")
454
+ else:
455
+ return ActionResult.failure_result(f"Unknown status: {status}", "UNKNOWN_ERROR")
456
+
457
+ def _handle_trade_action(self, action: Action) -> ActionResult:
458
+ """Handle trade-related actions."""
459
+ if action.action_type == ActionType.TRADE_PROPOSE:
460
+ return self._execute_trade_propose(action)
461
+ elif action.action_type == ActionType.TRADE_BANK:
462
+ return self._execute_trade_bank(action)
463
+ else:
464
+ # TRADE_ACCEPT and TRADE_REJECT should not be called directly
465
+ # They are handled internally by _execute_trade_propose
466
+ return ActionResult.failure_result(
467
+ f"Trade action {action.action_type} cannot be called directly",
468
+ "INVALID_ACTION"
469
+ )
470
+
471
+ def _execute_trade_propose(self, action: Action) -> ActionResult:
472
+ """
473
+ Execute a trade proposal between players.
474
+
475
+ This function:
476
+ 1. Validates that both players have the required cards
477
+ 2. Requests input from the target player (accept/reject)
478
+ 3. Executes the trade if accepted
479
+ """
480
+ try:
481
+ proposer_id = action.player_id
482
+ target_id = action.parameters['target_player']
483
+ offer = action.parameters['offer'] # {resource: amount}
484
+ request = action.parameters['request'] # {resource: amount}
485
+
486
+ # Get player names for messages
487
+ proposer_name = self.users[proposer_id].name
488
+ target_name = self.users[target_id].name
489
+
490
+ # Convert offer/request dicts to card lists for Game.trade()
491
+ from pycatan.card import ResCard
492
+
493
+ offer_cards = []
494
+ for resource, amount in offer.items():
495
+ card_type = self._resource_name_to_card(resource)
496
+ offer_cards.extend([card_type] * amount)
497
+
498
+ request_cards = []
499
+ for resource, amount in request.items():
500
+ card_type = self._resource_name_to_card(resource)
501
+ request_cards.extend([card_type] * amount)
502
+
503
+ # Validate that both players have the required cards
504
+ if not self.game.players[proposer_id].has_cards(offer_cards):
505
+ print(f" ✗ You don't have the required cards to offer")
506
+ return ActionResult.failure_result(
507
+ f"You don't have the required cards to offer",
508
+ "INSUFFICIENT_RESOURCES"
509
+ )
510
+
511
+ if not self.game.players[target_id].has_cards(request_cards):
512
+ print(f" ✗ {target_name} doesn't have the required cards")
513
+ return ActionResult.failure_result(
514
+ f"{target_name} doesn't have the required cards",
515
+ "INSUFFICIENT_RESOURCES"
516
+ )
517
+
518
+ # Format the trade offer message
519
+ offer_str = ", ".join([f"{amt} {res}" for res, amt in offer.items()])
520
+ request_str = ", ".join([f"{amt} {res}" for res, amt in request.items()])
521
+
522
+ # Ask the target player to accept or reject
523
+ print(f"\n📢 Trade Proposal:")
524
+ print(f" {proposer_name} offers: {offer_str}")
525
+ print(f" {proposer_name} wants: {request_str}")
526
+ print(f" {target_name}, do you accept? (yes/no)")
527
+
528
+ # Get response from target player
529
+ target_user = self.users[target_id]
530
+ response = target_user.get_input(
531
+ self.get_full_state(),
532
+ f"{target_name}, accept trade?",
533
+ allowed_actions=[ActionType.TRADE_ACCEPT.name, ActionType.TRADE_REJECT.name]
534
+ )
535
+
536
+ # Handle response
537
+ if response.action_type == ActionType.TRADE_ACCEPT:
538
+ # Execute the trade
539
+ status = self.game.trade(proposer_id, target_id, offer_cards, request_cards)
540
+
541
+ if status == Statuses.ALL_GOOD:
542
+ print(f" ✓ Trade completed between {proposer_name} and {target_name}!")
543
+ return ActionResult.success_result(
544
+ self.get_full_state(),
545
+ affected_players=[proposer_id, target_id]
546
+ )
547
+ else:
548
+ return self._map_status_to_result(status)
549
+ else:
550
+ # Trade rejected
551
+ print(f" ✗ {target_name} rejected the trade")
552
+ return ActionResult.failure_result(
553
+ f"{target_name} rejected your trade offer",
554
+ "TRADE_REJECTED"
555
+ )
556
+
557
+ except Exception as e:
558
+ return ActionResult.failure_result(
559
+ f"Error executing trade: {str(e)}",
560
+ "EXECUTION_ERROR"
561
+ )
562
+
563
+ def _execute_trade_bank(self, action: Action) -> ActionResult:
564
+ """Execute a trade with the bank."""
565
+ try:
566
+ player_id = action.player_id
567
+ offer = action.parameters['offer'] # {resource: amount}
568
+ request = action.parameters['request'] # {resource: amount}
569
+
570
+ # Convert to card lists
571
+ from pycatan.card import ResCard
572
+
573
+ offer_cards = []
574
+ for resource, amount in offer.items():
575
+ card_type = self._resource_name_to_card(resource)
576
+ offer_cards.extend([card_type] * amount)
577
+
578
+ request_cards = []
579
+ for resource, amount in request.items():
580
+ card_type = self._resource_name_to_card(resource)
581
+ request_cards.extend([card_type] * amount)
582
+
583
+ # Execute bank trade
584
+ status = self.game.trade_to_bank(player_id, offer_cards, request_cards)
585
+
586
+ if status == Statuses.ALL_GOOD:
587
+ offer_str = ", ".join([f"{amt} {res}" for res, amt in offer.items()])
588
+ request_str = ", ".join([f"{amt} {res}" for res, amt in request.items()])
589
+ print(f" ✓ Bank trade: gave {offer_str}, received {request_str}")
590
+ return ActionResult.success_result(
591
+ self.get_full_state(),
592
+ affected_players=[player_id]
593
+ )
594
+ else:
595
+ return self._map_status_to_result(status)
596
+
597
+ except Exception as e:
598
+ return ActionResult.failure_result(
599
+ f"Error executing bank trade: {str(e)}",
600
+ "EXECUTION_ERROR"
601
+ )
602
+
603
+ def _resource_name_to_card(self, resource_name: str):
604
+ """Convert resource name string to ResCard enum."""
605
+ from pycatan.card import ResCard
606
+
607
+ resource_map = {
608
+ 'wood': ResCard.Wood,
609
+ 'brick': ResCard.Brick,
610
+ 'sheep': ResCard.Sheep,
611
+ 'wheat': ResCard.Wheat,
612
+ 'ore': ResCard.Ore
613
+ }
614
+
615
+ return resource_map.get(resource_name.lower())
616
+
617
+ def start_game(self) -> bool:
618
+ """
619
+ Start the game session.
620
+
621
+ Initializes the game state and begins the main game loop.
622
+
623
+ Returns:
624
+ bool: True if game started successfully
625
+ """
626
+ if self._is_running:
627
+ return False # Already running
628
+
629
+ # Initialize game state
630
+ self._is_running = True
631
+ self._is_paused = False
632
+
633
+ # Notify all users
634
+ self._notify_all_users(
635
+ "game_start",
636
+ f"Game {self.game_id} has started with {self.num_players} players!"
637
+ )
638
+
639
+ # Display Turn 0 immediately when game starts
640
+ self._display_current_turn_start()
641
+
642
+ return True
643
+
644
+ def pause_game(self) -> bool:
645
+ """Pause the game."""
646
+ if not self._is_running or self._is_paused:
647
+ return False
648
+
649
+ self._is_paused = True
650
+ self._notify_all_users("game_pause", "Game has been paused.")
651
+ return True
652
+
653
+ def resume_game(self) -> bool:
654
+ """Resume a paused game."""
655
+ if not self._is_running or not self._is_paused:
656
+ return False
657
+
658
+ self._is_paused = False
659
+ self._notify_all_users("game_resume", "Game has been resumed.")
660
+ return True
661
+
662
+ def end_game(self) -> bool:
663
+ """End the game session."""
664
+ if not self._is_running:
665
+ return False
666
+
667
+ self._is_running = False
668
+ self._is_paused = False
669
+
670
+ # TODO: Calculate final scores, determine winner
671
+ self._notify_all_users("game_end", "Game has ended.")
672
+ return True
673
+
674
+ def request_user_input(self, user_id: int, prompt: str,
675
+ allowed_actions: Optional[List[str]] = None) -> Action:
676
+ """
677
+ Request input from a specific user.
678
+
679
+ Args:
680
+ user_id: ID of the user to request input from
681
+ prompt: Message explaining what input is needed
682
+ allowed_actions: Optional list of allowed action types
683
+
684
+ Returns:
685
+ Action: The action chosen by the user
686
+
687
+ Raises:
688
+ UserInputError: If user input fails
689
+ """
690
+ if user_id >= len(self.users):
691
+ raise UserInputError(f"Invalid user ID: {user_id}")
692
+
693
+ user = self.users[user_id]
694
+
695
+ if not user.is_active:
696
+ raise UserInputError(f"User {user_id} is not active")
697
+
698
+ try:
699
+ return user.get_input(
700
+ self.get_full_state(),
701
+ prompt,
702
+ allowed_actions
703
+ )
704
+ except Exception as e:
705
+ raise UserInputError(f"Failed to get input from user {user_id}: {e}", user)
706
+
707
+ def _notify_all_users(self, event_type: str, message: str,
708
+ affected_players: Optional[List[int]] = None) -> None:
709
+ """Notify all users about a game event."""
710
+ for user in self.users:
711
+ if user.is_active:
712
+ user.notify_game_event(event_type, message, affected_players)
713
+
714
+ def _notify_user(self, user_id: int, action: Action, success: bool, message: str = "") -> None:
715
+ """Notify a specific user about an action result."""
716
+ if user_id < len(self.users) and self.users[user_id].is_active:
717
+ self.users[user_id].notify_action(action, success, message)
718
+
719
+ def get_action_history(self) -> List[Action]:
720
+ """Get the complete action history for this game."""
721
+ return self._action_history.copy()
722
+
723
+ def get_user_by_id(self, user_id: int) -> Optional[User]:
724
+ """Get a user by their ID."""
725
+ if 0 <= user_id < len(self.users):
726
+ return self.users[user_id]
727
+ return None
728
+
729
+ def game_loop(self) -> None:
730
+ """
731
+ Main game loop that runs the entire game from start to finish.
732
+
733
+ This is the central function that orchestrates the entire game flow.
734
+ It manages turn order, requests input from users, executes actions,
735
+ and updates all systems until the game ends.
736
+
737
+ Flow:
738
+ 1. Check if game is running and not paused
739
+ 2. Get input from current player
740
+ 3. Attempt to execute the action
741
+ 4. Update all systems (visualizations, users)
742
+ 5. Check win conditions
743
+ 6. Move to next turn if needed
744
+
745
+ The function runs until the game ends or is explicitly stopped.
746
+ """
747
+ # Continue running until game ends or is explicitly stopped
748
+ while self._is_running and not self._check_game_end_conditions():
749
+
750
+ # If game is paused, wait
751
+ if self._is_paused:
752
+ # In paused state, just continue loop without doing anything
753
+ continue
754
+
755
+ # Run a single turn for the current player
756
+ try:
757
+ turn_ended = self._handle_single_turn()
758
+
759
+ # Reset error count on successful turn processing
760
+ self._player_error_count[self.current_player_id] = 0
761
+
762
+ # Only advance to next player if turn actually ended
763
+ if turn_ended:
764
+ self._advance_to_next_player()
765
+
766
+ except Exception as e:
767
+ # Increment error count for current player
768
+ self._player_error_count[self.current_player_id] += 1
769
+
770
+ # If error occurred, notify current player
771
+ self._notify_all_users(
772
+ "error",
773
+ f"Error during player {self.current_player_id}'s turn: {str(e)}."
774
+ )
775
+
776
+ # If too many consecutive errors for this player, skip their turn
777
+ if self._player_error_count[self.current_player_id] >= 3:
778
+ self._notify_all_users(
779
+ "player_skip",
780
+ f"Player {self.current_player_id} had too many errors. Skipping turn."
781
+ )
782
+ self._advance_to_next_player()
783
+ else:
784
+ self._notify_all_users(
785
+ "retry",
786
+ f"Player {self.current_player_id} can try again."
787
+ )
788
+
789
+ # Game has ended - handle cleanup
790
+ self._handle_game_end()
791
+
792
+ def _handle_single_turn(self) -> bool:
793
+ """
794
+ Handles a single turn of one player.
795
+
796
+ This function manages one complete turn of a single player:
797
+ 1. Requests an action from the current user
798
+ 2. Attempts to execute the action
799
+ 3. Updates all systems about the result
800
+ 4. Determines if the turn should end or continue
801
+
802
+ Special handling for discard phase when 7 is rolled:
803
+ - During discard phase, each player who needs to discard gets prompted in turn
804
+
805
+ Returns:
806
+ bool: True if the turn ended, False if player wants to continue
807
+ """
808
+ # Special handling for discard phase - ask each player who needs to discard
809
+ if self._current_game_state.turn_phase == TurnPhase.DISCARD_PHASE:
810
+ return self._handle_discard_phase_turn()
811
+
812
+ # Get the current player's action
813
+ action_result = self._process_user_action()
814
+
815
+ # Update all systems about what happened
816
+ if hasattr(action_result, 'action'):
817
+ self._update_all_systems(action_result.action, action_result)
818
+
819
+ # Determine if turn should end
820
+ if action_result.success and hasattr(action_result, 'action'):
821
+ action = action_result.action
822
+
823
+ # END_TURN action explicitly ends the turn
824
+ if action.action_type == ActionType.END_TURN:
825
+ return True
826
+
827
+ # Auto-end turn in setup phase if both actions are done
828
+ phase = self._current_game_state.game_phase
829
+ if phase in [GamePhase.SETUP_FIRST_ROUND, GamePhase.SETUP_SECOND_ROUND]:
830
+ if self._setup_turn_progress['settlement'] and self._setup_turn_progress['road']:
831
+ return True
832
+
833
+ # For other successful actions, player can continue
834
+ return False
835
+ else:
836
+ # If action failed, player can try again (don't end turn)
837
+ return False
838
+
839
+ def _handle_discard_phase_turn(self) -> bool:
840
+ """
841
+ Handle the discard phase when 7 is rolled.
842
+
843
+ Each player who needs to discard is prompted in turn order.
844
+ After all players have discarded, the phase moves to robber move.
845
+
846
+ Returns:
847
+ bool: Always False (don't advance to next player during discard)
848
+ """
849
+ # Find the next player who needs to discard
850
+ players_needing_discard = self._current_game_state.players_must_discard
851
+
852
+ if not players_needing_discard:
853
+ # All done discarding - this shouldn't happen but handle it
854
+ self._current_game_state.turn_phase = TurnPhase.ROBBER_MOVE
855
+ return False
856
+
857
+ # Get the first player who still needs to discard
858
+ discard_player_id = min(players_needing_discard.keys())
859
+ discard_count = players_needing_discard[discard_player_id]
860
+
861
+ # Get the user for this player
862
+ discard_user = self.users[discard_player_id]
863
+ player_name = discard_user.name if hasattr(discard_user, 'name') else f"Player {discard_player_id}"
864
+
865
+ # Request discard action from this player
866
+ allowed_actions = [ActionType.DISCARD_CARDS.name]
867
+
868
+ try:
869
+ action = discard_user.get_input(
870
+ self.get_full_state(),
871
+ f"{player_name}, you must discard {discard_count} cards. Use: drop [amount] [resource] ...",
872
+ allowed_actions
873
+ )
874
+
875
+ # Override the player_id to match the discarding player (not current turn player)
876
+ action.player_id = discard_player_id
877
+
878
+ # Execute the discard action
879
+ result = self._handle_discard_cards(action)
880
+
881
+ # Update systems
882
+ self._update_all_systems(action, result)
883
+
884
+ except Exception as e:
885
+ self._notify_all_users(
886
+ "error",
887
+ f"Error during {player_name}'s discard: {str(e)}"
888
+ )
889
+
890
+ # Don't end the turn - continue with discard phase or move to robber
891
+ return False
892
+
893
+ def _process_user_action(self) -> ActionResult:
894
+ """
895
+ Requests an action from the current user and attempts to execute it.
896
+
897
+ This function:
898
+ 1. Identifies the current player
899
+ 2. Requests them to choose an action via get_input()
900
+ 3. Validates the action
901
+ 4. Attempts to execute the action via execute_action()
902
+ 5. Returns the result
903
+
904
+ Returns:
905
+ ActionResult: The result of executing the action
906
+ """
907
+ try:
908
+ # Get the current user
909
+ current_user = self.current_user
910
+
911
+ # Get allowed actions for current state
912
+ allowed_actions = self.get_available_actions()
913
+
914
+ # Request action from the current user
915
+ action = current_user.get_input(
916
+ self.get_full_state(),
917
+ f"Player {self.current_player_id}, choose your action:",
918
+ allowed_actions
919
+ )
920
+
921
+ # Validate that the action is for the current player
922
+ if action.player_id != self.current_player_id:
923
+ return ActionResult.failure_result(
924
+ f"Action player_id {action.player_id} doesn't match current player {self.current_player_id}",
925
+ "INVALID_PLAYER_ID"
926
+ )
927
+
928
+ # Execute the action
929
+ result = self.execute_action(action)
930
+
931
+ # Add the action to the result for reference
932
+ if hasattr(result, 'action'):
933
+ result.action = action
934
+ else:
935
+ # If ActionResult doesn't have action field, add it dynamically
936
+ setattr(result, 'action', action)
937
+
938
+ return result
939
+
940
+ except Exception as e:
941
+ # Handle any errors during action processing
942
+ return ActionResult.failure_result(
943
+ f"Error processing user action: {str(e)}",
944
+ "ACTION_PROCESSING_ERROR"
945
+ )
946
+
947
+ def _update_all_systems(self, action: Action, result: ActionResult) -> None:
948
+ """
949
+ Updates all systems after an action has been executed.
950
+
951
+ This function ensures that all parts of the system are informed
952
+ about what happened and can update their displays accordingly.
953
+
954
+ Updates:
955
+ 1. Notifies all users about the action and its result
956
+ 2. Updates visualizations with new game state
957
+ 3. Logs the action for history/debugging
958
+ 4. Handles any side effects of the action
959
+
960
+ Args:
961
+ action: The action that was executed
962
+ result: The result of the action execution
963
+ """
964
+ # Notify the specific user who performed the action
965
+ self._notify_user(
966
+ action.player_id,
967
+ action,
968
+ result.success,
969
+ result.error_message or ""
970
+ )
971
+
972
+ # If action was successful, notify all users about the action
973
+ if result.success:
974
+ action_description = self._get_action_description(action)
975
+ self._notify_all_users(
976
+ "action_performed",
977
+ f"Player {action.player_id} {action_description}",
978
+ result.affected_players if hasattr(result, 'affected_players') else [action.player_id]
979
+ )
980
+
981
+ # Update visualizations if available
982
+ if self.visualization_manager:
983
+ try:
984
+ # Add player name to action parameters for better visualization
985
+ if not hasattr(action, 'parameters') or action.parameters is None:
986
+ action.parameters = {}
987
+
988
+ # Add player name if not already present
989
+ if 'player_name' not in action.parameters:
990
+ player_name = self.users[action.player_id].name if hasattr(self.users[action.player_id], 'name') else f"Player {action.player_id}"
991
+ action.parameters['player_name'] = player_name
992
+
993
+ # Display the action result (success or failure)
994
+ self.visualization_manager.display_action(action, result)
995
+
996
+ current_state = self.get_full_state()
997
+ # Pass GameState object directly so visualizations can extract what they need
998
+ self.visualization_manager.display_game_state(current_state)
999
+ except Exception as e:
1000
+ # Log visualization errors
1001
+ print(f"Error updating visualizations: {e}")
1002
+
1003
+ # Log the action and result for debugging
1004
+ if self.config.get('debug', False):
1005
+ # Only print if debug config is explicitly enabled
1006
+ pass
1007
+
1008
+ def _gamestate_to_dict(self, game_state) -> Dict[str, Any]:
1009
+ """Convert GameState object to dict format expected by visualizations."""
1010
+ try:
1011
+ return {
1012
+ 'game_id': game_state.game_id,
1013
+ 'turn_number': game_state.turn_number,
1014
+ 'current_player': game_state.current_player,
1015
+ 'current_player_name': self.users[game_state.current_player].name if hasattr(self.users[game_state.current_player], 'name') else f"Player {game_state.current_player}",
1016
+ 'game_phase': game_state.game_phase.name if hasattr(game_state.game_phase, 'name') else str(game_state.game_phase),
1017
+ 'turn_phase': game_state.turn_phase.name if hasattr(game_state.turn_phase, 'name') else str(game_state.turn_phase),
1018
+ 'players': [
1019
+ {
1020
+ 'id': i,
1021
+ 'name': self.users[i].name if hasattr(self.users[i], 'name') else f"Player {i}",
1022
+ 'victory_points': player.victory_points,
1023
+ 'cards': len(player.cards),
1024
+ 'settlements': len(player.settlements),
1025
+ 'cities': len(player.cities),
1026
+ 'roads': len(player.roads),
1027
+ 'longest_road_length': player.longest_road_length,
1028
+ 'has_longest_road': player.has_longest_road,
1029
+ 'has_largest_army': player.has_largest_army,
1030
+ 'knights_played': player.knights_played
1031
+ }
1032
+ for i, player in enumerate(game_state.players_state)
1033
+ ],
1034
+ 'board': {
1035
+ 'tiles_count': len(game_state.board_state.tiles),
1036
+ 'robber_position': game_state.board_state.robber_position,
1037
+ 'buildings_count': len(game_state.board_state.buildings),
1038
+ 'roads_count': len(game_state.board_state.roads)
1039
+ }
1040
+ }
1041
+ except Exception as e:
1042
+ # Fallback dict if conversion fails
1043
+ return {
1044
+ 'turn_number': getattr(game_state, 'turn_number', 0),
1045
+ 'current_player': getattr(game_state, 'current_player', 0),
1046
+ 'current_player_name': f"Player {getattr(game_state, 'current_player', 0)}",
1047
+ 'game_phase': 'UNKNOWN',
1048
+ 'players': [],
1049
+ 'board': {}
1050
+ }
1051
+
1052
+ def _get_action_description(self, action: Action) -> str:
1053
+ """
1054
+ Get a human-readable description of an action.
1055
+
1056
+ Args:
1057
+ action: The action to describe
1058
+
1059
+ Returns:
1060
+ str: Human-readable description
1061
+ """
1062
+ if action.action_type == ActionType.BUILD_SETTLEMENT:
1063
+ return "built a settlement"
1064
+ elif action.action_type == ActionType.BUILD_CITY:
1065
+ return "built a city"
1066
+ elif action.action_type == ActionType.BUILD_ROAD:
1067
+ return "built a road"
1068
+ elif action.action_type == ActionType.END_TURN:
1069
+ return "ended their turn"
1070
+ elif action.action_type == ActionType.TRADE_PROPOSE:
1071
+ return "proposed a trade"
1072
+ else:
1073
+ return f"performed action: {action.action_type}"
1074
+
1075
+ def _advance_to_next_player(self) -> None:
1076
+ """
1077
+ Advances the game to the next player's turn.
1078
+
1079
+ This function handles the transition between players,
1080
+ updating turn counters and notifying all users.
1081
+ It implements the "Snake Draft" order for setup phase:
1082
+ Round 1: 0 -> 1 -> ... -> N-1
1083
+ Round 2: N-1 -> N-2 -> ... -> 0
1084
+ """
1085
+ # Reset setup progress for the new turn
1086
+ self._setup_turn_progress = {'settlement': False, 'road': False}
1087
+
1088
+ # Reset dice_rolled for the new turn (important for normal play!)
1089
+ self._current_game_state.dice_rolled = None
1090
+
1091
+ # Increment turn number
1092
+ self._current_game_state.turn_number += 1
1093
+ turn = self._current_game_state.turn_number
1094
+
1095
+ # Handle Setup Phase Logic
1096
+ if self._current_game_state.game_phase == GamePhase.SETUP_FIRST_ROUND:
1097
+ if turn < self.num_players:
1098
+ # Still in first round, standard order
1099
+ self._current_game_state.current_player = turn
1100
+ else:
1101
+ # Switch to second round
1102
+ self._current_game_state.game_phase = GamePhase.SETUP_SECOND_ROUND
1103
+ # The first player of second round is the last player of first round
1104
+ self._current_game_state.current_player = self.num_players - 1
1105
+ self._notify_all_users("phase_change", "First round of setup complete! Starting second round (reverse order).")
1106
+
1107
+ elif self._current_game_state.game_phase == GamePhase.SETUP_SECOND_ROUND:
1108
+ # Check if setup is done
1109
+ if turn >= self.num_players * 2:
1110
+ self._current_game_state.game_phase = GamePhase.NORMAL_PLAY
1111
+ self._current_game_state.current_player = 0
1112
+ self._notify_all_users("phase_change", "Setup complete! Entering Normal Play phase.")
1113
+ else:
1114
+ # Calculate reverse order for snake draft
1115
+ # Formula: (2 * num_players - 1) - turn
1116
+ self._current_game_state.current_player = (2 * self.num_players - 1) - turn
1117
+
1118
+ else: # Normal Play
1119
+ self._current_game_state.current_player = (self._current_game_state.current_player + 1) % self.num_players
1120
+
1121
+ # Display turn start
1122
+ self._display_current_turn_start()
1123
+
1124
+ def _display_current_turn_start(self) -> None:
1125
+ """Display turn start notification for the current player and turn."""
1126
+ # Notify all users about the turn change
1127
+ self._notify_all_users(
1128
+ "turn_change",
1129
+ f"Turn {self._current_game_state.turn_number}: Player {self._current_game_state.current_player}'s turn begins."
1130
+ )
1131
+
1132
+ # Notify visualization
1133
+ if self.visualization_manager:
1134
+ player_id = self._current_game_state.current_player
1135
+ player_name = self.users[player_id].name if hasattr(self.users[player_id], 'name') else f"Player {player_id + 1}"
1136
+ self.visualization_manager.display_turn_start(player_name, self._current_game_state.turn_number)
1137
+
1138
+ def _handle_game_end(self) -> None:
1139
+ """
1140
+ Handles the end of the game.
1141
+
1142
+ This function is called when the game loop exits,
1143
+ either due to win conditions or explicit termination.
1144
+ """
1145
+ # Set game as not running
1146
+ self._is_running = False
1147
+ self._is_paused = False
1148
+
1149
+ # TODO: Calculate final scores and determine winner
1150
+ # For now, just notify that game ended
1151
+ self._notify_all_users("game_end", "Game has ended.")
1152
+
1153
+ # TODO: Cleanup resources, save game state, etc.
1154
+
1155
+ def _check_game_end_conditions(self) -> bool:
1156
+ """
1157
+ Checks if the game has ended based on win conditions.
1158
+
1159
+ This function examines the current game state to determine
1160
+ if any player has achieved victory conditions.
1161
+
1162
+ Standard Catan win conditions:
1163
+ 1. First player to reach 10 victory points wins
1164
+ 2. Victory points come from: settlements (1), cities (2),
1165
+ development cards (1 each), longest road (2), largest army (2)
1166
+
1167
+ Returns:
1168
+ bool: True if game has ended (someone won), False if game continues
1169
+ """
1170
+ # Check victory points for each player
1171
+ for player_id in range(self.num_players):
1172
+ player = self.game.players[player_id]
1173
+
1174
+ # Calculate total victory points for this player
1175
+ # We include dev cards because we want to know if they actually won
1176
+ victory_points = player.get_VP(include_dev=True)
1177
+
1178
+ # Check if this player has won (10+ victory points)
1179
+ if victory_points >= 10:
1180
+ self._announce_winner(player_id, victory_points)
1181
+ return True
1182
+
1183
+ # No player has won yet
1184
+ return False
1185
+
1186
+ def _announce_winner(self, player_id: int, victory_points: int) -> None:
1187
+ """
1188
+ Announces the winner of the game.
1189
+
1190
+ Args:
1191
+ player_id: ID of the winning player
1192
+ victory_points: Number of victory points the winner achieved
1193
+ """
1194
+ winner_name = self.users[player_id].name if hasattr(self.users[player_id], 'name') else f"Player {player_id}"
1195
+
1196
+ self._notify_all_users(
1197
+ "game_winner",
1198
+ f"🎉 {winner_name} has won the game with {victory_points} victory points! 🎉"
1199
+ )
1200
+
1201
+ # Log the victory for debugging/statistics
1202
+ print(f"[GAME END] Player {player_id} ({winner_name}) won with {victory_points} victory points")
1203
+
1204
+ def _handle_roll_dice(self, action: Action) -> ActionResult:
1205
+ """Handle dice rolling."""
1206
+ # Check game phase
1207
+ if self._current_game_state.game_phase != GamePhase.NORMAL_PLAY:
1208
+ return ActionResult.failure_result(
1209
+ f"Cannot roll dice in {self._current_game_state.game_phase.name} phase.\n"
1210
+ "💡 Hint: In setup phase, use 'settlement <point> starting' and 'road <p1> <p2> starting'.",
1211
+ "INVALID_PHASE"
1212
+ )
1213
+
1214
+ # Check if dice already rolled this turn
1215
+ if self._current_game_state.dice_rolled:
1216
+ return ActionResult.failure_result("Dice already rolled this turn", "ALREADY_ROLLED")
1217
+
1218
+ # Roll dice
1219
+ die1 = random.randint(1, 6)
1220
+ die2 = random.randint(1, 6)
1221
+ total = die1 + die2
1222
+
1223
+ # Update state
1224
+ self._current_game_state.dice_rolled = (die1, die2)
1225
+
1226
+ # Notify visualization about dice roll
1227
+ if self.visualization_manager:
1228
+ player_name = self.users[action.player_id].name if hasattr(self.users[action.player_id], 'name') else f"Player {action.player_id + 1}"
1229
+ self.visualization_manager.display_dice_roll(player_name, [die1, die2], total)
1230
+
1231
+ # Distribute resources or handle robber
1232
+ if total != 7:
1233
+ distribution = self.game.add_yield_for_roll(total)
1234
+
1235
+ # Notify visualization about resources (even if empty)
1236
+ if self.visualization_manager:
1237
+ if distribution:
1238
+ self.visualization_manager.display_resource_distribution(distribution)
1239
+ message = f"Rolled {total} ({die1}+{die2}). Resources distributed."
1240
+ else:
1241
+ # No resources were distributed (no settlements on this number)
1242
+ message = f"Rolled {total} ({die1}+{die2}). No settlements on this number - no resources distributed."
1243
+ else:
1244
+ message = f"Rolled {total} ({die1}+{die2}). Resources distributed."
1245
+ else:
1246
+ # Rolled 7! Handle robber sequence
1247
+ message = f"Rolled 7 ({die1}+{die2})! 🏴‍☠️ Robber activated!"
1248
+ self._handle_rolled_seven()
1249
+
1250
+ # Notify
1251
+ self._notify_all_users("dice_roll", message)
1252
+
1253
+ return ActionResult.success_result(
1254
+ self.get_full_state()
1255
+ )
1256
+
1257
+ def _handle_rolled_seven(self) -> None:
1258
+ """
1259
+ Handle the effects of rolling a 7:
1260
+ 1. Check which players have more than 7 cards and need to discard
1261
+ 2. Set up the discard phase if needed
1262
+ 3. Prepare for robber movement
1263
+ """
1264
+ # Check which players need to discard (more than 7 cards)
1265
+ players_must_discard = {}
1266
+
1267
+ for player_id, player in enumerate(self.game.players):
1268
+ card_count = len(player.cards)
1269
+ if card_count > 7:
1270
+ # Must discard half, rounded down
1271
+ discard_count = card_count // 2
1272
+ players_must_discard[player_id] = discard_count
1273
+
1274
+ player_name = self.users[player_id].name if hasattr(self.users[player_id], 'name') else f"Player {player_id}"
1275
+ self._notify_all_users(
1276
+ "discard_required",
1277
+ f"⚠️ {player_name} has {card_count} cards and must discard {discard_count}."
1278
+ )
1279
+
1280
+ # Store the discard requirements in game state
1281
+ self._current_game_state.players_must_discard = players_must_discard
1282
+ self._current_game_state.robber_moved = False
1283
+ self._current_game_state.steal_pending = False
1284
+
1285
+ # Set the appropriate turn phase
1286
+ if players_must_discard:
1287
+ self._current_game_state.turn_phase = TurnPhase.DISCARD_PHASE
1288
+ else:
1289
+ # No one needs to discard, go straight to robber move
1290
+ self._current_game_state.turn_phase = TurnPhase.ROBBER_MOVE
1291
+ self._notify_all_users(
1292
+ "robber",
1293
+ f"🏴‍☠️ {self.users[self.current_player_id].name} must move the robber!"
1294
+ )
1295
+
1296
+ def _handle_discard_cards(self, action: Action) -> ActionResult:
1297
+ """
1298
+ Handle a player discarding cards (when 7 is rolled).
1299
+
1300
+ The action must contain:
1301
+ - cards: List of card names to discard
1302
+ """
1303
+ player_id = action.player_id
1304
+ cards_to_discard = action.parameters.get('cards', [])
1305
+
1306
+ # Check if this player needs to discard
1307
+ required_discard = self._current_game_state.players_must_discard.get(player_id, 0)
1308
+
1309
+ if required_discard == 0:
1310
+ return ActionResult.failure_result(
1311
+ "You don't need to discard any cards.",
1312
+ "NO_DISCARD_REQUIRED"
1313
+ )
1314
+
1315
+ # Check if they're discarding the right amount
1316
+ if len(cards_to_discard) != required_discard:
1317
+ return ActionResult.failure_result(
1318
+ f"You must discard exactly {required_discard} cards, but you're trying to discard {len(cards_to_discard)}.",
1319
+ "WRONG_DISCARD_COUNT"
1320
+ )
1321
+
1322
+ # Convert card names to ResCard enum and verify player has them
1323
+ from pycatan.card import ResCard
1324
+
1325
+ player = self.game.players[player_id]
1326
+ cards_enum = []
1327
+
1328
+ for card_name in cards_to_discard:
1329
+ try:
1330
+ card = ResCard[card_name]
1331
+ cards_enum.append(card)
1332
+ except KeyError:
1333
+ return ActionResult.failure_result(
1334
+ f"Unknown card type: {card_name}",
1335
+ "INVALID_CARD"
1336
+ )
1337
+
1338
+ # Check if player has all these cards
1339
+ if not player.has_cards(cards_enum):
1340
+ return ActionResult.failure_result(
1341
+ "You don't have all the cards you're trying to discard.",
1342
+ "MISSING_CARDS"
1343
+ )
1344
+
1345
+ # Remove the cards from player
1346
+ player.remove_cards(cards_enum)
1347
+
1348
+ # Update discard tracking
1349
+ del self._current_game_state.players_must_discard[player_id]
1350
+
1351
+ player_name = self.users[player_id].name if hasattr(self.users[player_id], 'name') else f"Player {player_id}"
1352
+ self._notify_all_users(
1353
+ "discard_complete",
1354
+ f"✓ {player_name} discarded {len(cards_to_discard)} cards."
1355
+ )
1356
+
1357
+ # Check if all players have finished discarding
1358
+ if not self._current_game_state.players_must_discard:
1359
+ # All discards complete, move to robber phase
1360
+ self._current_game_state.turn_phase = TurnPhase.ROBBER_MOVE
1361
+ current_player_name = self.users[self.current_player_id].name if hasattr(self.users[self.current_player_id], 'name') else f"Player {self.current_player_id}"
1362
+ self._notify_all_users(
1363
+ "robber",
1364
+ f"🏴‍☠️ {current_player_name} must now move the robber!"
1365
+ )
1366
+
1367
+ return ActionResult.success_result(self.get_full_state())
1368
+
1369
+ def _handle_robber_move(self, action: Action) -> ActionResult:
1370
+ """
1371
+ Handle moving the robber to a new tile.
1372
+
1373
+ The action must contain:
1374
+ - tile_coords: [row, index] of the new robber position
1375
+ """
1376
+ tile_coords = action.parameters.get('tile_coords')
1377
+
1378
+ if not tile_coords:
1379
+ return ActionResult.failure_result(
1380
+ "Robber move requires tile coordinates.",
1381
+ "MISSING_COORDS"
1382
+ )
1383
+
1384
+ row, index = tile_coords
1385
+
1386
+ # Validate the tile exists
1387
+ try:
1388
+ tile = self.game.board.tiles[row][index]
1389
+ except (IndexError, KeyError):
1390
+ return ActionResult.failure_result(
1391
+ f"Invalid tile coordinates: [{row}, {index}]",
1392
+ "INVALID_COORDS"
1393
+ )
1394
+
1395
+ # Can't place robber on desert (already there) - check if it's the same position
1396
+ current_robber_pos = getattr(self.game.board, 'robber_tile', None)
1397
+ if current_robber_pos and current_robber_pos == (row, index):
1398
+ return ActionResult.failure_result(
1399
+ "You must move the robber to a different tile.",
1400
+ "SAME_POSITION"
1401
+ )
1402
+
1403
+ # Move the robber
1404
+ # First, remove robber from current position
1405
+ if current_robber_pos:
1406
+ old_row, old_index = current_robber_pos
1407
+ try:
1408
+ self.game.board.tiles[old_row][old_index].has_robber = False
1409
+ except (IndexError, AttributeError):
1410
+ pass
1411
+
1412
+ # Place robber on new position
1413
+ tile.has_robber = True
1414
+ self.game.board.robber_tile = (row, index)
1415
+
1416
+ self._current_game_state.robber_moved = True
1417
+
1418
+ # Find players adjacent to this tile who can be stolen from
1419
+ stealable_players = self._get_stealable_players(row, index)
1420
+
1421
+ player_name = self.users[action.player_id].name if hasattr(self.users[action.player_id], 'name') else f"Player {action.player_id}"
1422
+ self._notify_all_users(
1423
+ "robber_moved",
1424
+ f"��‍☠️ {player_name} moved the robber to [{row}, {index}]."
1425
+ )
1426
+
1427
+ if stealable_players:
1428
+ # There are players to steal from
1429
+ self._current_game_state.turn_phase = TurnPhase.ROBBER_STEAL
1430
+ self._current_game_state.steal_pending = True
1431
+
1432
+ stealable_names = [self.users[pid].name for pid in stealable_players]
1433
+ self._notify_all_users(
1434
+ "steal_available",
1435
+ f"🎯 {player_name} can steal from: {', '.join(stealable_names)}"
1436
+ )
1437
+ else:
1438
+ # No one to steal from, proceed to normal play
1439
+ self._current_game_state.turn_phase = TurnPhase.PLAYER_ACTIONS
1440
+ self._notify_all_users(
1441
+ "robber_complete",
1442
+ "No players with cards adjacent to robber. Proceeding with turn."
1443
+ )
1444
+
1445
+ return ActionResult.success_result(self.get_full_state())
1446
+
1447
+ def _get_stealable_players(self, tile_row: int, tile_index: int) -> List[int]:
1448
+ """
1449
+ Get list of player IDs who have settlements/cities adjacent to the given tile
1450
+ and have at least 1 card (excluding the current player).
1451
+ """
1452
+ stealable = []
1453
+ current_player = self.current_player_id
1454
+
1455
+ try:
1456
+ tile = self.game.board.tiles[tile_row][tile_index]
1457
+ except (IndexError, KeyError):
1458
+ return []
1459
+
1460
+ # Get all points adjacent to this tile
1461
+ adjacent_points = tile.points if hasattr(tile, 'points') else []
1462
+
1463
+ for point in adjacent_points:
1464
+ if point.building is not None:
1465
+ owner_id = point.building.owner
1466
+ # Don't include current player, and don't include players with no cards
1467
+ if owner_id != current_player and owner_id not in stealable:
1468
+ if len(self.game.players[owner_id].cards) > 0:
1469
+ stealable.append(owner_id)
1470
+
1471
+ return stealable
1472
+
1473
+ def _handle_steal_card(self, action: Action) -> ActionResult:
1474
+ """
1475
+ Handle stealing a card from a player adjacent to the robber.
1476
+
1477
+ The action must contain:
1478
+ - target_player: Player ID to steal from (or None if no one to steal from)
1479
+ """
1480
+ target_player = action.parameters.get('target_player')
1481
+
1482
+ if target_player is None:
1483
+ # No one to steal from
1484
+ self._current_game_state.turn_phase = TurnPhase.PLAYER_ACTIONS
1485
+ self._current_game_state.steal_pending = False
1486
+ return ActionResult.success_result(self.get_full_state())
1487
+
1488
+ # Validate target player
1489
+ if target_player < 0 or target_player >= self.num_players:
1490
+ return ActionResult.failure_result(
1491
+ f"Invalid player ID: {target_player}",
1492
+ "INVALID_PLAYER"
1493
+ )
1494
+
1495
+ if target_player == action.player_id:
1496
+ return ActionResult.failure_result(
1497
+ "You cannot steal from yourself!",
1498
+ "STEAL_SELF"
1499
+ )
1500
+
1501
+ # Check target has cards
1502
+ target = self.game.players[target_player]
1503
+ if len(target.cards) == 0:
1504
+ return ActionResult.failure_result(
1505
+ f"Player {target_player} has no cards to steal.",
1506
+ "NO_CARDS"
1507
+ )
1508
+
1509
+ # Check target is adjacent to robber
1510
+ robber_pos = getattr(self.game.board, 'robber_tile', None)
1511
+ if robber_pos:
1512
+ stealable = self._get_stealable_players(robber_pos[0], robber_pos[1])
1513
+ if target_player not in stealable:
1514
+ return ActionResult.failure_result(
1515
+ f"Player {target_player} is not adjacent to the robber.",
1516
+ "NOT_ADJACENT"
1517
+ )
1518
+
1519
+ # Steal a random card
1520
+ import random
1521
+ stolen_card = random.choice(target.cards)
1522
+ target.remove_cards([stolen_card])
1523
+ self.game.players[action.player_id].add_cards([stolen_card])
1524
+
1525
+ # Update state
1526
+ self._current_game_state.turn_phase = TurnPhase.PLAYER_ACTIONS
1527
+ self._current_game_state.steal_pending = False
1528
+
1529
+ # Notify (don't reveal what card was stolen to everyone)
1530
+ thief_name = self.users[action.player_id].name if hasattr(self.users[action.player_id], 'name') else f"Player {action.player_id}"
1531
+ victim_name = self.users[target_player].name if hasattr(self.users[target_player], 'name') else f"Player {target_player}"
1532
+
1533
+ self._notify_all_users(
1534
+ "steal_complete",
1535
+ f"🎯 {thief_name} stole a card from {victim_name}!"
1536
+ )
1537
+
1538
+ # Notify the thief specifically what they got
1539
+ self._notify_user(
1540
+ action.player_id,
1541
+ action,
1542
+ True,
1543
+ f"You stole a {stolen_card.name}!"
1544
+ )
1545
+
1546
+ return ActionResult.success_result(self.get_full_state())
1547
+
1548
+ def __str__(self) -> str:
1549
+ """String representation of the GameManager."""
1550
+ status = "running" if self._is_running else "stopped"
1551
+ if self._is_paused:
1552
+ status = "paused"
1553
+
1554
+ return f"GameManager(id={self.game_id[:8]}, players={self.num_players}, status={status})"
1555
+
1556
+ def __repr__(self) -> str:
1557
+ """Detailed string representation of the GameManager."""
1558
+ return (f"GameManager(game_id='{self.game_id}', "
1559
+ f"players={self.num_players}, "
1560
+ f"current_player={self.current_player_id}, "
1561
+ f"turn={self._current_game_state.turn_number}, "
1562
+ f"running={self._is_running}, "
1563
+ f"paused={self._is_paused})")
pycatan/game_moves.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ s 3
2
+ road 3 4
3
+ s 34
4
+ road 34 23
5
+ s 12
6
+ road 12 22
7
+ s 31
8
+ road 31 32
9
+ roll
10
+ t player v wood ore
11
+ t player v wood sheep
12
+ y
13
+ t player v brick wood
14
+ y
15
+ t player v wood brick
16
+ n
17
+ roll
18
+ end
19
+ roll
pycatan/harbor.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+ from pycatan.card import ResCard
3
+
4
+ # The different types of harbors found throughout the game
5
+ class HarborType(Enum):
6
+ # the different 2:1 types
7
+ Wood = 0
8
+ Sheep = 1
9
+ Brick = 2
10
+ Wheat = 3
11
+ Ore = 4
12
+
13
+ # the 3:1 type
14
+ Any = 5
15
+
16
+ # represents a catan harbor
17
+ class Harbor:
18
+
19
+ def __init__(self, point_one, point_two, type):
20
+ # sets the type
21
+ self.type = type
22
+
23
+ # sets the points
24
+ self.point_one = point_one
25
+ self.point_two = point_two
26
+
27
+ def __repr__(self):
28
+ return "Harbor %s, %s Type %s" % (self.point_one, self.point_two, self.type)
29
+
30
+ def get_points(self):
31
+ return [self.point_one, self.point_two]
32
+
33
+ # returns a string representation of the type
34
+ # Ex: 3:1, 2:1S, 2:1Wh
35
+ def get_type(self):
36
+
37
+ if self.type == HarborType.Wood:
38
+ return "2:1W"
39
+
40
+ elif self.type == HarborType.Sheep:
41
+ return "2:1S"
42
+
43
+ elif self.type == HarborType.Brick:
44
+ return "2:1B"
45
+
46
+ elif self.type == HarborType.Wheat:
47
+ return "2:1Wh"
48
+
49
+ elif self.type == HarborType.Ore:
50
+ return "2:1O"
51
+
52
+ elif self.type == HarborType.Any:
53
+ return "3:1"
54
+
55
+ @staticmethod
56
+ def get_card_from_harbor_type(h_type):
57
+ if h_type == HarborType.Wood:
58
+ return ResCard.Wood
59
+ elif h_type == HarborType.Brick:
60
+ return ResCard.Brick
61
+ elif h_type == HarborType.Wheat:
62
+ return ResCard.Wheat
63
+ elif h_type == HarborType.Ore:
64
+ return ResCard.Ore
65
+ elif h_type == HarborType.Sheep:
66
+ return ResCard.Sheep
67
+ elif h_type == HarborType.Any:
68
+ return None
69
+ else:
70
+ raise Exception("Harbor has invalid type %s" % h_type)
pycatan/human_user.py ADDED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Human User Implementation for PyCatan Game Management
3
+
4
+ This module implements HumanUser, which provides a command-line interface
5
+ for human players to interact with the game.
6
+ """
7
+
8
+ from typing import List, Optional, Dict, Tuple
9
+ from pycatan.user import User, UserInputError
10
+ from pycatan.actions import Action, ActionType, GameState
11
+ from pycatan.card import ResCard, DevCard
12
+ from pycatan.board_definition import board_definition
13
+
14
+
15
+ class HumanUser(User):
16
+ """
17
+ Human user implementation with command-line interface.
18
+
19
+ This class provides a text-based interface for human players to interact
20
+ with the game. It parses text commands and converts them to Action objects.
21
+ """
22
+
23
+ def __init__(self, name: str, user_id: int):
24
+ """Initialize a HumanUser with CLI interface."""
25
+ super().__init__(name, user_id)
26
+ self.command_history = []
27
+
28
+ def get_input(self, game_state: GameState, prompt_message: str,
29
+ allowed_actions: Optional[List[str]] = None) -> Action:
30
+ """
31
+ Get input from human player via command line.
32
+
33
+ Args:
34
+ game_state: Current state of the game
35
+ prompt_message: Message explaining what input is needed
36
+ allowed_actions: Optional list of allowed action types
37
+
38
+ Returns:
39
+ Action: The action the player wants to perform
40
+
41
+ Raises:
42
+ UserInputError: If input parsing fails or action is invalid
43
+ """
44
+ while True:
45
+ try:
46
+ # Show game status
47
+ self._display_game_status(game_state)
48
+
49
+ # Show prompt with clear format
50
+ print(f"\n>>> {self.name}'s Turn")
51
+
52
+ # Show allowed actions in a compact format
53
+ if allowed_actions:
54
+ # Format actions nicely (e.g., "BUILD_SETTLEMENT" -> "build settlement")
55
+ formatted_actions = [self._format_action_name(a) for a in allowed_actions]
56
+ actions_str = " | ".join(formatted_actions)
57
+ print(f" Options: {actions_str}")
58
+
59
+ # Get user input with clean prompt
60
+ user_input = input(f" {self.name} > ").strip()
61
+
62
+ if not user_input:
63
+ continue
64
+
65
+ # Store in history
66
+ self.command_history.append(user_input)
67
+
68
+ # Parse the input into an action
69
+ action = self._parse_input(user_input, game_state)
70
+
71
+ # Validate against allowed actions if provided
72
+ if allowed_actions and action.action_type.name not in allowed_actions:
73
+ print(f" ✗ '{self._format_action_name(action.action_type.name)}' is not allowed right now.")
74
+ continue
75
+
76
+ return action
77
+
78
+ except UserInputError as e:
79
+ print(f" ✗ {e.message}")
80
+ except KeyboardInterrupt:
81
+ print("\n Game interrupted by user.")
82
+ return Action(ActionType.END_TURN, self.user_id)
83
+
84
+ def _display_game_status(self, game_state: GameState) -> None:
85
+ return
86
+ """Display current game status to the user."""
87
+ print("\n" + "="*60)
88
+ print(f"🎮 GAME STATUS - Turn {game_state.turn_number}")
89
+ print("="*60)
90
+
91
+ # Show all players
92
+ for player in game_state.players_state:
93
+ is_current = player.player_id == game_state.current_player
94
+ marker = "👉 " if is_current else " "
95
+
96
+ print(f"\n{marker}Player {player.player_id}: {player.name}")
97
+ print(f" 🏆 Victory Points: {player.victory_points}")
98
+
99
+ # Count resources
100
+ resource_count = {}
101
+ for card in player.cards:
102
+ resource_count[card] = resource_count.get(card, 0) + 1
103
+
104
+ if resource_count:
105
+ # Show only if it's the current user or if viewing own cards
106
+ if player.player_id == self.user_id:
107
+ print(f" 📦 Resources: ", end="")
108
+ cards_str = ", ".join([f"{count} {res}" for res, count in resource_count.items()])
109
+ print(cards_str)
110
+ else:
111
+ # Just show total count for other players
112
+ print(f" 📦 Resources: {len(player.cards)} cards")
113
+ else:
114
+ print(f" 📦 Resources: none")
115
+
116
+ print(f" 🏘️ Settlements: {len(player.settlements)} | Cities: {len(player.cities)} | Roads: {len(player.roads)}")
117
+
118
+ if player.dev_cards:
119
+ print(f" 🎴 Dev Cards: {len(player.dev_cards)}")
120
+
121
+ if player.has_longest_road:
122
+ print(f" 🛣️ Has Longest Road!")
123
+ if player.has_largest_army:
124
+ print(f" ⚔️ Has Largest Army!")
125
+
126
+ print("\n" + "="*60)
127
+
128
+ def _format_action_name(self, action_name: str) -> str:
129
+ """Convert action enum name to readable format."""
130
+ # Convert "BUILD_SETTLEMENT" to "build settlement"
131
+ words = action_name.replace("_", " ").lower()
132
+ return words
133
+
134
+ def _parse_input(self, user_input: str, game_state: GameState) -> Action:
135
+ """
136
+ Parse user input text into an Action object.
137
+
138
+ Args:
139
+ user_input: Text command from user
140
+ game_state: Current game state for context
141
+
142
+ Returns:
143
+ Action: Parsed action
144
+
145
+ Raises:
146
+ UserInputError: If parsing fails
147
+ """
148
+ parts = user_input.lower().split()
149
+ if not parts:
150
+ raise UserInputError("Empty command")
151
+
152
+ command = parts[0]
153
+
154
+ # Handle different commands
155
+ if command in ['help', 'h', '?']:
156
+ self._show_help()
157
+ raise UserInputError("Help displayed, please enter a command")
158
+
159
+ elif command in ['quit', 'exit', 'q']:
160
+ raise KeyboardInterrupt("User requested to quit")
161
+
162
+ elif command in ['points', 'p']:
163
+ self._show_points()
164
+ raise UserInputError("Points displayed, please enter a command")
165
+
166
+ elif command in ['status', 'info', 'i']:
167
+ self._show_game_status(game_state)
168
+ raise UserInputError("Game status displayed, please enter a command")
169
+
170
+ elif command in ['end', 'pass', 'done']:
171
+ return Action(ActionType.END_TURN, self.user_id)
172
+
173
+ elif command in ['roll', 'dice', 'r']:
174
+ return Action(ActionType.ROLL_DICE, self.user_id)
175
+
176
+ elif command in ['settlement', 'settle', 's', 'set']:
177
+ return self._parse_build_settlement(parts, game_state)
178
+
179
+ elif command in ['city', 'c']:
180
+ return self._parse_build_city(parts, game_state)
181
+
182
+ elif command in ['road', 'rd']:
183
+ return self._parse_build_road(parts, game_state)
184
+
185
+ elif command in ['trade', 't']:
186
+ return self._parse_trade(parts, game_state)
187
+
188
+ elif command in ['buy', 'dev']:
189
+ return Action(ActionType.BUY_DEV_CARD, self.user_id)
190
+
191
+ elif command in ['use']:
192
+ return self._parse_use_dev_card(parts, game_state)
193
+
194
+ elif command in ['robber', 'rob']:
195
+ return self._parse_robber_move(parts, game_state)
196
+
197
+ elif command in ['drop', 'discard']:
198
+ return self._parse_discard_cards(parts, game_state)
199
+
200
+ elif command in ['steal']:
201
+ return self._parse_steal(parts, game_state)
202
+
203
+ elif command in ['yes', 'y', 'accept']:
204
+ return Action(ActionType.TRADE_ACCEPT, self.user_id)
205
+
206
+ elif command in ['no', 'n', 'reject', 'decline']:
207
+ return Action(ActionType.TRADE_REJECT, self.user_id)
208
+
209
+ else:
210
+ raise UserInputError(f"Unknown command: {command}. Type 'help' for available commands.")
211
+
212
+ def _parse_build_settlement(self, parts: List[str], game_state: GameState) -> Action:
213
+ """Parse settlement building command."""
214
+ # Support both old coordinate format and new point format
215
+ if len(parts) < 2:
216
+ raise UserInputError("Settlement command requires a point number or coordinates. Example: 'settlement 12' or 'settlement 0 5'")
217
+
218
+ # Check if using new point format (1 number) or old coordinate format (2 numbers)
219
+ if len(parts) == 2 or (len(parts) == 3 and parts[2].lower() in ['start', 'starting']):
220
+ # New point format: settlement <point> [start]
221
+ try:
222
+ point = int(parts[1])
223
+ is_starting = len(parts) > 2 and parts[2].lower() in ['start', 'starting']
224
+
225
+ # Convert point to coordinates using BoardDefinition
226
+ coords = board_definition.point_id_to_game_coords(point)
227
+ if coords is None:
228
+ raise UserInputError(f"Invalid point number: {point}. Valid points: 1-{len(board_definition.get_all_point_ids())}")
229
+
230
+ row, index = coords
231
+
232
+ except ValueError:
233
+ raise UserInputError("Point number must be a valid integer. Example: 'settlement 12'")
234
+
235
+ else:
236
+ # Old coordinate format: settlement <row> <index> [start]
237
+ if len(parts) < 3:
238
+ raise UserInputError("Old format settlement command requires row and index. Example: 'settlement 0 5'")
239
+
240
+ try:
241
+ row = int(parts[1])
242
+ index = int(parts[2])
243
+ is_starting = len(parts) > 3 and parts[3].lower() in ['start', 'starting']
244
+
245
+ except ValueError:
246
+ raise UserInputError("Row and index must be numbers. Example: 'settlement 0 5'")
247
+
248
+ # Auto-detect if we're in setup phase to determine action type
249
+ in_setup = hasattr(game_state, 'game_phase') and hasattr(game_state.game_phase, 'name') and \
250
+ 'SETUP' in game_state.game_phase.name
251
+
252
+ # Force starting settlement if in setup phase
253
+ if in_setup:
254
+ action_type = ActionType.PLACE_STARTING_SETTLEMENT
255
+ else:
256
+ action_type = ActionType.PLACE_STARTING_SETTLEMENT if is_starting else ActionType.BUILD_SETTLEMENT
257
+
258
+ params = {
259
+ 'point_coords': [row, index],
260
+ 'is_starting': in_setup or is_starting
261
+ }
262
+
263
+ return Action(action_type, self.user_id, params)
264
+
265
+ def _parse_build_city(self, parts: List[str], game_state: GameState) -> Action:
266
+ """Parse city building command."""
267
+ # Support both old coordinate format and new point format
268
+ if len(parts) < 2:
269
+ raise UserInputError("City command requires a point number or coordinates. Example: 'city 12' or 'city 0 5'")
270
+
271
+ # Check if using new point format (1 number) or old coordinate format (2 numbers)
272
+ if len(parts) == 2:
273
+ # New point format: city <point>
274
+ try:
275
+ point = int(parts[1])
276
+
277
+ # Convert point to coordinates
278
+ coords = point_to_coords(point)
279
+ if coords is None:
280
+ raise UserInputError(f"Invalid point number: {point}. Valid points: 1-{len(get_all_points())}")
281
+
282
+ row, index = coords
283
+
284
+ except ValueError:
285
+ raise UserInputError("Point number must be a valid integer. Example: 'city 12'")
286
+
287
+ else:
288
+ # Old coordinate format: city <row> <index>
289
+ if len(parts) < 3:
290
+ raise UserInputError("Old format city command requires row and index. Example: 'city 0 5'")
291
+
292
+ try:
293
+ row = int(parts[1])
294
+ index = int(parts[2])
295
+
296
+ except ValueError:
297
+ raise UserInputError("Row and index must be numbers. Example: 'city 0 5'")
298
+
299
+ params = {'point_coords': [row, index]}
300
+ return Action(ActionType.BUILD_CITY, self.user_id, params)
301
+
302
+ def _parse_build_road(self, parts: List[str], game_state: GameState) -> Action:
303
+ """Parse road building command."""
304
+ # Support both old coordinate format and new point format
305
+ if len(parts) < 3:
306
+ raise UserInputError("Road command requires 2 points. Example: 'road 5 6' or 'road 0 5 0 6'")
307
+
308
+ # Check if using new point format (2 numbers) or old coordinate format (4 numbers)
309
+ if len(parts) == 3 or (len(parts) == 4 and parts[3].lower() in ['start', 'starting']):
310
+ # New point format: road <point1> <point2> [start]
311
+ try:
312
+ point1 = int(parts[1])
313
+ point2 = int(parts[2])
314
+ is_starting = len(parts) > 3 and parts[3].lower() in ['start', 'starting']
315
+
316
+ # Validate road placement using BoardDefinition
317
+ if not board_definition.is_valid_road_placement(point1, point2):
318
+ raise UserInputError(f"Cannot build road between points {point1} and {point2} - they are not adjacent")
319
+
320
+ # Convert points to coordinates using BoardDefinition
321
+ start_coords = board_definition.point_id_to_game_coords(point1)
322
+ end_coords = board_definition.point_id_to_game_coords(point2)
323
+
324
+ if start_coords is None:
325
+ raise UserInputError(f"Invalid point number: {point1}. Valid points: 1-{len(board_definition.get_all_point_ids())}")
326
+ if end_coords is None:
327
+ raise UserInputError(f"Invalid point number: {point2}. Valid points: 1-{len(board_definition.get_all_point_ids())}")
328
+
329
+ start_row, start_index = start_coords
330
+ end_row, end_index = end_coords
331
+
332
+ except ValueError:
333
+ raise UserInputError("Point numbers must be valid integers. Example: 'road 5 6'")
334
+
335
+ else:
336
+ # Old coordinate format: road <start_row> <start_index> <end_row> <end_index> [start]
337
+ if len(parts) < 5:
338
+ raise UserInputError("Old format road command requires start and end coordinates. Example: 'road 0 5 0 6'")
339
+
340
+ try:
341
+ start_row = int(parts[1])
342
+ start_index = int(parts[2])
343
+ end_row = int(parts[3])
344
+ end_index = int(parts[4])
345
+ is_starting = len(parts) > 5 and parts[5].lower() in ['start', 'starting']
346
+
347
+ except ValueError:
348
+ raise UserInputError("Coordinates must be numbers. Example: 'road 0 5 0 6'")
349
+
350
+ # Auto-detect if we're in setup phase to determine action type
351
+ in_setup = hasattr(game_state, 'game_phase') and hasattr(game_state.game_phase, 'name') and \
352
+ 'SETUP' in game_state.game_phase.name
353
+
354
+ # Force starting road if in setup phase
355
+ if in_setup:
356
+ action_type = ActionType.PLACE_STARTING_ROAD
357
+ else:
358
+ action_type = ActionType.PLACE_STARTING_ROAD if is_starting else ActionType.BUILD_ROAD
359
+
360
+ params = {
361
+ 'start_coords': [start_row, start_index],
362
+ 'end_coords': [end_row, end_index],
363
+ 'is_starting': in_setup or is_starting
364
+ }
365
+
366
+ return Action(action_type, self.user_id, params)
367
+
368
+ def _parse_trade(self, parts: List[str], game_state: GameState) -> Action:
369
+ """Parse trading command."""
370
+ if len(parts) < 2:
371
+ raise UserInputError("Trade command needs more info. Examples: 'trade bank wood 4 wheat 1' or 'trade player 1 wood sheep'")
372
+
373
+ if parts[1].lower() == 'bank':
374
+ return self._parse_bank_trade(parts[2:])
375
+ elif parts[1].lower() == 'player':
376
+ return self._parse_player_trade(parts[2:], game_state)
377
+ else:
378
+ raise UserInputError("Trade must specify 'bank' or 'player'. Examples: 'trade bank wood 4 wheat 1' or 'trade player 1 wood sheep'")
379
+
380
+ def _parse_bank_trade(self, parts: List[str]) -> Action:
381
+ """Parse bank trading command."""
382
+ if len(parts) < 4:
383
+ raise UserInputError("Bank trade format: 'trade bank [give_resource] [give_amount] [get_resource] [get_amount]'")
384
+
385
+ try:
386
+ give_resource = self._parse_resource(parts[0])
387
+ give_amount = int(parts[1])
388
+ get_resource = self._parse_resource(parts[2])
389
+ get_amount = int(parts[3])
390
+
391
+ params = {
392
+ 'offer': {give_resource.name.lower(): give_amount},
393
+ 'request': {get_resource.name.lower(): get_amount}
394
+ }
395
+
396
+ return Action(ActionType.TRADE_BANK, self.user_id, params)
397
+
398
+ except (ValueError, KeyError):
399
+ raise UserInputError("Invalid trade format or resource names")
400
+
401
+ def _parse_player_trade(self, parts: List[str], game_state: GameState = None) -> Action:
402
+ """Parse player trading command."""
403
+ if len(parts) < 3:
404
+ raise UserInputError("Player trade format: 'trade player [player_id_or_name] [your_resource] [their_resource]'")
405
+
406
+ try:
407
+ # Try to parse as player ID first
408
+ try:
409
+ target_player = int(parts[0])
410
+ except ValueError:
411
+ # Not a number, try to find by name
412
+ player_name = parts[0].lower()
413
+ target_player = None
414
+
415
+ if game_state and game_state.players_state:
416
+ for player in game_state.players_state:
417
+ if player.name.lower() == player_name:
418
+ target_player = player.player_id
419
+ break
420
+
421
+ if target_player is None:
422
+ raise UserInputError(f"Player '{parts[0]}' not found. Use player name or ID (0-{len(game_state.players_state)-1 if game_state else 3})")
423
+
424
+ give_resource = self._parse_resource(parts[1])
425
+ get_resource = self._parse_resource(parts[2])
426
+
427
+ params = {
428
+ 'target_player': target_player,
429
+ 'offer': {give_resource.name.lower(): 1},
430
+ 'request': {get_resource.name.lower(): 1}
431
+ }
432
+
433
+ return Action(ActionType.TRADE_PROPOSE, self.user_id, params)
434
+
435
+ except (ValueError, KeyError):
436
+ raise UserInputError("Invalid trade format or resource names")
437
+
438
+ def _parse_use_dev_card(self, parts: List[str], game_state: GameState) -> Action:
439
+ """Parse development card usage command."""
440
+ if len(parts) < 2:
441
+ raise UserInputError("Use command requires card type. Example: 'use knight' or 'use road'")
442
+
443
+ card_name = parts[1].lower()
444
+ params = {'card_type': card_name}
445
+
446
+ # Add specific parameters based on card type
447
+ if card_name == 'knight' and len(parts) >= 5:
448
+ try:
449
+ robber_row = int(parts[2])
450
+ robber_index = int(parts[3])
451
+ victim_player = int(parts[4]) if parts[4] != 'none' else None
452
+
453
+ params.update({
454
+ 'tile_coords': [robber_row, robber_index],
455
+ 'victim': victim_player
456
+ })
457
+ except ValueError:
458
+ raise UserInputError("Knight card format: 'use knight [robber_row] [robber_index] [victim_player_or_none]'")
459
+
460
+ return Action(ActionType.USE_DEV_CARD, self.user_id, params)
461
+
462
+ def _parse_robber_move(self, parts: List[str], game_state: GameState) -> Action:
463
+ """Parse robber movement command.
464
+
465
+ Format: 'robber [row] [index]' or 'rob [row] [index]'
466
+ The steal action is now separate via 'steal [player]' command.
467
+ """
468
+ if len(parts) < 3:
469
+ raise UserInputError("Robber command format: 'robber [row] [index]'. Example: 'robber 2 1'")
470
+
471
+ try:
472
+ row = int(parts[1])
473
+ index = int(parts[2])
474
+
475
+ params = {
476
+ 'tile_coords': [row, index]
477
+ }
478
+
479
+ return Action(ActionType.ROBBER_MOVE, self.user_id, params)
480
+
481
+ except ValueError:
482
+ raise UserInputError("Robber coordinates must be numbers. Example: 'robber 2 1'")
483
+
484
+ def _parse_steal(self, parts: List[str], game_state: GameState) -> Action:
485
+ """Parse steal card command.
486
+
487
+ Format: 'steal [player_id_or_name]' or 'steal none'
488
+ """
489
+ if len(parts) < 2:
490
+ raise UserInputError("Steal command format: 'steal [player_id_or_name]' or 'steal none'")
491
+
492
+ target = parts[1].lower()
493
+
494
+ if target == 'none':
495
+ # No one to steal from (all adjacent players have 0 cards)
496
+ params = {'target_player': None}
497
+ else:
498
+ try:
499
+ # Try to parse as player ID first
500
+ target_player = int(target)
501
+ except ValueError:
502
+ # Try to find by name
503
+ target_player = None
504
+ if game_state and game_state.players_state:
505
+ for player in game_state.players_state:
506
+ if player.name.lower() == target:
507
+ target_player = player.player_id
508
+ break
509
+
510
+ if target_player is None:
511
+ raise UserInputError(f"Player '{parts[1]}' not found.")
512
+
513
+ params = {'target_player': target_player}
514
+
515
+ return Action(ActionType.STEAL_CARD, self.user_id, params)
516
+
517
+ def _parse_discard_cards(self, parts: List[str], game_state: GameState) -> Action:
518
+ """Parse discard cards command.
519
+
520
+ Format: 'drop [amount1] [resource1] [amount2] [resource2] ...'
521
+ Example: 'drop 2 wood 1 brick' means discard 2 wood and 1 brick
522
+
523
+ The game will validate that the total discarded equals the required amount
524
+ and that the player has those cards.
525
+ """
526
+ if len(parts) < 3:
527
+ raise UserInputError(
528
+ "Discard command format: 'drop [amount] [resource] [amount] [resource] ...'\n"
529
+ "Example: 'drop 2 wood 1 brick' to discard 2 wood and 1 brick"
530
+ )
531
+
532
+ # Parse pairs of (amount, resource)
533
+ cards_to_discard = []
534
+ i = 1
535
+
536
+ while i < len(parts) - 1:
537
+ try:
538
+ amount = int(parts[i])
539
+ resource_name = parts[i + 1].lower()
540
+
541
+ # Parse the resource
542
+ resource = self._parse_resource(resource_name)
543
+
544
+ # Add the cards to discard list (one entry per card)
545
+ for _ in range(amount):
546
+ cards_to_discard.append(resource.name)
547
+
548
+ i += 2
549
+ except ValueError:
550
+ raise UserInputError(
551
+ f"Invalid format at '{parts[i]}'. Expected: [amount] [resource]\n"
552
+ "Example: 'drop 2 wood 1 brick'"
553
+ )
554
+
555
+ if not cards_to_discard:
556
+ raise UserInputError("You must specify at least one card to discard.")
557
+
558
+ params = {'cards': cards_to_discard}
559
+ return Action(ActionType.DISCARD_CARDS, self.user_id, params)
560
+
561
+ def _parse_resource(self, resource_name: str) -> ResCard:
562
+ """Parse resource name to ResCard enum."""
563
+ resource_mapping = {
564
+ 'wood': ResCard.Wood,
565
+ 'lumber': ResCard.Wood,
566
+ 'brick': ResCard.Brick,
567
+ 'sheep': ResCard.Sheep,
568
+ 'wool': ResCard.Sheep,
569
+ 'wheat': ResCard.Wheat,
570
+ 'grain': ResCard.Wheat,
571
+ 'ore': ResCard.Ore,
572
+ 'stone': ResCard.Ore
573
+ }
574
+
575
+ resource_name = resource_name.lower()
576
+ if resource_name not in resource_mapping:
577
+ raise UserInputError(f"Unknown resource: {resource_name}. Valid resources: {list(resource_mapping.keys())}")
578
+
579
+ return resource_mapping[resource_name]
580
+
581
+ def _show_points(self) -> None:
582
+ """Display all available points on the board."""
583
+ print("\n" + "="*60)
584
+ print("BOARD POINTS MAP")
585
+ print("="*60)
586
+
587
+ # Get all points using BoardDefinition
588
+ all_points_list = board_definition.get_all_point_ids()
589
+
590
+ # Group points by row for cleaner display
591
+ points_by_row = {}
592
+ for point_id in all_points_list:
593
+ coords = board_definition.point_id_to_game_coords(point_id)
594
+ if coords:
595
+ row, index = coords
596
+ if row not in points_by_row:
597
+ points_by_row[row] = []
598
+ points_by_row[row].append((point_id, index))
599
+
600
+ # Display by row
601
+ for row in sorted(points_by_row.keys()):
602
+ points_in_row = sorted(points_by_row[row], key=lambda x: x[1])
603
+ point_list = [f"{pid}({idx})" for pid, idx in points_in_row]
604
+ print(f"Row {row}: {', '.join(point_list)}")
605
+
606
+ print()
607
+ print("Format: Point_ID(Index)")
608
+ print("Usage: 'settlement 12' builds at point 12")
609
+ print(" 'road 5 6' builds road between points 5 and 6")
610
+ print("="*60)
611
+
612
+ def _show_help(self) -> None:
613
+ """Display help information to the user."""
614
+ print("\n" + "="*60)
615
+ print("🎮 PYCATAN COMMANDS HELP")
616
+ print("="*60)
617
+ print("🏗️ BUILDING (Points 1-54):")
618
+ print(" s <point> - Build settlement (short: s, settle, settlement)")
619
+ print(" road <p1> <p2> - Build road between points (short: rd)")
620
+ print(" city <point> - Upgrade settlement to city (short: c)")
621
+ print()
622
+ print("💰 TRADING:")
623
+ print(" trade bank <give> <amount> <get> <amount>")
624
+ print(" trade player <id_or_name> <give> <get> (short: t)")
625
+ print(" Examples: 'trade bank wood 4 sheep 1' or 't player v wood sheep'")
626
+ print()
627
+ print("🃏 DEVELOPMENT CARDS:")
628
+ print(" buy - Buy development card (short: dev)")
629
+ print(" use <card_type> - Use development card")
630
+ print()
631
+ print("🎲 TURN ACTIONS:")
632
+ print(" roll - Roll dice (short: r, dice)")
633
+ print(" end - End turn (short: pass, done)")
634
+ print()
635
+ print("ℹ️ INFO:")
636
+ print(" help - Show this help (short: h, ?)")
637
+ print(" status - Show all players' status (short: info, i)")
638
+ print(" points - Show all valid points (short: p)")
639
+ print()
640
+ print("📦 RESOURCES: wood, brick, sheep, wheat, ore")
641
+ print("🎯 POINTS: Use numbers 1-54. Example: 's 12' builds settlement at point 12")
642
+ print("🔗 ROADS: Example: 'road 5 6' builds road between points 5 and 6")
643
+ print("="*60)
644
+
645
+ def notify_action(self, action: Action, success: bool, message: str = "") -> None:
646
+ """Notify the user about an action result."""
647
+ # Don't print here - the console visualization already displays this
648
+ # This method is kept for compatibility but doesn't produce output
649
+ pass
650
+
651
+ def notify_game_event(self, event_type: str, message: str,
652
+ affected_players: Optional[List[int]] = None) -> None:
653
+ """Notify the user about general game events."""
654
+ # Only notify for specific important events - avoid clutter
655
+ skip_events = [
656
+ 'turn_change', # Already handled by display_turn_start
657
+ 'action_performed', # Already handled by notify_action + visualization
658
+ 'phase_change' # Important, show this
659
+ ]
660
+
661
+ # Skip most events to avoid clutter
662
+ if event_type not in ['phase_change']:
663
+ return
664
+
665
+ # For phase changes, show them clearly
666
+ if event_type == 'phase_change':
667
+ print(f"\n ✨ {message}\n")
pycatan/player.py ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pycatan.building import Building
2
+ from pycatan.statuses import Statuses
3
+ from pycatan.card import ResCard, DevCard
4
+
5
+ import math
6
+
7
+ # The player class for
8
+ class Player:
9
+
10
+ def __init__ (self, game, num):
11
+ # the game the player belongs to
12
+ self.game = game
13
+ # the player number for this player
14
+ self.num = num
15
+ # the starting roads for this player
16
+ # used to determine the longest road
17
+ self.starting_roads = []
18
+ # the number of victory points
19
+ self.victory_points = 0
20
+ # the cards the player has
21
+ # each will be a number corresponding with the static variables CARD_<type>
22
+ self.cards = []
23
+ # the development cards this player has
24
+ self.dev_cards = []
25
+ # the number of knight cards the player has played
26
+ self.knight_cards = 0
27
+ # the longest road segment this player has
28
+ self.longest_road_length = 0
29
+
30
+ # builds a settlement belonging to this player
31
+ def build_settlement(self, point, is_starting=False):
32
+
33
+ if not is_starting:
34
+ # makes sure the player has the cards to build a settlements
35
+ cards_needed = [
36
+ ResCard.Wood,
37
+ ResCard.Brick,
38
+ ResCard.Sheep,
39
+ ResCard.Wheat
40
+ ]
41
+
42
+ # checks the player has the cards
43
+ if not self.has_cards(cards_needed):
44
+ return Statuses.ERR_CARDS
45
+
46
+ # checks it is connected to a road owned by the player
47
+ connected_by_road = False
48
+ # gets the roads
49
+ roads = self.game.board.roads
50
+
51
+ for r in roads:
52
+ # checks if the road is connected
53
+ if r.point_one is point or r.point_two is point:
54
+ # checks this player owns the road
55
+ if r.owner == self.num:
56
+ connected_by_road = True
57
+
58
+ if not connected_by_road:
59
+ return Statuses.ERR_ISOLATED
60
+
61
+ # checks that a building does not already exist there
62
+ if point.building != None:
63
+ return Statuses.ERR_BLOCKED
64
+
65
+ # checks all other settlements are at least 2 away
66
+ # gets the connecting point's coords
67
+ points = point.connected_points
68
+ for p in points:
69
+
70
+ # checks if the point is occupied
71
+ if p.building != None:
72
+ return Statuses.ERR_BLOCKED
73
+
74
+ if not is_starting:
75
+ # removes the cards
76
+ self.remove_cards(cards_needed)
77
+
78
+ # adds the settlement
79
+ self.game.board.add_building(Building(
80
+ owner = self.num,
81
+ type = Building.BUILDING_SETTLEMENT,
82
+ point_one = point),
83
+ point = point)
84
+ # adds a victory point
85
+ self.victory_points += 1
86
+
87
+ return Statuses.ALL_GOOD
88
+
89
+ # checks if the player has all of the cards given in an array
90
+ def has_cards(self, cards):
91
+
92
+ # needs to duplicate the cards, and then delete them once found
93
+ # otherwise checking if the player has multiple of the same card
94
+ # will return true with only one card
95
+
96
+ # cards_dup stands for cards duplicate
97
+ cards_dup = self.cards[:]
98
+ for c in cards:
99
+ if cards_dup.count(c) == 0:
100
+ return False
101
+ else:
102
+ index = cards_dup.index(c)
103
+ del cards_dup[index]
104
+
105
+ return True
106
+
107
+ # adds some cards to a player's hand
108
+ def add_cards(self, cards):
109
+ for c in cards:
110
+ self.cards.append(c)
111
+
112
+ # removes cards from a player's hand
113
+ def remove_cards(self, cards):
114
+ # makes sure it has all the cards before deleting any
115
+ if not self.has_cards(cards):
116
+ return Statuses.ERR_CARDS
117
+
118
+ else:
119
+ # removes the cards
120
+ for c in cards:
121
+ index = self.cards.index(c)
122
+ del self.cards[index]
123
+
124
+ #adds a development card
125
+ def add_dev_card(self, dev_card):
126
+ self.dev_cards.append(dev_card)
127
+
128
+ # removes a dev card
129
+ def remove_dev_card(self, card):
130
+ # finds the card
131
+ for i in range(len(self.dev_cards)):
132
+ if self.dev_cards[i] == card:
133
+
134
+ # deletes the card
135
+ del self.dev_cards[i]
136
+ return Statuses.ALL_GOOD
137
+
138
+ # error if the player does not have the cards
139
+ return Statuses.ERR_CARDS
140
+
141
+ # checks a road location is valid
142
+ def road_location_is_valid(self, start, end):
143
+ # checks the two points are connected
144
+ connected = False
145
+ # gets the points connected to start
146
+ points = start.connected_points
147
+
148
+ for p in points:
149
+ if end == p:
150
+ connected = True
151
+ break
152
+
153
+ if not connected:
154
+ return Statuses.ERR_NOT_CON
155
+
156
+ connected_by_road = False
157
+ for road in self.game.board.roads:
158
+ # checks the road does not already exists with these points
159
+ if road.point_one == start or road.point_two == start:
160
+ if road.point_one == end or road.point_two == end:
161
+ return Statuses.ERR_BLOCKED
162
+
163
+ # check this player has a settlement on one of these points or a connecting road
164
+ is_connected = False
165
+
166
+ if start.building != None:
167
+ # checks if this player owns the settlement/city
168
+ if start.building.owner == self.num:
169
+ is_connected = True
170
+
171
+ # does the same for the other point
172
+ elif end.building != None:
173
+ if end.building.owner == self.num:
174
+ is_connected = True
175
+
176
+ # then checks if there is a road connecting them
177
+ roads = self.game.board.roads
178
+ points = [start, end]
179
+
180
+ for r in roads:
181
+ for p in points:
182
+ if r.point_one == p or r.point_two == p:
183
+
184
+ # checks that there is not another player's settlement here, so that it's not going through it
185
+ if p.building == None:
186
+ is_connected = True
187
+
188
+ # if theere is a settlement/city there, the road can be built if this player owns it
189
+ elif p.building.owner == self.num:
190
+ is_connected = True
191
+
192
+ if not is_connected:
193
+ return Statuses.ERR_ISOLATED
194
+
195
+ return Statuses.ALL_GOOD
196
+
197
+ # builds a road
198
+ def build_road(self, start, end, is_starting=False):
199
+
200
+ # checks the location is valid
201
+ location_status = self.road_location_is_valid(start=start, end=end)
202
+
203
+ if not location_status == Statuses.ALL_GOOD:
204
+ return location_status
205
+
206
+ # if the road is being created on the starting turn, the player does not needed
207
+ # to have the cards
208
+ if not is_starting:
209
+
210
+ # checks that it has the proper cards
211
+ cards_needed = [
212
+ ResCard.Wood,
213
+ ResCard.Brick
214
+ ]
215
+ if not self.has_cards(cards_needed):
216
+ return Statuses.ERR_CARDS
217
+
218
+ # removes the cards
219
+ self.remove_cards(cards_needed)
220
+
221
+ # adds the road
222
+ road = Building(owner=self.num, type=Building.BUILDING_ROAD, point_one=start, point_two=end)
223
+ (self.game).board.add_road(road)
224
+
225
+ self.get_longest_road(new_road=road)
226
+
227
+ return Statuses.ALL_GOOD
228
+
229
+ # returns an array of all the harbors the player has access to
230
+ def get_connected_harbor_types(self):
231
+
232
+ # gets the settlements/cities belonging to this player
233
+ harbors = []
234
+ all_harbors = self.game.board.harbors
235
+ buildings = self.game.board.get_buildings()
236
+
237
+ for b in buildings:
238
+ # checks the building belongs to this player
239
+ if b.owner == self.num:
240
+ # checks if the building is connected to any harbors
241
+ for h in all_harbors:
242
+ print(h)
243
+ print(b.point)
244
+ if h.point_one is b.point or h.point_two is b.point:
245
+ print("A")
246
+ # adds the type
247
+ if harbors.count(h.type) == 0:
248
+ harbors.append(h.type)
249
+
250
+ return harbors
251
+
252
+ # gets the longest road segment this player has which includes the road given
253
+ # should be called whenever a new road is build
254
+ # since this player's longest road will only change if a new road is build
255
+ def get_longest_road(self, new_road):
256
+
257
+ # gets the roads that belong to this player
258
+ roads = self.get_roads()
259
+ del roads[roads.index(new_road)]
260
+
261
+ # checks for longest road
262
+ self.check_connected_roads(road=new_road, all_roads=roads, length=1)
263
+
264
+ # checks the roads for connected roads, and then checks those roads until there are no more
265
+ def check_connected_roads(self, road, all_roads, length):
266
+
267
+ # do both point one and two
268
+ points = [
269
+ road.point_one,
270
+ road.point_two
271
+ ]
272
+
273
+ for p in points:
274
+ # gets the connected roads
275
+ connected = self.get_connected_roads(point=p, roads=all_roads)
276
+ # if there are no new connected roads
277
+ if len(connected) == 0:
278
+ # if this is the longest road so far
279
+ if length > self.longest_road_length:
280
+ # records the length
281
+ self.longest_road_length = length
282
+ # self.begin_celebration()
283
+
284
+ # if there are connected roads
285
+ else:
286
+ # check each of them for connections if they have not been used
287
+ for c in connected:
288
+ # checks it hasn't used this road before
289
+ if all_roads.count(c) > 0:
290
+ # copies all usable roads
291
+ c_roads = all_roads[:]
292
+ # removes this road from them
293
+ del c_roads[c_roads.index(c)]
294
+ # checks for connected roads to this road
295
+ self.check_connected_roads(c, c_roads, length + 1)
296
+
297
+ # returns which roads in the roads array are connected to the point
298
+ def get_connected_roads(self, point, roads):
299
+ con_roads = []
300
+ for r in roads:
301
+ if r.point_one == point or r.point_two == point:
302
+ con_roads.append(r)
303
+
304
+ return con_roads
305
+
306
+ # returns an array of all the roads belonging to this player
307
+ def get_roads(self):
308
+ # gets all the roads on the board
309
+ all_roads = (self.game).board.roads
310
+ # filters out roads that do not belong to this player
311
+ roads = []
312
+ for r in all_roads:
313
+ if r.owner == self.num:
314
+ roads.append(r)
315
+
316
+ return roads
317
+
318
+ # checks if the player has some development cards
319
+ def has_dev_cards(self, cards):
320
+ card_duplicate = self.dev_cards[:]
321
+ for c in cards:
322
+ if not card_duplicate.count(c) > 0:
323
+ return False
324
+ else:
325
+ del card_duplicate[card_duplicate.index(c)]
326
+
327
+ return True
328
+
329
+ # returns the number of VP
330
+ # if include_dev is False, it will not include points from developement cards
331
+ # because other players aren't able to see them
332
+ def get_VP(self, include_dev=False):
333
+
334
+ # gets the victory points from settlements and cities
335
+ points = self.victory_points
336
+
337
+ # adds VPs from longest road
338
+ if self.game.longest_road_owner == self.num:
339
+ points += 2
340
+
341
+ # adds VPs from largest army
342
+ if self.game.largest_army == self.num:
343
+ points += 2
344
+
345
+ # adds VPs from developement cards
346
+ if include_dev:
347
+ for d in self.dev_cards:
348
+ if d == DevCard.VP:
349
+ points += 1
350
+
351
+ return points
352
+
353
+ # prints the cards given
354
+ @staticmethod
355
+ def print_cards(cards):
356
+ print("[")
357
+ for c in cards:
358
+
359
+ card_name = ""
360
+
361
+ if c == ResCard.Wood:
362
+ card_name = "Wood"
363
+
364
+ elif c == ResCard.Sheep:
365
+ card_name = "Sheep"
366
+
367
+ elif c == ResCard.Brick:
368
+ card_name = "Brick"
369
+
370
+ elif c == ResCard.Wheat:
371
+ card_name = "Wheat"
372
+
373
+ elif c == ResCard.Ore:
374
+ card_name = "Ore"
375
+
376
+ else:
377
+ print("INVALID CARD %s" % c)
378
+ continue
379
+
380
+ if cards.index(c) < len(cards) - 1:
381
+ card_name += ","
382
+
383
+ print(" %s" % card_name)
384
+
385
+ print("]")
pycatan/point.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ class Point:
2
+ def __init__(self, tiles, position):
3
+ self.tiles = tiles
4
+ self.building = None
5
+ self.position = position
6
+
7
+ def __repr__(self):
8
+ return "| Point at r=%s, i=%s |" % (self.position[0], self.position[1])
pycatan/point_mapping.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Point Mapping System for PyCatan
3
+
4
+ This module provides translation between user-friendly point IDs (1, 2, 3...)
5
+ and internal coordinate system ([row, index]).
6
+
7
+ This creates a unified point reference system for both human input and visualization.
8
+ """
9
+
10
+ from typing import Dict, List, Tuple, Optional
11
+ import json
12
+ import os
13
+
14
+
15
+ class PointMapper:
16
+ """
17
+ Manages mapping between point IDs and coordinates.
18
+
19
+ Point IDs are simple numbers (1, 2, 3...) that users can easily reference.
20
+ Coordinates are [row, index] pairs used internally by the game engine.
21
+ """
22
+
23
+ def __init__(self):
24
+ """Initialize the point mapper."""
25
+ self.point_to_coords: Dict[int, List[int]] = {}
26
+ self.coords_to_point: Dict[str, int] = {}
27
+ self._load_default_mapping()
28
+
29
+ def _load_default_mapping(self):
30
+ """Load the default Catan board point mapping."""
31
+ # Standard Catan board layout - 54 intersection points
32
+ # This follows the hexagonal board structure with 19 tiles
33
+
34
+ default_mapping = [
35
+ # Top row (7 points) - wider at the top
36
+ [0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [0, 6],
37
+
38
+ # Second row (9 points)
39
+ [1, 0], [1, 1], [1, 2], [1, 3], [1, 4], [1, 5], [1, 6], [1, 7], [1, 8],
40
+
41
+ # Third row (11 points) - widest row
42
+ [2, 0], [2, 1], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [2, 7], [2, 8], [2, 9], [2, 10],
43
+
44
+ # Fourth row (11 points) - also widest
45
+ [3, 0], [3, 1], [3, 2], [3, 3], [3, 4], [3, 5], [3, 6], [3, 7], [3, 8], [3, 9], [3, 10],
46
+
47
+ # Fifth row (9 points)
48
+ [4, 0], [4, 1], [4, 2], [4, 3], [4, 4], [4, 5], [4, 6], [4, 7], [4, 8],
49
+
50
+ # Bottom row (7 points) - narrows at the bottom
51
+ [5, 0], [5, 1], [5, 2], [5, 3], [5, 4], [5, 5], [5, 6]
52
+ ]
53
+
54
+ # Create both mappings
55
+ for point_id, coords in enumerate(default_mapping, 1):
56
+ self.point_to_coords[point_id] = coords
57
+ self.coords_to_point[f"{coords[0]},{coords[1]}"] = point_id
58
+
59
+ def point_to_coordinate(self, point_id: int) -> Optional[List[int]]:
60
+ """Convert point ID to coordinates."""
61
+ return self.point_to_coords.get(point_id)
62
+
63
+ def coordinate_to_point(self, row: int, index: int) -> Optional[int]:
64
+ """Convert coordinates to point ID."""
65
+ return self.coords_to_point.get(f"{row},{index}")
66
+
67
+ def get_all_points(self) -> List[int]:
68
+ """Get all valid point IDs."""
69
+ return sorted(self.point_to_coords.keys())
70
+
71
+ def get_adjacent_points(self, point_id: int) -> List[int]:
72
+ """
73
+ Get points adjacent to the given point (for road validation).
74
+
75
+ This is a simplified version - in a real implementation,
76
+ this would check actual board topology.
77
+ """
78
+ coords = self.point_to_coordinate(point_id)
79
+ if not coords:
80
+ return []
81
+
82
+ row, index = coords
83
+ adjacent_coords = [
84
+ [row-1, index-1], [row-1, index], [row-1, index+1],
85
+ [row, index-1], [row, index+1],
86
+ [row+1, index-1], [row+1, index], [row+1, index+1]
87
+ ]
88
+
89
+ adjacent_points = []
90
+ for adj_coords in adjacent_coords:
91
+ adj_point = self.coordinate_to_point(adj_coords[0], adj_coords[1])
92
+ if adj_point:
93
+ adjacent_points.append(adj_point)
94
+
95
+ return adjacent_points
96
+
97
+ def validate_road_placement(self, start_point: int, end_point: int) -> bool:
98
+ """Check if two points can be connected by a road."""
99
+ if start_point == end_point:
100
+ return False
101
+
102
+ # Check if points are adjacent
103
+ adjacent_to_start = self.get_adjacent_points(start_point)
104
+ return end_point in adjacent_to_start
105
+
106
+ def export_mapping(self, filename: str = "point_mapping.json"):
107
+ """Export mapping to JSON file for use by visualizations."""
108
+ export_data = {
109
+ "point_to_coords": self.point_to_coords,
110
+ "coords_to_point": self.coords_to_point,
111
+ "total_points": len(self.point_to_coords)
112
+ }
113
+
114
+ with open(filename, 'w') as f:
115
+ json.dump(export_data, f, indent=2)
116
+
117
+ print(f"Point mapping exported to {filename}")
118
+
119
+ def import_mapping(self, filename: str):
120
+ """Import mapping from JSON file."""
121
+ if not os.path.exists(filename):
122
+ print(f"Mapping file {filename} not found, using default mapping")
123
+ return
124
+
125
+ with open(filename, 'r') as f:
126
+ data = json.load(f)
127
+
128
+ # Convert string keys back to integers for point_to_coords
129
+ self.point_to_coords = {int(k): v for k, v in data["point_to_coords"].items()}
130
+ self.coords_to_point = data["coords_to_point"]
131
+
132
+ print(f"Point mapping imported from {filename}")
133
+
134
+ def print_mapping(self):
135
+ """Print the current mapping for debugging."""
136
+ print("Point ID -> Coordinates mapping:")
137
+ print("=" * 40)
138
+
139
+ for point_id in sorted(self.point_to_coords.keys()):
140
+ coords = self.point_to_coords[point_id]
141
+ print(f"Point {point_id:2d} -> [{coords[0]}, {coords[1]}]")
142
+
143
+ print(f"\\nTotal points: {len(self.point_to_coords)}")
144
+
145
+
146
+ # Global point mapper instance
147
+ point_mapper = PointMapper()
148
+
149
+
150
+ # Convenience functions for easy import
151
+ def point_to_coords(point_id: int) -> Optional[List[int]]:
152
+ """Convert point ID to coordinates."""
153
+ return point_mapper.point_to_coordinate(point_id)
154
+
155
+
156
+ def coords_to_point(row: int, index: int) -> Optional[int]:
157
+ """Convert coordinates to point ID."""
158
+ return point_mapper.coordinate_to_point(row, index)
159
+
160
+
161
+ def validate_road(start_point: int, end_point: int) -> bool:
162
+ """Check if a road can be placed between two points."""
163
+ return point_mapper.validate_road_placement(start_point, end_point)
164
+
165
+
166
+ def get_all_points() -> List[int]:
167
+ """Get all valid point IDs."""
168
+ return point_mapper.get_all_points()
169
+
170
+
171
+ if __name__ == "__main__":
172
+ # Demo and testing
173
+ print("PyCatan Point Mapping System")
174
+ print("=" * 40)
175
+
176
+ # Print the mapping
177
+ point_mapper.print_mapping()
178
+
179
+ # Test some conversions
180
+ print("\\n" + "=" * 40)
181
+ print("Testing conversions:")
182
+
183
+ test_points = [1, 10, 25, 54]
184
+ for point in test_points:
185
+ coords = point_to_coords(point)
186
+ if coords:
187
+ back_to_point = coords_to_point(coords[0], coords[1])
188
+ print(f"Point {point} -> {coords} -> Point {back_to_point}")
189
+
190
+ # Test road validation
191
+ print("\\n" + "=" * 40)
192
+ print("Testing road placements:")
193
+
194
+ test_roads = [(1, 2), (1, 10), (25, 26), (1, 54)]
195
+ for start, end in test_roads:
196
+ valid = validate_road(start, end)
197
+ status = "✓" if valid else "✗"
198
+ print(f"Road {start} -> {end}: {status}")
199
+
200
+ # Export mapping for web visualization
201
+ print("\\n" + "=" * 40)
202
+ point_mapper.export_mapping("pycatan/static/js/point_mapping.json")
pycatan/real_game.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RealGame - Complete Interactive Catan Game Experience
3
+
4
+ This class orchestrates a full Catan game with multiple interfaces:
5
+ - Main console for player input and game commands
6
+ - Console visualization for game state display
7
+ - Web browser interface for interactive board view
8
+
9
+ The game provides a complete, multi-interface gaming experience.
10
+ """
11
+
12
+ import threading
13
+ import time
14
+ import webbrowser
15
+ from typing import List, Optional
16
+ import subprocess
17
+ import sys
18
+ import os
19
+
20
+ from .game_manager import GameManager
21
+ from .human_user import HumanUser
22
+ from .console_visualization import ConsoleVisualization
23
+ from .web_visualization import WebVisualization
24
+ from .visualization import VisualizationManager
25
+
26
+
27
+ class RealGame:
28
+ """
29
+ Complete interactive Catan game with multiple interfaces.
30
+
31
+ Features:
32
+ - Player setup (names, count)
33
+ - Main game loop with human input
34
+ - Real-time console visualization
35
+ - Web browser board display
36
+ - Coordinated multi-interface experience
37
+ """
38
+
39
+ def __init__(self):
40
+ """Initialize the real game manager."""
41
+ self.num_players = 0
42
+ self.player_names = []
43
+ self.users = []
44
+ self.game_manager = None
45
+ self.visualization_manager = None
46
+ self.console_viz = None
47
+ self.web_viz = None
48
+ self.web_thread = None
49
+ self.console_thread = None
50
+ self.is_running = False
51
+
52
+ def setup_game(self) -> bool:
53
+ """
54
+ Interactive setup for the game.
55
+ Collects number of players and their names.
56
+
57
+ Returns:
58
+ bool: True if setup successful
59
+ """
60
+ print("🎮 Welcome to PyCatan - Interactive Settlers of Catan!")
61
+ print("=" * 60)
62
+
63
+ # Get number of players
64
+ while True:
65
+ try:
66
+ self.num_players = int(input("How many players? (2-4): "))
67
+ if 2 <= self.num_players <= 4:
68
+ break
69
+ else:
70
+ print("Please enter a number between 2 and 4.")
71
+ except ValueError:
72
+ print("Please enter a valid number.")
73
+
74
+ print(f"\nGreat! Setting up a game for {self.num_players} players.")
75
+ print("=" * 40)
76
+
77
+ # Get player names
78
+ self.player_names = []
79
+ for i in range(self.num_players):
80
+ while True:
81
+ name = input(f"Enter name for Player {i + 1}: ").strip()
82
+ if name and len(name) <= 20:
83
+ self.player_names.append(name)
84
+ break
85
+ elif not name:
86
+ print("Name cannot be empty. Please try again.")
87
+ else:
88
+ print("Name too long (max 20 characters). Please try again.")
89
+
90
+ print(f"\n✅ Players registered: {', '.join(self.player_names)}")
91
+
92
+ # Create user objects
93
+ self.users = []
94
+ for i, name in enumerate(self.player_names):
95
+ user = HumanUser(name, i)
96
+ self.users.append(user)
97
+
98
+ print("✅ Player objects created successfully!")
99
+ return True
100
+
101
+ def setup_interfaces(self) -> bool:
102
+ """
103
+ Setup all game interfaces (console, web).
104
+
105
+ Returns:
106
+ bool: True if setup successful
107
+ """
108
+ print("\n🖥️ Setting up game interfaces...")
109
+ print("=" * 40)
110
+
111
+ try:
112
+ # Define log file for visualization
113
+ self.viz_log_file = os.path.abspath("game_viz.log")
114
+ # Clear existing log file
115
+ with open(self.viz_log_file, 'w', encoding='utf-8') as f:
116
+ f.write("")
117
+
118
+ # Create console visualization pointing to log file
119
+ self.console_viz = ConsoleVisualization(
120
+ use_colors=True,
121
+ compact_mode=False,
122
+ output_file=self.viz_log_file
123
+ )
124
+ print("✅ Console visualization ready (redirected to separate window)")
125
+
126
+ # Create web visualization
127
+ self.web_viz = WebVisualization(
128
+ port=5000,
129
+ auto_open=False, # We'll open manually
130
+ debug=False
131
+ )
132
+ print("✅ Web visualization ready")
133
+
134
+ # Create visualization manager
135
+ self.visualization_manager = VisualizationManager()
136
+ self.visualization_manager.add_visualization(self.console_viz)
137
+ self.visualization_manager.add_visualization(self.web_viz)
138
+
139
+ print("✅ Visualization manager configured")
140
+
141
+ # Open separate console for visualization (Windows only)
142
+ if os.name == 'nt': # Windows
143
+ try:
144
+ self._open_visualization_console()
145
+ print("✅ Separate visualization console opened")
146
+ except Exception as e:
147
+ print(f"⚠️ Could not open separate console: {e}")
148
+
149
+ return True
150
+
151
+ except Exception as e:
152
+ print(f"❌ Failed to setup interfaces: {e}")
153
+ return False
154
+
155
+ def _open_visualization_console(self):
156
+ """Open a separate console window for visualization (Windows only)."""
157
+ if os.name != 'nt':
158
+ return # Only works on Windows
159
+
160
+ # Create a Python script that will run in the new console
161
+ # This script tails the log file
162
+ script_content = f'''
163
+ # -*- coding: utf-8 -*-
164
+ import sys
165
+ import time
166
+ import os
167
+
168
+ log_file = r"{self.viz_log_file}"
169
+
170
+ print("PyCatan - Game Visualization Console")
171
+ print("=" * 50)
172
+ print("This window shows real-time game state updates.")
173
+ print("Keep this window open while playing!")
174
+ print("=" * 50)
175
+ print(f"Reading from: {{log_file}}")
176
+
177
+ # Wait for file to exist
178
+ while not os.path.exists(log_file):
179
+ time.sleep(0.1)
180
+
181
+ # Tail the file
182
+ with open(log_file, 'r', encoding='utf-8') as f:
183
+ # Go to the end of file
184
+ # f.seek(0, 2)
185
+ # Actually start from beginning since we just created it
186
+
187
+ while True:
188
+ line = f.readline()
189
+ if line:
190
+ print(line, end='')
191
+ else:
192
+ time.sleep(0.1)
193
+ '''
194
+
195
+ # Write the script to a temporary file
196
+ temp_script = "temp_viz_console.py"
197
+ with open(temp_script, 'w', encoding='utf-8') as f:
198
+ f.write(script_content)
199
+
200
+ # Open new console window
201
+ try:
202
+ # Use proper path without extra quotes
203
+ cmd_args = [
204
+ 'cmd', '/k',
205
+ f'python {temp_script}'
206
+ ]
207
+ subprocess.Popen(cmd_args, creationflags=subprocess.CREATE_NEW_CONSOLE)
208
+ except Exception as e:
209
+ print(f"Failed to open console: {e}")
210
+
211
+ def start_game(self) -> bool:
212
+ """
213
+ Start the actual game with all interfaces.
214
+
215
+ Returns:
216
+ bool: True if game started successfully
217
+ """
218
+ print("\n🚀 Starting the game...")
219
+ print("=" * 40)
220
+
221
+ try:
222
+ # Create users if they don't exist
223
+ if not self.users:
224
+ from .human_user import HumanUser
225
+ self.users = []
226
+ for i, name in enumerate(self.player_names):
227
+ user = HumanUser(name, i)
228
+ self.users.append(user)
229
+ print(f"✅ Created {len(self.users)} user objects")
230
+
231
+ # Create GameManager
232
+ self.game_manager = GameManager(
233
+ users=self.users,
234
+ game_config={"enable_visualizations": True},
235
+ random_seed=0
236
+ )
237
+
238
+ # Set up visualizations with GameManager
239
+ self.game_manager.visualization_manager = self.visualization_manager
240
+
241
+ # Start the game
242
+ self.game_manager.start_game()
243
+ print("✅ Game engine started")
244
+
245
+ # Start web server in background thread
246
+ self.web_thread = threading.Thread(
247
+ target=self._run_web_server,
248
+ daemon=True
249
+ )
250
+ self.web_thread.start()
251
+
252
+ # Give web server time to start
253
+ time.sleep(2)
254
+
255
+ # Update web interface with initial game state
256
+ if self.web_viz:
257
+ try:
258
+ self.web_viz.update_full_state(self.game_manager.get_full_state())
259
+ print("✅ Initial web state updated")
260
+ except Exception as e:
261
+ print(f"⚠️ Initial web update failed: {e}")
262
+
263
+ # Open web browser
264
+ print("🌐 Opening web browser for board view...")
265
+ webbrowser.open('http://localhost:5000')
266
+
267
+ print("✅ Web interface launched")
268
+
269
+ self.is_running = True
270
+ return True
271
+
272
+ except Exception as e:
273
+ print(f"❌ Failed to start game: {e}")
274
+ return False
275
+
276
+ def _run_web_server(self):
277
+ """Run the web visualization server in background thread."""
278
+ try:
279
+ # Suppress Flask/Werkzeug logs
280
+ import logging
281
+ logging.getLogger('werkzeug').setLevel(logging.ERROR)
282
+ logging.getLogger('flask').setLevel(logging.ERROR)
283
+
284
+ self.web_viz.__enter__() # Start the server
285
+ # The server runs in its own event loop
286
+ except Exception as e:
287
+ print(f"❌ Web server error: {e}")
288
+
289
+ def _cleanup(self):
290
+ """Clean up resources when game ends."""
291
+ print("\n🧹 Cleaning up...")
292
+
293
+ try:
294
+ # Stop game manager
295
+ if self.game_manager:
296
+ self.game_manager.end_game()
297
+
298
+ # Stop web server
299
+ if self.web_viz:
300
+ self.web_viz.__exit__(None, None, None)
301
+
302
+ # Clean up temp files
303
+ if os.name == 'nt': # Windows
304
+ try:
305
+ if os.path.exists("temp_viz_console.py"):
306
+ os.remove("temp_viz_console.py")
307
+ except:
308
+ pass
309
+
310
+ print("✅ Cleanup completed")
311
+
312
+ except Exception as e:
313
+ print(f"⚠️ Cleanup error: {e}")
314
+
315
+ def run(self):
316
+ """
317
+ Run the complete game experience.
318
+ This is the main entry point for starting a full game.
319
+ """
320
+ try:
321
+ # Step 1: Setup game (players, names)
322
+ if not self.setup_game():
323
+ return False
324
+
325
+ # Step 2: Setup interfaces (console, web)
326
+ if not self.setup_interfaces():
327
+ return False
328
+
329
+ # Step 3: Start game engine
330
+ if not self.start_game():
331
+ return False
332
+
333
+ # Step 4: Play the game (delegate to GameManager)
334
+ print("\n🎯 Game Started! Control passed to GameManager.")
335
+ print("=" * 60)
336
+ print("🔥 Multiple interfaces are now active:")
337
+ print(" 📱 This console - for entering commands")
338
+ print(" 🖥️ Console visualization - for game state display")
339
+ print(" 🌐 Web browser - for interactive board view")
340
+ print("=" * 60)
341
+
342
+ try:
343
+ self.game_manager.game_loop()
344
+ except KeyboardInterrupt:
345
+ print("\n\n🛑 Game interrupted by user.")
346
+ finally:
347
+ self._cleanup()
348
+
349
+ return True
350
+
351
+ except Exception as e:
352
+ print(f"❌ Fatal error: {e}")
353
+ return False
354
+
355
+
356
+ def main():
357
+ """Main entry point for running a complete Catan game."""
358
+ print("🏴‍☠️ Starting PyCatan Real Game Experience...")
359
+
360
+ real_game = RealGame()
361
+ success = real_game.run()
362
+
363
+ if success:
364
+ print("\n🎉 Thanks for playing PyCatan!")
365
+ else:
366
+ print("\n😔 Game ended with errors.")
367
+
368
+ print("👋 Goodbye!")
369
+
370
+
371
+ if __name__ == "__main__":
372
+ main()
pycatan/starting_board.json ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "hexes": [
3
+ [
4
+ "fo",
5
+ "p",
6
+ "fi"
7
+ ],
8
+ [
9
+ "h",
10
+ "m",
11
+ "h",
12
+ "p"
13
+ ],
14
+ [
15
+ "d",
16
+ "fo",
17
+ "fi",
18
+ "fo",
19
+ "fi"
20
+ ],
21
+ [
22
+ "h",
23
+ "p",
24
+ "p",
25
+ "m"
26
+ ],
27
+ [
28
+ "m",
29
+ "fi",
30
+ "fo"
31
+ ]
32
+ ],
33
+ "hex_nums": [
34
+ [
35
+ 11,
36
+ 12,
37
+ 9
38
+ ],
39
+ [
40
+ 4,
41
+ 6,
42
+ 5,
43
+ 10
44
+ ],
45
+ [
46
+ null,
47
+ 3,
48
+ 11,
49
+ 4,
50
+ 8
51
+ ],
52
+ [
53
+ 8,
54
+ 10,
55
+ 9,
56
+ 3
57
+ ],
58
+ [
59
+ 5,
60
+ 2,
61
+ 6
62
+ ]
63
+ ]
64
+ }
pycatan/static/css/style.css ADDED
@@ -0,0 +1,774 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Reset and basic styling */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ :root {
9
+ --primary-color: #2c3e50;
10
+ --secondary-color: #3498db;
11
+ --success-color: #27ae60;
12
+ --danger-color: #e74c3c;
13
+ --warning-color: #f39c12;
14
+ --accent-color: #9b59b6;
15
+ --light-bg: #ecf0f1;
16
+ --border-radius: 12px;
17
+ --shadow: 0 8px 24px rgba(0,0,0,0.15);
18
+ --transition: 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
19
+ }
20
+
21
+ body {
22
+ font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
23
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
24
+ min-height: 100vh;
25
+ padding: 0;
26
+ direction: ltr;
27
+ overflow: hidden;
28
+ color: var(--primary-color);
29
+ }
30
+
31
+ h1 {
32
+ color: white;
33
+ text-align: center;
34
+ margin: 0;
35
+ font-size: 2.2em;
36
+ font-weight: 700;
37
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
38
+ letter-spacing: 1px;
39
+ }
40
+
41
+ .subtitle {
42
+ color: rgba(255,255,255,0.8);
43
+ font-size: 0.9em;
44
+ margin: 5px 0 0 0;
45
+ font-weight: 300;
46
+ }
47
+
48
+ /* Header */
49
+ .game-header {
50
+ width: 100%;
51
+ padding: 20px;
52
+ background: linear-gradient(135deg, rgba(44, 62, 80, 0.95), rgba(52, 152, 219, 0.9));
53
+ box-shadow: var(--shadow);
54
+ border-bottom: 3px solid rgba(255,255,255,0.1);
55
+ }
56
+
57
+ /* מכולת הלוח */
58
+ .game-container {
59
+ display: flex;
60
+ flex-direction: column;
61
+ width: 100%;
62
+ height: 100vh;
63
+ }
64
+
65
+ .content-wrapper {
66
+ display: flex;
67
+ flex: 1;
68
+ gap: 12px;
69
+ padding: 12px;
70
+ overflow: hidden;
71
+ }
72
+
73
+ .board-wrapper {
74
+ flex: 1;
75
+ display: flex;
76
+ justify-content: center;
77
+ align-items: center;
78
+ min-width: 0;
79
+ }
80
+
81
+ .board-container {
82
+ background: linear-gradient(135deg, #3498db, #5dade2);
83
+ border: 3px solid rgba(255,255,255,0.2);
84
+ border-radius: var(--border-radius);
85
+ padding: 12px;
86
+ box-shadow: var(--shadow);
87
+ position: relative;
88
+ overflow: hidden;
89
+ cursor: grab;
90
+ width: 100%;
91
+ height: 100%;
92
+ max-width: 1400px;
93
+ }
94
+
95
+ .board-container:active {
96
+ cursor: grabbing;
97
+ }
98
+
99
+ /* כפתורי בקרה */
100
+ .board-controls {
101
+ position: absolute;
102
+ top: 12px;
103
+ right: 12px;
104
+ z-index: 100;
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: 8px;
108
+ background: rgba(255,255,255,0.1);
109
+ padding: 8px;
110
+ border-radius: 12px;
111
+ backdrop-filter: blur(10px);
112
+ }
113
+
114
+ .control-btn {
115
+ width: 44px;
116
+ height: 44px;
117
+ border-radius: 10px;
118
+ border: 2px solid rgba(255,255,255,0.3);
119
+ background: linear-gradient(135deg, rgba(255,255,255,0.2), rgba(255,255,255,0.05));
120
+ color: white;
121
+ font-size: 18px;
122
+ cursor: pointer;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ transition: all var(--transition);
127
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
128
+ font-weight: 600;
129
+ }
130
+
131
+ .control-btn:hover {
132
+ background: linear-gradient(135deg, rgba(255,255,255,0.3), rgba(255,255,255,0.15));
133
+ transform: translateY(-2px);
134
+ box-shadow: 0 6px 16px rgba(0,0,0,0.2);
135
+ }
136
+
137
+ .control-btn:active {
138
+ transform: translateY(0);
139
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
140
+ }
141
+
142
+ /* SVG הלוח */
143
+ #catan-board {
144
+ width: 100%;
145
+ height: 100%;
146
+ transition: transform 0.1s ease;
147
+ }
148
+
149
+ /* Sidebars */
150
+ .sidebar {
151
+ background: rgba(255, 255, 255, 0.95);
152
+ border-radius: var(--border-radius);
153
+ padding: 16px;
154
+ box-shadow: var(--shadow);
155
+ backdrop-filter: blur(10px);
156
+ overflow-y: auto;
157
+ direction: ltr;
158
+ }
159
+
160
+ .sidebar-left {
161
+ width: 300px;
162
+ flex-shrink: 0;
163
+ }
164
+
165
+ .sidebar-right {
166
+ width: 320px;
167
+ flex-shrink: 0;
168
+ }
169
+
170
+ /* משושים */
171
+ .hexagon {
172
+ stroke: #dbc08e;
173
+ stroke-width: 5;
174
+ stroke-linejoin: round;
175
+ vector-effect: non-scaling-stroke;
176
+ cursor: pointer;
177
+ transition: all 0.3s ease;
178
+ }
179
+
180
+ .hexagon:hover {
181
+ opacity: 0.1;
182
+ }
183
+
184
+ /* מספרים על משושים */
185
+ .hex-number {
186
+ font-size: 18px;
187
+ font-weight: bold;
188
+ text-anchor: middle;
189
+ dominant-baseline: middle;
190
+ fill: #2c3e50;
191
+ pointer-events: none;
192
+ text-shadow: 2px 2px 4px rgba(255,255,255,0.8);
193
+ }
194
+
195
+ .hex-number.red {
196
+ fill: #e74c3c;
197
+ font-weight: 900;
198
+ }
199
+
200
+ /* שודד */
201
+ .robber {
202
+ fill: #2c3e50;
203
+ stroke: #34495e;
204
+ stroke-width: 3;
205
+ cursor: pointer;
206
+ filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.5));
207
+ transition: opacity 0.3s ease;
208
+ }
209
+
210
+ .robber:hover {
211
+ opacity: 0.1;
212
+ }
213
+
214
+ .robber-text {
215
+ font-size: 16px;
216
+ font-weight: bold;
217
+ text-anchor: middle;
218
+ dominant-baseline: middle;
219
+ fill: white;
220
+ pointer-events: none;
221
+ }
222
+
223
+ /* יישובים */
224
+ .settlement {
225
+ stroke: #2c3e50;
226
+ stroke-width: 2;
227
+ cursor: pointer;
228
+ filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
229
+ transition: opacity 0.3s ease;
230
+ }
231
+
232
+ .settlement:hover {
233
+ opacity: 0.1;
234
+ }
235
+
236
+ .settlement.player1 { fill: #FF4444; }
237
+ .settlement.player2 { fill: #4444FF; }
238
+ .settlement.player3 { fill: #44FF44; }
239
+ .settlement.player4 { fill: #FFAA00; }
240
+
241
+ /* ערים */
242
+ .city {
243
+ stroke: #2c3e50;
244
+ stroke-width: 3;
245
+ cursor: pointer;
246
+ filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.3));
247
+ transition: opacity 0.3s ease;
248
+ }
249
+
250
+ .city:hover {
251
+ opacity: 0.1;
252
+ }
253
+
254
+ .city.player1 { fill: #FF4444; }
255
+ .city.player2 { fill: #4444FF; }
256
+ .city.player3 { fill: #44FF44; }
257
+ .city.player4 { fill: #FFAA00; }
258
+
259
+ /* דרכים */
260
+ .road {
261
+ stroke-width: 6;
262
+ stroke-linecap: round;
263
+ cursor: pointer;
264
+ transition: all 0.3s ease;
265
+ filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
266
+ }
267
+
268
+ .road:hover {
269
+ opacity: 0.1;
270
+ }
271
+
272
+ .road.player1 { stroke: #FF4444; }
273
+ .road.player2 { stroke: #4444FF; }
274
+ .road.player3 { stroke: #44FF44; }
275
+ .road.player4 { stroke: #FFAA00; }
276
+
277
+ /* קודקודים */
278
+ .vertex {
279
+ fill: #e74c3c;
280
+ stroke: #ffffff;
281
+ stroke-width: 3;
282
+ opacity: 0;
283
+ transition: opacity 0.3s ease;
284
+ cursor: pointer;
285
+ }
286
+
287
+ .vertex:hover {
288
+ opacity: 0.1;
289
+ }
290
+
291
+ .vertices-visible .vertex {
292
+ opacity: 0.9;
293
+ }
294
+
295
+ .vertex-number {
296
+ font-size: 14px;
297
+ font-weight: 900;
298
+ text-anchor: middle;
299
+ dominant-baseline: middle;
300
+ fill: white;
301
+ pointer-events: none;
302
+ opacity: 0;
303
+ transition: opacity 0.3s ease;
304
+ text-shadow:
305
+ -1px -1px 0 #000,
306
+ 1px -1px 0 #000,
307
+ -1px 1px 0 #000,
308
+ 1px 1px 0 #000;
309
+ }
310
+
311
+ .vertices-visible .vertex-number {
312
+ opacity: 1;
313
+ }
314
+
315
+ /* רספונסיביות */
316
+ @media (max-width: 1400px) {
317
+ .sidebar-left,
318
+ .sidebar-right {
319
+ width: 280px;
320
+ }
321
+ }
322
+
323
+ @media (max-width: 1200px) {
324
+ .content-wrapper {
325
+ gap: 8px;
326
+ }
327
+
328
+ .sidebar-left,
329
+ .sidebar-right {
330
+ width: 260px;
331
+ }
332
+ }
333
+
334
+ @media (max-width: 1024px) {
335
+ .content-wrapper {
336
+ flex-wrap: wrap;
337
+ }
338
+
339
+ .sidebar-left,
340
+ .sidebar-right {
341
+ width: 100%;
342
+ max-height: 30vh;
343
+ }
344
+
345
+ .board-wrapper {
346
+ width: 100%;
347
+ min-height: 50vh;
348
+ }
349
+ }
350
+
351
+ @media (max-width: 768px) {
352
+ .game-header {
353
+ padding: 15px;
354
+ }
355
+
356
+ h1 {
357
+ font-size: 1.8em;
358
+ }
359
+
360
+ .subtitle {
361
+ font-size: 0.8em;
362
+ }
363
+
364
+ .content-wrapper {
365
+ padding: 8px;
366
+ }
367
+
368
+ .sidebar-left,
369
+ .sidebar-right {
370
+ width: 100%;
371
+ max-height: 25vh;
372
+ }
373
+
374
+ .board-wrapper {
375
+ width: 100%;
376
+ min-height: 60vh;
377
+ }
378
+
379
+ .board-controls {
380
+ top: 8px;
381
+ right: 8px;
382
+ flex-direction: row;
383
+ gap: 6px;
384
+ }
385
+
386
+ .control-btn {
387
+ width: 40px;
388
+ height: 40px;
389
+ font-size: 16px;
390
+ }
391
+ }
392
+
393
+ @media (max-width: 480px) {
394
+ body {
395
+ padding: 0;
396
+ }
397
+
398
+ .game-header {
399
+ padding: 12px;
400
+ }
401
+
402
+ h1 {
403
+ font-size: 1.4em;
404
+ }
405
+
406
+ .subtitle {
407
+ font-size: 0.7em;
408
+ }
409
+
410
+ .sidebar-left,
411
+ .sidebar-right {
412
+ max-height: 20vh;
413
+ }
414
+
415
+ .game-info,
416
+ .action-log {
417
+ font-size: 0.8em;
418
+ }
419
+ }
420
+
421
+ /* Game Info */
422
+ .game-info {
423
+ display: flex;
424
+ flex-direction: column;
425
+ gap: 10px;
426
+ }
427
+
428
+ .game-info h3 {
429
+ margin: 0;
430
+ color: var(--primary-color);
431
+ font-size: 1.1em;
432
+ border-bottom: 2px solid var(--secondary-color);
433
+ padding-bottom: 8px;
434
+ }
435
+
436
+ .game-info .loading {
437
+ text-align: center;
438
+ color: var(--secondary-color);
439
+ padding: 20px;
440
+ font-style: italic;
441
+ }
442
+
443
+ .player-info {
444
+ margin-bottom: 8px;
445
+ padding: 10px 12px;
446
+ border-radius: 8px;
447
+ border-left: 4px solid transparent;
448
+ background: var(--light-bg);
449
+ cursor: pointer;
450
+ transition: all var(--transition);
451
+ }
452
+
453
+ .player-info:hover {
454
+ background: rgba(52, 152, 219, 0.15);
455
+ transform: translateX(-4px);
456
+ }
457
+
458
+ .player-info.active {
459
+ border-left-color: var(--secondary-color);
460
+ background: rgba(52, 152, 219, 0.2);
461
+ font-weight: 600;
462
+ }
463
+
464
+ .player-info h4 {
465
+ margin: 0 0 6px 0;
466
+ font-size: 0.95em;
467
+ color: var(--primary-color);
468
+ }
469
+
470
+ .player-resources {
471
+ font-size: 0.85em;
472
+ color: #555;
473
+ line-height: 1.4;
474
+ margin: 4px 0;
475
+ }
476
+
477
+ .log-header {
478
+ display: flex;
479
+ justify-content: space-between;
480
+ align-items: center;
481
+ margin-bottom: 12px;
482
+ }
483
+
484
+ .log-header h3 {
485
+ margin: 0;
486
+ color: var(--primary-color);
487
+ font-size: 1.1em;
488
+ border-bottom: 2px solid var(--secondary-color);
489
+ padding-bottom: 8px;
490
+ flex: 1;
491
+ }
492
+
493
+ .clear-log-btn {
494
+ background: var(--light-bg);
495
+ border: none;
496
+ padding: 6px 10px;
497
+ border-radius: 6px;
498
+ cursor: pointer;
499
+ font-size: 0.9em;
500
+ transition: all var(--transition);
501
+ }
502
+
503
+ .clear-log-btn:hover {
504
+ background: var(--danger-color);
505
+ color: white;
506
+ }
507
+
508
+ .action-log {
509
+ display: flex;
510
+ flex-direction: column;
511
+ gap: 6px;
512
+ font-family: 'Consolas', 'Monaco', monospace;
513
+ font-size: 0.85em;
514
+ direction: ltr;
515
+ max-height: calc(100vh - 180px);
516
+ overflow-y: auto;
517
+ }
518
+
519
+ .action-log::-webkit-scrollbar {
520
+ width: 6px;
521
+ }
522
+
523
+ .action-log::-webkit-scrollbar-track {
524
+ background: rgba(0,0,0,0.05);
525
+ border-radius: 3px;
526
+ }
527
+
528
+ .action-log::-webkit-scrollbar-thumb {
529
+ background: rgba(0,0,0,0.2);
530
+ border-radius: 3px;
531
+ }
532
+
533
+ .action-log::-webkit-scrollbar-thumb:hover {
534
+ background: rgba(0,0,0,0.3);
535
+ }
536
+
537
+ .action-log div {
538
+ margin: 0;
539
+ padding: 8px 10px;
540
+ border-radius: 6px;
541
+ border-left: 4px solid var(--secondary-color);
542
+ background: rgba(52, 152, 219, 0.1);
543
+ line-height: 1.3;
544
+ word-break: break-word;
545
+ }
546
+
547
+ .action-log .success {
548
+ color: var(--success-color);
549
+ border-left-color: var(--success-color);
550
+ background: rgba(39, 174, 96, 0.1);
551
+ }
552
+
553
+ .action-log .error {
554
+ color: var(--danger-color);
555
+ border-left-color: var(--danger-color);
556
+ background: rgba(231, 76, 60, 0.1);
557
+ font-weight: 500;
558
+ }
559
+
560
+ .action-log .info {
561
+ color: var(--secondary-color);
562
+ border-left-color: var(--secondary-color);
563
+ background: rgba(52, 152, 219, 0.1);
564
+ }
565
+
566
+ .action-log .log-dice {
567
+ color: var(--warning-color);
568
+ border-left-color: var(--warning-color);
569
+ background: rgba(243, 156, 18, 0.1);
570
+ font-weight: 600;
571
+ }
572
+
573
+ .action-log .log-turn {
574
+ color: white;
575
+ background: linear-gradient(90deg, var(--secondary-color), var(--accent-color));
576
+ border: none;
577
+ text-align: center;
578
+ font-weight: 700;
579
+ padding: 10px;
580
+ font-size: 0.9em;
581
+ border-radius: 8px;
582
+ box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3);
583
+ }
584
+
585
+ .action-log .log-resource {
586
+ color: var(--accent-color);
587
+ border-left-color: var(--accent-color);
588
+ background: rgba(155, 89, 182, 0.1);
589
+ font-style: italic;
590
+ }
591
+
592
+ .action-log .log-build {
593
+ color: var(--success-color);
594
+ border-left-color: var(--success-color);
595
+ background: rgba(39, 174, 96, 0.1);
596
+ font-weight: 600;
597
+ }
598
+
599
+ /* Player Cards Display */
600
+ .player-cards {
601
+ display: none;
602
+ margin-top: 8px;
603
+ padding: 8px;
604
+ background: white;
605
+ border-radius: 6px;
606
+ border: 1px solid rgba(52, 152, 219, 0.2);
607
+ animation: slideDown var(--transition);
608
+ }
609
+
610
+ .player-info.expanded .player-cards {
611
+ display: block;
612
+ }
613
+
614
+ @keyframes slideDown {
615
+ from {
616
+ opacity: 0;
617
+ transform: translateY(-8px);
618
+ }
619
+ to {
620
+ opacity: 1;
621
+ transform: translateY(0);
622
+ }
623
+ }
624
+
625
+ .player-cards div {
626
+ margin-bottom: 6px;
627
+ }
628
+
629
+ .player-cards strong {
630
+ color: var(--primary-color);
631
+ font-size: 0.85em;
632
+ }
633
+
634
+ .card-list {
635
+ list-style: none;
636
+ padding: 0;
637
+ margin: 4px 0 0 0;
638
+ font-size: 0.8em;
639
+ display: flex;
640
+ flex-wrap: wrap;
641
+ gap: 4px;
642
+ }
643
+
644
+ .card-list li {
645
+ display: inline-block;
646
+ padding: 4px 8px;
647
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-color));
648
+ color: white;
649
+ border-radius: 4px;
650
+ border: 1px solid rgba(52, 152, 219, 0.3);
651
+ font-weight: 500;
652
+ white-space: nowrap;
653
+ transition: all var(--transition);
654
+ }
655
+
656
+ .card-list li:hover {
657
+ transform: translateY(-2px);
658
+ box-shadow: 0 3px 8px rgba(52, 152, 219, 0.3);
659
+ }
660
+
661
+ /* Modal for Building Costs */
662
+ .modal {
663
+ position: fixed;
664
+ top: 0;
665
+ left: 0;
666
+ width: 100%;
667
+ height: 100%;
668
+ background: rgba(0, 0, 0, 0.5);
669
+ display: flex;
670
+ align-items: center;
671
+ justify-content: center;
672
+ z-index: 1000;
673
+ backdrop-filter: blur(4px);
674
+ animation: fadeIn var(--transition);
675
+ }
676
+
677
+ .modal.hidden {
678
+ display: none;
679
+ }
680
+
681
+ @keyframes fadeIn {
682
+ from {
683
+ opacity: 0;
684
+ }
685
+ to {
686
+ opacity: 1;
687
+ }
688
+ }
689
+
690
+ .modal-content {
691
+ background: white;
692
+ border-radius: var(--border-radius);
693
+ box-shadow: var(--shadow);
694
+ max-width: 500px;
695
+ width: 90%;
696
+ max-height: 80vh;
697
+ overflow-y: auto;
698
+ animation: slideUp var(--transition);
699
+ }
700
+
701
+ @keyframes slideUp {
702
+ from {
703
+ transform: translateY(30px);
704
+ opacity: 0;
705
+ }
706
+ to {
707
+ transform: translateY(0);
708
+ opacity: 1;
709
+ }
710
+ }
711
+
712
+ .modal-header {
713
+ display: flex;
714
+ justify-content: space-between;
715
+ align-items: center;
716
+ padding: 20px;
717
+ border-bottom: 2px solid var(--secondary-color);
718
+ }
719
+
720
+ .modal-header h3 {
721
+ margin: 0;
722
+ color: var(--primary-color);
723
+ font-size: 1.3em;
724
+ }
725
+
726
+ .modal-close {
727
+ background: none;
728
+ border: none;
729
+ font-size: 24px;
730
+ cursor: pointer;
731
+ color: var(--danger-color);
732
+ transition: all var(--transition);
733
+ }
734
+
735
+ .modal-close:hover {
736
+ transform: scale(1.2);
737
+ }
738
+
739
+ .modal-body {
740
+ padding: 20px;
741
+ }
742
+
743
+ .costs-table {
744
+ width: 100%;
745
+ border-collapse: collapse;
746
+ margin-bottom: 16px;
747
+ }
748
+
749
+ .costs-table thead {
750
+ background: linear-gradient(135deg, var(--secondary-color), var(--accent-color));
751
+ color: white;
752
+ }
753
+
754
+ .costs-table th {
755
+ padding: 12px;
756
+ text-align: left;
757
+ font-weight: 600;
758
+ }
759
+
760
+ .costs-table td {
761
+ padding: 12px;
762
+ border-bottom: 1px solid var(--light-bg);
763
+ }
764
+
765
+ .costs-table tbody tr:hover {
766
+ background: rgba(52, 152, 219, 0.1);
767
+ }
768
+
769
+ .costs-note {
770
+ font-size: 0.85em;
771
+ color: #666;
772
+ font-style: italic;
773
+ margin: 0;
774
+ }
pycatan/static/images/Desert.png ADDED
pycatan/static/images/Fields.png ADDED
pycatan/static/images/Forest.png ADDED
pycatan/static/images/Hills.png ADDED
pycatan/static/images/Mountains.png ADDED
pycatan/static/images/Pasture.png ADDED
pycatan/static/js/board.js ADDED
@@ -0,0 +1,763 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // CatanBoard class for managing the game board visualization
2
+ class CatanBoard {
3
+ constructor() {
4
+ this.svg = document.getElementById('catan-board');
5
+ this.hexRadius = 45;
6
+ this.centerX = 400;
7
+ this.centerY = 300;
8
+
9
+ this.zoomLevel = 1;
10
+ this.panX = 0;
11
+ this.panY = 0;
12
+ this.isDragging = false;
13
+ this.lastMouseX = 0;
14
+ this.lastMouseY = 0;
15
+ this.showVertices = true;
16
+
17
+ // Board mapping from server
18
+ this.boardMapping = null;
19
+ this.vertices = [];
20
+
21
+ this.init();
22
+ }
23
+
24
+ async init() {
25
+ this.setupEventListeners();
26
+ // Load board mapping from server
27
+ await this.loadBoardMapping();
28
+
29
+ if (this.boardMapping && this.boardMapping.points) {
30
+ console.log("Using server-provided board mapping for vertices");
31
+ this.generateVerticesFromServer();
32
+ } else {
33
+ // Generate vertices derived directly from hex geometry
34
+ // This ensures perfect visual alignment
35
+ this.generateVerticesFromHexes();
36
+ }
37
+
38
+ this.createBoard();
39
+ }
40
+
41
+ async loadBoardMapping() {
42
+ try {
43
+ const response = await fetch('/api/board_mapping');
44
+ this.boardMapping = await response.json();
45
+ console.log('Board mapping loaded from server:', this.boardMapping);
46
+ } catch (error) {
47
+ console.error('Failed to load board mapping:', error);
48
+ // Fallback to default if server fails
49
+ this.boardMapping = null;
50
+ }
51
+ }
52
+
53
+ setupEventListeners() {
54
+ // Zoom and pan events
55
+ this.svg.addEventListener('wheel', (e) => this.handleZoom(e));
56
+ this.svg.addEventListener('mousedown', (e) => this.startDrag(e));
57
+ this.svg.addEventListener('mousemove', (e) => this.handleDrag(e));
58
+ this.svg.addEventListener('mouseup', () => this.endDrag());
59
+ this.svg.addEventListener('mouseleave', () => this.endDrag());
60
+ }
61
+
62
+ // Convert hex coordinates to pixels
63
+ hexToPixel(q, r) {
64
+ const x = this.hexRadius * (3/2 * q);
65
+ const y = this.hexRadius * (Math.sqrt(3)/2 * q + Math.sqrt(3) * r);
66
+ return {
67
+ x: this.centerX + x,
68
+ y: this.centerY + y
69
+ };
70
+ }
71
+
72
+ // Get hexagon vertices
73
+ getHexagonVertices(q, r) {
74
+ const center = this.hexToPixel(q, r);
75
+ const vertices = [];
76
+
77
+ for (let i = 0; i < 6; i++) {
78
+ const angle = (Math.PI / 3) * i;
79
+ const x = center.x + this.hexRadius * Math.cos(angle);
80
+ const y = center.y + this.hexRadius * Math.sin(angle);
81
+ vertices.push({x: x, y: y});
82
+ }
83
+
84
+ return vertices;
85
+ }
86
+
87
+ generateVerticesFromHexes() {
88
+ console.log('Generating vertices derived from hex geometry...');
89
+ this.vertices = [];
90
+ const uniqueVerticesMap = new Map(); // To prevent duplicates
91
+
92
+ // Get hex data (from game state, board mapping, or default)
93
+ let hexes;
94
+ if (this.currentGameState && this.currentGameState.hexes && this.currentGameState.hexes.length > 0) {
95
+ hexes = this.currentGameState.hexes;
96
+ } else if (this.boardMapping && this.boardMapping.hexes) {
97
+ hexes = this.boardMapping.hexes;
98
+ } else {
99
+ hexes = GAMEDATA.hexes;
100
+ }
101
+
102
+ hexes.forEach(hex => {
103
+ // Get the 6 corners of the current hex
104
+ const corners = this.getHexagonVertices(hex.q, hex.r);
105
+
106
+ corners.forEach(corner => {
107
+ // Create unique key based on position (rounded to handle floating point)
108
+ const keyX = Math.round(corner.x);
109
+ const keyY = Math.round(corner.y);
110
+ const key = `${keyX},${keyY}`;
111
+
112
+ if (!uniqueVerticesMap.has(key)) {
113
+ uniqueVerticesMap.set(key, {
114
+ x: corner.x,
115
+ y: corner.y,
116
+ adjacent_hexes: [hex.id]
117
+ });
118
+ } else {
119
+ // If point exists, add hex to its adjacent list
120
+ const entry = uniqueVerticesMap.get(key);
121
+ if (!entry.adjacent_hexes.includes(hex.id)) {
122
+ entry.adjacent_hexes.push(hex.id);
123
+ }
124
+ }
125
+ });
126
+ });
127
+
128
+ // Convert map to array
129
+ let tempVertices = Array.from(uniqueVerticesMap.values());
130
+
131
+ // Sort: First by Y (rows), then by X (columns)
132
+ // This attempts to match the server's ID generation order (row by row, left to right)
133
+ tempVertices.sort((a, b) => {
134
+ // Use a tolerance for Y comparison to group vertices into rows
135
+ if (Math.abs(a.y - b.y) > 10) return a.y - b.y;
136
+ return a.x - b.x;
137
+ });
138
+
139
+ // Create final structure with IDs
140
+ this.vertices = tempVertices.map((v, index) => ({
141
+ id: index + 1, // Renumber 1-54
142
+ x: v.x,
143
+ y: v.y,
144
+ game_coords: [], // Not critical for display
145
+ adjacent_points: [], // Will be calculated if needed
146
+ adjacent_hexes: v.adjacent_hexes,
147
+ buildings: []
148
+ }));
149
+
150
+ console.log(`Generated ${this.vertices.length} vertices aligned to hex corners`);
151
+ }
152
+
153
+ generateVerticesFromServer() {
154
+ // Generate vertices using the server-provided board mapping
155
+ if (!this.boardMapping || !this.boardMapping.points) {
156
+ console.error('No board mapping available from server, using fallback');
157
+ // Create a fallback basic vertex layout
158
+ this.generateFallbackVertices();
159
+ return;
160
+ }
161
+
162
+ this.vertices = [];
163
+
164
+ // Use the server-provided point data
165
+ for (const pointData of this.boardMapping.points) {
166
+ const vertex = {
167
+ id: pointData.id, // Point ID (1-54)
168
+ x: pointData.x, // Pixel coordinates from server
169
+ y: pointData.y,
170
+ game_coords: pointData.game_coords, // [row, col] for debugging
171
+ adjacent_points: pointData.adjacent_points || [],
172
+ adjacent_hexes: pointData.adjacent_hexes || [],
173
+ buildings: [] // Will be populated when buildings are added
174
+ };
175
+
176
+ this.vertices.push(vertex);
177
+ }
178
+
179
+ console.log(`Generated ${this.vertices.length} vertices from server data`);
180
+ }
181
+
182
+ generateFallbackVertices() {
183
+ // Generate basic vertices when server mapping is not available
184
+ console.log('Using fallback vertex generation');
185
+ this.vertices = [];
186
+
187
+ // Create a basic grid of vertices for testing
188
+ let vertexId = 1;
189
+ const rows = [7, 9, 11, 11, 9, 7]; // Standard Catan point distribution
190
+
191
+ for (let row = 0; row < rows.length; row++) {
192
+ const rowWidth = rows[row];
193
+ for (let col = 0; col < rowWidth; col++) {
194
+ // Simple grid positioning
195
+ const offsetX = -(rowWidth - 1) * this.hexRadius * 0.5 * 0.75;
196
+ const x = this.centerX + offsetX + col * this.hexRadius * 0.75;
197
+ const y = this.centerY + (row - 2.5) * this.hexRadius * 0.866;
198
+
199
+ this.vertices.push({
200
+ id: vertexId,
201
+ x: x,
202
+ y: y,
203
+ game_coords: [row, col],
204
+ adjacent_points: [],
205
+ adjacent_hexes: [],
206
+ buildings: []
207
+ });
208
+
209
+ vertexId++;
210
+ }
211
+ }
212
+
213
+ console.log(`Generated ${this.vertices.length} fallback vertices`);
214
+ }
215
+
216
+ // Get vertex by point ID
217
+ getVertexByPointId(pointId) {
218
+ return this.vertices.find(v => v.id === pointId);
219
+ }
220
+
221
+ // Get vertex by coordinates (for backward compatibility)
222
+ getVertexByCoords(x, y, tolerance = 20) {
223
+ return this.vertices.find(v => {
224
+ const dx = v.x - x;
225
+ const dy = v.y - y;
226
+ return Math.sqrt(dx * dx + dy * dy) < tolerance;
227
+ });
228
+ }
229
+
230
+ createBoard() {
231
+ // Create the game board with hexes and vertices
232
+ console.log('Creating game board...');
233
+
234
+ // Clear any existing content
235
+ this.svg.innerHTML = '';
236
+
237
+ // Create hexes first (either from server data or fallback)
238
+ this.createHexes();
239
+
240
+ // Create vertices
241
+ this.createVertices();
242
+
243
+ // Set initial transform
244
+ this.updateTransform();
245
+
246
+ console.log('Game board created successfully');
247
+ }
248
+
249
+ createHexes() {
250
+ // Create hexes on the board
251
+ // Use server data if available, otherwise fallback to GAMEDATA
252
+ let hexData;
253
+ if (this.currentGameState && this.currentGameState.hexes && this.currentGameState.hexes.length > 0) {
254
+ hexData = this.currentGameState.hexes;
255
+ console.log('Using hexes from game state');
256
+ } else if (this.boardMapping && this.boardMapping.hexes) {
257
+ hexData = this.boardMapping.hexes;
258
+ console.log('Using hexes from board mapping');
259
+ } else {
260
+ hexData = GAMEDATA.hexes;
261
+ console.log('Using fallback hex data from GAMEDATA');
262
+ }
263
+
264
+ hexData.forEach(hex => {
265
+ this.createHex(hex);
266
+ });
267
+ }
268
+
269
+ createBoard() {
270
+ // Clear existing content
271
+ this.svg.innerHTML = '';
272
+
273
+ // Determine which hex data to use
274
+ let hexData;
275
+ if (this.currentGameState && this.currentGameState.hexes && this.currentGameState.hexes.length > 0) {
276
+ hexData = this.currentGameState.hexes;
277
+ console.log(`Using server hexes: ${hexData.length} hexes`);
278
+ } else {
279
+ hexData = GAMEDATA.hexes;
280
+ console.log(`Using default GAMEDATA hexes: ${hexData.length} hexes`);
281
+ }
282
+
283
+ // Create hexes
284
+ hexData.forEach(hex => {
285
+ this.createHex(hex);
286
+ });
287
+
288
+ // Create vertices
289
+ this.createVertices();
290
+
291
+ // Create buildings only if we don't have current game state
292
+ // (when called directly, not from updateFromGameState)
293
+ if (!this.currentGameState) {
294
+ // Create settlements from GAMEDATA (fallback)
295
+ GAMEDATA.settlements.forEach(settlement => {
296
+ this.createSettlement(settlement);
297
+ });
298
+
299
+ // Create cities from GAMEDATA (fallback)
300
+ GAMEDATA.cities.forEach(city => {
301
+ this.createCity(city);
302
+ });
303
+
304
+ // Create roads from GAMEDATA (fallback)
305
+ GAMEDATA.roads.forEach(road => {
306
+ this.createRoad(road);
307
+ });
308
+ }
309
+
310
+ this.updateTransform();
311
+ }
312
+
313
+ createHex(hex) {
314
+ const vertices = this.getHexagonVertices(hex.q, hex.r);
315
+ const center = this.hexToPixel(hex.q, hex.r);
316
+
317
+ // Calculate bounding rectangle for the hex
318
+ const minX = Math.min(...vertices.map(v => v.x));
319
+ const maxX = Math.max(...vertices.map(v => v.x));
320
+ const minY = Math.min(...vertices.map(v => v.y));
321
+ const maxY = Math.max(...vertices.map(v => v.y));
322
+
323
+ // Create group for hex
324
+ const hexGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
325
+
326
+ // Create clipPath for hex
327
+ const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
328
+ clipPath.setAttribute('id', `clip-${hex.id}`);
329
+
330
+ const pathData = vertices.map((vertex, index) => {
331
+ const command = index === 0 ? 'M' : 'L';
332
+ return `${command} ${vertex.x.toFixed(2)} ${vertex.y.toFixed(2)}`;
333
+ }).join(' ') + ' Z';
334
+
335
+ const clipPathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
336
+ clipPathElement.setAttribute('d', pathData);
337
+ clipPath.appendChild(clipPathElement);
338
+
339
+ // Add clipPath to defs
340
+ let defs = this.svg.querySelector('defs');
341
+ if (!defs) {
342
+ defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
343
+ this.svg.appendChild(defs);
344
+ }
345
+ defs.appendChild(clipPath);
346
+
347
+ // Create image that fills the hex
348
+ const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
349
+ image.setAttribute('href', `static/images/${RESOURCE_FILES[hex.type]}`);
350
+ image.setAttribute('x', minX);
351
+ image.setAttribute('y', minY);
352
+ image.setAttribute('width', maxX - minX);
353
+ image.setAttribute('height', maxY - minY);
354
+ image.setAttribute('preserveAspectRatio', 'xMidYMid slice');
355
+ image.setAttribute('clip-path', `url(#clip-${hex.id})`);
356
+
357
+ hexGroup.appendChild(image);
358
+
359
+ // Create hex element (for borders only)
360
+ const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
361
+ pathElement.setAttribute('d', pathData);
362
+ pathElement.setAttribute('class', `hexagon hex-${hex.type}`);
363
+ pathElement.setAttribute('data-hex-id', hex.id);
364
+ pathElement.style.fill = 'transparent';
365
+
366
+ hexGroup.appendChild(pathElement);
367
+ this.svg.appendChild(hexGroup);
368
+
369
+ // Add hex number (if not desert)
370
+ if (hex.number !== null) {
371
+ const numberElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
372
+ numberElement.setAttribute('x', center.x);
373
+ numberElement.setAttribute('y', center.y);
374
+ numberElement.textContent = hex.number;
375
+ numberElement.setAttribute('class', hex.number === 6 || hex.number === 8 ? 'hex-number red' : 'hex-number');
376
+ this.svg.appendChild(numberElement);
377
+ }
378
+
379
+ // Add robber if present
380
+ if (hex.robber) {
381
+ this.createRobber(center.x, center.y, hex.id);
382
+ }
383
+ }
384
+
385
+ createRobber(x, y, hexId) {
386
+ // Create robber group
387
+ const robberGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
388
+ robberGroup.setAttribute('class', 'robber');
389
+ robberGroup.setAttribute('data-hex-id', hexId);
390
+
391
+ // Create robber circle
392
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
393
+ circle.setAttribute('cx', x);
394
+ circle.setAttribute('cy', y);
395
+ circle.setAttribute('r', 18);
396
+
397
+ // Create robber text
398
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
399
+ text.setAttribute('x', x);
400
+ text.setAttribute('y', y);
401
+ text.textContent = 'R';
402
+ text.setAttribute('class', 'robber-text');
403
+
404
+ robberGroup.appendChild(circle);
405
+ robberGroup.appendChild(text);
406
+ this.svg.appendChild(robberGroup);
407
+ }
408
+
409
+ createVertices() {
410
+ // Create vertices group
411
+ const verticesGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
412
+ verticesGroup.setAttribute('id', 'vertices');
413
+ if (this.showVertices) {
414
+ verticesGroup.classList.add('vertices-visible');
415
+ }
416
+
417
+ this.vertices.forEach(vertex => {
418
+ // Create vertex circle
419
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
420
+ circle.setAttribute('cx', vertex.x);
421
+ circle.setAttribute('cy', vertex.y);
422
+ circle.setAttribute('r', 8);
423
+ circle.setAttribute('class', 'vertex');
424
+ circle.setAttribute('data-vertex-id', vertex.id);
425
+
426
+ // Create vertex number text
427
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
428
+ text.setAttribute('x', vertex.x);
429
+ text.setAttribute('y', vertex.y);
430
+ text.textContent = vertex.id;
431
+ text.setAttribute('class', 'vertex-number');
432
+
433
+ verticesGroup.appendChild(circle);
434
+ verticesGroup.appendChild(text);
435
+ });
436
+
437
+ this.svg.appendChild(verticesGroup);
438
+ }
439
+
440
+ createSettlement(settlement) {
441
+ // Find vertex by ID (handle both string and number IDs)
442
+ const vertexId = parseInt(settlement.vertex);
443
+ const vertex = this.vertices.find(v => v.id === vertexId);
444
+
445
+ if (!vertex) {
446
+ console.warn(`Could not find vertex ${settlement.vertex} for settlement`);
447
+ return;
448
+ }
449
+
450
+ // Create settlement polygon (house shape)
451
+ const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
452
+ const points = [
453
+ [vertex.x, vertex.y - 12], // top
454
+ [vertex.x - 8, vertex.y - 4], // top-left
455
+ [vertex.x - 8, vertex.y + 8], // bottom-left
456
+ [vertex.x + 8, vertex.y + 8], // bottom-right
457
+ [vertex.x + 8, vertex.y - 4] // top-right
458
+ ].map(p => p.join(',')).join(' ');
459
+
460
+ polygon.setAttribute('points', points);
461
+ polygon.setAttribute('class', `settlement player${settlement.player}`);
462
+ polygon.setAttribute('data-settlement-id', settlement.id);
463
+ polygon.setAttribute('data-vertex-id', settlement.vertex);
464
+
465
+ this.svg.appendChild(polygon);
466
+ }
467
+
468
+ createCity(city) {
469
+ // Find vertex by ID (handle both string and number IDs)
470
+ const vertexId = parseInt(city.vertex);
471
+ const vertex = this.vertices.find(v => v.id === vertexId);
472
+
473
+ if (!vertex) {
474
+ console.warn(`Could not find vertex ${city.vertex} for city`);
475
+ return;
476
+ }
477
+
478
+ // Create city polygon (larger building)
479
+ const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
480
+ const points = [
481
+ [vertex.x, vertex.y - 16], // top
482
+ [vertex.x - 12, vertex.y - 8], // top-left
483
+ [vertex.x - 12, vertex.y + 12], // bottom-left
484
+ [vertex.x + 12, vertex.y + 12], // bottom-right
485
+ [vertex.x + 12, vertex.y - 8] // top-right
486
+ ].map(p => p.join(',')).join(' ');
487
+
488
+ polygon.setAttribute('points', points);
489
+ polygon.setAttribute('class', `city player${city.player}`);
490
+ polygon.setAttribute('data-city-id', city.id);
491
+ polygon.setAttribute('data-vertex-id', city.vertex);
492
+
493
+ this.svg.appendChild(polygon);
494
+ }
495
+
496
+ createRoad(road) {
497
+ // Find vertices by ID (handle both string and number IDs)
498
+ const fromId = parseInt(road.from);
499
+ const toId = parseInt(road.to);
500
+
501
+ const fromVertex = this.vertices.find(v => v.id === fromId);
502
+ const toVertex = this.vertices.find(v => v.id === toId);
503
+
504
+ if (!fromVertex || !toVertex) {
505
+ console.warn(`Could not find vertices ${road.from}->${road.to} for road`);
506
+ return;
507
+ }
508
+
509
+ // Create road line
510
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
511
+ line.setAttribute('x1', fromVertex.x);
512
+ line.setAttribute('y1', fromVertex.y);
513
+ line.setAttribute('x2', toVertex.x);
514
+ line.setAttribute('y2', toVertex.y);
515
+ line.setAttribute('class', `road player${road.player}`);
516
+ line.setAttribute('data-road-id', road.id);
517
+ line.setAttribute('data-from-vertex', road.from);
518
+ line.setAttribute('data-to-vertex', road.to);
519
+
520
+ this.svg.appendChild(line);
521
+ }
522
+
523
+ // Update board from game state (called when receiving updates from server)
524
+ updateFromGameState(gameState) {
525
+ console.log('Updating board from game state:', gameState);
526
+
527
+ // Store the current game state
528
+ this.currentGameState = gameState;
529
+
530
+ // Don't regenerate vertices - they're already loaded from server in init()
531
+ // this.generateVertices(); // <- This function doesn't exist!
532
+
533
+ // Clear and rebuild board with new data (but not buildings)
534
+ this.svg.innerHTML = '';
535
+
536
+ // Create hexes from game state
537
+ if (gameState.hexes && gameState.hexes.length > 0) {
538
+ console.log(`Creating ${gameState.hexes.length} hexes from server data`);
539
+ gameState.hexes.forEach(hex => {
540
+ this.createHex(hex);
541
+ });
542
+ }
543
+
544
+ // Create vertices
545
+ this.createVertices();
546
+
547
+ // Add buildings from server data
548
+ this.updateBuildings(gameState);
549
+ this.updateRobberFromGameState(gameState);
550
+
551
+ // Update transform
552
+ this.updateTransform();
553
+ }
554
+
555
+ updateBuildings(gameState) {
556
+ // Remove existing buildings
557
+ const existingBuildings = this.svg.querySelectorAll('.settlement, .city, .road');
558
+ existingBuildings.forEach(building => building.remove());
559
+
560
+ // Add settlements from server data
561
+ if (gameState.settlements && gameState.settlements.length > 0) {
562
+ console.log('Adding settlements:', gameState.settlements);
563
+ gameState.settlements.forEach(settlement => {
564
+ this.createSettlement(settlement);
565
+ });
566
+ }
567
+
568
+ // Add cities from server data
569
+ if (gameState.cities && gameState.cities.length > 0) {
570
+ console.log('Adding cities:', gameState.cities);
571
+ gameState.cities.forEach(city => {
572
+ this.createCity(city);
573
+ });
574
+ }
575
+
576
+ // Add roads from server data
577
+ if (gameState.roads && gameState.roads.length > 0) {
578
+ console.log('Adding roads:', gameState.roads);
579
+ gameState.roads.forEach(road => {
580
+ this.createRoad(road);
581
+ });
582
+ }
583
+ }
584
+
585
+ updateRobberFromGameState(gameState) {
586
+ // Remove existing robber
587
+ const existingRobber = this.svg.querySelector('.robber');
588
+ if (existingRobber) {
589
+ existingRobber.remove();
590
+ }
591
+
592
+ // Add robber from server data
593
+ if (gameState.robber_position) {
594
+ // Find hex with robber position
595
+ const robberHex = gameState.hexes ?
596
+ gameState.hexes.find(h => h.robber === true) : null;
597
+
598
+ if (robberHex) {
599
+ const center = this.hexToPixel(robberHex.q, robberHex.r);
600
+ this.createRobber(center.x, center.y, robberHex.id);
601
+ console.log('Robber placed at hex:', robberHex.id);
602
+ }
603
+ }
604
+ }
605
+
606
+ updateRobberPosition(newPosition) {
607
+ // Remove existing robber
608
+ const existingRobber = this.svg.querySelector('.robber');
609
+ if (existingRobber) {
610
+ existingRobber.remove();
611
+ }
612
+
613
+ // Add robber to new position
614
+ const hex = GAMEDATA.hexes.find(h => h.id === newPosition);
615
+ if (hex) {
616
+ const center = this.hexToPixel(hex.q, hex.r);
617
+ this.createRobber(center.x, center.y, hex.id);
618
+ }
619
+ }
620
+
621
+ // Zoom and pan functionality
622
+ handleZoom(e) {
623
+ e.preventDefault();
624
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
625
+ this.zoomLevel = Math.max(0.5, Math.min(3, this.zoomLevel * delta));
626
+ this.updateTransform();
627
+ }
628
+
629
+ startDrag(e) {
630
+ this.isDragging = true;
631
+ this.lastMouseX = e.clientX;
632
+ this.lastMouseY = e.clientY;
633
+ }
634
+
635
+ handleDrag(e) {
636
+ if (!this.isDragging) return;
637
+
638
+ const deltaX = e.clientX - this.lastMouseX;
639
+ const deltaY = e.clientY - this.lastMouseY;
640
+
641
+ this.panX += deltaX;
642
+ this.panY += deltaY;
643
+
644
+ this.lastMouseX = e.clientX;
645
+ this.lastMouseY = e.clientY;
646
+
647
+ this.updateTransform();
648
+ }
649
+
650
+ endDrag() {
651
+ this.isDragging = false;
652
+ }
653
+
654
+ updateTransform() {
655
+ this.svg.style.transform = `translate(${this.panX}px, ${this.panY}px) scale(${this.zoomLevel})`;
656
+ }
657
+
658
+ // Control functions
659
+ zoomIn() {
660
+ this.zoomLevel = Math.min(3, this.zoomLevel * 1.2);
661
+ this.updateTransform();
662
+ }
663
+
664
+ zoomOut() {
665
+ this.zoomLevel = Math.max(0.5, this.zoomLevel * 0.8);
666
+ this.updateTransform();
667
+ }
668
+
669
+ resetZoom() {
670
+ this.zoomLevel = 1;
671
+ this.panX = 0;
672
+ this.panY = 0;
673
+ this.updateTransform();
674
+ }
675
+
676
+ toggleVertices() {
677
+ this.showVertices = !this.showVertices;
678
+ const verticesGroup = this.svg.querySelector('#vertices');
679
+ if (verticesGroup) {
680
+ if (this.showVertices) {
681
+ verticesGroup.classList.add('vertices-visible');
682
+ } else {
683
+ verticesGroup.classList.remove('vertices-visible');
684
+ }
685
+ }
686
+
687
+ // Update button text
688
+ const button = document.getElementById('toggleVertices');
689
+ if (button) {
690
+ button.textContent = this.showVertices ? '🔍' : '📍';
691
+ }
692
+ }
693
+
694
+ // עדכון vertex IDs בהתבסס על מיפוי אמיתי מהשרת
695
+ updateVertexIDsFromMapping() {
696
+ if (!window.pointMapping || !this.vertices) {
697
+ console.warn('⚠️ לא ניתן לעדכן vertex IDs - חסר מיפוי או vertices');
698
+ return;
699
+ }
700
+
701
+ console.log('🔄 מעדכן vertex IDs לפי המיפוי האמיתי...');
702
+
703
+ // עבור על כל vertex ובדוק אם יש לו מיפוי מתאים
704
+ this.vertices.forEach((vertex, index) => {
705
+ // נסה למצוא התאמה במיפוי לפי מיקום יחסי או אינדקס
706
+ // זהו approx - במציאות צריך מיפוי מדויק יותר
707
+
708
+ // משתמש באינדקס כתוצאת הדמוי
709
+ const mappedPointId = index + 1;
710
+
711
+ // עדכון ה-ID של הvertex
712
+ vertex.originalId = vertex.id; // שומר את הID המקורי
713
+ vertex.id = mappedPointId; // מעדכן לID הנכון
714
+ });
715
+
716
+ // עדכון התצוגה אם הvertices מוצגים
717
+ this.refreshVertexDisplay();
718
+
719
+ console.log('✅ vertex IDs עודכנו בהתבסס על המיפוי');
720
+ }
721
+
722
+ // רענון תצוגת vertices עם IDs מעודכנים
723
+ refreshVertexDisplay() {
724
+ const verticesGroup = this.svg.querySelector('#vertices');
725
+ if (!verticesGroup) return;
726
+
727
+ // עדכון הטקסטים עם המספרים החדשים
728
+ const vertexTexts = verticesGroup.querySelectorAll('.vertex-number');
729
+ vertexTexts.forEach((text, index) => {
730
+ if (this.vertices[index]) {
731
+ text.textContent = this.vertices[index].id;
732
+ }
733
+ });
734
+
735
+ // עדכון ה-data attributes
736
+ const vertexCircles = verticesGroup.querySelectorAll('.vertex');
737
+ vertexCircles.forEach((circle, index) => {
738
+ if (this.vertices[index]) {
739
+ circle.setAttribute('data-vertex-id', this.vertices[index].id);
740
+ }
741
+ });
742
+ }
743
+
744
+ // Debug functions
745
+ logAllVertices() {
746
+ console.log('All vertices:', this.vertices);
747
+ }
748
+
749
+ logVertexConnections() {
750
+ // Show examples of connected vertices
751
+ const examples = this.vertices.slice(0, 5);
752
+ examples.forEach(vertex => {
753
+ const connected = this.getConnectedVertices(vertex.id);
754
+ console.log(`Vertex ${vertex.id} connects to vertices: ${connected.join(', ')}`);
755
+ });
756
+ }
757
+
758
+ getConnectedVertices(vertexId) {
759
+ // This would need implementation based on hex grid logic
760
+ // For now, return empty array
761
+ return [];
762
+ }
763
+ }
pycatan/static/js/gameData.js ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // נתוני המשחק - אובייקט GAMEDATA (ברירת מחדל כאשר אין חיבור לשרת)
2
+ const GAMEDATA = {
3
+ // משושים (19 משושים ברמת קושי רגילה)
4
+ hexes: [
5
+ // שורה עליונה (3 משושים)
6
+ { id: 1, q: 0, r: -2, type: 'wood', number: 11, robber: false },
7
+ { id: 2, q: 1, r: -2, type: 'sheep', number: 12, robber: false },
8
+ { id: 3, q: 2, r: -2, type: 'wheat', number: 9, robber: false },
9
+
10
+ // שורה שנייה (4 משושים)
11
+ { id: 4, q: -1, r: -1, type: 'brick', number: 4, robber: false },
12
+ { id: 5, q: 0, r: -1, type: 'ore', number: 6, robber: false },
13
+ { id: 6, q: 1, r: -1, type: 'sheep', number: 5, robber: false },
14
+ { id: 7, q: 2, r: -1, type: 'wheat', number: 10, robber: false },
15
+
16
+ // שורה אמצעית (5 משושים)
17
+ { id: 8, q: -2, r: 0, type: 'wood', number: 3, robber: false },
18
+ { id: 9, q: -1, r: 0, type: 'brick', number: 11, robber: false },
19
+ { id: 10, q: 0, r: 0, type: 'desert', number: null, robber: true },
20
+ { id: 11, q: 1, r: 0, type: 'wheat', number: 4, robber: false },
21
+ { id: 12, q: 2, r: 0, type: 'ore', number: 8, robber: false },
22
+
23
+ // שורה רביעית (4 משושים)
24
+ { id: 13, q: -2, r: 1, type: 'ore', number: 8, robber: false },
25
+ { id: 14, q: -1, r: 1, type: 'sheep', number: 10, robber: false },
26
+ { id: 15, q: 0, r: 1, type: 'wood', number: 9, robber: false },
27
+ { id: 16, q: 1, r: 1, type: 'brick', number: 3, robber: false },
28
+
29
+ // שורה תחתונה (3 משושים)
30
+ { id: 17, q: -2, r: 2, type: 'wheat', number: 2, robber: false },
31
+ { id: 18, q: -1, r: 2, type: 'sheep', number: 5, robber: false },
32
+ { id: 19, q: 0, r: 2, type: 'ore', number: 6, robber: false }
33
+ ],
34
+
35
+ // יישובים - מתחילים ריקים
36
+ settlements: [],
37
+
38
+ // ערים - מתחילים ריקות
39
+ cities: [],
40
+
41
+ // דרכים - מתחילים ריקות
42
+ roads: [],
43
+
44
+ // מיקום השודד הנוכחי
45
+ robberPosition: 10,
46
+
47
+ // שחקנים (תוסף חדש)
48
+ players: [
49
+ { id: 0, name: 'שחקן 1', victory_points: 2, total_cards: 5 },
50
+ { id: 1, name: 'שחקן 2', victory_points: 3, total_cards: 7 },
51
+ { id: 2, name: 'שחקן 3', victory_points: 1, total_cards: 4 },
52
+ { id: 3, name: 'שחקן 4', victory_points: 2, total_cards: 6 }
53
+ ],
54
+
55
+ // מידע נוכחי על המשחק
56
+ current_player: 0,
57
+ current_phase: 'ACTION',
58
+ dice_result: [3, 4]
59
+ };
60
+
61
+ // מיפוי סוגי משאבים לקבצי התמונות
62
+ const RESOURCE_FILES = {
63
+ 'wood': 'Forest.png',
64
+ 'brick': 'Hills.png',
65
+ 'sheep': 'Pasture.png',
66
+ 'wheat': 'Fields.png',
67
+ 'ore': 'Mountains.png',
68
+ 'desert': 'Desert.png'
69
+ };
70
+
71
+ // מיפוי תכונות Tile ל-Hex בפורמט שלנו
72
+ function tileToHex(tile) {
73
+ const tileTypeMap = {
74
+ 'FOREST': 'wood',
75
+ 'HILLS': 'brick',
76
+ 'PASTURE': 'sheep',
77
+ 'FIELDS': 'wheat',
78
+ 'MOUNTAINS': 'ore',
79
+ 'DESERT': 'desert'
80
+ };
81
+
82
+ return {
83
+ id: tile.id || tile.position,
84
+ q: tile.position ? tile.position[0] : 0,
85
+ r: tile.position ? tile.position[1] : 0,
86
+ type: tileTypeMap[tile.type] || 'desert',
87
+ number: tile.token,
88
+ robber: tile.has_robber || false
89
+ };
90
+ }
91
+
92
+ // המרת GameState מPyCatan לפורמט שלנו
93
+ function convertGameState(pyGameState) {
94
+ const converted = {
95
+ hexes: [],
96
+ settlements: [],
97
+ cities: [],
98
+ roads: [],
99
+ players: [],
100
+ robberPosition: null,
101
+ current_player: pyGameState.current_player || 0,
102
+ current_phase: pyGameState.current_phase || 'ACTION',
103
+ dice_result: pyGameState.dice_result || null
104
+ };
105
+
106
+ // המר משושים
107
+ if (pyGameState.board && pyGameState.board.tiles) {
108
+ converted.hexes = pyGameState.board.tiles.map((tile, index) => tileToHex({
109
+ ...tile,
110
+ id: index + 1
111
+ }));
112
+ }
113
+
114
+ // המר שחקנים
115
+ if (pyGameState.players) {
116
+ converted.players = pyGameState.players.map((player, index) => ({
117
+ id: index,
118
+ name: player.name || `שחקן ${index + 1}`,
119
+ victory_points: player.victory_points || 0,
120
+ total_cards: (player.cards && player.cards.length) || 0
121
+ }));
122
+ }
123
+
124
+ // המר מבנים
125
+ if (pyGameState.buildings) {
126
+ pyGameState.buildings.forEach(building => {
127
+ if (building.type === 'settlement') {
128
+ converted.settlements.push({
129
+ id: building.id,
130
+ vertex: building.point_id,
131
+ player: building.player + 1
132
+ });
133
+ } else if (building.type === 'city') {
134
+ converted.cities.push({
135
+ id: building.id,
136
+ vertex: building.point_id,
137
+ player: building.player + 1
138
+ });
139
+ }
140
+ });
141
+ }
142
+
143
+ // המר דרכים
144
+ if (pyGameState.roads) {
145
+ converted.roads = pyGameState.roads.map((road, index) => ({
146
+ id: index + 1,
147
+ from: road.start_point_id,
148
+ to: road.end_point_id,
149
+ player: road.player + 1
150
+ }));
151
+ }
152
+
153
+ return converted;
154
+ }
pycatan/static/js/main.js ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Main file - Game initialization
2
+ let catanBoard;
3
+ let gameState = null;
4
+ let eventSource = null;
5
+ let pointMapping = null; // Point mapping - will be loaded from server
6
+ let playerNames = {}; // Store player names from game
7
+
8
+ // Global functions for control buttons
9
+ function zoomIn() {
10
+ if (catanBoard) {
11
+ catanBoard.zoomIn();
12
+ }
13
+ }
14
+
15
+ function zoomOut() {
16
+ if (catanBoard) {
17
+ catanBoard.zoomOut();
18
+ }
19
+ }
20
+
21
+ function resetZoom() {
22
+ if (catanBoard) {
23
+ catanBoard.resetZoom();
24
+ }
25
+ }
26
+
27
+ function toggleVertices() {
28
+ if (catanBoard) {
29
+ catanBoard.toggleVertices();
30
+ }
31
+ }
32
+
33
+ // Toggle building costs modal
34
+ function toggleBuildingCosts() {
35
+ const modal = document.getElementById('buildingCostsModal');
36
+ if (modal) {
37
+ modal.classList.toggle('show');
38
+ }
39
+ }
40
+
41
+ // Clear action log
42
+ function clearActionLog() {
43
+ const logDiv = document.getElementById('action-log');
44
+ if (logDiv) {
45
+ logDiv.innerHTML = '<div class="info">Log cleared ✓</div>';
46
+ }
47
+ }
48
+
49
+ // טעינת מיפוי נקודות מהשרת
50
+ function loadPointMapping() {
51
+ return fetch('/api/point_mapping')
52
+ .then(response => response.json())
53
+ .then(data => {
54
+ pointMapping = data;
55
+ // console.log('🗺️ Point mapping loaded:', `${data.total_points} points`);
56
+ // console.log(' Example: point 1 at', data.point_to_coords[1]);
57
+
58
+ // Make mapping global so board.js can access it
59
+ window.pointMapping = pointMapping;
60
+
61
+ return pointMapping;
62
+ })
63
+ .catch(error => {
64
+ console.error('❌ Error loading point mapping:', error);
65
+ // fallback - create basic mapping
66
+ pointMapping = {
67
+ point_to_coords: {},
68
+ coords_to_point: {},
69
+ total_points: 54,
70
+ all_points: Array.from({length: 54}, (_, i) => i + 1)
71
+ };
72
+ return pointMapping;
73
+ });
74
+ }
75
+
76
+ // חיבור ל-Flask server
77
+ function connectToServer() {
78
+ console.log('🔗 Connecting to server...');
79
+
80
+ // First load point mapping
81
+ loadPointMapping().then(() => {
82
+ console.log('✓ Point mapping loaded');
83
+
84
+ // Now load game state
85
+ return Promise.all([
86
+ fetch('/api/game-state', {timeout: 5000}),
87
+ fetch('/api/actions')
88
+ ]);
89
+ })
90
+ .then(responses => {
91
+ return Promise.all(responses.map(r => {
92
+ if (!r.ok) throw new Error(`Server responded with ${r.status}`);
93
+ return r.json();
94
+ }));
95
+ })
96
+ .then(([gameStateData, actionsData]) => {
97
+ console.log('📥 Game state received from server:', gameStateData);
98
+
99
+ // Check if state is empty (no hexes)
100
+ if (!gameStateData.hexes || gameStateData.hexes.length === 0) {
101
+ console.log('⚠️ Server state is empty, using GAMEDATA as fallback');
102
+ updateGameState(GAMEDATA);
103
+ } else {
104
+ updateGameState(gameStateData);
105
+ }
106
+
107
+ // Store player names for later use
108
+ if (gameStateData.players) {
109
+ gameStateData.players.forEach((player, index) => {
110
+ playerNames[index] = player.name || `Player ${index + 1}`;
111
+ });
112
+ }
113
+
114
+ // Load action history
115
+ if (actionsData && Array.isArray(actionsData)) {
116
+ console.log(`📥 Loaded ${actionsData.length} previous actions`);
117
+ actionsData.forEach(action => logAction(action));
118
+ }
119
+
120
+ console.log('✓ Server connection established successfully');
121
+
122
+ // Connect to real-time updates
123
+ connectToSSE();
124
+ })
125
+ .catch(error => {
126
+ console.error('❌ Error connecting to server:', error);
127
+ console.log('🔄 Using GAMEDATA as fallback');
128
+ updateGameState(GAMEDATA);
129
+ });
130
+ }
131
+
132
+ // Connect to Server-Sent Events for real-time updates
133
+ function connectToSSE() {
134
+ try {
135
+ eventSource = new EventSource('/api/events');
136
+
137
+ eventSource.onmessage = function(event) {
138
+ const data = JSON.parse(event.data);
139
+ // console.log('📡 Update from server:', data);
140
+
141
+ if (data.type === 'game_update' || data.type === 'state_updated') {
142
+ // Update player names if we get new player data
143
+ if (data.payload.players) {
144
+ data.payload.players.forEach((player, index) => {
145
+ playerNames[index] = player.name || `Player ${index + 1}`;
146
+ });
147
+ }
148
+ updateGameState(data.payload);
149
+ } else if (data.type === 'action_executed') {
150
+ logAction(data.payload);
151
+ } else if (data.type === 'dice_roll') {
152
+ logEvent(data.payload, 'log-dice');
153
+ } else if (data.type === 'resource_distribution') {
154
+ logResourceDistribution(data.payload);
155
+ } else if (data.type === 'turn_start') {
156
+ logEvent(data.payload, 'log-turn');
157
+ } else if (data.type === 'message') {
158
+ logEvent(data.payload, 'info');
159
+ } else if (data.type === 'error') {
160
+ logEvent(data.payload, 'error');
161
+ }
162
+ };
163
+
164
+ eventSource.onerror = function(error) {
165
+ console.error('❌ Error in SSE connection:', error);
166
+ };
167
+
168
+ console.log('✅ Connected to real-time updates');
169
+ } catch (error) {
170
+ console.warn('⚠️ Unable to connect to SSE:', error);
171
+ }
172
+ }
173
+
174
+ // Update game state
175
+ function updateGameState(newState) {
176
+ gameState = newState;
177
+
178
+ if (catanBoard) {
179
+ catanBoard.updateFromGameState(gameState);
180
+
181
+ // Update vertex IDs if we have mapping
182
+ if (pointMapping) {
183
+ catanBoard.updateVertexIDsFromMapping();
184
+ }
185
+ }
186
+
187
+ updateGameInfo(gameState);
188
+ }
189
+
190
+ // Update player information display
191
+ function updateGameInfo(state) {
192
+ const gameInfoDiv = document.getElementById('game-info');
193
+ if (!gameInfoDiv) return;
194
+
195
+ // Preserve expanded state
196
+ const expandedPlayers = new Set();
197
+ document.querySelectorAll('.player-info.expanded').forEach(el => {
198
+ expandedPlayers.add(el.dataset.playerId);
199
+ });
200
+
201
+ let html = '<h3>📋 Game Info</h3>';
202
+
203
+ if (state.players) {
204
+ state.players.forEach((player, index) => {
205
+ const activeClass = state.current_player === index ? 'active' : '';
206
+ const isExpanded = expandedPlayers.has(String(index)) ? 'expanded' : '';
207
+ const playerColors = ['#FF4444', '#4444FF', '#44FF44', '#FFAA00'];
208
+ const playerColor = playerColors[index % 4];
209
+
210
+ // Get player name from stored names or use default
211
+ const playerName = playerNames[index] || player.name || `Player ${index + 1}`;
212
+
213
+ // Format cards lists
214
+ let cardsHtml = '';
215
+ if (player.cards_list && player.cards_list.length > 0) {
216
+ // Count cards by type
217
+ const cardCounts = {};
218
+ player.cards_list.forEach(card => {
219
+ cardCounts[card] = (cardCounts[card] || 0) + 1;
220
+ });
221
+
222
+ cardsHtml += '<div><strong>Resources:</strong><ul class="card-list">';
223
+ for (const [card, count] of Object.entries(cardCounts)) {
224
+ cardsHtml += `<li>${card}: ${count}</li>`;
225
+ }
226
+ cardsHtml += '</ul></div>';
227
+ } else {
228
+ cardsHtml += '<div><em>No resource cards</em></div>';
229
+ }
230
+
231
+ let devCardsHtml = '';
232
+ if (player.dev_cards_list && player.dev_cards_list.length > 0) {
233
+ // Count dev cards by type
234
+ const devCardCounts = {};
235
+ player.dev_cards_list.forEach(card => {
236
+ devCardCounts[card] = (devCardCounts[card] || 0) + 1;
237
+ });
238
+
239
+ devCardsHtml += '<div style="margin-top:5px;"><strong>Development:</strong><ul class="card-list">';
240
+ for (const [card, count] of Object.entries(devCardCounts)) {
241
+ devCardsHtml += `<li>${card}: ${count}</li>`;
242
+ }
243
+ devCardsHtml += '</ul></div>';
244
+ }
245
+
246
+ html += `
247
+ <div class="player-info ${activeClass} ${isExpanded}" data-player-id="${index}" onclick="togglePlayerInfo(this)" style="border-left-color: ${playerColor};">
248
+ <h4>👤 ${playerName}</h4>
249
+ <div class="player-resources">
250
+ <strong>🏆 VP:</strong> ${player.victory_points || 0} |
251
+ <strong>🎴 Cards:</strong> ${player.total_cards || 0}
252
+ </div>
253
+ <div class="player-resources">
254
+ <strong>🏘️:</strong> ${player.settlements || 0} |
255
+ <strong>🏛️:</strong> ${player.cities || 0} |
256
+ <strong>🛣️:</strong> ${player.roads || 0}
257
+ </div>
258
+ <div class="player-cards">
259
+ ${cardsHtml}
260
+ ${devCardsHtml}
261
+ </div>
262
+ </div>
263
+ `;
264
+ });
265
+ }
266
+
267
+ if (state.current_phase) {
268
+ html += `<div style="margin-top: 10px; padding: 10px; background: rgba(52, 152, 219, 0.1); border-radius: 6px;"><strong>📍 Current Phase:</strong> ${state.current_phase}</div>`;
269
+ }
270
+
271
+ gameInfoDiv.innerHTML = html;
272
+ }
273
+
274
+ // Toggle player info visibility
275
+ window.togglePlayerInfo = function(element) {
276
+ element.classList.toggle('expanded');
277
+ }
278
+
279
+ // Log action
280
+ function logAction(actionData) {
281
+ const logDiv = document.getElementById('action-log');
282
+ if (!logDiv) return;
283
+
284
+ const actionElement = document.createElement('div');
285
+
286
+ // Determine class based on action type if needed, or just success/error
287
+ let className = actionData.success ? 'success' : 'error';
288
+ let prefix = actionData.success ? '✓' : '✗';
289
+
290
+ // Add specific classes for certain actions
291
+ if (actionData.success) {
292
+ if (actionData.action_type && actionData.action_type.includes('BUILD')) {
293
+ className = 'log-build';
294
+ prefix = '🔨';
295
+ }
296
+ }
297
+
298
+ actionElement.className = className;
299
+ const timestamp = actionData.timestamp || new Date().toLocaleTimeString('en-GB', { hour12: false });
300
+ actionElement.textContent = `${prefix} ${actionData.message}`;
301
+
302
+ appendToLog(logDiv, actionElement);
303
+ }
304
+
305
+ // Log generic event
306
+ function logEvent(data, className) {
307
+ const logDiv = document.getElementById('action-log');
308
+ if (!logDiv) return;
309
+
310
+ const element = document.createElement('div');
311
+ element.className = className;
312
+
313
+ const timestamp = data.timestamp || new Date().toLocaleTimeString('en-GB', { hour12: false });
314
+
315
+ // Add emoji prefix based on type
316
+ let prefix = '';
317
+ if (className === 'log-dice') prefix = '🎲';
318
+ else if (className === 'log-turn') prefix = '➤';
319
+ else if (className === 'info') prefix = 'ℹ️';
320
+ else if (className === 'error') prefix = '⚠️';
321
+
322
+ element.textContent = `${prefix} ${data.message}`;
323
+
324
+ appendToLog(logDiv, element);
325
+ }
326
+
327
+ // Log resource distribution specifically
328
+ function logResourceDistribution(data) {
329
+ const logDiv = document.getElementById('action-log');
330
+ if (!logDiv) return;
331
+
332
+ const timestamp = data.timestamp || new Date().toLocaleTimeString('en-GB', { hour12: false });
333
+
334
+ // If we have detailed distributions, log them
335
+ if (data.distributions) {
336
+ for (const [player, resources] of Object.entries(data.distributions)) {
337
+ if (resources && resources.length > 0) {
338
+ const element = document.createElement('div');
339
+ element.className = 'log-resource';
340
+
341
+ // Count resources
342
+ const counts = {};
343
+ resources.forEach(r => counts[r] = (counts[r] || 0) + 1);
344
+
345
+ const resourceStr = Object.entries(counts)
346
+ .map(([res, count]) => `${count}×${res}`)
347
+ .join(' ');
348
+
349
+ element.textContent = `📦 ${player}: ${resourceStr}`;
350
+ appendToLog(logDiv, element);
351
+ }
352
+ }
353
+ } else {
354
+ // Fallback to generic message
355
+ const element = document.createElement('div');
356
+ element.className = 'log-resource';
357
+ element.textContent = `📦 ${data.message}`;
358
+ appendToLog(logDiv, element);
359
+ }
360
+ }
361
+
362
+ // Helper to append to log and scroll
363
+ function appendToLog(container, element) {
364
+ container.appendChild(element);
365
+
366
+ // Keep only last 100 items
367
+ while (container.children.length > 100) {
368
+ container.removeChild(container.firstChild);
369
+ }
370
+
371
+ // Scroll to bottom
372
+ container.scrollTop = container.scrollHeight;
373
+ }
374
+
375
+ // Game initialization
376
+ document.addEventListener('DOMContentLoaded', async function() {
377
+ console.log('🎲 Starting Catan board with server connection...');
378
+
379
+ try {
380
+ // Create board instance (async)
381
+ catanBoard = new CatanBoard();
382
+ window.catanBoard = catanBoard; // Expose to window for console access
383
+
384
+ // Wait for board initialization
385
+ if (catanBoard.init && typeof catanBoard.init === 'function') {
386
+ console.log('⏳ Waiting for board initialization...');
387
+ await catanBoard.init();
388
+ console.log('✓ Board initialized successfully');
389
+ }
390
+
391
+ // Connect to server
392
+ connectToServer();
393
+
394
+ console.log('✅ Catan board created successfully!');
395
+ console.log('🎮 Usage instructions:');
396
+ console.log(' - Click on hex to move robber');
397
+ console.log(' - Use mouse wheel to zoom');
398
+ console.log(' - Drag mouse to pan');
399
+ console.log(' - Click 📍 to see vertex numbers');
400
+
401
+ } catch (error) {
402
+ console.error('❌ Error initializing board:', error);
403
+ // Try to create simple board anyway
404
+ catanBoard = new CatanBoard();
405
+ connectToServer();
406
+ }
407
+ });
408
+
409
+ // Cleanup on close
410
+ window.addEventListener('beforeunload', function() {
411
+ if (eventSource) {
412
+ eventSource.close();
413
+ }
414
+ });
pycatan/static/js/manual_mapping.js ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Manual Mapping Logic
2
+ class ManualMapper extends CatanBoard {
3
+ constructor() {
4
+ super();
5
+ this.mapping = {
6
+ hexes: {},
7
+ points: {}
8
+ };
9
+ this.currentId = 1;
10
+ this.mode = 'hex'; // 'hex' or 'point'
11
+ this.history = [];
12
+ }
13
+
14
+ async init() {
15
+ await this.initManual();
16
+ }
17
+
18
+ async initManual() {
19
+ // Generate visual board from scratch
20
+ this.generateVerticesFromHexes();
21
+ this.createBoard();
22
+ this.setupMappingListeners();
23
+ this.updateUI();
24
+
25
+ console.log("=== GAME LOGIC EXPECTATIONS ===");
26
+ console.log("Paste the output from 'python print_game_logic.py' here for reference if needed.");
27
+ }
28
+
29
+ setupMappingListeners() {
30
+ console.log("Setting up mapping listeners...");
31
+
32
+ // Use event delegation on the SVG to catch all clicks
33
+ this.svg.addEventListener('click', (e) => {
34
+ console.log("Click detected on:", e.target.tagName, e.target.className);
35
+
36
+ // Handle Hex Click
37
+ if (this.mode === 'hex') {
38
+ // Check if we clicked the hexagon path directly
39
+ if (e.target.classList.contains('hexagon')) {
40
+ this.handleHexClick(e.target);
41
+ return;
42
+ }
43
+
44
+ // Check if we clicked the image inside the hex group
45
+ if (e.target.tagName === 'image' && e.target.parentNode) {
46
+ const group = e.target.parentNode;
47
+ const hexPath = group.querySelector('.hexagon');
48
+ if (hexPath) {
49
+ this.handleHexClick(hexPath);
50
+ return;
51
+ }
52
+ }
53
+ }
54
+
55
+ // Handle Point Click
56
+ if (this.mode === 'point') {
57
+ if (e.target.classList.contains('vertex')) {
58
+ this.handlePointClick(e.target);
59
+ return;
60
+ }
61
+ }
62
+ });
63
+
64
+ // Mode switching
65
+ document.querySelectorAll('input[name="mode"]').forEach(radio => {
66
+ radio.addEventListener('change', (e) => {
67
+ this.mode = e.target.value;
68
+ this.currentId = this.getNextId();
69
+ this.updateUI();
70
+ });
71
+ });
72
+ }
73
+
74
+ getNextId() {
75
+ const map = this.mode === 'hex' ? this.mapping.hexes : this.mapping.points;
76
+ let id = 1;
77
+ while (map[id]) id++;
78
+ return id;
79
+ }
80
+
81
+ handleHexClick(element) {
82
+ // In manual mapping, we use the visual ID (from generateVerticesFromHexes) as a temporary key
83
+ // But wait, generateVerticesFromHexes assigns IDs 1-19 arbitrarily.
84
+ // We want to assign OUR ID (1, 2, 3...) to this visual element.
85
+
86
+ // The element has data-hex-id which is the visual ID.
87
+ // We want to map: User Chosen ID -> Visual Coordinates (q, r)
88
+
89
+ // Find the hex data
90
+ const visualId = parseInt(element.getAttribute('data-hex-id'));
91
+ const hexData = this.vertices.find(v => v.adjacent_hexes.includes(visualId))
92
+ ? GAMEDATA.hexes.find(h => h.id === visualId)
93
+ : null; // This is tricky, we need to find the hex object
94
+
95
+ // Actually, we can just find it in GAMEDATA.hexes by ID since createBoard used that
96
+ const hex = GAMEDATA.hexes.find(h => h.id === visualId);
97
+
98
+ if (this.mapping.hexes[this.currentId]) {
99
+ alert(`Hex ${this.currentId} already mapped!`);
100
+ return;
101
+ }
102
+
103
+ // Save mapping
104
+ this.mapping.hexes[this.currentId] = {
105
+ q: hex.q,
106
+ r: hex.r
107
+ };
108
+
109
+ // Visual feedback
110
+ element.classList.add('mapped');
111
+
112
+ // Add text label
113
+ const center = this.hexToPixel(hex.q, hex.r);
114
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
115
+ text.setAttribute('x', center.x);
116
+ text.setAttribute('y', center.y);
117
+ text.textContent = this.currentId;
118
+ text.setAttribute('class', 'hex-number mapped-text');
119
+ text.setAttribute('pointer-events', 'none');
120
+ this.svg.appendChild(text);
121
+
122
+ this.history.push({
123
+ type: 'hex',
124
+ id: this.currentId,
125
+ element: element,
126
+ textElement: text
127
+ });
128
+
129
+ this.currentId++;
130
+ this.updateUI();
131
+ }
132
+
133
+ handlePointClick(element) {
134
+ const visualId = parseInt(element.getAttribute('data-vertex-id'));
135
+ const vertex = this.vertices.find(v => v.id === visualId);
136
+
137
+ if (this.mapping.points[this.currentId]) {
138
+ alert(`Point ${this.currentId} already mapped!`);
139
+ return;
140
+ }
141
+
142
+ // Save mapping
143
+ this.mapping.points[this.currentId] = {
144
+ x: vertex.x,
145
+ y: vertex.y
146
+ };
147
+
148
+ // Visual feedback
149
+ element.classList.add('mapped');
150
+
151
+ // Update text
152
+ // Find the text element associated with this vertex
153
+ // It's the next sibling in our DOM structure
154
+ const text = element.nextElementSibling;
155
+ if (text && text.classList.contains('vertex-number')) {
156
+ text.textContent = this.currentId;
157
+ text.classList.add('mapped-text');
158
+ text.style.opacity = 1;
159
+ }
160
+
161
+ this.history.push({
162
+ type: 'point',
163
+ id: this.currentId,
164
+ element: element,
165
+ textElement: text,
166
+ originalText: visualId
167
+ });
168
+
169
+ this.currentId++;
170
+ this.updateUI();
171
+ }
172
+
173
+ undoLast() {
174
+ const last = this.history.pop();
175
+ if (!last) return;
176
+
177
+ if (last.type === 'hex') {
178
+ delete this.mapping.hexes[last.id];
179
+ last.element.classList.remove('mapped');
180
+ last.textElement.remove();
181
+ if (this.mode === 'hex') this.currentId = last.id;
182
+ } else {
183
+ delete this.mapping.points[last.id];
184
+ last.element.classList.remove('mapped');
185
+ last.textElement.textContent = last.originalText;
186
+ last.textElement.classList.remove('mapped-text');
187
+ if (this.mode === 'point') this.currentId = last.id;
188
+ }
189
+ this.updateUI();
190
+ }
191
+
192
+ getExpectedCoords(id, type) {
193
+ let count = 0;
194
+
195
+ if (type === 'hex') {
196
+ const rows = [3, 4, 5, 4, 3];
197
+ for (let r = 0; r < rows.length; r++) {
198
+ if (id <= count + rows[r]) {
199
+ const col = id - count - 1;
200
+ return `Row ${r}, Col ${col}`;
201
+ }
202
+ count += rows[r];
203
+ }
204
+ } else {
205
+ const rows = [7, 9, 11, 11, 9, 7];
206
+ for (let r = 0; r < rows.length; r++) {
207
+ if (id <= count + rows[r]) {
208
+ const col = id - count - 1;
209
+ return `Row ${r}, Col ${col}`;
210
+ }
211
+ count += rows[r];
212
+ }
213
+ }
214
+ return "Done";
215
+ }
216
+
217
+ updateUI() {
218
+ console.log("Updating UI", this.currentId, this.mode);
219
+ document.getElementById('nextId').textContent = this.currentId;
220
+ const hint = this.getExpectedCoords(this.currentId, this.mode);
221
+ const hintEl = document.getElementById('coordsHint');
222
+ if (hintEl) {
223
+ hintEl.textContent = hint;
224
+ hintEl.style.color = this.mode === 'hex' ? '#e74c3c' : '#2980b9';
225
+ }
226
+ }
227
+
228
+ exportMapping() {
229
+ const output = JSON.stringify(this.mapping, null, 2);
230
+ document.getElementById('output').value = output;
231
+ console.log("Mapping exported:", this.mapping);
232
+ }
233
+ }
234
+
235
+ // Initialize
236
+ const mapper = new ManualMapper();
237
+
238
+ // Global functions for buttons
239
+ window.undoLast = () => mapper.undoLast();
240
+ window.exportMapping = () => mapper.exportMapping();
pycatan/statuses.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # different statuses in the Game module
2
+ # skips 0 and 1 because there are already equal to True and False
3
+ class Statuses:
4
+
5
+ # the action was successfully completed
6
+ ALL_GOOD = 2
7
+
8
+ # the action cannot be completed because:
9
+
10
+ # the player does not have the correct cards
11
+ ERR_CARDS = 3
12
+ # a building is blocking the action
13
+ ERR_BLOCKED = 4
14
+ # the point given is not on the board
15
+ ERR_BAD_POINT = 5
16
+ # the road's points are not connected
17
+ ERR_NOT_CON = 6
18
+ # the building in not connected to any of the player's buildings
19
+ ERR_ISOLATED = 7
20
+ # the player is trying to use a harbor they are not connected to
21
+ ERR_HARBOR = 8
22
+ # the player is trying to use a building that does not exist
23
+ ERR_NOT_EXIST = 9
24
+ # the player is trying to use a building that does not belong to them
25
+ ERR_BAD_OWNER = 10
26
+ # the player is trying to build a city on another city rather than a settlement
27
+ ERR_UPGRADE_CITY = 11
28
+ # there are not enough cards in the deck to perform this action
29
+ ERR_DECK = 12
30
+ # the input given is missing components/invalid
31
+ ERR_INPUT = 13
32
+ # when running the testing module, an error was found
33
+ ERR_TEST = 14
pycatan/templates/index.html ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Catan - Game Simulation</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ </head>
9
+ <body>
10
+ <div class="game-container">
11
+ <!-- Header -->
12
+ <div class="game-header">
13
+ <h1>🎲 Catan - Game Simulation</h1>
14
+ <p class="subtitle">Settlers of Catan - Web Visualization</p>
15
+ </div>
16
+
17
+ <div class="content-wrapper">
18
+ <!-- Left Sidebar - Game Info -->
19
+ <aside class="sidebar sidebar-left">
20
+ <div id="game-info" class="game-info">
21
+ <h3>📋 Game Info</h3>
22
+ <div class="loading">Loading...</div>
23
+ </div>
24
+ </aside>
25
+
26
+ <!-- Center - Board -->
27
+ <main class="board-wrapper">
28
+ <!-- Board Container -->
29
+ <div class="board-container" id="boardContainer">
30
+ <!-- Board Controls -->
31
+ <div class="board-controls">
32
+ <button onclick="zoomIn()" class="control-btn" title="Zoom In">🔍+</button>
33
+ <button onclick="zoomOut()" class="control-btn" title="Zoom Out">🔍−</button>
34
+ <button onclick="resetZoom()" class="control-btn" title="Reset View">⌂</button>
35
+ <button id="toggleVertices" onclick="toggleVertices()" class="control-btn" title="Show Vertices">📍</button>
36
+ <button id="toggleInfo" onclick="toggleBuildingCosts()" class="control-btn" title="Building Costs">ℹ️</button>
37
+ </div>
38
+
39
+ <!-- Game Board -->
40
+ <svg id="catan-board" width="100%" height="100%">
41
+ <!-- Hexes and game elements will be added here by JavaScript -->
42
+ </svg>
43
+
44
+ <!-- Building Costs Modal -->
45
+ <div id="buildingCostsModal" class="modal hidden">
46
+ <div class="modal-content">
47
+ <div class="modal-header">
48
+ <h3>Building Costs</h3>
49
+ <button class="modal-close" onclick="toggleBuildingCosts()">✕</button>
50
+ </div>
51
+ <div class="modal-body">
52
+ <table class="costs-table">
53
+ <thead>
54
+ <tr>
55
+ <th>Building</th>
56
+ <th>Resources Required</th>
57
+ <th>VP</th>
58
+ </tr>
59
+ </thead>
60
+ <tbody>
61
+ <tr>
62
+ <td>Road</td>
63
+ <td>1 Brick + 1 Lumber</td>
64
+ <td>—</td>
65
+ </tr>
66
+ <tr>
67
+ <td>Settlement</td>
68
+ <td>1 Brick + 1 Lumber + 1 Wheat + 1 Sheep</td>
69
+ <td>1</td>
70
+ </tr>
71
+ <tr>
72
+ <td>City</td>
73
+ <td>3 Ore + 2 Wheat</td>
74
+ <td>2</td>
75
+ </tr>
76
+ <tr>
77
+ <td>Dev Card</td>
78
+ <td>1 Ore + 1 Sheep + 1 Wheat</td>
79
+ <td>1*</td>
80
+ </tr>
81
+ </tbody>
82
+ </table>
83
+ <p class="costs-note">* VP awarded only for Victory Point dev cards</p>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </main>
89
+
90
+ <!-- Right Sidebar - Action Log -->
91
+ <aside class="sidebar sidebar-right">
92
+ <div class="log-header">
93
+ <h3>📜 Action Log</h3>
94
+ <button class="clear-log-btn" onclick="clearActionLog()" title="Clear Log">🗑️</button>
95
+ </div>
96
+ <div id="action-log" class="action-log">
97
+ <div class="info">Waiting for updates...</div>
98
+ </div>
99
+ </aside>
100
+ </div>
101
+ </div>
102
+
103
+ <script src="{{ url_for('static', filename='js/gameData.js') }}"></script>
104
+ <script src="{{ url_for('static', filename='js/board.js') }}"></script>
105
+ <script src="{{ url_for('static', filename='js/main.js') }}"></script>
106
+ </body>
107
+ </html>