Spaces:
Configuration error
Configuration error
EZTIME2025 commited on
Commit ·
c903325
1
Parent(s): 74d81e4
from_old_account
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +14 -0
- .github/copilot-instructions.md +84 -0
- .github/instructions/ARCHITECTURE.md +146 -0
- .github/instructions/BUILD_PLAN.md +179 -0
- .github/instructions/STEP_BY_STEP_GUIDE.md +4 -0
- .github/instructions/WEB_VISUALIZATION_GUIDE.md +336 -0
- .gitignore +65 -0
- CHANGES.txt +1 -0
- LICENSE.txt +19 -0
- MANIFEST.in +1 -0
- Pipfile +6 -0
- Pipfile.lock +45 -0
- board_definition.json +1435 -0
- demo_point_system.py +157 -0
- dist/pycatan-0.1.tar.gz +0 -0
- examples/board_renderer.py +193 -0
- game_viz.log +442 -0
- play_catan.py +33 -0
- print_game_logic.py +40 -0
- pycatan/__init__.py +33 -0
- pycatan/actions.py +253 -0
- pycatan/board.py +219 -0
- pycatan/board_definition.py +556 -0
- pycatan/building.py +33 -0
- pycatan/card.py +21 -0
- pycatan/console_visualization.py +645 -0
- pycatan/default_board.py +230 -0
- pycatan/game.py +508 -0
- pycatan/game_manager.py +1563 -0
- pycatan/game_moves.txt +19 -0
- pycatan/harbor.py +70 -0
- pycatan/human_user.py +667 -0
- pycatan/player.py +385 -0
- pycatan/point.py +8 -0
- pycatan/point_mapping.py +202 -0
- pycatan/real_game.py +372 -0
- pycatan/starting_board.json +64 -0
- pycatan/static/css/style.css +774 -0
- pycatan/static/images/Desert.png +0 -0
- pycatan/static/images/Fields.png +0 -0
- pycatan/static/images/Forest.png +0 -0
- pycatan/static/images/Hills.png +0 -0
- pycatan/static/images/Mountains.png +0 -0
- pycatan/static/images/Pasture.png +0 -0
- pycatan/static/js/board.js +763 -0
- pycatan/static/js/gameData.js +154 -0
- pycatan/static/js/main.js +414 -0
- pycatan/static/js/manual_mapping.js +240 -0
- pycatan/statuses.py +33 -0
- 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 |
+
[1m[94m>>> Turn 0: a's turn[0m
|
| 3 |
+
[92m✓[0m a built a settlement
|
| 4 |
+
|
| 5 |
+
[1m[96m==================================================[0m
|
| 6 |
+
[1m[96m GAME STATE [0m
|
| 7 |
+
[1m[96m==================================================[0m
|
| 8 |
+
|
| 9 |
+
Turn: [1m0[0m
|
| 10 |
+
Current Player: [1m[92m► a[0m
|
| 11 |
+
|
| 12 |
+
[1m[93mPLAYERS[0m
|
| 13 |
+
[93m-------[0m
|
| 14 |
+
|
| 15 |
+
[1m[92m► a[0m
|
| 16 |
+
Victory Points: [97m1[0m
|
| 17 |
+
Resources: None
|
| 18 |
+
Buildings: Settlements: [92m1[0m, Cities: [91m0[0m, Roads: [91m0[0m
|
| 19 |
+
|
| 20 |
+
[97mV[0m
|
| 21 |
+
Victory Points: [97m0[0m
|
| 22 |
+
Resources: None
|
| 23 |
+
Buildings: Settlements: [91m0[0m, Cities: [91m0[0m, Roads: [91m0[0m
|
| 24 |
+
|
| 25 |
+
[1m[93mBOARD[0m
|
| 26 |
+
[93m-----[0m
|
| 27 |
+
Board Tiles: 19 tiles configured
|
| 28 |
+
|
| 29 |
+
[92m✓[0m a built a road
|
| 30 |
+
|
| 31 |
+
[1m[96m==================================================[0m
|
| 32 |
+
[1m[96m GAME STATE [0m
|
| 33 |
+
[1m[96m==================================================[0m
|
| 34 |
+
|
| 35 |
+
Turn: [1m0[0m
|
| 36 |
+
Current Player: [1m[92m► a[0m
|
| 37 |
+
|
| 38 |
+
[1m[93mPLAYERS[0m
|
| 39 |
+
[93m-------[0m
|
| 40 |
+
|
| 41 |
+
[1m[92m► a[0m
|
| 42 |
+
Victory Points: [97m1[0m
|
| 43 |
+
Resources: None
|
| 44 |
+
Buildings: Settlements: [92m1[0m, Cities: [91m0[0m, Roads: [92m1[0m
|
| 45 |
+
|
| 46 |
+
[97mV[0m
|
| 47 |
+
Victory Points: [97m0[0m
|
| 48 |
+
Resources: None
|
| 49 |
+
Buildings: Settlements: [91m0[0m, Cities: [91m0[0m, Roads: [91m0[0m
|
| 50 |
+
|
| 51 |
+
[1m[93mBOARD[0m
|
| 52 |
+
[93m-----[0m
|
| 53 |
+
Board Tiles: 19 tiles configured
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
[1m[94m>>> Turn 1: V's turn[0m
|
| 57 |
+
[92m✓[0m V built a settlement
|
| 58 |
+
|
| 59 |
+
[1m[96m==================================================[0m
|
| 60 |
+
[1m[96m GAME STATE [0m
|
| 61 |
+
[1m[96m==================================================[0m
|
| 62 |
+
|
| 63 |
+
Turn: [1m1[0m
|
| 64 |
+
Current Player: [1m[92m► V[0m
|
| 65 |
+
|
| 66 |
+
[1m[93mPLAYERS[0m
|
| 67 |
+
[93m-------[0m
|
| 68 |
+
|
| 69 |
+
[97ma[0m
|
| 70 |
+
Victory Points: [97m1[0m
|
| 71 |
+
Resources: None
|
| 72 |
+
Buildings: Settlements: [92m1[0m, Cities: [91m0[0m, Roads: [92m1[0m
|
| 73 |
+
|
| 74 |
+
[1m[92m► V[0m
|
| 75 |
+
Victory Points: [97m1[0m
|
| 76 |
+
Resources: None
|
| 77 |
+
Buildings: Settlements: [92m1[0m, Cities: [91m0[0m, Roads: [91m0[0m
|
| 78 |
+
|
| 79 |
+
[1m[93mBOARD[0m
|
| 80 |
+
[93m-----[0m
|
| 81 |
+
Board Tiles: 19 tiles configured
|
| 82 |
+
|
| 83 |
+
[92m✓[0m V built a road
|
| 84 |
+
|
| 85 |
+
[1m[96m==================================================[0m
|
| 86 |
+
[1m[96m GAME STATE [0m
|
| 87 |
+
[1m[96m==================================================[0m
|
| 88 |
+
|
| 89 |
+
Turn: [1m1[0m
|
| 90 |
+
Current Player: [1m[92m► V[0m
|
| 91 |
+
|
| 92 |
+
[1m[93mPLAYERS[0m
|
| 93 |
+
[93m-------[0m
|
| 94 |
+
|
| 95 |
+
[97ma[0m
|
| 96 |
+
Victory Points: [97m1[0m
|
| 97 |
+
Resources: None
|
| 98 |
+
Buildings: Settlements: [92m1[0m, Cities: [91m0[0m, Roads: [92m1[0m
|
| 99 |
+
|
| 100 |
+
[1m[92m► V[0m
|
| 101 |
+
Victory Points: [97m1[0m
|
| 102 |
+
Resources: None
|
| 103 |
+
Buildings: Settlements: [92m1[0m, Cities: [91m0[0m, Roads: [92m1[0m
|
| 104 |
+
|
| 105 |
+
[1m[93mBOARD[0m
|
| 106 |
+
[93m-----[0m
|
| 107 |
+
Board Tiles: 19 tiles configured
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
[1m[94m>>> Turn 2: V's turn[0m
|
| 111 |
+
|
| 112 |
+
📦 Resources distributed:
|
| 113 |
+
V: [92mWood, Brick, Sheep[0m
|
| 114 |
+
[92m✓[0m V built a settlement
|
| 115 |
+
|
| 116 |
+
[1m[96m==================================================[0m
|
| 117 |
+
[1m[96m GAME STATE [0m
|
| 118 |
+
[1m[96m==================================================[0m
|
| 119 |
+
|
| 120 |
+
Turn: [1m2[0m
|
| 121 |
+
Current Player: [1m[92m► V[0m
|
| 122 |
+
|
| 123 |
+
[1m[93mPLAYERS[0m
|
| 124 |
+
[93m-------[0m
|
| 125 |
+
|
| 126 |
+
[97ma[0m
|
| 127 |
+
Victory Points: [97m1[0m
|
| 128 |
+
Resources: None
|
| 129 |
+
Buildings: Settlements: [92m1[0m, Cities: [91m0[0m, Roads: [92m1[0m
|
| 130 |
+
|
| 131 |
+
[1m[92m► V[0m
|
| 132 |
+
Victory Points: [97m2[0m
|
| 133 |
+
Resources: None
|
| 134 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m1[0m
|
| 135 |
+
|
| 136 |
+
[1m[93mBOARD[0m
|
| 137 |
+
[93m-----[0m
|
| 138 |
+
Board Tiles: 19 tiles configured
|
| 139 |
+
|
| 140 |
+
[92m✓[0m V built a road
|
| 141 |
+
|
| 142 |
+
[1m[96m==================================================[0m
|
| 143 |
+
[1m[96m GAME STATE [0m
|
| 144 |
+
[1m[96m==================================================[0m
|
| 145 |
+
|
| 146 |
+
Turn: [1m2[0m
|
| 147 |
+
Current Player: [1m[92m► V[0m
|
| 148 |
+
|
| 149 |
+
[1m[93mPLAYERS[0m
|
| 150 |
+
[93m-------[0m
|
| 151 |
+
|
| 152 |
+
[97ma[0m
|
| 153 |
+
Victory Points: [97m1[0m
|
| 154 |
+
Resources: None
|
| 155 |
+
Buildings: Settlements: [92m1[0m, Cities: [91m0[0m, Roads: [92m1[0m
|
| 156 |
+
|
| 157 |
+
[1m[92m► V[0m
|
| 158 |
+
Victory Points: [97m2[0m
|
| 159 |
+
Resources: None
|
| 160 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 161 |
+
|
| 162 |
+
[1m[93mBOARD[0m
|
| 163 |
+
[93m-----[0m
|
| 164 |
+
Board Tiles: 19 tiles configured
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
[1m[94m>>> Turn 3: a's turn[0m
|
| 168 |
+
|
| 169 |
+
📦 Resources distributed:
|
| 170 |
+
a: [92mWheat, Brick, Wood[0m
|
| 171 |
+
[92m✓[0m a built a settlement
|
| 172 |
+
|
| 173 |
+
[1m[96m==================================================[0m
|
| 174 |
+
[1m[96m GAME STATE [0m
|
| 175 |
+
[1m[96m==================================================[0m
|
| 176 |
+
|
| 177 |
+
Turn: [1m3[0m
|
| 178 |
+
Current Player: [1m[92m► a[0m
|
| 179 |
+
|
| 180 |
+
[1m[93mPLAYERS[0m
|
| 181 |
+
[93m-------[0m
|
| 182 |
+
|
| 183 |
+
[1m[92m► a[0m
|
| 184 |
+
Victory Points: [97m2[0m
|
| 185 |
+
Resources: None
|
| 186 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m1[0m
|
| 187 |
+
|
| 188 |
+
[97mV[0m
|
| 189 |
+
Victory Points: [97m2[0m
|
| 190 |
+
Resources: None
|
| 191 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 192 |
+
|
| 193 |
+
[1m[93mBOARD[0m
|
| 194 |
+
[93m-----[0m
|
| 195 |
+
Board Tiles: 19 tiles configured
|
| 196 |
+
|
| 197 |
+
[92m✓[0m a built a road
|
| 198 |
+
|
| 199 |
+
[1m[96m==================================================[0m
|
| 200 |
+
[1m[96m GAME STATE [0m
|
| 201 |
+
[1m[96m==================================================[0m
|
| 202 |
+
|
| 203 |
+
Turn: [1m3[0m
|
| 204 |
+
Current Player: [1m[92m► a[0m
|
| 205 |
+
|
| 206 |
+
[1m[93mPLAYERS[0m
|
| 207 |
+
[93m-------[0m
|
| 208 |
+
|
| 209 |
+
[1m[92m► a[0m
|
| 210 |
+
Victory Points: [97m2[0m
|
| 211 |
+
Resources: None
|
| 212 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 213 |
+
|
| 214 |
+
[97mV[0m
|
| 215 |
+
Victory Points: [97m2[0m
|
| 216 |
+
Resources: None
|
| 217 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 218 |
+
|
| 219 |
+
[1m[93mBOARD[0m
|
| 220 |
+
[93m-----[0m
|
| 221 |
+
Board Tiles: 19 tiles configured
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
[1m[94m>>> Turn 4: a's turn[0m
|
| 225 |
+
|
| 226 |
+
[1m🎲 a rolled: 5 + 3 = [97m8[0m
|
| 227 |
+
[92m✓[0m a rolled the dice
|
| 228 |
+
|
| 229 |
+
[1m[96m==================================================[0m
|
| 230 |
+
[1m[96m GAME STATE [0m
|
| 231 |
+
[1m[96m==================================================[0m
|
| 232 |
+
|
| 233 |
+
Turn: [1m4[0m
|
| 234 |
+
Current Player: [1m[92m► a[0m
|
| 235 |
+
|
| 236 |
+
[1m[93mPLAYERS[0m
|
| 237 |
+
[93m-------[0m
|
| 238 |
+
|
| 239 |
+
[1m[92m► a[0m
|
| 240 |
+
Victory Points: [97m2[0m
|
| 241 |
+
Resources: None
|
| 242 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 243 |
+
|
| 244 |
+
[97mV[0m
|
| 245 |
+
Victory Points: [97m2[0m
|
| 246 |
+
Resources: None
|
| 247 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 248 |
+
|
| 249 |
+
[1m[93mBOARD[0m
|
| 250 |
+
[93m-----[0m
|
| 251 |
+
Board Tiles: 19 tiles configured
|
| 252 |
+
|
| 253 |
+
[91m✗[0m a proposed a trade
|
| 254 |
+
[91mError: V doesn't have the required cards[0m
|
| 255 |
+
|
| 256 |
+
[1m[96m==================================================[0m
|
| 257 |
+
[1m[96m GAME STATE [0m
|
| 258 |
+
[1m[96m==================================================[0m
|
| 259 |
+
|
| 260 |
+
Turn: [1m4[0m
|
| 261 |
+
Current Player: [1m[92m► a[0m
|
| 262 |
+
|
| 263 |
+
[1m[93mPLAYERS[0m
|
| 264 |
+
[93m-------[0m
|
| 265 |
+
|
| 266 |
+
[1m[92m► a[0m
|
| 267 |
+
Victory Points: [97m2[0m
|
| 268 |
+
Resources: None
|
| 269 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 270 |
+
|
| 271 |
+
[97mV[0m
|
| 272 |
+
Victory Points: [97m2[0m
|
| 273 |
+
Resources: None
|
| 274 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 275 |
+
|
| 276 |
+
[1m[93mBOARD[0m
|
| 277 |
+
[93m-----[0m
|
| 278 |
+
Board Tiles: 19 tiles configured
|
| 279 |
+
|
| 280 |
+
[92m✓[0m a proposed a trade
|
| 281 |
+
|
| 282 |
+
[1m[96m==================================================[0m
|
| 283 |
+
[1m[96m GAME STATE [0m
|
| 284 |
+
[1m[96m==================================================[0m
|
| 285 |
+
|
| 286 |
+
Turn: [1m4[0m
|
| 287 |
+
Current Player: [1m[92m► a[0m
|
| 288 |
+
|
| 289 |
+
[1m[93mPLAYERS[0m
|
| 290 |
+
[93m-------[0m
|
| 291 |
+
|
| 292 |
+
[1m[92m► a[0m
|
| 293 |
+
Victory Points: [97m2[0m
|
| 294 |
+
Resources: None
|
| 295 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 296 |
+
|
| 297 |
+
[97mV[0m
|
| 298 |
+
Victory Points: [97m2[0m
|
| 299 |
+
Resources: None
|
| 300 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 301 |
+
|
| 302 |
+
[1m[93mBOARD[0m
|
| 303 |
+
[93m-----[0m
|
| 304 |
+
Board Tiles: 19 tiles configured
|
| 305 |
+
|
| 306 |
+
[92m✓[0m a proposed a trade
|
| 307 |
+
|
| 308 |
+
[1m[96m==================================================[0m
|
| 309 |
+
[1m[96m GAME STATE [0m
|
| 310 |
+
[1m[96m==================================================[0m
|
| 311 |
+
|
| 312 |
+
Turn: [1m4[0m
|
| 313 |
+
Current Player: [1m[92m► a[0m
|
| 314 |
+
|
| 315 |
+
[1m[93mPLAYERS[0m
|
| 316 |
+
[93m-------[0m
|
| 317 |
+
|
| 318 |
+
[1m[92m► a[0m
|
| 319 |
+
Victory Points: [97m2[0m
|
| 320 |
+
Resources: None
|
| 321 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 322 |
+
|
| 323 |
+
[97mV[0m
|
| 324 |
+
Victory Points: [97m2[0m
|
| 325 |
+
Resources: None
|
| 326 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 327 |
+
|
| 328 |
+
[1m[93mBOARD[0m
|
| 329 |
+
[93m-----[0m
|
| 330 |
+
Board Tiles: 19 tiles configured
|
| 331 |
+
|
| 332 |
+
[91m✗[0m a proposed a trade
|
| 333 |
+
[91mError: V rejected your trade offer[0m
|
| 334 |
+
|
| 335 |
+
[1m[96m==================================================[0m
|
| 336 |
+
[1m[96m GAME STATE [0m
|
| 337 |
+
[1m[96m==================================================[0m
|
| 338 |
+
|
| 339 |
+
Turn: [1m4[0m
|
| 340 |
+
Current Player: [1m[92m► a[0m
|
| 341 |
+
|
| 342 |
+
[1m[93mPLAYERS[0m
|
| 343 |
+
[93m-------[0m
|
| 344 |
+
|
| 345 |
+
[1m[92m► a[0m
|
| 346 |
+
Victory Points: [97m2[0m
|
| 347 |
+
Resources: None
|
| 348 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 349 |
+
|
| 350 |
+
[97mV[0m
|
| 351 |
+
Victory Points: [97m2[0m
|
| 352 |
+
Resources: None
|
| 353 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 354 |
+
|
| 355 |
+
[1m[93mBOARD[0m
|
| 356 |
+
[93m-----[0m
|
| 357 |
+
Board Tiles: 19 tiles configured
|
| 358 |
+
|
| 359 |
+
[92m✓[0m a ended their turn
|
| 360 |
+
|
| 361 |
+
[1m[96m==================================================[0m
|
| 362 |
+
[1m[96m GAME STATE [0m
|
| 363 |
+
[1m[96m==================================================[0m
|
| 364 |
+
|
| 365 |
+
Turn: [1m4[0m
|
| 366 |
+
Current Player: [1m[92m► a[0m
|
| 367 |
+
|
| 368 |
+
[1m[93mPLAYERS[0m
|
| 369 |
+
[93m-------[0m
|
| 370 |
+
|
| 371 |
+
[1m[92m► a[0m
|
| 372 |
+
Victory Points: [97m2[0m
|
| 373 |
+
Resources: None
|
| 374 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 375 |
+
|
| 376 |
+
[97mV[0m
|
| 377 |
+
Victory Points: [97m2[0m
|
| 378 |
+
Resources: None
|
| 379 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 380 |
+
|
| 381 |
+
[1m[93mBOARD[0m
|
| 382 |
+
[93m-----[0m
|
| 383 |
+
Board Tiles: 19 tiles configured
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
[1m[94m>>> Turn 5: V's turn[0m
|
| 387 |
+
|
| 388 |
+
[1m🎲 V rolled: 6 + 1 = [91m7[0m
|
| 389 |
+
[92m✓[0m V rolled the dice
|
| 390 |
+
|
| 391 |
+
[1m[96m==================================================[0m
|
| 392 |
+
[1m[96m GAME STATE [0m
|
| 393 |
+
[1m[96m==================================================[0m
|
| 394 |
+
|
| 395 |
+
Turn: [1m5[0m
|
| 396 |
+
Current Player: [1m[92m► V[0m
|
| 397 |
+
|
| 398 |
+
[1m[93mPLAYERS[0m
|
| 399 |
+
[93m-------[0m
|
| 400 |
+
|
| 401 |
+
[97ma[0m
|
| 402 |
+
Victory Points: [97m2[0m
|
| 403 |
+
Resources: None
|
| 404 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 405 |
+
|
| 406 |
+
[1m[92m► V[0m
|
| 407 |
+
Victory Points: [97m2[0m
|
| 408 |
+
Resources: None
|
| 409 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 410 |
+
|
| 411 |
+
[1m[93mBOARD[0m
|
| 412 |
+
[93m-----[0m
|
| 413 |
+
Board Tiles: 19 tiles configured
|
| 414 |
+
|
| 415 |
+
[92m✓[0m V ended their turn
|
| 416 |
+
|
| 417 |
+
[1m[96m==================================================[0m
|
| 418 |
+
[1m[96m GAME STATE [0m
|
| 419 |
+
[1m[96m==================================================[0m
|
| 420 |
+
|
| 421 |
+
Turn: [1m5[0m
|
| 422 |
+
Current Player: [1m[92m► V[0m
|
| 423 |
+
|
| 424 |
+
[1m[93mPLAYERS[0m
|
| 425 |
+
[93m-------[0m
|
| 426 |
+
|
| 427 |
+
[97ma[0m
|
| 428 |
+
Victory Points: [97m2[0m
|
| 429 |
+
Resources: None
|
| 430 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 431 |
+
|
| 432 |
+
[1m[92m► V[0m
|
| 433 |
+
Victory Points: [97m2[0m
|
| 434 |
+
Resources: None
|
| 435 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 436 |
+
|
| 437 |
+
[1m[93mBOARD[0m
|
| 438 |
+
[93m-----[0m
|
| 439 |
+
Board Tiles: 19 tiles configured
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
[1m[94m>>> Turn 6: a's turn[0m
|
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>
|