electro-sb commited on
Commit
100a6dd
·
0 Parent(s):

first commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +10 -0
  2. .gitignore +24 -0
  3. Dockerfile +55 -0
  4. LICENSE-ASSETS.md +29 -0
  5. README.md +90 -0
  6. app.py +9 -0
  7. chess_engine/__init__.py +13 -0
  8. chess_engine/ai/__init__.py +4 -0
  9. chess_engine/ai/evaluation.py +535 -0
  10. chess_engine/ai/stockfish_wrapper.py +422 -0
  11. chess_engine/api/__init__.py +3 -0
  12. chess_engine/api/cli.py +223 -0
  13. chess_engine/api/game_controller.py +402 -0
  14. chess_engine/api/rest_api.py +271 -0
  15. chess_engine/board.py +341 -0
  16. chess_engine/pieces.py +316 -0
  17. chess_engine/promotion.py +306 -0
  18. docker-entrypoint.sh +19 -0
  19. main.py +62 -0
  20. requirements.api.txt +14 -0
  21. web/.npmrc +2 -0
  22. web/IMPLEMENTATION_DETAILS.md +82 -0
  23. web/IMPLEMENTATION_SUMMARY.md +88 -0
  24. web/OVERVIEW.md +91 -0
  25. web/README.md +125 -0
  26. web/SETUP_GUIDE.md +123 -0
  27. web/SETUP_INSTRUCTIONS.md +108 -0
  28. web/TYPESCRIPT_FIXES.md +69 -0
  29. web/WSL_SETUP.md +100 -0
  30. web/copy-assets-fixed.sh +31 -0
  31. web/copy-assets.sh +16 -0
  32. web/fix-dependencies.bat +8 -0
  33. web/fix-dependencies.sh +8 -0
  34. web/fix-react-typescript.sh +62 -0
  35. web/index.html +13 -0
  36. web/node_modules/.bin/acorn +1 -0
  37. web/node_modules/.bin/autoprefixer +1 -0
  38. web/node_modules/.bin/browserslist +1 -0
  39. web/node_modules/.bin/cssesc +1 -0
  40. web/node_modules/.bin/esbuild +1 -0
  41. web/node_modules/.bin/glob +1 -0
  42. web/node_modules/.bin/jiti +1 -0
  43. web/node_modules/.bin/jsesc +1 -0
  44. web/node_modules/.bin/json5 +1 -0
  45. web/node_modules/.bin/loose-envify +1 -0
  46. web/node_modules/.bin/nanoid +1 -0
  47. web/node_modules/.bin/node-which +1 -0
  48. web/node_modules/.bin/parser +1 -0
  49. web/node_modules/.bin/resolve +1 -0
  50. web/node_modules/.bin/rollup +1 -0
.gitattributes ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Image files
2
+ *.png filter=lfs diff=lfs merge=lfs -text
3
+ *.jpg filter=lfs diff=lfs merge=lfs -text
4
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
5
+ *.gif filter=lfs diff=lfs merge=lfs -text
6
+ *.svg filter=lfs diff=lfs merge=lfs -text
7
+
8
+ # Specifically track assets directory with LFS
9
+ frontend/assets/** filter=lfs diff=lfs merge=lfs -text
10
+ frontend/streamlit/assets/** filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .pytest_cache
12
+ .env
13
+ .kiro
14
+ .vscode
15
+ # Local configuration files
16
+ .env
17
+ .env.local
18
+ .env.development.local
19
+ .env.test.local
20
+ .env.production.local
21
+ .env.staging.local
22
+ .env.*.local
23
+
24
+
Dockerfile ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build the React frontend (can remain Alpine, it's a separate build environment)
2
+ FROM node:18-alpine AS frontend-builder
3
+
4
+ WORKDIR /app/frontend/web
5
+
6
+ COPY frontend/web/package*.json ./
7
+ RUN npm install
8
+ COPY frontend/web/ ./
9
+
10
+ # Stage 2: Build the Python backend (switching to Debian-based image)
11
+ FROM python:3.11-slim AS backend-builder
12
+
13
+ WORKDIR /app
14
+
15
+ # Install build tools using apt-get
16
+ RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/*
17
+
18
+ COPY requirements.api.txt .
19
+ RUN pip install --no-cache-dir -r requirements.api.txt
20
+
21
+ COPY chess_engine/ ./chess_engine
22
+ COPY main.py .
23
+ COPY app.py .
24
+
25
+ # Stage 3: Final image (switching to Debian-based image)
26
+ FROM python:3.11-slim
27
+
28
+ # Install Node.js, npm, Stockfish, and dos2unix using apt-get
29
+ RUN apt-get update && \
30
+ apt-get install -y --no-install-recommends curl dos2unix && \
31
+ curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
32
+ apt-get install -y --no-install-recommends nodejs stockfish && \
33
+ rm -rf /var/lib/apt/lists/*
34
+
35
+ WORKDIR /app
36
+
37
+ # Copy the frontend code and dependencies
38
+ COPY --from=frontend-builder /app/frontend/web/ ./frontend/web/
39
+
40
+ # Copy the installed Python dependencies
41
+ COPY --from=backend-builder /usr/local/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/
42
+ COPY --from=backend-builder /app/chess_engine ./chess_engine
43
+ COPY --from=backend-builder /app/main.py .
44
+ COPY --from=backend-builder /app/app.py .
45
+
46
+ # Copy the entrypoint script and ensure it's executable
47
+ COPY docker-entrypoint.sh .
48
+ RUN dos2unix docker-entrypoint.sh && \
49
+ chmod +x docker-entrypoint.sh
50
+
51
+ # Expose the ports
52
+ EXPOSE 8000
53
+ EXPOSE 5173
54
+
55
+ ENTRYPOINT ["./docker-entrypoint.sh"]
LICENSE-ASSETS.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Asset Licensing Information
2
+
3
+ ## Chess Pieces and Board Squares
4
+ The chess piece images and board square assets in this project were sourced from [OpenGameArt.org](https://opengameart.org/) under the [Creative Commons Attribution-ShareAlike (CC-BY-SA) license](https://creativecommons.org/licenses/by-sa/4.0/).
5
+
6
+ ### Attribution
7
+ - Source: "Chess Pieces and Board squares" from OpenGameArt.org
8
+ - Original Author: As specified on OpenGameArt.org
9
+ - License: Creative Commons Attribution-ShareAlike (CC-BY-SA)
10
+ - Link: https://opengameart.org/
11
+
12
+ ### License Compliance
13
+ These assets are used in compliance with the CC-BY-SA license, which requires:
14
+ 1. **Attribution**: We have provided appropriate credit to the original creator.
15
+ 2. **ShareAlike**: Any derivative works incorporating these assets must be distributed under the same license terms.
16
+
17
+ ## License Compatibility Notice
18
+
19
+ This project uses the MIT License for code, while some assets are under the CC-BY-SA license. Please note:
20
+
21
+ 1. The MIT License applies to all software code in this repository.
22
+ 2. The CC-BY-SA license applies to the chess piece images and board square assets in the `frontend/assets` and `frontend/streamlit/assets` directories.
23
+
24
+ ### Important:
25
+ If you redistribute this project or create derivative works:
26
+ - You may use the code under the terms of the MIT License.
27
+ - You must comply with the CC-BY-SA license for the chess piece assets, which includes providing attribution and sharing any modifications to these assets under the same CC-BY-SA license.
28
+
29
+ This dual licensing approach is common in projects that combine code with creative assets from different sources.
README.md ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chess Engine with Stockfish Integration
2
+
3
+ ---
4
+ title: Image Captioning with BLIP
5
+ emoji: ♟️🏁♟️
6
+ colorFrom: green
7
+ colorTo: orange
8
+ sdk: docker
9
+ app_port: 5173
10
+ ---
11
+
12
+
13
+
14
+ A Python chess engine with Stockfish integration, providing both a command-line interface and a REST API for playing chess against an AI opponent.
15
+
16
+ ## Features
17
+
18
+ - **AI Integration**: Play against Stockfish, one of the strongest chess engines available
19
+ - **Multiple Interfaces**:
20
+ - Command-line interface for quick games
21
+ - REST API for integration with web or desktop applications
22
+ - Streamlit web interface for graphical gameplay
23
+ - **Adjustable Difficulty**: Six difficulty levels from beginner to master
24
+ - **Position Analysis**: Get detailed evaluation of board positions
25
+ - **Move Hints**: Receive suggestions and explanations for optimal moves
26
+ - **Notation Support**: Works with both UCI (e2e4) and SAN (e4) chess notations
27
+ - **Game Management**: Start, play, analyze, and save games
28
+ - **Comprehensive Evaluation**: Material balance, piece positioning, king safety, mobility, and pawn structure
29
+
30
+ ## Requirements
31
+
32
+ - Python 3.8+
33
+ - Stockfish chess engine (optional, but recommended for AI play)
34
+ - Dependencies listed in requirements.txt
35
+
36
+
37
+ ## API Documentation
38
+
39
+ For information please refer Dockerfile
40
+
41
+ ### Example API Usage
42
+
43
+ Start a new game:
44
+ ```bash
45
+ curl -X POST "http://localhost:8000/game/new" \
46
+ -H "Content-Type: application/json" \
47
+ -d '{"player_color": "white", "difficulty": "medium"}'
48
+ ```
49
+
50
+ Make a move:
51
+ ```bash
52
+ curl -X POST "http://localhost:8000/game/move" \
53
+ -H "Content-Type: application/json" \
54
+ -d '{"move": "e2e4"}'
55
+ ```
56
+
57
+ Get current game state:
58
+ ```bash
59
+ curl -X GET "http://localhost:8000/game/state"
60
+ ```
61
+
62
+ ## Testing
63
+
64
+ Run the test suite:
65
+
66
+ ```bash
67
+ python test/run_tests.py
68
+ ```
69
+
70
+ For more detailed output:
71
+
72
+ ```bash
73
+ python test/run_tests.py -v
74
+ ```
75
+
76
+ The test suite includes:
77
+ - Board representation and move validation tests
78
+ - Position evaluation tests
79
+ - Game controller tests
80
+ - REST API endpoint tests
81
+
82
+ ## License
83
+
84
+ ### Code
85
+ The code in this repository is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
86
+
87
+ ### Assets
88
+ The chess piece images and board square assets are licensed under the Creative Commons Attribution-ShareAlike (CC-BY-SA) license. See the [LICENSE-ASSETS.md](LICENSE-ASSETS.md) file for details on attribution and compliance requirements.
89
+
90
+ **Note:** If you redistribute this project or create derivative works, you must comply with both licenses - MIT for the code and CC-BY-SA for the assets.
app.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chess Engine API Server
3
+ This file provides the FastAPI app for the chess engine.
4
+ """
5
+
6
+ from chess_engine.api.rest_api import app
7
+
8
+ # This file is used by Uvicorn to run the API server
9
+ # The app variable is imported from chess_engine.api.rest_api
chess_engine/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chess_engine/__init__.py
2
+
3
+ """
4
+ Chess Engine with Stockfish Integration
5
+
6
+ A Python chess engine with Stockfish integration, providing both a
7
+ command-line interface and a REST API.
8
+ """
9
+
10
+ __version__ = "0.1.0"
11
+
12
+ from chess_engine.board import ChessBoard
13
+ from chess_engine.pieces import ChessPiece, PieceManager
chess_engine/ai/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # chess_engine/ai/__init__.py
2
+
3
+ from chess_engine.ai.evaluation import ChessEvaluator, PositionEvaluation
4
+ from chess_engine.ai.stockfish_wrapper import StockfishWrapper, DifficultyLevel
chess_engine/ai/evaluation.py ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chess_engine/ai/evaluation.py
2
+
3
+ import chess
4
+ from typing import Dict, List, Tuple, Optional
5
+ from dataclasses import dataclass
6
+ import math
7
+
8
+ @dataclass
9
+ class PositionEvaluation:
10
+ """Complete position evaluation data"""
11
+ total_score: float
12
+ material_score: float
13
+ positional_score: float
14
+ safety_score: float
15
+ mobility_score: float
16
+ pawn_structure_score: float
17
+ endgame_score: float
18
+ white_advantage: float
19
+ evaluation_breakdown: Dict[str, float]
20
+
21
+ class ChessEvaluator:
22
+ """
23
+ Chess position evaluator with various strategic factors
24
+ """
25
+
26
+ # Piece values in centipawns
27
+ PIECE_VALUES = {
28
+ chess.PAWN: 100,
29
+ chess.KNIGHT: 320,
30
+ chess.BISHOP: 330,
31
+ chess.ROOK: 500,
32
+ chess.QUEEN: 900,
33
+ chess.KING: 20000
34
+ }
35
+
36
+ # Piece-square tables for positional evaluation
37
+ PAWN_TABLE = [
38
+ [0, 0, 0, 0, 0, 0, 0, 0],
39
+ [50, 50, 50, 50, 50, 50, 50, 50],
40
+ [10, 10, 20, 30, 30, 20, 10, 10],
41
+ [5, 5, 10, 25, 25, 10, 5, 5],
42
+ [0, 0, 0, 20, 20, 0, 0, 0],
43
+ [5, -5,-10, 0, 0,-10, -5, 5],
44
+ [5, 10, 10,-20,-20, 10, 10, 5],
45
+ [0, 0, 0, 0, 0, 0, 0, 0]
46
+ ]
47
+
48
+ KNIGHT_TABLE = [
49
+ [-50,-40,-30,-30,-30,-30,-40,-50],
50
+ [-40,-20, 0, 0, 0, 0,-20,-40],
51
+ [-30, 0, 10, 15, 15, 10, 0,-30],
52
+ [-30, 5, 15, 20, 20, 15, 5,-30],
53
+ [-30, 0, 15, 20, 20, 15, 0,-30],
54
+ [-30, 5, 10, 15, 15, 10, 5,-30],
55
+ [-40,-20, 0, 5, 5, 0,-20,-40],
56
+ [-50,-40,-30,-30,-30,-30,-40,-50]
57
+ ]
58
+
59
+ BISHOP_TABLE = [
60
+ [-20,-10,-10,-10,-10,-10,-10,-20],
61
+ [-10, 0, 0, 0, 0, 0, 0,-10],
62
+ [-10, 0, 5, 10, 10, 5, 0,-10],
63
+ [-10, 5, 5, 10, 10, 5, 5,-10],
64
+ [-10, 0, 10, 10, 10, 10, 0,-10],
65
+ [-10, 10, 10, 10, 10, 10, 10,-10],
66
+ [-10, 5, 0, 0, 0, 0, 5,-10],
67
+ [-20,-10,-10,-10,-10,-10,-10,-20]
68
+ ]
69
+
70
+ ROOK_TABLE = [
71
+ [0, 0, 0, 0, 0, 0, 0, 0],
72
+ [5, 10, 10, 10, 10, 10, 10, 5],
73
+ [-5, 0, 0, 0, 0, 0, 0, -5],
74
+ [-5, 0, 0, 0, 0, 0, 0, -5],
75
+ [-5, 0, 0, 0, 0, 0, 0, -5],
76
+ [-5, 0, 0, 0, 0, 0, 0, -5],
77
+ [-5, 0, 0, 0, 0, 0, 0, -5],
78
+ [0, 0, 0, 5, 5, 0, 0, 0]
79
+ ]
80
+
81
+ QUEEN_TABLE = [
82
+ [-20,-10,-10, -5, -5,-10,-10,-20],
83
+ [-10, 0, 0, 0, 0, 0, 0,-10],
84
+ [-10, 0, 5, 5, 5, 5, 0,-10],
85
+ [-5, 0, 5, 5, 5, 5, 0, -5],
86
+ [0, 0, 5, 5, 5, 5, 0, -5],
87
+ [-10, 5, 5, 5, 5, 5, 0,-10],
88
+ [-10, 0, 5, 0, 0, 0, 0,-10],
89
+ [-20,-10,-10, -5, -5,-10,-10,-20]
90
+ ]
91
+
92
+ KING_TABLE_MIDDLEGAME = [
93
+ [-30,-40,-40,-50,-50,-40,-40,-30],
94
+ [-30,-40,-40,-50,-50,-40,-40,-30],
95
+ [-30,-40,-40,-50,-50,-40,-40,-30],
96
+ [-30,-40,-40,-50,-50,-40,-40,-30],
97
+ [-20,-30,-30,-40,-40,-30,-30,-20],
98
+ [-10,-20,-20,-20,-20,-20,-20,-10],
99
+ [20, 20, 0, 0, 0, 0, 20, 20],
100
+ [20, 30, 10, 0, 0, 10, 30, 20]
101
+ ]
102
+
103
+ KING_TABLE_ENDGAME = [
104
+ [-50,-40,-30,-20,-20,-30,-40,-50],
105
+ [-30,-20,-10, 0, 0,-10,-20,-30],
106
+ [-30,-10, 20, 30, 30, 20,-10,-30],
107
+ [-30,-10, 30, 40, 40, 30,-10,-30],
108
+ [-30,-10, 30, 40, 40, 30,-10,-30],
109
+ [-30,-10, 20, 30, 30, 20,-10,-30],
110
+ [-30,-30, 0, 0, 0, 0,-30,-30],
111
+ [-50,-30,-30,-30,-30,-30,-30,-50]
112
+ ]
113
+
114
+ def __init__(self):
115
+ """Initialize the evaluator"""
116
+ self.piece_square_tables = {
117
+ chess.PAWN: self.PAWN_TABLE,
118
+ chess.KNIGHT: self.KNIGHT_TABLE,
119
+ chess.BISHOP: self.BISHOP_TABLE,
120
+ chess.ROOK: self.ROOK_TABLE,
121
+ chess.QUEEN: self.QUEEN_TABLE,
122
+ chess.KING: self.KING_TABLE_MIDDLEGAME
123
+ }
124
+
125
+ def evaluate_position(self, board: chess.Board) -> PositionEvaluation:
126
+ """
127
+ Comprehensive position evaluation
128
+
129
+ Args:
130
+ board: Chess board to evaluate
131
+
132
+ Returns:
133
+ PositionEvaluation object with detailed analysis
134
+ """
135
+ if board.is_checkmate():
136
+ score = -20000 if board.turn == chess.WHITE else 20000
137
+ return PositionEvaluation(
138
+ total_score=score,
139
+ material_score=score,
140
+ positional_score=0,
141
+ safety_score=0,
142
+ mobility_score=0,
143
+ pawn_structure_score=0,
144
+ endgame_score=0,
145
+ white_advantage=score,
146
+ evaluation_breakdown={"checkmate": score}
147
+ )
148
+
149
+ if board.is_stalemate() or board.is_insufficient_material():
150
+ return PositionEvaluation(
151
+ total_score=0,
152
+ material_score=0,
153
+ positional_score=0,
154
+ safety_score=0,
155
+ mobility_score=0,
156
+ pawn_structure_score=0,
157
+ endgame_score=0,
158
+ white_advantage=0,
159
+ evaluation_breakdown={"draw": 0}
160
+ )
161
+
162
+ # Calculate individual evaluation components
163
+ material_score = self._evaluate_material(board)
164
+ positional_score = self._evaluate_position_tables(board)
165
+ safety_score = self._evaluate_king_safety(board)
166
+ mobility_score = self._evaluate_mobility(board)
167
+ pawn_structure_score = self._evaluate_pawn_structure(board)
168
+ endgame_score = self._evaluate_endgame_factors(board)
169
+
170
+ # Combine scores
171
+ total_score = (
172
+ material_score +
173
+ positional_score +
174
+ safety_score +
175
+ mobility_score +
176
+ pawn_structure_score +
177
+ endgame_score
178
+ )
179
+
180
+ # Create breakdown
181
+ breakdown = {
182
+ "material": material_score,
183
+ "positional": positional_score,
184
+ "safety": safety_score,
185
+ "mobility": mobility_score,
186
+ "pawn_structure": pawn_structure_score,
187
+ "endgame": endgame_score
188
+ }
189
+
190
+ return PositionEvaluation(
191
+ total_score=total_score,
192
+ material_score=material_score,
193
+ positional_score=positional_score,
194
+ safety_score=safety_score,
195
+ mobility_score=mobility_score,
196
+ pawn_structure_score=pawn_structure_score,
197
+ endgame_score=endgame_score,
198
+ white_advantage=total_score,
199
+ evaluation_breakdown=breakdown
200
+ )
201
+
202
+ def _evaluate_material(self, board: chess.Board) -> float:
203
+ """Evaluate material balance"""
204
+ score = 0
205
+
206
+ for square in chess.SQUARES:
207
+ piece = board.piece_at(square)
208
+ if piece:
209
+ value = self.PIECE_VALUES[piece.piece_type]
210
+ score += value if piece.color == chess.WHITE else -value
211
+
212
+ return score
213
+
214
+ def _evaluate_position_tables(self, board: chess.Board) -> float:
215
+ """Evaluate piece positions using piece-square tables"""
216
+ score = 0
217
+ is_endgame = self._is_endgame(board)
218
+
219
+ for square in chess.SQUARES:
220
+ piece = board.piece_at(square)
221
+ if piece:
222
+ rank = chess.square_rank(square)
223
+ file = chess.square_file(square)
224
+
225
+ # Choose appropriate table for king
226
+ if piece.piece_type == chess.KING:
227
+ table = self.KING_TABLE_ENDGAME if is_endgame else self.KING_TABLE_MIDDLEGAME
228
+ else:
229
+ table = self.piece_square_tables[piece.piece_type]
230
+
231
+ # Flip table for black pieces
232
+ if piece.color == chess.WHITE:
233
+ value = table[rank][file]
234
+ else:
235
+ value = -table[7-rank][file]
236
+
237
+ score += value
238
+
239
+ return score
240
+
241
+ def _evaluate_king_safety(self, board: chess.Board) -> float:
242
+ """Evaluate king safety"""
243
+ score = 0
244
+
245
+ # Check for king exposure
246
+ for color in [chess.WHITE, chess.BLACK]:
247
+ king_square = board.king(color)
248
+ if king_square is None:
249
+ continue
250
+
251
+ # Count attackers around king
252
+ attackers = 0
253
+ defenders = 0
254
+
255
+ for square in chess.SQUARES:
256
+ if chess.square_distance(king_square, square) <= 2:
257
+ if board.is_attacked_by(not color, square):
258
+ attackers += 1
259
+ if board.is_attacked_by(color, square):
260
+ defenders += 1
261
+
262
+ safety = (defenders - attackers) * 10
263
+ score += safety if color == chess.WHITE else -safety
264
+
265
+ return score
266
+
267
+ def _evaluate_mobility(self, board: chess.Board) -> float:
268
+ """Evaluate piece mobility"""
269
+ white_mobility = len(list(board.legal_moves)) if board.turn == chess.WHITE else 0
270
+
271
+ # Switch turn to count black mobility
272
+ board.turn = not board.turn
273
+ black_mobility = len(list(board.legal_moves)) if board.turn == chess.BLACK else 0
274
+ board.turn = not board.turn # Switch back
275
+
276
+ return (white_mobility - black_mobility) * 1.5
277
+
278
+ def _evaluate_pawn_structure(self, board: chess.Board) -> float:
279
+ """Evaluate pawn structure"""
280
+ score = 0
281
+
282
+ # Get pawn positions
283
+ white_pawns = [sq for sq in chess.SQUARES if board.piece_at(sq) and
284
+ board.piece_at(sq).piece_type == chess.PAWN and
285
+ board.piece_at(sq).color == chess.WHITE]
286
+
287
+ black_pawns = [sq for sq in chess.SQUARES if board.piece_at(sq) and
288
+ board.piece_at(sq).piece_type == chess.PAWN and
289
+ board.piece_at(sq).color == chess.BLACK]
290
+
291
+ # Evaluate doubled pawns
292
+ score += self._evaluate_doubled_pawns(white_pawns, chess.WHITE)
293
+ score += self._evaluate_doubled_pawns(black_pawns, chess.BLACK)
294
+
295
+ # Evaluate isolated pawns
296
+ score += self._evaluate_isolated_pawns(white_pawns, chess.WHITE)
297
+ score += self._evaluate_isolated_pawns(black_pawns, chess.BLACK)
298
+
299
+ # Evaluate passed pawns
300
+ score += self._evaluate_passed_pawns(board, white_pawns, chess.WHITE)
301
+ score += self._evaluate_passed_pawns(board, black_pawns, chess.BLACK)
302
+
303
+ return score
304
+
305
+ def _evaluate_doubled_pawns(self, pawns: List[int], color: chess.Color) -> float:
306
+ """Evaluate doubled pawns penalty"""
307
+ files = {}
308
+ for pawn in pawns:
309
+ file = chess.square_file(pawn)
310
+ files[file] = files.get(file, 0) + 1
311
+
312
+ doubled_count = sum(max(0, count - 1) for count in files.values())
313
+ penalty = doubled_count * -20
314
+
315
+ return penalty if color == chess.WHITE else -penalty
316
+
317
+ def _evaluate_isolated_pawns(self, pawns: List[int], color: chess.Color) -> float:
318
+ """Evaluate isolated pawns penalty"""
319
+ files = set(chess.square_file(pawn) for pawn in pawns)
320
+ isolated_count = 0
321
+
322
+ for file in files:
323
+ if (file - 1 not in files) and (file + 1 not in files):
324
+ isolated_count += 1
325
+
326
+ penalty = isolated_count * -15
327
+ return penalty if color == chess.WHITE else -penalty
328
+
329
+ def _evaluate_passed_pawns(self, board: chess.Board, pawns: List[int], color: chess.Color) -> float:
330
+ """Evaluate passed pawns bonus"""
331
+ bonus = 0
332
+ opponent_color = not color
333
+
334
+ for pawn in pawns:
335
+ file = chess.square_file(pawn)
336
+ rank = chess.square_rank(pawn)
337
+
338
+ # Check if pawn is passed
339
+ is_passed = True
340
+ direction = 1 if color == chess.WHITE else -1
341
+
342
+ # Check files that could block this pawn
343
+ for check_file in [file - 1, file, file + 1]:
344
+ if 0 <= check_file <= 7:
345
+ for check_rank in range(rank + direction, 8 if color == chess.WHITE else -1, direction):
346
+ if 0 <= check_rank <= 7:
347
+ square = chess.square(check_file, check_rank)
348
+ piece = board.piece_at(square)
349
+ if piece and piece.piece_type == chess.PAWN and piece.color == opponent_color:
350
+ is_passed = False
351
+ break
352
+ if not is_passed:
353
+ break
354
+
355
+ if is_passed:
356
+ # Bonus increases with advancement
357
+ advancement = rank if color == chess.WHITE else 7 - rank
358
+ bonus += advancement * 10
359
+
360
+ return bonus if color == chess.WHITE else -bonus
361
+
362
+ def _is_endgame(self, board: chess.Board) -> bool:
363
+ """
364
+ Determine if the position is in the endgame
365
+
366
+ Args:
367
+ board: Chess board to evaluate
368
+
369
+ Returns:
370
+ True if position is in endgame, False otherwise
371
+ """
372
+ # Count major pieces (queens and rooks)
373
+ queens = 0
374
+ rooks = 0
375
+ total_material = 0
376
+
377
+ for square in chess.SQUARES:
378
+ piece = board.piece_at(square)
379
+ if piece:
380
+ if piece.piece_type == chess.QUEEN:
381
+ queens += 1
382
+ elif piece.piece_type == chess.ROOK:
383
+ rooks += 1
384
+ total_material += self.PIECE_VALUES[piece.piece_type]
385
+
386
+ # Endgame conditions:
387
+ # 1. No queens
388
+ # 2. Only one queen total and no other major pieces
389
+ # 3. Less than 25% of starting material
390
+ return (queens == 0) or (queens == 1 and rooks <= 1) or (total_material < 3200)
391
+
392
+ def _evaluate_endgame_factors(self, board: chess.Board) -> float:
393
+ """Evaluate endgame-specific factors"""
394
+ if not self._is_endgame(board):
395
+ return 0
396
+
397
+ score = 0
398
+
399
+ # King centralization in endgame
400
+ score += self._evaluate_king_centralization(board)
401
+
402
+ # Passed pawns become more valuable in endgame
403
+ score += self._evaluate_endgame_passed_pawns(board)
404
+
405
+ # Rook on open files
406
+ score += self._evaluate_rooks_on_open_files(board)
407
+
408
+ return score
409
+
410
+ def _evaluate_king_centralization(self, board: chess.Board) -> float:
411
+ """
412
+ Evaluate king centralization in endgame
413
+ Kings should move to the center in endgames
414
+ """
415
+ score = 0
416
+
417
+ for color in [chess.WHITE, chess.BLACK]:
418
+ king_square = board.king(color)
419
+ if king_square is None:
420
+ continue
421
+
422
+ # Calculate distance from center (d4, d5, e4, e5)
423
+ file = chess.square_file(king_square)
424
+ rank = chess.square_rank(king_square)
425
+
426
+ # Distance from center files (0-3.5)
427
+ file_distance = abs(3.5 - file)
428
+
429
+ # Distance from center ranks (0-3.5)
430
+ rank_distance = abs(3.5 - rank)
431
+
432
+ # Manhattan distance from center
433
+ center_distance = file_distance + rank_distance
434
+
435
+ # Bonus for being close to center (max 15 points)
436
+ centralization_bonus = (7 - center_distance) * 3
437
+
438
+ if color == chess.WHITE:
439
+ score += centralization_bonus
440
+ else:
441
+ score -= centralization_bonus
442
+
443
+ return score
444
+
445
+ def _evaluate_endgame_passed_pawns(self, board: chess.Board) -> float:
446
+ """
447
+ Evaluate passed pawns in endgame - they're more valuable
448
+ """
449
+ score = 0
450
+
451
+ # Get pawn positions
452
+ white_pawns = [sq for sq in chess.SQUARES if board.piece_at(sq) and
453
+ board.piece_at(sq).piece_type == chess.PAWN and
454
+ board.piece_at(sq).color == chess.WHITE]
455
+
456
+ black_pawns = [sq for sq in chess.SQUARES if board.piece_at(sq) and
457
+ board.piece_at(sq).piece_type == chess.PAWN and
458
+ board.piece_at(sq).color == chess.BLACK]
459
+
460
+ # Check for passed pawns
461
+ for pawn in white_pawns:
462
+ if self._is_passed_pawn(board, pawn, chess.WHITE):
463
+ rank = chess.square_rank(pawn)
464
+ # Bonus increases dramatically with advancement
465
+ bonus = (rank * rank) * 5
466
+ score += bonus
467
+
468
+ for pawn in black_pawns:
469
+ if self._is_passed_pawn(board, pawn, chess.BLACK):
470
+ rank = 7 - chess.square_rank(pawn) # Flip for black
471
+ # Bonus increases dramatically with advancement
472
+ bonus = (rank * rank) * 5
473
+ score -= bonus
474
+
475
+ return score
476
+
477
+ def _is_passed_pawn(self, board: chess.Board, square: int, color: chess.Color) -> bool:
478
+ """Check if a pawn is passed"""
479
+ file = chess.square_file(square)
480
+ rank = chess.square_rank(square)
481
+
482
+ # Direction of pawn movement
483
+ direction = 1 if color == chess.WHITE else -1
484
+
485
+ # Check files that could block this pawn
486
+ for check_file in [file - 1, file, file + 1]:
487
+ if 0 <= check_file <= 7:
488
+ for check_rank in range(rank + direction, 8 if color == chess.WHITE else -1, direction):
489
+ if 0 <= check_rank <= 7:
490
+ check_square = chess.square(check_file, check_rank)
491
+ piece = board.piece_at(check_square)
492
+ if piece and piece.piece_type == chess.PAWN and piece.color != color:
493
+ return False
494
+
495
+ return True
496
+
497
+ def _evaluate_rooks_on_open_files(self, board: chess.Board) -> float:
498
+ """Evaluate rooks on open or semi-open files"""
499
+ score = 0
500
+
501
+ # Get all files with pawns
502
+ files_with_white_pawns = set()
503
+ files_with_black_pawns = set()
504
+
505
+ for square in chess.SQUARES:
506
+ piece = board.piece_at(square)
507
+ if piece and piece.piece_type == chess.PAWN:
508
+ file = chess.square_file(square)
509
+ if piece.color == chess.WHITE:
510
+ files_with_white_pawns.add(file)
511
+ else:
512
+ files_with_black_pawns.add(file)
513
+
514
+ # Check rooks
515
+ for square in chess.SQUARES:
516
+ piece = board.piece_at(square)
517
+ if piece and piece.piece_type == chess.ROOK:
518
+ file = chess.square_file(square)
519
+
520
+ # Open file (no pawns)
521
+ if file not in files_with_white_pawns and file not in files_with_black_pawns:
522
+ bonus = 25
523
+ # Semi-open file (no friendly pawns)
524
+ elif (piece.color == chess.WHITE and file not in files_with_white_pawns) or \
525
+ (piece.color == chess.BLACK and file not in files_with_black_pawns):
526
+ bonus = 15
527
+ else:
528
+ bonus = 0
529
+
530
+ if piece.color == chess.WHITE:
531
+ score += bonus
532
+ else:
533
+ score -= bonus
534
+
535
+ return score
chess_engine/ai/stockfish_wrapper.py ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chess_engine/ai/stockfish_wrapper.py
2
+
3
+ import chess
4
+ import chess.engine
5
+ from stockfish import Stockfish
6
+ from typing import Optional, Dict, List, Tuple, Any
7
+ from enum import Enum
8
+ import asyncio
9
+ import logging
10
+ from dataclasses import dataclass
11
+
12
+ class DifficultyLevel(Enum):
13
+ BEGINNER = 1
14
+ EASY = 3
15
+ MEDIUM = 5
16
+ HARD = 8
17
+ EXPERT = 12
18
+ MASTER = 15
19
+
20
+ @dataclass
21
+ class EngineConfig:
22
+ """Configuration for Stockfish engine"""
23
+ depth: int = 10
24
+ time_limit: float = 1.0 # seconds
25
+ threads: int = 1
26
+ hash_size: int = 16 # MB
27
+ skill_level: int = 20 # 0-20 (20 is strongest)
28
+ contempt: int = 0
29
+ ponder: bool = False
30
+
31
+ @dataclass
32
+ class MoveAnalysis:
33
+ """Analysis result for a move"""
34
+ best_move: str
35
+ evaluation: float
36
+ depth: int
37
+ principal_variation: List[str]
38
+ time_taken: float
39
+ nodes_searched: int
40
+
41
+ class StockfishWrapper:
42
+ """
43
+ Wrapper for Stockfish chess engine with both python-chess and stockfish library support
44
+ """
45
+
46
+ def __init__(self, stockfish_path: Optional[str] = None, config: Optional[EngineConfig] = None):
47
+ """
48
+ Initialize Stockfish wrapper
49
+
50
+ Args:
51
+ stockfish_path: Path to Stockfish executable
52
+ config: Engine configuration
53
+ """
54
+ self.config = config or EngineConfig()
55
+ self.stockfish_path = stockfish_path
56
+ self.engine = None
57
+ self.stockfish = None
58
+ self.is_initialized = False
59
+
60
+ # Setup logging
61
+ self.logger = logging.getLogger(__name__)
62
+
63
+ def initialize(self) -> bool:
64
+ """
65
+ Initialize the Stockfish engine
66
+
67
+ Returns:
68
+ True if successful, False otherwise
69
+ """
70
+ try:
71
+ # Try to initialize with stockfish library first
72
+ if self.stockfish_path:
73
+ self.stockfish = Stockfish(
74
+ path=self.stockfish_path,
75
+ depth=self.config.depth,
76
+ parameters={
77
+ "Threads": self.config.threads,
78
+ "Hash": self.config.hash_size,
79
+ "Skill Level": self.config.skill_level,
80
+ "Contempt": self.config.contempt,
81
+ "Ponder": self.config.ponder
82
+ }
83
+ )
84
+ else:
85
+ # Use default system Stockfish
86
+ self.stockfish = Stockfish(
87
+ depth=self.config.depth,
88
+ parameters={
89
+ "Threads": self.config.threads,
90
+ "Hash": self.config.hash_size,
91
+ "Skill Level": self.config.skill_level,
92
+ "Contempt": self.config.contempt,
93
+ "Ponder": self.config.ponder
94
+ }
95
+ )
96
+
97
+ # Test if engine is working
98
+ if self.stockfish.is_fen_valid("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"):
99
+ self.is_initialized = True
100
+ self.logger.info("Stockfish engine initialized successfully")
101
+ return True
102
+ else:
103
+ self.logger.error("Stockfish engine test failed")
104
+ return False
105
+
106
+ except Exception as e:
107
+ self.logger.error(f"Failed to initialize Stockfish: {e}")
108
+ return False
109
+
110
+ async def initialize_async(self) -> bool:
111
+ """
112
+ Initialize the chess.engine for async operations
113
+
114
+ Returns:
115
+ True if successful, False otherwise
116
+ """
117
+ try:
118
+ if self.stockfish_path:
119
+ self.engine = await chess.engine.SimpleEngine.popen_uci(self.stockfish_path)
120
+ else:
121
+ # Try common Stockfish paths
122
+ paths = [
123
+ "/usr/bin/stockfish",
124
+ "/usr/local/bin/stockfish",
125
+ "stockfish",
126
+ "stockfish.exe"
127
+ ]
128
+
129
+ for path in paths:
130
+ try:
131
+ self.engine = await chess.engine.SimpleEngine.popen_uci(path)
132
+ break
133
+ except FileNotFoundError:
134
+ continue
135
+
136
+ if not self.engine:
137
+ raise FileNotFoundError("Stockfish executable not found")
138
+
139
+ # Configure engine
140
+ await self.engine.configure({
141
+ "Threads": self.config.threads,
142
+ "Hash": self.config.hash_size,
143
+ "Skill Level": self.config.skill_level,
144
+ "Contempt": self.config.contempt,
145
+ "Ponder": self.config.ponder
146
+ })
147
+
148
+ self.is_initialized = True
149
+ self.logger.info("Async Stockfish engine initialized successfully")
150
+ return True
151
+
152
+ except Exception as e:
153
+ self.logger.error(f"Failed to initialize async Stockfish: {e}")
154
+ return False
155
+
156
+ def get_best_move(self, board: chess.Board, time_limit: Optional[float] = None) -> Optional[str]:
157
+ """
158
+ Get the best move for current position
159
+
160
+ Args:
161
+ board: Current chess board position
162
+ time_limit: Time limit in seconds (overrides config)
163
+
164
+ Returns:
165
+ Best move in UCI notation or None if error
166
+ """
167
+ if not self.is_initialized or not self.stockfish:
168
+ return None
169
+
170
+ try:
171
+ # Set position
172
+ self.stockfish.set_fen_position(board.fen())
173
+
174
+ # Get best move
175
+ best_move = self.stockfish.get_best_move_time(
176
+ time_limit or self.config.time_limit * 1000 # Convert to milliseconds
177
+ )
178
+
179
+ return best_move
180
+
181
+ except Exception as e:
182
+ self.logger.error(f"Error getting best move: {e}")
183
+ return None
184
+
185
+ async def get_best_move_async(self, board: chess.Board, time_limit: Optional[float] = None) -> Optional[MoveAnalysis]:
186
+ """
187
+ Get the best move asynchronously with detailed analysis
188
+
189
+ Args:
190
+ board: Current chess board position
191
+ time_limit: Time limit in seconds
192
+
193
+ Returns:
194
+ MoveAnalysis object or None if error
195
+ """
196
+ if not self.is_initialized or not self.engine:
197
+ return None
198
+
199
+ try:
200
+ # Set up analysis parameters
201
+ limit = chess.engine.Limit(
202
+ time=time_limit or self.config.time_limit,
203
+ depth=self.config.depth
204
+ )
205
+
206
+ # Analyze position
207
+ info = await self.engine.analyse(board, limit)
208
+
209
+ # Get best move
210
+ result = await self.engine.play(board, limit)
211
+
212
+ # Extract information
213
+ evaluation = info.get("score", chess.engine.Cp(0))
214
+ pv = info.get("pv", [])
215
+
216
+ # Convert evaluation to numerical value
217
+ if evaluation.is_mate():
218
+ eval_value = 1000.0 if evaluation.mate() > 0 else -1000.0
219
+ else:
220
+ eval_value = evaluation.score() / 100.0 # Convert centipawns to pawns
221
+
222
+ return MoveAnalysis(
223
+ best_move=result.move.uci() if result.move else "",
224
+ evaluation=eval_value,
225
+ depth=info.get("depth", 0),
226
+ principal_variation=[move.uci() for move in pv],
227
+ time_taken=info.get("time", 0.0),
228
+ nodes_searched=info.get("nodes", 0)
229
+ )
230
+
231
+ except Exception as e:
232
+ self.logger.error(f"Error in async move analysis: {e}")
233
+ return None
234
+
235
+ def evaluate_position(self, board: chess.Board) -> Optional[float]:
236
+ """
237
+ Evaluate current position
238
+
239
+ Args:
240
+ board: Current chess board position
241
+
242
+ Returns:
243
+ Evaluation in pawns (positive for white advantage)
244
+ """
245
+ if not self.is_initialized or not self.stockfish:
246
+ return None
247
+
248
+ try:
249
+ self.stockfish.set_fen_position(board.fen())
250
+ evaluation = self.stockfish.get_evaluation()
251
+
252
+ if evaluation is None:
253
+ return 0.0
254
+
255
+ if evaluation["type"] == "cp":
256
+ return evaluation["value"] / 100.0 # Convert centipawns to pawns
257
+ elif evaluation["type"] == "mate":
258
+ return 1000.0 if evaluation["value"] > 0 else -1000.0
259
+
260
+ return 0.0
261
+
262
+ except Exception as e:
263
+ self.logger.error(f"Error evaluating position: {e}")
264
+ return None
265
+
266
+ def get_legal_moves_with_evaluation(self, board: chess.Board) -> List[Tuple[str, float]]:
267
+ """
268
+ Get all legal moves with their evaluations
269
+
270
+ Args:
271
+ board: Current chess board position
272
+
273
+ Returns:
274
+ List of (move, evaluation) tuples
275
+ """
276
+ if not self.is_initialized or not self.stockfish:
277
+ return []
278
+
279
+ moves_with_eval = []
280
+
281
+ try:
282
+ for move in board.legal_moves:
283
+ # Make move temporarily
284
+ board.push(move)
285
+
286
+ # Evaluate position
287
+ evaluation = self.evaluate_position(board)
288
+
289
+ # Undo move
290
+ board.pop()
291
+
292
+ if evaluation is not None:
293
+ moves_with_eval.append((move.uci(), -evaluation)) # Negate for opponent's perspective
294
+
295
+ # Sort by evaluation (best first)
296
+ moves_with_eval.sort(key=lambda x: x[1], reverse=True)
297
+
298
+ return moves_with_eval
299
+
300
+ except Exception as e:
301
+ self.logger.error(f"Error getting moves with evaluation: {e}")
302
+ return []
303
+
304
+ def set_difficulty_level(self, level: DifficultyLevel):
305
+ """
306
+ Set AI difficulty level
307
+
308
+ Args:
309
+ level: Difficulty level enum
310
+ """
311
+ skill_levels = {
312
+ DifficultyLevel.BEGINNER: 1,
313
+ DifficultyLevel.EASY: 3,
314
+ DifficultyLevel.MEDIUM: 8,
315
+ DifficultyLevel.HARD: 12,
316
+ DifficultyLevel.EXPERT: 17,
317
+ DifficultyLevel.MASTER: 20
318
+ }
319
+
320
+ depths = {
321
+ DifficultyLevel.BEGINNER: 3,
322
+ DifficultyLevel.EASY: 5,
323
+ DifficultyLevel.MEDIUM: 8,
324
+ DifficultyLevel.HARD: 12,
325
+ DifficultyLevel.EXPERT: 15,
326
+ DifficultyLevel.MASTER: 20
327
+ }
328
+
329
+ self.config.skill_level = skill_levels[level]
330
+ self.config.depth = depths[level]
331
+
332
+ # Update engine parameters if initialized
333
+ if self.is_initialized and self.stockfish:
334
+ self.stockfish.set_depth(self.config.depth)
335
+ self.stockfish.set_skill_level(self.config.skill_level)
336
+
337
+ def get_engine_info(self) -> Dict[str, Any]:
338
+ """
339
+ Get engine information and statistics
340
+
341
+ Returns:
342
+ Dictionary with engine info
343
+ """
344
+ if not self.is_initialized:
345
+ return {"status": "not_initialized"}
346
+
347
+ return {
348
+ "status": "initialized",
349
+ "config": {
350
+ "depth": self.config.depth,
351
+ "skill_level": self.config.skill_level,
352
+ "time_limit": self.config.time_limit,
353
+ "threads": self.config.threads,
354
+ "hash_size": self.config.hash_size
355
+ },
356
+ "stockfish_available": self.stockfish is not None,
357
+ "async_engine_available": self.engine is not None
358
+ }
359
+
360
+ def is_move_blunder(self, board: chess.Board, move: str, threshold: float = 2.0) -> bool:
361
+ """
362
+ Check if a move is a blunder
363
+
364
+ Args:
365
+ board: Current chess board position
366
+ move: Move to check in UCI notation
367
+ threshold: Evaluation drop threshold for blunder detection
368
+
369
+ Returns:
370
+ True if move is a blunder
371
+ """
372
+ if not self.is_initialized:
373
+ return False
374
+
375
+ try:
376
+ # Get current position evaluation
377
+ current_eval = self.evaluate_position(board)
378
+ if current_eval is None:
379
+ return False
380
+
381
+ # Make the move
382
+ move_obj = chess.Move.from_uci(move)
383
+ if move_obj not in board.legal_moves:
384
+ return True # Illegal move is definitely a blunder
385
+
386
+ board.push(move_obj)
387
+
388
+ # Get evaluation after move
389
+ new_eval = self.evaluate_position(board)
390
+
391
+ # Undo move
392
+ board.pop()
393
+
394
+ if new_eval is None:
395
+ return False
396
+
397
+ # Check if evaluation dropped significantly
398
+ # Note: negate new_eval because it's opponent's turn
399
+ eval_drop = current_eval - (-new_eval)
400
+
401
+ return eval_drop > threshold
402
+
403
+ except Exception as e:
404
+ self.logger.error(f"Error checking blunder: {e}")
405
+ return False
406
+
407
+ def close(self):
408
+ """Close the engine connection"""
409
+ if self.engine:
410
+ asyncio.create_task(self.engine.quit())
411
+
412
+ self.is_initialized = False
413
+ self.logger.info("Stockfish engine closed")
414
+
415
+ def __enter__(self):
416
+ """Context manager entry"""
417
+ self.initialize()
418
+ return self
419
+
420
+ def __exit__(self, exc_type, exc_val, exc_tb):
421
+ """Context manager exit"""
422
+ self.close()
chess_engine/api/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # chess_engine/api/__init__.py
2
+
3
+ from chess_engine.api.game_controller import GameController, GameOptions
chess_engine/api/cli.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chess_engine/api/cli.py
2
+
3
+ import chess
4
+ import argparse
5
+ import sys
6
+ import os
7
+ from typing import Optional, Dict, Any
8
+ import time
9
+
10
+ from chess_engine.api.game_controller import GameController, GameOptions
11
+ from chess_engine.ai.stockfish_wrapper import DifficultyLevel
12
+
13
+ class ChessCLI:
14
+ """Command-line interface for chess engine"""
15
+
16
+ def __init__(self):
17
+ self.controller = None
18
+ self.options = None
19
+
20
+ def setup_game(self, args):
21
+ """Setup game with command line arguments"""
22
+ # Parse color
23
+ player_color = chess.WHITE
24
+ if args.color and args.color.lower() == 'black':
25
+ player_color = chess.BLACK
26
+
27
+ # Parse difficulty
28
+ difficulty_map = {
29
+ 'beginner': DifficultyLevel.BEGINNER,
30
+ 'easy': DifficultyLevel.EASY,
31
+ 'medium': DifficultyLevel.MEDIUM,
32
+ 'hard': DifficultyLevel.HARD,
33
+ 'expert': DifficultyLevel.EXPERT,
34
+ 'master': DifficultyLevel.MASTER
35
+ }
36
+
37
+ difficulty = difficulty_map.get(args.difficulty.lower(), DifficultyLevel.MEDIUM)
38
+
39
+ # Create options
40
+ self.options = GameOptions(
41
+ player_color=player_color,
42
+ difficulty=difficulty,
43
+ time_limit=args.time,
44
+ use_opening_book=not args.no_book,
45
+ enable_analysis=not args.no_analysis,
46
+ stockfish_path=args.stockfish_path
47
+ )
48
+
49
+ # Create controller
50
+ self.controller = GameController(self.options)
51
+
52
+ def print_board(self, unicode: bool = True):
53
+ """Print chess board to console"""
54
+ board = self.controller.board.board
55
+
56
+ # Get board as string
57
+ if unicode:
58
+ board_str = str(board)
59
+ else:
60
+ board_str = board.unicode()
61
+
62
+ print("\n" + board_str + "\n")
63
+
64
+ # Print whose turn it is
65
+ turn = "White" if board.turn == chess.WHITE else "Black"
66
+ print(f"{turn} to move")
67
+
68
+ # Print check status
69
+ if board.is_check():
70
+ print("CHECK!")
71
+
72
+ def print_analysis(self, analysis: Dict[str, Any]):
73
+ """Print position analysis"""
74
+ if not analysis:
75
+ return
76
+
77
+ eval_data = analysis.get("evaluation", {})
78
+ best_moves = analysis.get("best_moves", [])
79
+
80
+ print("\nPosition Analysis:")
81
+ print(f"Evaluation: {eval_data.get('total', 0):.2f} pawns")
82
+ print(f"Stockfish: {eval_data.get('stockfish', 0):.2f} pawns")
83
+
84
+ if best_moves:
85
+ print("\nBest moves:")
86
+ for i, (move, eval_score) in enumerate(best_moves[:3], 1):
87
+ print(f"{i}. {move} ({eval_score:.2f})")
88
+
89
+ def run(self):
90
+ """Run the CLI game loop"""
91
+ # Start new game
92
+ result = self.controller.start_new_game()
93
+
94
+ if result["status"] == "error":
95
+ print(f"Error: {result['message']}")
96
+ return
97
+
98
+ print("Chess game started!")
99
+ print("Enter moves in UCI notation (e.g., 'e2e4') or SAN (e.g., 'e4')")
100
+ print("Commands: 'quit', 'help', 'hint', 'undo', 'board', 'resign'")
101
+
102
+ self.print_board()
103
+
104
+ # Game loop
105
+ while True:
106
+ # Get user input
107
+ try:
108
+ user_input = input("\nYour move: ").strip()
109
+ except (KeyboardInterrupt, EOFError):
110
+ print("\nGame terminated.")
111
+ break
112
+
113
+ # Process commands
114
+ if user_input.lower() in ('quit', 'exit', 'q'):
115
+ print("Game terminated.")
116
+ break
117
+
118
+ elif user_input.lower() == 'help':
119
+ print("\nCommands:")
120
+ print(" <move> - Make a move (e.g., 'e2e4' or 'e4')")
121
+ print(" board - Display the board")
122
+ print(" hint - Get a move suggestion")
123
+ print(" undo - Undo last move")
124
+ print(" resign - Resign the game")
125
+ print(" quit - Exit the game")
126
+ continue
127
+
128
+ elif user_input.lower() == 'board':
129
+ self.print_board()
130
+ continue
131
+
132
+ elif user_input.lower() == 'hint':
133
+ hint_result = self.controller.get_hint()
134
+ if hint_result["status"] == "success":
135
+ print(f"\nHint: {hint_result['hint']}")
136
+ print(f"Explanation: {hint_result['explanation']}")
137
+ else:
138
+ print(f"Error: {hint_result['message']}")
139
+ continue
140
+
141
+ elif user_input.lower() == 'undo':
142
+ undo_result = self.controller.undo_move()
143
+ if undo_result["status"] in ("success", "partial"):
144
+ print("Move undone.")
145
+ self.print_board()
146
+ else:
147
+ print(f"Error: {undo_result['message']}")
148
+ continue
149
+
150
+ elif user_input.lower() == 'resign':
151
+ resign_result = self.controller.resign()
152
+ print("You resigned. Game over.")
153
+ break
154
+
155
+ # Process move
156
+ move_result = self.controller.make_player_move(user_input)
157
+
158
+ if move_result["status"] == "error":
159
+ print(f"Error: {move_result['message']}")
160
+ continue
161
+
162
+ # Print board after move
163
+ self.print_board()
164
+
165
+ # Print AI's move
166
+ if "ai_move" in move_result and move_result["ai_move"]:
167
+ print(f"AI plays: {move_result['ai_move']}")
168
+
169
+ # Print analysis if available
170
+ if "analysis" in move_result and move_result["analysis"]:
171
+ self.print_analysis(move_result["analysis"])
172
+
173
+ # Check if game is over
174
+ if move_result["status"] == "game_over":
175
+ result = move_result["result"]
176
+ reason = move_result["reason"]
177
+
178
+ print("\nGame over!")
179
+
180
+ if result == "draw":
181
+ print(f"Result: Draw ({reason})")
182
+ else:
183
+ winner = move_result["winner"].capitalize()
184
+ print(f"Result: {winner} wins by {reason}")
185
+
186
+ break
187
+
188
+ # Clean up
189
+ if self.controller:
190
+ self.controller.close()
191
+
192
+ def main(args=None):
193
+ """
194
+ Main entry point for CLI
195
+
196
+ Args:
197
+ args: Command-line arguments (optional, parsed from sys.argv if None)
198
+ """
199
+ if args is None:
200
+ # Parse arguments only if not provided
201
+ parser = argparse.ArgumentParser(description="Chess Engine CLI")
202
+ parser.add_argument("--color", "-c", choices=["white", "black"], default="white",
203
+ help="Player's color (default: white)")
204
+ parser.add_argument("--difficulty", "-d",
205
+ choices=["beginner", "easy", "medium", "hard", "expert", "master"],
206
+ default="medium", help="AI difficulty level (default: medium)")
207
+ parser.add_argument("--time", "-t", type=float, default=1.0,
208
+ help="Time limit for AI moves in seconds (default: 1.0)")
209
+ parser.add_argument("--stockfish-path", "-s", type=str, default=None,
210
+ help="Path to Stockfish executable")
211
+ parser.add_argument("--no-book", action="store_true",
212
+ help="Disable opening book")
213
+ parser.add_argument("--no-analysis", action="store_true",
214
+ help="Disable position analysis")
215
+
216
+ args = parser.parse_args()
217
+
218
+ cli = ChessCLI()
219
+ cli.setup_game(args)
220
+ cli.run()
221
+
222
+ if __name__ == "__main__":
223
+ main()
chess_engine/api/game_controller.py ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chess_engine/api/game_controller.py
2
+
3
+ import chess
4
+ from typing import Dict, List, Optional, Tuple, Any
5
+ from enum import Enum
6
+ from dataclasses import dataclass
7
+
8
+ from chess_engine.board import ChessBoard, MoveResult, GameState
9
+ from chess_engine.ai.stockfish_wrapper import StockfishWrapper, DifficultyLevel
10
+ from chess_engine.ai.evaluation import ChessEvaluator
11
+ from chess_engine.promotion import PromotionMoveHandler
12
+
13
+ @dataclass
14
+ class GameOptions:
15
+ """Game configuration options"""
16
+ player_color: chess.Color = chess.WHITE
17
+ difficulty: DifficultyLevel = DifficultyLevel.MEDIUM
18
+ time_limit: float = 1.0 # seconds for AI to think
19
+ use_opening_book: bool = True
20
+ enable_analysis: bool = True
21
+ stockfish_path: Optional[str] = None
22
+
23
+ class GameController:
24
+ """
25
+ Main game controller that handles game flow, user moves, and AI responses
26
+ """
27
+
28
+ def __init__(self, options: Optional[GameOptions] = None):
29
+ """
30
+ Initialize game controller
31
+
32
+ Args:
33
+ options: Game configuration options
34
+ """
35
+ self.options = options or GameOptions()
36
+ self.board = ChessBoard()
37
+ self.engine = StockfishWrapper(stockfish_path=self.options.stockfish_path)
38
+ self.evaluator = ChessEvaluator()
39
+ self.game_active = False
40
+ self.last_analysis = None
41
+
42
+ def start_new_game(self, options: Optional[GameOptions] = None) -> Dict[str, Any]:
43
+ """
44
+ Start a new game
45
+
46
+ Args:
47
+ options: Game options (optional, uses current options if None)
48
+
49
+ Returns:
50
+ Game state information
51
+ """
52
+ if options:
53
+ self.options = options
54
+
55
+ # Reset board
56
+ self.board.reset_board()
57
+
58
+ # Initialize engine
59
+ if not self.engine.is_initialized:
60
+ engine_initialized = self.engine.initialize()
61
+ if not engine_initialized:
62
+ return {
63
+ "status": "error",
64
+ "message": "Failed to initialize chess engine",
65
+ "board_state": self.board.get_board_state()
66
+ }
67
+
68
+ # Set difficulty
69
+ self.engine.set_difficulty_level(self.options.difficulty)
70
+
71
+ self.game_active = True
72
+
73
+ # If AI plays white, make first move
74
+ if self.options.player_color == chess.BLACK:
75
+ ai_move = self._get_ai_move()
76
+ if ai_move:
77
+ self.board.make_move(ai_move)
78
+
79
+ return {
80
+ "status": "success",
81
+ "message": "New game started",
82
+ "board_state": self.board.get_board_state(),
83
+ "player_color": "white" if self.options.player_color == chess.WHITE else "black"
84
+ }
85
+
86
+ def make_player_move(self, move_str: str) -> Dict[str, Any]:
87
+ """
88
+ Process a player's move
89
+
90
+ Args:
91
+ move_str: Move in UCI notation (e.g., 'e2e4', 'e7e8q') or SAN (e.g., 'e4', 'e8=Q')
92
+
93
+ Returns:
94
+ Response with move result and updated game state
95
+ """
96
+ if not self.game_active:
97
+ return {
98
+ "status": "error",
99
+ "message": "No active game",
100
+ "board_state": self.board.get_board_state()
101
+ }
102
+
103
+ # Check if it's player's turn
104
+ if self.board.board.turn != self.options.player_color:
105
+ return {
106
+ "status": "error",
107
+ "message": "Not your turn",
108
+ "board_state": self.board.get_board_state()
109
+ }
110
+
111
+ # Make the move
112
+ result, move_info = self.board.make_move(move_str)
113
+
114
+ # Handle promotion requirement
115
+ if result == MoveResult.INVALID and move_info and move_info.promotion_required:
116
+ return {
117
+ "status": "promotion_required",
118
+ "message": "Pawn promotion requires piece selection",
119
+ "promotion_details": {
120
+ "from": move_info.uci[:2],
121
+ "to": move_info.uci[2:4],
122
+ "available_pieces": ["queen", "rook", "bishop", "knight"]
123
+ },
124
+ "board_state": self.board.get_board_state()
125
+ }
126
+
127
+ if result != MoveResult.VALID:
128
+ return {
129
+ "status": "error",
130
+ "message": f"Invalid move: {move_str}",
131
+ "board_state": self.board.get_board_state()
132
+ }
133
+
134
+ # Check if game is over after player's move
135
+ game_state = self._check_game_state()
136
+ if game_state:
137
+ return game_state
138
+
139
+ # Make AI move
140
+ ai_move = self._get_ai_move()
141
+ if ai_move:
142
+ self.board.make_move(ai_move)
143
+
144
+ # Check if game is over after AI's move
145
+ game_state = self._check_game_state()
146
+ if game_state:
147
+ return game_state
148
+
149
+ # Analyze position if enabled
150
+ analysis = None
151
+ if self.options.enable_analysis:
152
+ analysis = self._analyze_position()
153
+ self.last_analysis = analysis
154
+
155
+ return {
156
+ "status": "success",
157
+ "player_move": move_str,
158
+ "ai_move": ai_move,
159
+ "board_state": self.board.get_board_state(),
160
+ "analysis": analysis
161
+ }
162
+
163
+ def _get_ai_move(self) -> Optional[str]:
164
+ """
165
+ Get AI's move using Stockfish
166
+
167
+ Returns:
168
+ Move in UCI notation or None if error
169
+ """
170
+ return self.engine.get_best_move(
171
+ self.board.board,
172
+ time_limit=self.options.time_limit
173
+ )
174
+
175
+ def _check_game_state(self) -> Optional[Dict[str, Any]]:
176
+ """
177
+ Check if game is over
178
+
179
+ Returns:
180
+ Game result dict if game is over, None otherwise
181
+ """
182
+ board_state = self.board.get_board_state()
183
+ game_state = board_state["game_state"]
184
+
185
+ if game_state in [GameState.CHECKMATE, GameState.STALEMATE, GameState.DRAW]:
186
+ self.game_active = False
187
+
188
+ result = "draw"
189
+ winner = None
190
+
191
+ if game_state == GameState.CHECKMATE:
192
+ # Winner is the opposite of who's turn it is
193
+ winner = "black" if self.board.board.turn == chess.WHITE else "white"
194
+ result = f"{winner}_win"
195
+
196
+ return {
197
+ "status": "game_over",
198
+ "result": result,
199
+ "winner": winner,
200
+ "reason": game_state.value,
201
+ "board_state": board_state
202
+ }
203
+
204
+ return None
205
+
206
+ def _analyze_position(self) -> Dict[str, Any]:
207
+ """
208
+ Analyze current position
209
+
210
+ Returns:
211
+ Analysis data
212
+ """
213
+ # Get evaluation from our evaluator
214
+ evaluation = self.evaluator.evaluate_position(self.board.board)
215
+
216
+ # Get Stockfish evaluation
217
+ stockfish_eval = self.engine.evaluate_position(self.board.board)
218
+
219
+ # Get best moves with evaluation
220
+ best_moves = self.engine.get_legal_moves_with_evaluation(self.board.board)[:3]
221
+
222
+ return {
223
+ "evaluation": {
224
+ "total": evaluation.total_score / 100, # Convert to pawns
225
+ "material": evaluation.material_score / 100,
226
+ "positional": evaluation.positional_score / 100,
227
+ "safety": evaluation.safety_score / 100,
228
+ "mobility": evaluation.mobility_score / 100,
229
+ "pawn_structure": evaluation.pawn_structure_score / 100,
230
+ "endgame": evaluation.endgame_score / 100,
231
+ "white_advantage": evaluation.white_advantage / 100,
232
+ "stockfish": stockfish_eval
233
+ },
234
+ "best_moves": best_moves
235
+ }
236
+
237
+ def get_hint(self) -> Dict[str, Any]:
238
+ """
239
+ Get a hint for the current position
240
+
241
+ Returns:
242
+ Hint information
243
+ """
244
+ if not self.game_active:
245
+ return {
246
+ "status": "error",
247
+ "message": "No active game"
248
+ }
249
+
250
+ # Get best move from engine
251
+ best_move = self._get_ai_move()
252
+
253
+ if not best_move:
254
+ return {
255
+ "status": "error",
256
+ "message": "Could not generate hint"
257
+ }
258
+
259
+ # Get move explanation
260
+ explanation = self._get_move_explanation(best_move)
261
+
262
+ return {
263
+ "status": "success",
264
+ "hint": best_move,
265
+ "explanation": explanation,
266
+ "board_state": self.board.get_board_state()
267
+ }
268
+
269
+ def _get_move_explanation(self, move_str: str) -> str:
270
+ """
271
+ Generate a simple explanation for a move
272
+
273
+ Args:
274
+ move_str: Move in UCI notation
275
+
276
+ Returns:
277
+ Human-readable explanation
278
+ """
279
+ try:
280
+ move = chess.Move.from_uci(move_str)
281
+ board = self.board.board
282
+
283
+ from_square = chess.square_name(move.from_square)
284
+ to_square = chess.square_name(move.to_square)
285
+
286
+ piece = board.piece_at(move.from_square)
287
+ if not piece:
288
+ return "Unknown move"
289
+
290
+ piece_name = chess.piece_name(piece.piece_type).capitalize()
291
+
292
+ # Check if move is a capture
293
+ capture = ""
294
+ if board.is_capture(move):
295
+ captured_piece = board.piece_at(move.to_square)
296
+ if captured_piece:
297
+ captured_name = chess.piece_name(captured_piece.piece_type).capitalize()
298
+ capture = f", capturing {captured_name}"
299
+ else:
300
+ # En passant
301
+ capture = ", capturing Pawn en passant"
302
+
303
+ # Check if move gives check
304
+ check = ""
305
+ if board.gives_check(move):
306
+ check = ", giving check"
307
+
308
+ # Check if castling
309
+ if board.is_castling(move):
310
+ if chess.square_file(move.to_square) > chess.square_file(move.from_square):
311
+ return "Kingside castling"
312
+ else:
313
+ return "Queenside castling"
314
+
315
+ # Check if promotion
316
+ promotion = ""
317
+ if move.promotion:
318
+ promoted_piece = chess.piece_name(move.promotion).capitalize()
319
+ promotion = f", promoting to {promoted_piece}"
320
+ elif piece.piece_type == chess.PAWN and self.board.is_promotion_move(from_square, to_square):
321
+ # This is a promotion move but piece not specified
322
+ promotion = ", promotion required"
323
+
324
+ return f"{piece_name} from {from_square} to {to_square}{capture}{promotion}{check}"
325
+
326
+ except Exception:
327
+ return "Move analysis not available"
328
+
329
+ def undo_move(self, count: int = 2) -> Dict[str, Any]:
330
+ """
331
+ Undo moves (both player and AI)
332
+
333
+ Args:
334
+ count: Number of half-moves to undo (default 2 for one full move)
335
+
336
+ Returns:
337
+ Updated game state
338
+ """
339
+ if not self.game_active:
340
+ return {
341
+ "status": "error",
342
+ "message": "No active game",
343
+ "board_state": self.board.get_board_state()
344
+ }
345
+
346
+ success = True
347
+ for _ in range(count):
348
+ if not self.board.undo_move():
349
+ success = False
350
+ break
351
+
352
+ return {
353
+ "status": "success" if success else "partial",
354
+ "message": f"Undid {count} moves" if success else "Could not undo all requested moves",
355
+ "board_state": self.board.get_board_state()
356
+ }
357
+
358
+ def resign(self) -> Dict[str, Any]:
359
+ """
360
+ Resign the current game
361
+
362
+ Returns:
363
+ Game result
364
+ """
365
+ if not self.game_active:
366
+ return {
367
+ "status": "error",
368
+ "message": "No active game"
369
+ }
370
+
371
+ self.game_active = False
372
+ winner = "black" if self.options.player_color == chess.WHITE else "white"
373
+
374
+ return {
375
+ "status": "game_over",
376
+ "result": f"{winner}_win",
377
+ "winner": winner,
378
+ "reason": "resignation",
379
+ "board_state": self.board.get_board_state()
380
+ }
381
+
382
+ def get_game_state(self) -> Dict[str, Any]:
383
+ """
384
+ Get current game state
385
+
386
+ Returns:
387
+ Game state information
388
+ """
389
+ board_state = self.board.get_board_state()
390
+
391
+ return {
392
+ "status": "active" if self.game_active else "inactive",
393
+ "player_color": "white" if self.options.player_color == chess.WHITE else "black",
394
+ "board_state": board_state,
395
+ "difficulty": self.options.difficulty.name,
396
+ "last_analysis": self.last_analysis
397
+ }
398
+
399
+ def close(self):
400
+ """Clean up resources"""
401
+ if self.engine:
402
+ self.engine.close()
chess_engine/api/rest_api.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chess_engine/api/rest_api.py
2
+
3
+ from fastapi import FastAPI, HTTPException, Depends
4
+ from pydantic import BaseModel, Field
5
+ from typing import Dict, List, Optional, Any
6
+ import chess
7
+ from enum import Enum
8
+
9
+ from chess_engine.api.game_controller import GameController, GameOptions
10
+ from chess_engine.ai.stockfish_wrapper import DifficultyLevel
11
+
12
+ # Define API models
13
+ class ColorEnum(str, Enum):
14
+ WHITE = "white"
15
+ BLACK = "black"
16
+
17
+ class DifficultyEnum(str, Enum):
18
+ BEGINNER = "beginner"
19
+ EASY = "easy"
20
+ MEDIUM = "medium"
21
+ HARD = "hard"
22
+ EXPERT = "expert"
23
+ MASTER = "master"
24
+
25
+ class NewGameRequest(BaseModel):
26
+ player_color: ColorEnum = ColorEnum.WHITE
27
+ difficulty: DifficultyEnum = DifficultyEnum.MEDIUM
28
+ time_limit: float = Field(default=1.0, ge=0.1, le=10.0)
29
+ use_opening_book: bool = True
30
+ enable_analysis: bool = True
31
+ stockfish_path: Optional[str] = None
32
+
33
+ class MoveRequest(BaseModel):
34
+ move: str = Field(..., description="Move in UCI notation (e.g., 'e2e4', 'e7e8q') or SAN (e.g., 'e4', 'e8=Q')")
35
+
36
+ class PromotionDetails(BaseModel):
37
+ """Details about a required promotion move"""
38
+ from_square: str = Field(..., description="Starting square of the promotion move (e.g., 'e7')")
39
+ to_square: str = Field(..., description="Destination square of the promotion move (e.g., 'e8')")
40
+ available_pieces: List[str] = Field(..., description="Available pieces for promotion: ['queen', 'rook', 'bishop', 'knight']")
41
+
42
+ class GameResponse(BaseModel):
43
+ """Enhanced game response that can include promotion requirements"""
44
+ status: str = Field(..., description="Response status: 'success', 'error', 'promotion_required', 'game_over'")
45
+ message: Optional[str] = Field(None, description="Response message")
46
+ player_move: Optional[str] = Field(None, description="Player's move in UCI notation")
47
+ ai_move: Optional[str] = Field(None, description="AI's move in UCI notation")
48
+ board_state: Optional[Dict[str, Any]] = Field(None, description="Current board state")
49
+ analysis: Optional[Dict[str, Any]] = Field(None, description="Position analysis")
50
+ promotion_details: Optional[PromotionDetails] = Field(None, description="Promotion requirements when status is 'promotion_required'")
51
+ result: Optional[str] = Field(None, description="Game result when status is 'game_over'")
52
+ winner: Optional[str] = Field(None, description="Winner when game is over")
53
+ reason: Optional[str] = Field(None, description="Reason for game end")
54
+
55
+ class UndoRequest(BaseModel):
56
+ count: int = Field(default=2, ge=1, le=10, description="Number of half-moves to undo")
57
+
58
+ class PromotionRequest(BaseModel):
59
+ """Request for completing a promotion move"""
60
+ from_square: str = Field(..., description="Starting square of the promotion move (e.g., 'e7')")
61
+ to_square: str = Field(..., description="Destination square of the promotion move (e.g., 'e8')")
62
+ promotion_piece: str = Field(..., description="Piece to promote to: 'queen', 'rook', 'bishop', 'knight', or 'q', 'r', 'b', 'n'")
63
+
64
+ # Create FastAPI app
65
+ app = FastAPI(
66
+ title="Chess Engine API",
67
+ description="REST API for chess engine with Stockfish integration",
68
+ version="1.0.0"
69
+ )
70
+
71
+ # Game controller instance
72
+ game_controller = GameController()
73
+
74
+ # Helper function to get game controller
75
+ def get_game_controller():
76
+ return game_controller
77
+
78
+ @app.post("/game/new", response_model=Dict[str, Any], tags=["Game Control"])
79
+ async def new_game(request: NewGameRequest, controller: GameController = Depends(get_game_controller)):
80
+ """Start a new game with specified options"""
81
+ # Convert enum values to appropriate types
82
+ options = GameOptions(
83
+ player_color=chess.WHITE if request.player_color == ColorEnum.WHITE else chess.BLACK,
84
+ difficulty=DifficultyLevel[request.difficulty.upper()],
85
+ time_limit=request.time_limit,
86
+ use_opening_book=request.use_opening_book,
87
+ enable_analysis=request.enable_analysis,
88
+ stockfish_path=request.stockfish_path
89
+ )
90
+
91
+ result = controller.start_new_game(options)
92
+
93
+ if result["status"] == "error":
94
+ raise HTTPException(status_code=500, detail=result["message"])
95
+
96
+ return result
97
+
98
+ @app.post("/game/move", response_model=GameResponse, tags=["Game Play"])
99
+ async def make_move(request: MoveRequest, controller: GameController = Depends(get_game_controller)):
100
+ """
101
+ Make a move on the board.
102
+
103
+ This endpoint handles regular moves and promotion moves. When a pawn reaches the back rank
104
+ without specifying a promotion piece, it returns a 'promotion_required' status with details
105
+ about the required promotion.
106
+
107
+ **Move Formats:**
108
+ - Regular moves: 'e2e4', 'Nf3', 'O-O'
109
+ - Promotion moves: 'e7e8q' (UCI) or 'e8=Q' (SAN)
110
+
111
+ **Promotion Pieces:**
112
+ - 'q' or 'queen' for Queen
113
+ - 'r' or 'rook' for Rook
114
+ - 'b' or 'bishop' for Bishop
115
+ - 'n' or 'knight' for Knight
116
+
117
+ **Response Status Values:**
118
+ - 'success': Move completed successfully
119
+ - 'promotion_required': Pawn promotion requires piece selection
120
+ - 'error': Invalid move or game state error
121
+ - 'game_over': Game ended after this move
122
+ """
123
+ result = controller.make_player_move(request.move)
124
+
125
+ # Handle different response statuses
126
+ if result["status"] == "error":
127
+ raise HTTPException(status_code=400, detail=result["message"])
128
+ elif result["status"] == "promotion_required":
129
+ # Return 200 with promotion_required status - this is not an error
130
+ return GameResponse(
131
+ status="promotion_required",
132
+ message=result["message"],
133
+ board_state=result["board_state"],
134
+ promotion_details=PromotionDetails(
135
+ from_square=result["promotion_details"]["from"],
136
+ to_square=result["promotion_details"]["to"],
137
+ available_pieces=result["promotion_details"]["available_pieces"]
138
+ )
139
+ )
140
+ elif result["status"] == "game_over":
141
+ return GameResponse(
142
+ status="game_over",
143
+ message=result.get("message"),
144
+ board_state=result["board_state"],
145
+ result=result["result"],
146
+ winner=result.get("winner"),
147
+ reason=result["reason"]
148
+ )
149
+ else:
150
+ # Success case
151
+ return GameResponse(
152
+ status="success",
153
+ player_move=result.get("player_move"),
154
+ ai_move=result.get("ai_move"),
155
+ board_state=result["board_state"],
156
+ analysis=result.get("analysis")
157
+ )
158
+
159
+ @app.get("/game/state", response_model=Dict[str, Any], tags=["Game Info"])
160
+ async def get_game_state(controller: GameController = Depends(get_game_controller)):
161
+ """Get current game state"""
162
+ return controller.get_game_state()
163
+
164
+ @app.post("/game/hint", response_model=Dict[str, Any], tags=["Game Help"])
165
+ async def get_hint(controller: GameController = Depends(get_game_controller)):
166
+ """Get a hint for the current position"""
167
+ result = controller.get_hint()
168
+
169
+ if result["status"] == "error":
170
+ raise HTTPException(status_code=400, detail=result["message"])
171
+
172
+ return result
173
+
174
+ @app.post("/game/undo", response_model=Dict[str, Any], tags=["Game Control"])
175
+ async def undo_move(request: UndoRequest, controller: GameController = Depends(get_game_controller)):
176
+ """Undo moves"""
177
+ result = controller.undo_move(request.count)
178
+
179
+ if result["status"] == "error":
180
+ raise HTTPException(status_code=400, detail=result["message"])
181
+
182
+ return result
183
+
184
+ @app.post("/game/promotion", response_model=GameResponse, tags=["Game Play"])
185
+ async def complete_promotion(request: PromotionRequest, controller: GameController = Depends(get_game_controller)):
186
+ """
187
+ Complete a pawn promotion by specifying the promotion piece.
188
+
189
+ This endpoint is used when a previous move request returned 'promotion_required' status.
190
+ It completes the promotion move with the specified piece.
191
+
192
+ **Promotion Pieces:**
193
+ - 'queen' or 'q' for Queen (most common choice)
194
+ - 'rook' or 'r' for Rook
195
+ - 'bishop' or 'b' for Bishop
196
+ - 'knight' or 'n' for Knight
197
+
198
+ **Example Usage:**
199
+ 1. Make move 'e7e8' -> returns promotion_required
200
+ 2. Call this endpoint with promotion_piece='queen' to complete the move
201
+ """
202
+ from chess_engine.promotion import PromotionMoveHandler
203
+
204
+ # Create the full promotion move in UCI notation
205
+ promotion_move = PromotionMoveHandler.create_promotion_move(
206
+ request.from_square,
207
+ request.to_square,
208
+ request.promotion_piece
209
+ )
210
+
211
+ # Make the promotion move
212
+ result = controller.make_player_move(promotion_move)
213
+
214
+ # Handle different response statuses
215
+ if result["status"] == "error":
216
+ raise HTTPException(status_code=400, detail=result["message"])
217
+ elif result["status"] == "promotion_required":
218
+ # This shouldn't happen with a properly formed promotion move
219
+ raise HTTPException(status_code=400, detail="Invalid promotion move format")
220
+ elif result["status"] == "game_over":
221
+ return GameResponse(
222
+ status="game_over",
223
+ message=result.get("message"),
224
+ board_state=result["board_state"],
225
+ result=result["result"],
226
+ winner=result.get("winner"),
227
+ reason=result["reason"]
228
+ )
229
+ else:
230
+ # Success case
231
+ return GameResponse(
232
+ status="success",
233
+ player_move=result.get("player_move"),
234
+ ai_move=result.get("ai_move"),
235
+ board_state=result["board_state"],
236
+ analysis=result.get("analysis")
237
+ )
238
+
239
+ @app.post("/game/resign", response_model=Dict[str, Any], tags=["Game Control"])
240
+ async def resign_game(controller: GameController = Depends(get_game_controller)):
241
+ """Resign the current game"""
242
+ result = controller.resign()
243
+
244
+ if result["status"] == "error":
245
+ raise HTTPException(status_code=400, detail=result["message"])
246
+
247
+ return result
248
+
249
+ @app.get("/game/promotion/pieces", response_model=Dict[str, Any], tags=["Game Info"])
250
+ async def get_promotion_pieces():
251
+ """
252
+ Get available promotion pieces with their details.
253
+
254
+ Returns information about all pieces that a pawn can be promoted to,
255
+ including their names, symbols, UCI notation, and relative values.
256
+ """
257
+ from chess_engine.promotion import PromotionMoveHandler
258
+
259
+ return {
260
+ "status": "success",
261
+ "promotion_pieces": PromotionMoveHandler.get_available_promotions()
262
+ }
263
+
264
+ @app.get("/health", response_model=Dict[str, str], tags=["System"])
265
+ async def health_check():
266
+ """Health check endpoint for container monitoring"""
267
+ return {
268
+ "status": "healthy",
269
+ "service": "chess-engine-api",
270
+ "version": "1.0.0"
271
+ }
chess_engine/board.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chess_engine/board.py
2
+
3
+ import chess
4
+ import chess.engine
5
+ from typing import Optional, List, Tuple, Dict, Any
6
+ from enum import Enum
7
+ from dataclasses import dataclass
8
+ from .promotion import PromotionMoveHandler
9
+
10
+ class GameState(Enum):
11
+ PLAYING = "playing"
12
+ CHECK = "check"
13
+ CHECKMATE = "checkmate"
14
+ STALEMATE = "stalemate"
15
+ DRAW = "draw"
16
+
17
+ class MoveResult(Enum):
18
+ VALID = "valid"
19
+ INVALID = "invalid"
20
+ ILLEGAL = "illegal"
21
+
22
+ @dataclass
23
+ class MoveInfo:
24
+ move: chess.Move
25
+ san: str # Standard Algebraic Notation
26
+ uci: str # Universal Chess Interface notation
27
+ is_capture: bool
28
+ is_check: bool
29
+ is_checkmate: bool
30
+ is_castling: bool
31
+ promoted_piece: Optional[chess.PieceType] = None
32
+ is_promotion: bool = False
33
+ promotion_required: bool = False
34
+ available_promotions: Optional[List[str]] = None
35
+
36
+ class ChessBoard:
37
+ """
38
+ Enhanced chess board class that wraps python-chess with additional functionality
39
+ """
40
+
41
+ def __init__(self, fen: Optional[str] = None):
42
+ """
43
+ Initialize chess board
44
+
45
+ Args:
46
+ fen: FEN string to initialize position, defaults to starting position
47
+ """
48
+ self.board = chess.Board(fen) if fen else chess.Board()
49
+ self.move_history: List[MoveInfo] = []
50
+ self.position_history: List[str] = [self.board.fen()]
51
+
52
+ def get_board_state(self) -> Dict[str, Any]:
53
+ """Get current board state as dictionary"""
54
+ return {
55
+ 'fen': self.board.fen(),
56
+ 'turn': 'white' if self.board.turn == chess.WHITE else 'black',
57
+ 'game_state': self._get_game_state(),
58
+ 'castling_rights': {
59
+ 'white_kingside': self.board.has_kingside_castling_rights(chess.WHITE),
60
+ 'white_queenside': self.board.has_queenside_castling_rights(chess.WHITE),
61
+ 'black_kingside': self.board.has_kingside_castling_rights(chess.BLACK),
62
+ 'black_queenside': self.board.has_queenside_castling_rights(chess.BLACK),
63
+ },
64
+ 'en_passant': self.board.ep_square,
65
+ 'halfmove_clock': self.board.halfmove_clock,
66
+ 'fullmove_number': self.board.fullmove_number,
67
+ 'legal_moves': [move.uci() for move in self.board.legal_moves],
68
+ 'in_check': self.board.is_check(),
69
+ 'move_count': len(self.move_history)
70
+ }
71
+
72
+ def get_piece_at(self, square: str) -> Optional[Dict[str, Any]]:
73
+ """
74
+ Get piece information at given square
75
+
76
+ Args:
77
+ square: Square in algebraic notation (e.g., 'e4')
78
+
79
+ Returns:
80
+ Dictionary with piece info or None if empty square
81
+ """
82
+ try:
83
+ square_index = chess.parse_square(square)
84
+ piece = self.board.piece_at(square_index)
85
+
86
+ if piece is None:
87
+ return None
88
+
89
+ return {
90
+ 'type': piece.piece_type,
91
+ 'color': 'white' if piece.color == chess.WHITE else 'black',
92
+ 'symbol': piece.symbol(),
93
+ 'unicode': piece.unicode_symbol(),
94
+ 'square': square
95
+ }
96
+ except ValueError:
97
+ return None
98
+
99
+ def get_all_pieces(self) -> Dict[str, Dict[str, Any]]:
100
+ """Get all pieces on the board"""
101
+ pieces = {}
102
+ for square in chess.SQUARES:
103
+ square_name = chess.square_name(square)
104
+ piece_info = self.get_piece_at(square_name)
105
+ if piece_info:
106
+ pieces[square_name] = piece_info
107
+ return pieces
108
+
109
+ def make_move(self, move_str: str) -> Tuple[MoveResult, Optional[MoveInfo]]:
110
+ """
111
+ Make a move on the board
112
+
113
+ Args:
114
+ move_str: Move in UCI notation (e.g., 'e2e4', 'e7e8q') or SAN (e.g., 'e4', 'e8=Q')
115
+
116
+ Returns:
117
+ Tuple of (MoveResult, MoveInfo if successful)
118
+ """
119
+ try:
120
+ # Try to parse as UCI first, then SAN
121
+ try:
122
+ move = chess.Move.from_uci(move_str)
123
+ except ValueError:
124
+ move = self.board.parse_san(move_str)
125
+
126
+ # Check if this is a promotion move that requires piece specification
127
+ from_square = chess.square_name(move.from_square)
128
+ to_square = chess.square_name(move.to_square)
129
+
130
+ # If this is a promotion move but no promotion piece is specified, return promotion required
131
+ if self.is_promotion_move(from_square, to_square) and move.promotion is None:
132
+ # Get available promotion moves for this pawn
133
+ available_promotions = self.get_promotion_moves(from_square)
134
+
135
+ move_info = MoveInfo(
136
+ move=move,
137
+ san="", # Will be empty since move is incomplete
138
+ uci=move.uci(),
139
+ is_capture=self.board.is_capture(move),
140
+ is_check=False, # Cannot determine without promotion piece
141
+ is_checkmate=False,
142
+ is_castling=False,
143
+ promoted_piece=None,
144
+ is_promotion=True,
145
+ promotion_required=True,
146
+ available_promotions=available_promotions
147
+ )
148
+
149
+ return MoveResult.INVALID, move_info
150
+
151
+ # Check if move is legal
152
+ if move not in self.board.legal_moves:
153
+ return MoveResult.ILLEGAL, None
154
+
155
+ # Store move information before making the move
156
+ is_promotion = move.promotion is not None
157
+ move_info = MoveInfo(
158
+ move=move,
159
+ san=self.board.san(move),
160
+ uci=move.uci(),
161
+ is_capture=self.board.is_capture(move),
162
+ is_check=self.board.gives_check(move),
163
+ is_checkmate=False, # Will be updated after move
164
+ is_castling=self.board.is_castling(move),
165
+ promoted_piece=move.promotion,
166
+ is_promotion=is_promotion,
167
+ promotion_required=False,
168
+ available_promotions=None
169
+ )
170
+
171
+ # Make the move
172
+ self.board.push(move)
173
+
174
+ # Update move info with post-move state
175
+ move_info.is_checkmate = self.board.is_checkmate()
176
+
177
+ # Store in history
178
+ self.move_history.append(move_info)
179
+ self.position_history.append(self.board.fen())
180
+
181
+ return MoveResult.VALID, move_info
182
+
183
+ except ValueError:
184
+ return MoveResult.INVALID, None
185
+
186
+ def undo_move(self) -> bool:
187
+ """
188
+ Undo the last move
189
+
190
+ Returns:
191
+ True if successful, False if no moves to undo
192
+ """
193
+ if not self.move_history:
194
+ return False
195
+
196
+ self.board.pop()
197
+ self.move_history.pop()
198
+ self.position_history.pop()
199
+ return True
200
+
201
+ def get_legal_moves(self, square: Optional[str] = None) -> List[str]:
202
+ """
203
+ Get legal moves, optionally filtered by starting square
204
+
205
+ Args:
206
+ square: Starting square to filter moves (e.g., 'e2')
207
+
208
+ Returns:
209
+ List of legal moves in UCI notation
210
+ """
211
+ legal_moves = []
212
+
213
+ for move in self.board.legal_moves:
214
+ if square is None:
215
+ legal_moves.append(move.uci())
216
+ else:
217
+ try:
218
+ square_index = chess.parse_square(square)
219
+ if move.from_square == square_index:
220
+ legal_moves.append(move.uci())
221
+ except ValueError:
222
+ continue
223
+
224
+ return legal_moves
225
+
226
+ def get_move_history(self) -> List[Dict[str, Any]]:
227
+ """Get move history as list of dictionaries"""
228
+ return [
229
+ {
230
+ 'move_number': i + 1,
231
+ 'san': move.san,
232
+ 'uci': move.uci,
233
+ 'is_capture': move.is_capture,
234
+ 'is_check': move.is_check,
235
+ 'is_checkmate': move.is_checkmate,
236
+ 'is_castling': move.is_castling,
237
+ 'promoted_piece': move.promoted_piece,
238
+ 'is_promotion': move.is_promotion,
239
+ 'promotion_required': move.promotion_required,
240
+ 'available_promotions': move.available_promotions
241
+ }
242
+ for i, move in enumerate(self.move_history)
243
+ ]
244
+
245
+ def reset_board(self):
246
+ """Reset board to starting position"""
247
+ self.board = chess.Board()
248
+ self.move_history.clear()
249
+ self.position_history = [self.board.fen()]
250
+
251
+ def load_position(self, fen: str) -> bool:
252
+ """
253
+ Load position from FEN string
254
+
255
+ Args:
256
+ fen: FEN string
257
+
258
+ Returns:
259
+ True if successful, False if invalid FEN
260
+ """
261
+ try:
262
+ self.board = chess.Board(fen)
263
+ self.move_history.clear()
264
+ self.position_history = [fen]
265
+ return True
266
+ except ValueError:
267
+ return False
268
+
269
+ def _get_game_state(self) -> GameState:
270
+ """Determine current game state"""
271
+ if self.board.is_checkmate():
272
+ return GameState.CHECKMATE
273
+ elif self.board.is_stalemate():
274
+ return GameState.STALEMATE
275
+ elif self.board.is_insufficient_material() or \
276
+ self.board.is_seventyfive_moves() or \
277
+ self.board.is_fivefold_repetition():
278
+ return GameState.DRAW
279
+ elif self.board.is_check():
280
+ return GameState.CHECK
281
+ else:
282
+ return GameState.PLAYING
283
+
284
+ def get_board_array(self) -> List[List[Optional[str]]]:
285
+ """
286
+ Get board as 2D array for easier frontend rendering
287
+
288
+ Returns:
289
+ 8x8 array where each cell contains piece symbol or None
290
+ """
291
+ board_array = []
292
+ for rank in range(8):
293
+ row = []
294
+ for file in range(8):
295
+ square = chess.square(file, 7-rank) # Flip rank for display
296
+ piece = self.board.piece_at(square)
297
+ row.append(piece.symbol() if piece else None)
298
+ board_array.append(row)
299
+ return board_array
300
+
301
+ def get_attacked_squares(self, color: chess.Color) -> List[str]:
302
+ """Get squares attacked by given color"""
303
+ attacked = []
304
+ for square in chess.SQUARES:
305
+ if self.board.is_attacked_by(color, square):
306
+ attacked.append(chess.square_name(square))
307
+ return attacked
308
+
309
+ def is_square_attacked(self, square: str, by_color: str) -> bool:
310
+ """Check if square is attacked by given color"""
311
+ try:
312
+ square_index = chess.parse_square(square)
313
+ color = chess.WHITE if by_color.lower() == 'white' else chess.BLACK
314
+ return self.board.is_attacked_by(color, square_index)
315
+ except ValueError:
316
+ return False
317
+
318
+ def is_promotion_move(self, from_square: str, to_square: str) -> bool:
319
+ """
320
+ Detect if a move from one square to another would result in pawn promotion.
321
+
322
+ Args:
323
+ from_square: Starting square in algebraic notation (e.g., 'e7')
324
+ to_square: Destination square in algebraic notation (e.g., 'e8')
325
+
326
+ Returns:
327
+ True if the move is a pawn promotion, False otherwise
328
+ """
329
+ return PromotionMoveHandler.is_promotion_move(self.board, from_square, to_square)
330
+
331
+ def get_promotion_moves(self, square: str) -> List[str]:
332
+ """
333
+ Get all possible promotion moves for a pawn at the given square.
334
+
335
+ Args:
336
+ square: Square in algebraic notation (e.g., 'e7')
337
+
338
+ Returns:
339
+ List of promotion moves in UCI notation (e.g., ['e7e8q', 'e7e8r', 'e7e8b', 'e7e8n'])
340
+ """
341
+ return PromotionMoveHandler.get_promotion_moves(self.board, square)
chess_engine/pieces.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chess_engine/pieces.py
2
+
3
+ import chess
4
+ from typing import Dict, List, Optional, Tuple
5
+ from enum import Enum
6
+
7
+ class PieceType(Enum):
8
+ PAWN = chess.PAWN
9
+ KNIGHT = chess.KNIGHT
10
+ BISHOP = chess.BISHOP
11
+ ROOK = chess.ROOK
12
+ QUEEN = chess.QUEEN
13
+ KING = chess.KING
14
+
15
+ class PieceColor(Enum):
16
+ WHITE = chess.WHITE
17
+ BLACK = chess.BLACK
18
+
19
+ class ChessPiece:
20
+ """Represents a chess piece with its properties and behaviors"""
21
+
22
+ # Unicode symbols for pieces
23
+ UNICODE_PIECES = {
24
+ chess.WHITE: {
25
+ chess.PAWN: "♙", chess.KNIGHT: "♘", chess.BISHOP: "♗",
26
+ chess.ROOK: "♖", chess.QUEEN: "♕", chess.KING: "♔"
27
+ },
28
+ chess.BLACK: {
29
+ chess.PAWN: "♟", chess.KNIGHT: "♞", chess.BISHOP: "♝",
30
+ chess.ROOK: "♜", chess.QUEEN: "♛", chess.KING: "♚"
31
+ }
32
+ }
33
+
34
+ # Piece values for evaluation
35
+ PIECE_VALUES = {
36
+ chess.PAWN: 1,
37
+ chess.KNIGHT: 3,
38
+ chess.BISHOP: 3,
39
+ chess.ROOK: 5,
40
+ chess.QUEEN: 9,
41
+ chess.KING: 0 # King is invaluable
42
+ }
43
+
44
+ def __init__(self, piece_type: chess.PieceType, color: chess.Color):
45
+ self.piece_type = piece_type
46
+ self.color = color
47
+ self.piece = chess.Piece(piece_type, color)
48
+
49
+ @property
50
+ def symbol(self) -> str:
51
+ """Get the piece symbol (uppercase for white, lowercase for black)"""
52
+ return self.piece.symbol()
53
+
54
+ @property
55
+ def unicode_symbol(self) -> str:
56
+ """Get the Unicode symbol for the piece"""
57
+ return self.UNICODE_PIECES[self.color][self.piece_type]
58
+
59
+ @property
60
+ def value(self) -> int:
61
+ """Get the piece value"""
62
+ return self.PIECE_VALUES[self.piece_type]
63
+
64
+ @property
65
+ def name(self) -> str:
66
+ """Get the piece name"""
67
+ piece_names = {
68
+ chess.PAWN: "Pawn",
69
+ chess.KNIGHT: "Knight",
70
+ chess.BISHOP: "Bishop",
71
+ chess.ROOK: "Rook",
72
+ chess.QUEEN: "Queen",
73
+ chess.KING: "King"
74
+ }
75
+ return piece_names[self.piece_type]
76
+
77
+ @property
78
+ def color_name(self) -> str:
79
+ """Get the color name"""
80
+ return "White" if self.color == chess.WHITE else "Black"
81
+
82
+ def __str__(self) -> str:
83
+ return f"{self.color_name} {self.name}"
84
+
85
+ def __repr__(self) -> str:
86
+ return f"ChessPiece({self.piece_type}, {self.color})"
87
+
88
+ class PieceManager:
89
+ """Manages piece-related operations and utilities"""
90
+
91
+ @staticmethod
92
+ def create_piece(piece_type: str, color: str) -> Optional[ChessPiece]:
93
+ """
94
+ Create a piece from string representations
95
+
96
+ Args:
97
+ piece_type: 'pawn', 'knight', 'bishop', 'rook', 'queen', 'king'
98
+ color: 'white' or 'black'
99
+
100
+ Returns:
101
+ ChessPiece instance or None if invalid input
102
+ """
103
+ piece_type_map = {
104
+ 'pawn': chess.PAWN,
105
+ 'knight': chess.KNIGHT,
106
+ 'bishop': chess.BISHOP,
107
+ 'rook': chess.ROOK,
108
+ 'queen': chess.QUEEN,
109
+ 'king': chess.KING
110
+ }
111
+
112
+ color_map = {
113
+ 'white': chess.WHITE,
114
+ 'black': chess.BLACK
115
+ }
116
+
117
+ if piece_type.lower() not in piece_type_map or color.lower() not in color_map:
118
+ return None
119
+
120
+ return ChessPiece(
121
+ piece_type_map[piece_type.lower()],
122
+ color_map[color.lower()]
123
+ )
124
+
125
+ @staticmethod
126
+ def get_piece_moves(board: chess.Board, square: str) -> List[str]:
127
+ """
128
+ Get all possible moves for a piece at given square
129
+
130
+ Args:
131
+ board: Chess board instance
132
+ square: Square in algebraic notation
133
+
134
+ Returns:
135
+ List of destination squares in algebraic notation
136
+ """
137
+ try:
138
+ square_index = chess.parse_square(square)
139
+ piece = board.piece_at(square_index)
140
+
141
+ if piece is None:
142
+ return []
143
+
144
+ moves = []
145
+ for move in board.legal_moves:
146
+ if move.from_square == square_index:
147
+ moves.append(chess.square_name(move.to_square))
148
+
149
+ return moves
150
+
151
+ except ValueError:
152
+ return []
153
+
154
+ @staticmethod
155
+ def get_piece_attacks(board: chess.Board, square: str) -> List[str]:
156
+ """
157
+ Get all squares attacked by piece at given square
158
+
159
+ Args:
160
+ board: Chess board instance
161
+ square: Square in algebraic notation
162
+
163
+ Returns:
164
+ List of attacked squares in algebraic notation
165
+ """
166
+ try:
167
+ square_index = chess.parse_square(square)
168
+ piece = board.piece_at(square_index)
169
+
170
+ if piece is None:
171
+ return []
172
+
173
+ attacks = []
174
+ for target_square in chess.SQUARES:
175
+ if board.is_attacked_by(piece.color, target_square):
176
+ # Check if this specific piece is doing the attacking
177
+ # This is a simplified check - in a real game you might need more sophisticated logic
178
+ attacks.append(chess.square_name(target_square))
179
+
180
+ return attacks
181
+
182
+ except ValueError:
183
+ return []
184
+
185
+ @staticmethod
186
+ def get_material_count(board: chess.Board) -> Dict[str, Dict[str, int]]:
187
+ """
188
+ Get material count for both sides
189
+
190
+ Args:
191
+ board: Chess board instance
192
+
193
+ Returns:
194
+ Dictionary with material counts for white and black
195
+ """
196
+ white_pieces = {'pawn': 0, 'knight': 0, 'bishop': 0, 'rook': 0, 'queen': 0, 'king': 0}
197
+ black_pieces = {'pawn': 0, 'knight': 0, 'bishop': 0, 'rook': 0, 'queen': 0, 'king': 0}
198
+
199
+ piece_names = {
200
+ chess.PAWN: 'pawn',
201
+ chess.KNIGHT: 'knight',
202
+ chess.BISHOP: 'bishop',
203
+ chess.ROOK: 'rook',
204
+ chess.QUEEN: 'queen',
205
+ chess.KING: 'king'
206
+ }
207
+
208
+ for square in chess.SQUARES:
209
+ piece = board.piece_at(square)
210
+ if piece:
211
+ piece_name = piece_names[piece.piece_type]
212
+ if piece.color == chess.WHITE:
213
+ white_pieces[piece_name] += 1
214
+ else:
215
+ black_pieces[piece_name] += 1
216
+
217
+ return {
218
+ 'white': white_pieces,
219
+ 'black': black_pieces
220
+ }
221
+
222
+ @staticmethod
223
+ def get_material_value(board: chess.Board) -> Dict[str, int]:
224
+ """
225
+ Get total material value for both sides
226
+
227
+ Args:
228
+ board: Chess board instance
229
+
230
+ Returns:
231
+ Dictionary with material values for white and black
232
+ """
233
+ white_value = 0
234
+ black_value = 0
235
+
236
+ for square in chess.SQUARES:
237
+ piece = board.piece_at(square)
238
+ if piece:
239
+ value = ChessPiece.PIECE_VALUES[piece.piece_type]
240
+ if piece.color == chess.WHITE:
241
+ white_value += value
242
+ else:
243
+ black_value += value
244
+
245
+ return {
246
+ 'white': white_value,
247
+ 'black': black_value,
248
+ 'advantage': white_value - black_value
249
+ }
250
+
251
+ @staticmethod
252
+ def get_piece_list(board: chess.Board) -> Dict[str, List[Dict[str, str]]]:
253
+ """
254
+ Get list of all pieces on the board
255
+
256
+ Args:
257
+ board: Chess board instance
258
+
259
+ Returns:
260
+ Dictionary with lists of white and black pieces
261
+ """
262
+ white_pieces = []
263
+ black_pieces = []
264
+
265
+ for square in chess.SQUARES:
266
+ piece = board.piece_at(square)
267
+ if piece:
268
+ piece_info = {
269
+ 'type': chess.piece_name(piece.piece_type),
270
+ 'square': chess.square_name(square),
271
+ 'symbol': piece.symbol(),
272
+ 'unicode': ChessPiece.UNICODE_PIECES[piece.color][piece.piece_type]
273
+ }
274
+
275
+ if piece.color == chess.WHITE:
276
+ white_pieces.append(piece_info)
277
+ else:
278
+ black_pieces.append(piece_info)
279
+
280
+ return {
281
+ 'white': white_pieces,
282
+ 'black': black_pieces
283
+ }
284
+
285
+ @staticmethod
286
+ def is_promotion_move(board: chess.Board, move_str: str) -> bool:
287
+ """
288
+ Check if a move is a pawn promotion
289
+
290
+ Args:
291
+ board: Chess board instance
292
+ move_str: Move in UCI notation
293
+
294
+ Returns:
295
+ True if the move is a promotion
296
+ """
297
+ try:
298
+ move = chess.Move.from_uci(move_str)
299
+ return move.promotion is not None
300
+ except ValueError:
301
+ return False
302
+
303
+ @staticmethod
304
+ def get_promotion_pieces() -> List[Dict[str, str]]:
305
+ """
306
+ Get available promotion pieces
307
+
308
+ Returns:
309
+ List of promotion piece options
310
+ """
311
+ return [
312
+ {'type': 'queen', 'symbol': 'Q', 'name': 'Queen'},
313
+ {'type': 'rook', 'symbol': 'R', 'name': 'Rook'},
314
+ {'type': 'bishop', 'symbol': 'B', 'name': 'Bishop'},
315
+ {'type': 'knight', 'symbol': 'N', 'name': 'Knight'}
316
+ ]
chess_engine/promotion.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chess_engine/promotion.py
2
+
3
+ import chess
4
+ from typing import List, Optional, Tuple, Dict, Any
5
+ from enum import Enum
6
+
7
+ class PromotionPiece(Enum):
8
+ """Available pieces for pawn promotion"""
9
+ QUEEN = chess.QUEEN
10
+ ROOK = chess.ROOK
11
+ BISHOP = chess.BISHOP
12
+ KNIGHT = chess.KNIGHT
13
+
14
+ class PromotionMoveHandler:
15
+ """
16
+ Handles pawn promotion move detection, validation, and generation.
17
+
18
+ This class provides functionality to:
19
+ - Detect when a pawn move would result in promotion
20
+ - Validate promotion moves
21
+ - Generate all possible promotion moves for a given pawn
22
+ - Create UCI notation moves with promotion suffixes
23
+ """
24
+
25
+ # Mapping from piece names to chess.PieceType
26
+ PIECE_NAME_MAP = {
27
+ 'queen': chess.QUEEN,
28
+ 'rook': chess.ROOK,
29
+ 'bishop': chess.BISHOP,
30
+ 'knight': chess.KNIGHT,
31
+ 'q': chess.QUEEN,
32
+ 'r': chess.ROOK,
33
+ 'b': chess.BISHOP,
34
+ 'n': chess.KNIGHT
35
+ }
36
+
37
+ # Mapping from chess.PieceType to UCI notation
38
+ UCI_PIECE_MAP = {
39
+ chess.QUEEN: 'q',
40
+ chess.ROOK: 'r',
41
+ chess.BISHOP: 'b',
42
+ chess.KNIGHT: 'n'
43
+ }
44
+
45
+ @staticmethod
46
+ def is_promotion_move(board: chess.Board, from_square: str, to_square: str) -> bool:
47
+ """
48
+ Detect if a move from one square to another would result in pawn promotion.
49
+
50
+ Args:
51
+ board: Current chess board state
52
+ from_square: Starting square in algebraic notation (e.g., 'e7')
53
+ to_square: Destination square in algebraic notation (e.g., 'e8')
54
+
55
+ Returns:
56
+ True if the move is a pawn promotion, False otherwise
57
+ """
58
+ try:
59
+ from_idx = chess.parse_square(from_square)
60
+ to_idx = chess.parse_square(to_square)
61
+
62
+ # Get the piece at the from square
63
+ piece = board.piece_at(from_idx)
64
+
65
+ # Must be a pawn
66
+ if piece is None or piece.piece_type != chess.PAWN:
67
+ return False
68
+
69
+ # Check if pawn is moving to the back rank
70
+ to_rank = chess.square_rank(to_idx)
71
+
72
+ # White pawns promote on 8th rank (rank 7 in 0-indexed), black on 1st rank (rank 0)
73
+ if piece.color == chess.WHITE and to_rank == 7:
74
+ return True
75
+ elif piece.color == chess.BLACK and to_rank == 0:
76
+ return True
77
+
78
+ return False
79
+
80
+ except ValueError:
81
+ return False
82
+
83
+ @staticmethod
84
+ def can_pawn_promote(board: chess.Board, square: str) -> bool:
85
+ """
86
+ Check if a pawn at the given square can potentially promote.
87
+
88
+ Args:
89
+ board: Current chess board state
90
+ square: Square in algebraic notation (e.g., 'e7')
91
+
92
+ Returns:
93
+ True if the pawn can promote in the next move, False otherwise
94
+ """
95
+ try:
96
+ square_idx = chess.parse_square(square)
97
+ piece = board.piece_at(square_idx)
98
+
99
+ # Must be a pawn
100
+ if piece is None or piece.piece_type != chess.PAWN:
101
+ return False
102
+
103
+ rank = chess.square_rank(square_idx)
104
+
105
+ # White pawns on 7th rank (rank 6 in 0-indexed) can promote
106
+ # Black pawns on 2nd rank (rank 1 in 0-indexed) can promote
107
+ if piece.color == chess.WHITE and rank == 6:
108
+ return True
109
+ elif piece.color == chess.BLACK and rank == 1:
110
+ return True
111
+
112
+ return False
113
+
114
+ except ValueError:
115
+ return False
116
+
117
+ @staticmethod
118
+ def get_promotion_moves(board: chess.Board, square: str) -> List[str]:
119
+ """
120
+ Get all possible promotion moves for a pawn at the given square.
121
+
122
+ Args:
123
+ board: Current chess board state
124
+ square: Square in algebraic notation (e.g., 'e7')
125
+
126
+ Returns:
127
+ List of promotion moves in UCI notation (e.g., ['e7e8q', 'e7e8r', 'e7e8b', 'e7e8n'])
128
+ """
129
+ try:
130
+ from_idx = chess.parse_square(square)
131
+ piece = board.piece_at(from_idx)
132
+
133
+ # Must be a pawn that can promote
134
+ if not PromotionMoveHandler.can_pawn_promote(board, square):
135
+ return []
136
+
137
+ promotion_moves = []
138
+
139
+ # Check all legal moves from this square that are promotions
140
+ for move in board.legal_moves:
141
+ if move.from_square == from_idx and move.promotion is not None:
142
+ promotion_moves.append(move.uci())
143
+
144
+ return promotion_moves
145
+
146
+ except ValueError:
147
+ return []
148
+
149
+ @staticmethod
150
+ def validate_promotion_move(board: chess.Board, move_str: str) -> Tuple[bool, Optional[str]]:
151
+ """
152
+ Validate a promotion move in UCI notation.
153
+
154
+ Args:
155
+ board: Current chess board state
156
+ move_str: Move in UCI notation (e.g., 'e7e8q')
157
+
158
+ Returns:
159
+ Tuple of (is_valid, error_message)
160
+ """
161
+ try:
162
+ # Parse the move
163
+ if len(move_str) < 5:
164
+ return False, "Promotion move must include promotion piece (e.g., 'e7e8q')"
165
+
166
+ from_square = move_str[:2]
167
+ to_square = move_str[2:4]
168
+ promotion_piece = move_str[4:].lower()
169
+
170
+ # Validate squares
171
+ try:
172
+ from_idx = chess.parse_square(from_square)
173
+ to_idx = chess.parse_square(to_square)
174
+ except ValueError:
175
+ return False, "Invalid square notation"
176
+
177
+ # Validate promotion piece
178
+ if promotion_piece not in PromotionMoveHandler.UCI_PIECE_MAP.values():
179
+ return False, f"Invalid promotion piece '{promotion_piece}'. Must be one of: q, r, b, n"
180
+
181
+ # Check if this is actually a promotion move
182
+ if not PromotionMoveHandler.is_promotion_move(board, from_square, to_square):
183
+ return False, "Move is not a valid promotion move"
184
+
185
+ # Check if the move is legal
186
+ try:
187
+ move = chess.Move.from_uci(move_str)
188
+ if move not in board.legal_moves:
189
+ return False, "Move is not legal in current position"
190
+ except ValueError:
191
+ return False, "Invalid UCI move format"
192
+
193
+ return True, None
194
+
195
+ except Exception as e:
196
+ return False, f"Error validating promotion move: {str(e)}"
197
+
198
+ @staticmethod
199
+ def create_promotion_move(from_square: str, to_square: str, promotion_piece: str) -> str:
200
+ """
201
+ Create a UCI promotion move string.
202
+
203
+ Args:
204
+ from_square: Starting square (e.g., 'e7')
205
+ to_square: Destination square (e.g., 'e8')
206
+ promotion_piece: Piece to promote to ('queen', 'rook', 'bishop', 'knight', or 'q', 'r', 'b', 'n')
207
+
208
+ Returns:
209
+ UCI move string (e.g., 'e7e8q')
210
+ """
211
+ # Normalize promotion piece to UCI format
212
+ piece_lower = promotion_piece.lower()
213
+ if piece_lower in PromotionMoveHandler.PIECE_NAME_MAP:
214
+ piece_type = PromotionMoveHandler.PIECE_NAME_MAP[piece_lower]
215
+ uci_piece = PromotionMoveHandler.UCI_PIECE_MAP[piece_type]
216
+ else:
217
+ # Assume it's already in UCI format
218
+ uci_piece = piece_lower
219
+
220
+ return f"{from_square}{to_square}{uci_piece}"
221
+
222
+ @staticmethod
223
+ def get_available_promotions() -> List[Dict[str, Any]]:
224
+ """
225
+ Get list of available promotion pieces with their details.
226
+
227
+ Returns:
228
+ List of dictionaries containing promotion piece information
229
+ """
230
+ return [
231
+ {
232
+ 'type': 'queen',
233
+ 'symbol': 'Q',
234
+ 'uci': 'q',
235
+ 'name': 'Queen',
236
+ 'value': 9
237
+ },
238
+ {
239
+ 'type': 'rook',
240
+ 'symbol': 'R',
241
+ 'uci': 'r',
242
+ 'name': 'Rook',
243
+ 'value': 5
244
+ },
245
+ {
246
+ 'type': 'bishop',
247
+ 'symbol': 'B',
248
+ 'uci': 'b',
249
+ 'name': 'Bishop',
250
+ 'value': 3
251
+ },
252
+ {
253
+ 'type': 'knight',
254
+ 'symbol': 'N',
255
+ 'uci': 'n',
256
+ 'name': 'Knight',
257
+ 'value': 3
258
+ }
259
+ ]
260
+
261
+ @staticmethod
262
+ def parse_promotion_move(move_str: str) -> Optional[Dict[str, str]]:
263
+ """
264
+ Parse a promotion move string and extract components.
265
+
266
+ Args:
267
+ move_str: Move in UCI notation (e.g., 'e7e8q')
268
+
269
+ Returns:
270
+ Dictionary with move components or None if invalid
271
+ """
272
+ try:
273
+ if len(move_str) < 5:
274
+ return None
275
+
276
+ from_square = move_str[:2]
277
+ to_square = move_str[2:4]
278
+ promotion_piece = move_str[4:].lower()
279
+
280
+ # Validate components
281
+ chess.parse_square(from_square) # Will raise ValueError if invalid
282
+ chess.parse_square(to_square) # Will raise ValueError if invalid
283
+
284
+ if promotion_piece not in PromotionMoveHandler.UCI_PIECE_MAP.values():
285
+ return None
286
+
287
+ # Convert UCI piece to full name
288
+ piece_name_map = {v: k for k, v in PromotionMoveHandler.UCI_PIECE_MAP.items()}
289
+ piece_type = piece_name_map[promotion_piece]
290
+ piece_names = {
291
+ chess.QUEEN: 'queen',
292
+ chess.ROOK: 'rook',
293
+ chess.BISHOP: 'bishop',
294
+ chess.KNIGHT: 'knight'
295
+ }
296
+
297
+ return {
298
+ 'from_square': from_square,
299
+ 'to_square': to_square,
300
+ 'promotion_piece_uci': promotion_piece,
301
+ 'promotion_piece_name': piece_names[piece_type],
302
+ 'full_move': move_str
303
+ }
304
+
305
+ except ValueError:
306
+ return None
docker-entrypoint.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+
3
+ # Add /usr/games to PATH so stockfish can be found
4
+ export PATH=$PATH:/usr/games
5
+
6
+ # Start the backend server in the background
7
+ python -m uvicorn app:app --host 0.0.0.0 --port 8000 &
8
+ BACKEND_PID=$!
9
+
10
+ # Start the frontend server in the foreground
11
+ cd frontend/web
12
+ npm run dev -- --host 0.0.0.0 --port 5173
13
+ FRONTEND_PID=$!
14
+
15
+ # Wait for either process to exit
16
+ wait -n $BACKEND_PID $FRONTEND_PID
17
+
18
+ # Exit with the status of the process that exited
19
+ exit $?
main.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import sys
3
+ from chess_engine.api.cli import main as cli_main
4
+
5
+ # Try to import uvicorn, but don't fail if it's not available
6
+ try:
7
+ import uvicorn
8
+ UVICORN_AVAILABLE = True
9
+ except ImportError:
10
+ UVICORN_AVAILABLE = False
11
+
12
+ def main():
13
+ """Main entry point for the application"""
14
+ parser = argparse.ArgumentParser(description="Chess Engine with Stockfish")
15
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
16
+
17
+ # CLI command
18
+ cli_parser = subparsers.add_parser("cli", help="Run command-line interface")
19
+ cli_parser.add_argument("--color", "-c", choices=["white", "black"], default="white",
20
+ help="Player's color (default: white)")
21
+ cli_parser.add_argument("--difficulty", "-d",
22
+ choices=["beginner", "easy", "medium", "hard", "expert", "master"],
23
+ default="medium", help="AI difficulty level (default: medium)")
24
+ cli_parser.add_argument("--time", "-t", type=float, default=1.0,
25
+ help="Time limit for AI moves in seconds (default: 1.0)")
26
+ cli_parser.add_argument("--stockfish-path", "-s", type=str, default=None,
27
+ help="Path to Stockfish executable")
28
+ cli_parser.add_argument("--no-book", action="store_true",
29
+ help="Disable opening book")
30
+ cli_parser.add_argument("--no-analysis", action="store_true",
31
+ help="Disable position analysis")
32
+
33
+ # API command
34
+ api_parser = subparsers.add_parser("api", help="Run REST API server")
35
+ api_parser.add_argument("--host", default="127.0.0.1", help="Host to bind (default: 127.0.0.1)")
36
+ api_parser.add_argument("--port", "-p", type=int, default=8000, help="Port to bind (default: 8000)")
37
+ api_parser.add_argument("--reload", action="store_true", help="Enable auto-reload")
38
+
39
+ args = parser.parse_args()
40
+
41
+ if args.command == "cli":
42
+ cli_main(args)
43
+ elif args.command == "api":
44
+ if not UVICORN_AVAILABLE:
45
+ print("Error: uvicorn package is not installed. Please install it with:")
46
+ print("pip install uvicorn")
47
+ sys.exit(1)
48
+
49
+ try:
50
+ from chess_engine.api.rest_api import app
51
+ uvicorn.run(app, host=args.host, port=args.port, reload=args.reload)
52
+ except ImportError as e:
53
+ print(f"Error: {e}")
54
+ print("Make sure all required packages are installed:")
55
+ print("pip install fastapi uvicorn")
56
+ sys.exit(1)
57
+ else:
58
+ # Default to CLI if no command specified
59
+ cli_main()
60
+
61
+ if __name__ == "__main__":
62
+ main()
requirements.api.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ chess==1.10.0
2
+ stockfish==3.28.0
3
+ fastapi==0.104.1
4
+ uvicorn==0.24.0
5
+ websockets==11.0.2
6
+ pydantic==2.5.0
7
+ python-multipart==0.0.6
8
+ sqlalchemy==2.0.23
9
+ alembic==1.12.1
10
+ asyncpg==0.29.0
11
+ aiosqlite==0.19.0
12
+ python-dotenv==1.0.0
13
+ click==8.1.7
14
+ colorama==0.4.6
web/.npmrc ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ registry=https://registry.npmjs.org/
2
+ legacy-peer-deps=true
web/IMPLEMENTATION_DETAILS.md ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chess UI Implementation Details
2
+
3
+ ## Overview
4
+
5
+ This implementation provides a modern, interactive web UI for the chess engine using React with TypeScript and Tailwind CSS. The UI is designed to be fluid, responsive, and user-friendly, addressing the limitations of the previous Streamlit interface.
6
+
7
+ ## Key Features
8
+
9
+ 1. **Interactive Chess Board**:
10
+ - Pieces can be moved by clicking on the source and destination squares
11
+ - Valid moves are highlighted with green dots
12
+ - Hint moves are highlighted with a light color
13
+ - Last move is animated for better visibility
14
+
15
+ 2. **Visual Feedback**:
16
+ - Selected pieces are highlighted
17
+ - Legal moves are shown with green dots
18
+ - Hints highlight both source and destination squares
19
+ - Move animations provide visual feedback
20
+
21
+ 3. **Game Controls**:
22
+ - New Game with configurable options (difficulty, color, time limit)
23
+ - Undo Move functionality
24
+ - Resign option
25
+ - Board theme selection (brown or grey)
26
+
27
+ 4. **Game Information**:
28
+ - Current game status display
29
+ - Move history tracking
30
+ - Position analysis with evaluation bar
31
+ - Material and positional advantage indicators
32
+
33
+ ## Technical Implementation
34
+
35
+ ### Components Structure
36
+
37
+ 1. **App.tsx**: Main application component that organizes the layout
38
+ 2. **ChessBoard.tsx**: Handles the chess board rendering and interaction logic
39
+ 3. **GameControls.tsx**: Provides game control buttons and options
40
+ 4. **GameInfo.tsx**: Displays game status, move history, and analysis
41
+
42
+ ### API Integration
43
+
44
+ The UI communicates with the backend REST API using Axios. The main endpoints used are:
45
+
46
+ - `/game/new`: Start a new game with specified options
47
+ - `/game/move`: Make a move on the board
48
+ - `/game/state`: Get current game state
49
+ - `/game/hint`: Get a hint for the current position
50
+ - `/game/undo`: Undo moves
51
+ - `/game/resign`: Resign the current game
52
+
53
+ ### Chess Logic
54
+
55
+ - FEN parsing to render the board state
56
+ - Legal move validation through the API
57
+ - Piece movement handling
58
+ - Game state tracking
59
+
60
+ ### Styling
61
+
62
+ - Tailwind CSS for responsive design
63
+ - Custom CSS for chess-specific styling
64
+ - Animations for piece movement
65
+ - Theme support for different board styles
66
+
67
+ ## Asset Usage
68
+
69
+ The implementation uses the provided chess assets:
70
+
71
+ - Board squares from both brown and grey themes
72
+ - Chess piece images for both black and white pieces
73
+
74
+ ## Future Enhancements
75
+
76
+ 1. **Drag and Drop**: Implement drag-and-drop functionality for piece movement
77
+ 2. **Sound Effects**: Add sound effects for moves, captures, and game events
78
+ 3. **Move Notation**: Display algebraic notation for moves in the history panel
79
+ 4. **Time Control**: Add chess clock functionality
80
+ 5. **Opening Explorer**: Integrate an opening book explorer
81
+ 6. **Game Analysis**: Enhanced post-game analysis features
82
+ 7. **Local Storage**: Save game state to browser storage for resuming games
web/IMPLEMENTATION_SUMMARY.md ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chess UI Implementation Summary
2
+
3
+ ## Overview
4
+
5
+ This modern web UI for the chess engine is built using React with TypeScript and Tailwind CSS. It provides a fluid, interactive experience for users, addressing the limitations of the previous Streamlit interface.
6
+
7
+ ## Key Features
8
+
9
+ ### Interactive Chess Board
10
+ - Pieces can be moved by clicking on source and destination squares
11
+ - Valid moves are highlighted with green dots
12
+ - Hint system with highlighted source and destination squares
13
+ - Visual feedback for moves and selections
14
+ - Support for both mouse and touch interactions
15
+
16
+ ### Game Controls
17
+ - New game with configurable options (difficulty, color, time limit)
18
+ - Undo move functionality
19
+ - Resign option
20
+ - Board theme selection (brown and grey)
21
+ - Sound toggle
22
+
23
+ ### Game Information
24
+ - Game status display
25
+ - Move history in algebraic notation
26
+ - Position analysis with evaluation bar
27
+ - Captured pieces display with material advantage
28
+ - Chess timer for both players
29
+
30
+ ### Visual and Audio Feedback
31
+ - Animations for piece movement
32
+ - Highlighting of last move
33
+ - Sound effects for different move types (regular move, capture, check, etc.)
34
+ - High-resolution images for Retina displays
35
+
36
+ ## Technical Implementation
37
+
38
+ ### Component Structure
39
+ - **App.tsx**: Main application component that organizes the layout
40
+ - **ChessBoard.tsx**: Handles the chess board rendering and interaction logic
41
+ - **GameControls.tsx**: Provides game control buttons and options
42
+ - **GameInfo.tsx**: Displays game status, move history, and analysis
43
+ - **CapturedPieces.tsx**: Shows captured pieces and material advantage
44
+ - **ChessTimer.tsx**: Displays and manages the chess clock
45
+
46
+ ### State Management
47
+ - React hooks for local component state
48
+ - Polling mechanism to keep UI in sync with backend state
49
+ - FEN parsing to render the board state
50
+
51
+ ### API Integration
52
+ - Communication with the backend REST API using Axios
53
+ - Endpoints for game state, moves, hints, and game control
54
+
55
+ ### Styling
56
+ - Tailwind CSS for responsive design
57
+ - Custom CSS for chess-specific styling
58
+ - Animations for piece movement and square highlighting
59
+
60
+ ### Asset Management
61
+ - Support for both 1x and 2x resolution images
62
+ - Sound effects for different move types
63
+ - Multiple board themes
64
+
65
+ ## User Experience Improvements
66
+
67
+ 1. **Intuitive Interaction**: Users can simply click on a piece and then click on a valid destination square to make a move, with visual feedback at each step.
68
+
69
+ 2. **Visual Cues**: Valid moves are clearly marked with green dots, and the hint feature highlights both source and destination squares.
70
+
71
+ 3. **Responsive Design**: The UI adapts to different screen sizes, making it usable on both desktop and mobile devices.
72
+
73
+ 4. **Game Information**: Users can see the game status, move history, captured pieces, and position analysis all in one view.
74
+
75
+ 5. **Customization**: Users can choose between different board themes and toggle sound effects.
76
+
77
+ ## Future Enhancements
78
+
79
+ 1. **Drag and Drop**: Implement drag-and-drop functionality for piece movement
80
+ 2. **Opening Explorer**: Add an opening book explorer
81
+ 3. **Game Analysis**: Enhance post-game analysis features
82
+ 4. **Local Storage**: Save game state to browser storage for resuming games
83
+ 5. **Multiplayer**: Add support for playing against other users
84
+ 6. **Accessibility**: Improve keyboard navigation and screen reader support
85
+
86
+ ## Conclusion
87
+
88
+ This implementation provides a modern, interactive web UI for the chess engine that is both visually appealing and user-friendly. It addresses all the requirements specified in the initial request and adds several additional features to enhance the user experience.
web/OVERVIEW.md ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chess UI Implementation Overview
2
+
3
+ ## Architecture
4
+
5
+ The Chess UI is built using React with TypeScript and Tailwind CSS, providing a modern, fluid, and interactive user experience. The application communicates with the backend chess engine via a REST API.
6
+
7
+ ## Key Components
8
+
9
+ ### 1. ChessBoard
10
+ - Interactive chess board with piece movement
11
+ - Visual indicators for legal moves (green dots)
12
+ - Hint system with highlighted squares
13
+ - Last move highlighting
14
+ - Support for high-resolution (2x) assets
15
+ - Sound effects for moves, captures, and other game events
16
+
17
+ ### 2. GameControls
18
+ - New game button with configurable options
19
+ - Undo move functionality
20
+ - Resign option
21
+ - Board theme selection (brown or grey)
22
+
23
+ ### 3. GameInfo
24
+ - Game status display
25
+ - Move history in a tabular format
26
+ - Position analysis with evaluation bar
27
+ - Material advantage indicator
28
+ - Captured pieces display
29
+
30
+ ### 4. Additional Components
31
+ - **ChessTimer**: Chess clock for time control
32
+ - **CapturedPieces**: Visual display of captured pieces
33
+ - **PromotionDialog**: Dialog for pawn promotion
34
+
35
+ ## Features
36
+
37
+ ### Interactive Gameplay
38
+ - Click-to-move interface
39
+ - Valid moves are highlighted with green dots
40
+ - Pieces animate when moved
41
+ - Last move is highlighted
42
+
43
+ ### Visual Feedback
44
+ - Highlighted squares for selected pieces
45
+ - Visual indicators for check
46
+ - Animation for piece movement
47
+ - Hint highlighting
48
+
49
+ ### Game Analysis
50
+ - Position evaluation display
51
+ - Material advantage calculation
52
+ - Move history tracking
53
+
54
+ ### User Experience
55
+ - Responsive design for various screen sizes
56
+ - Multiple board themes
57
+ - Sound effects for game events
58
+ - Time control with chess clock
59
+
60
+ ## Technical Implementation
61
+
62
+ ### State Management
63
+ - React hooks for component state
64
+ - Polling mechanism for game state updates
65
+ - FEN parsing for board representation
66
+
67
+ ### API Integration
68
+ - Axios for API communication
69
+ - Endpoints for game control, moves, and analysis
70
+ - Error handling for failed requests
71
+
72
+ ### Styling
73
+ - Tailwind CSS for responsive design
74
+ - Custom CSS for chess-specific styling
75
+ - Animations for enhanced user experience
76
+
77
+ ### Asset Handling
78
+ - Support for both 1x and 2x resolution assets
79
+ - Dynamic loading based on device resolution
80
+ - Preloading of assets for smoother experience
81
+
82
+ ## Future Enhancements
83
+
84
+ 1. **Drag and Drop**: Implement drag-and-drop functionality for piece movement
85
+ 2. **Opening Explorer**: Add an opening book explorer
86
+ 3. **Game Analysis**: Enhanced post-game analysis features
87
+ 4. **Offline Support**: Add local storage for game state persistence
88
+ 5. **Multiplayer**: Add support for playing against other users
89
+ 6. **Accessibility**: Enhance keyboard navigation and screen reader support
90
+ 7. **Themes**: Add more board and piece themes
91
+ 8. **Annotations**: Allow users to annotate moves and positions
web/README.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chess Engine Web UI
2
+
3
+ A modern, interactive web interface for the chess engine using React and Tailwind CSS.
4
+
5
+ ## Features
6
+
7
+ - Interactive chess board with piece movement
8
+ - Visual indicators for legal moves (green dots)
9
+ - Hint system with highlighted squares
10
+ - Game controls (new game, undo, resign)
11
+ - Position analysis display
12
+ - Move history tracking
13
+ - Responsive design for various screen sizes
14
+ - Multiple board themes (brown and grey)
15
+
16
+ ## Setup
17
+
18
+ ### WSL Setup (Recommended)
19
+
20
+ If you're using Windows Subsystem for Linux (WSL):
21
+
22
+ 1. Run the setup script to install all dependencies:
23
+ ```bash
24
+ chmod +x setup.sh
25
+ ./setup.sh
26
+ ```
27
+
28
+ 2. Copy the chess assets to the public folder:
29
+ ```bash
30
+ chmod +x copy-assets.sh
31
+ ./copy-assets.sh
32
+ ```
33
+
34
+ 3. Start the development server:
35
+ ```bash
36
+ npm run dev
37
+ ```
38
+
39
+ For detailed WSL-specific instructions, see [WSL_SETUP.md](./WSL_SETUP.md).
40
+
41
+ ### Windows Setup
42
+
43
+ 1. Run the setup script to install all dependencies:
44
+ ```
45
+ setup.bat
46
+ ```
47
+
48
+ 2. Copy the chess assets to the public folder (see Asset Setup section below)
49
+
50
+ 3. Start the development server:
51
+ ```
52
+ npm run dev
53
+ ```
54
+
55
+ ### Manual Setup
56
+
57
+ 1. Install dependencies:
58
+ ```
59
+ npm install --save react react-dom axios
60
+ npm install --save-dev typescript @types/react @types/react-dom @vitejs/plugin-react vite tailwindcss postcss autoprefixer
61
+ ```
62
+
63
+ 2. Start the development server:
64
+ ```
65
+ npm run dev
66
+ ```
67
+
68
+ 3. Build for production:
69
+ ```
70
+ npm run build
71
+ ```
72
+
73
+ ## Troubleshooting TypeScript Errors
74
+
75
+ If you encounter TypeScript errors related to React, try these steps:
76
+
77
+ 1. Make sure all dependencies are installed:
78
+ ```
79
+ npm install
80
+ ```
81
+
82
+ 2. If you see "Cannot find module 'react'" errors, try:
83
+ ```bash
84
+ # WSL/Linux
85
+ ./fix-dependencies.sh
86
+
87
+ # Windows
88
+ fix-dependencies.bat
89
+ ```
90
+
91
+ 3. Restart your IDE or TypeScript server
92
+
93
+ 4. For detailed instructions, see [TYPESCRIPT_FIXES.md](./TYPESCRIPT_FIXES.md)
94
+
95
+ ## Asset Setup
96
+
97
+ Before running the application, you need to copy the chess assets to the public folder:
98
+
99
+ 1. Copy all files from `frontend/assets/pieces` to `frontend/web/public/assets/pieces`
100
+ 2. Copy all files from `frontend/assets/boards/brown` to `frontend/web/public/assets/boards/brown`
101
+ 3. Copy all files from `frontend/assets/boards/grey` to `frontend/web/public/assets/boards/grey`
102
+
103
+ Or use the provided script:
104
+ ```bash
105
+ # WSL/Linux
106
+ ./copy-assets.sh
107
+ ```
108
+
109
+ ### High-Resolution Assets
110
+
111
+ For high-resolution displays:
112
+ 1. Copy your 2x assets to the same folders, keeping the same naming convention but with "2x" instead of "1x"
113
+ 2. Example: `b_bishop_1x.png` and `b_bishop_2x.png` should both be in the pieces folder
114
+
115
+ ## Backend Integration
116
+
117
+ The web UI communicates with the chess engine's REST API. Make sure the backend API is running on port 8000 or update the proxy configuration in `vite.config.ts` to match your backend URL.
118
+
119
+ ## Technologies Used
120
+
121
+ - React 18
122
+ - TypeScript
123
+ - Tailwind CSS
124
+ - Vite
125
+ - Axios for API communication
web/SETUP_GUIDE.md ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chess UI Setup Guide
2
+
3
+ This guide will help you set up and run the Chess UI web application.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js (v14 or later)
8
+ - npm (v6 or later)
9
+ - Python with the chess engine backend running
10
+
11
+ ## Setup Steps
12
+
13
+ ### 1. Install Dependencies
14
+
15
+ Navigate to the web UI directory and install the required dependencies:
16
+
17
+ ```bash
18
+ cd frontend/web
19
+ npm install
20
+ ```
21
+
22
+ ### 2. Copy Chess Assets
23
+
24
+ The UI requires chess piece and board images. Copy them from the assets directory:
25
+
26
+ ```bash
27
+ # Create the necessary directories if they don't exist
28
+ mkdir -p public/assets/pieces
29
+ mkdir -p public/assets/boards/brown
30
+ mkdir -p public/assets/boards/grey
31
+ mkdir -p public/assets/sounds
32
+
33
+ # Copy the assets
34
+ cp ../../frontend/assets/pieces/*.png public/assets/pieces/
35
+ cp ../../frontend/assets/boards/brown/*.png public/assets/boards/brown/
36
+ cp ../../frontend/assets/boards/grey/*.png public/assets/boards/grey/
37
+ ```
38
+
39
+ ### 3. Add Sound Files (Optional)
40
+
41
+ For a better experience, add sound effects to the `public/assets/sounds` directory:
42
+ - move.mp3 - Sound for regular piece movement
43
+ - capture.mp3 - Sound for capturing a piece
44
+ - check.mp3 - Sound for when a king is in check
45
+ - castle.mp3 - Sound for castling
46
+ - promote.mp3 - Sound for pawn promotion
47
+ - game-start.mp3 - Sound for starting a new game
48
+ - game-end.mp3 - Sound for game ending
49
+
50
+ ### 4. Configure Backend URL
51
+
52
+ The UI is configured to connect to the backend at `http://localhost:8000`. If your backend is running on a different URL, update the `vite.config.ts` file:
53
+
54
+ ```typescript
55
+ server: {
56
+ proxy: {
57
+ '/api': {
58
+ target: 'http://your-backend-url:port',
59
+ changeOrigin: true,
60
+ rewrite: (path) => path.replace(/^\/api/, '')
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ### 5. Start the Development Server
67
+
68
+ Run the development server:
69
+
70
+ ```bash
71
+ npm run dev
72
+ ```
73
+
74
+ The UI will be available at `http://localhost:5173` (or another port if 5173 is in use).
75
+
76
+ ### 6. Build for Production
77
+
78
+ To create a production build:
79
+
80
+ ```bash
81
+ npm run build
82
+ ```
83
+
84
+ The build output will be in the `dist` directory, which you can serve using any static file server.
85
+
86
+ ## Troubleshooting
87
+
88
+ ### TypeScript Errors
89
+
90
+ If you encounter TypeScript errors related to React:
91
+
92
+ 1. Run the fix-dependencies script:
93
+ ```bash
94
+ ./fix-dependencies.bat
95
+ ```
96
+
97
+ 2. Or manually reinstall React dependencies:
98
+ ```bash
99
+ npm uninstall react react-dom @types/react @types/react-dom
100
+ npm install --save react react-dom
101
+ npm install --save-dev @types/react @types/react-dom
102
+ ```
103
+
104
+ ### Backend Connection Issues
105
+
106
+ If the UI can't connect to the backend:
107
+
108
+ 1. Make sure the backend server is running
109
+ 2. Check that the proxy configuration in `vite.config.ts` points to the correct URL
110
+ 3. Look for CORS errors in the browser console and update the backend to allow requests from the UI's origin
111
+
112
+ ## Features
113
+
114
+ - Interactive chess board with piece movement
115
+ - Visual indicators for legal moves (green dots)
116
+ - Hint system with highlighted squares
117
+ - Game controls (new game, undo, resign)
118
+ - Position analysis display
119
+ - Move history tracking
120
+ - Captured pieces display
121
+ - Chess timer
122
+ - Sound effects for moves and captures
123
+ - Multiple board themes (brown and grey)
web/SETUP_INSTRUCTIONS.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Chess UI Setup Instructions
2
+
3
+ This document provides detailed instructions for setting up and running the Chess UI.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js (v14 or later)
8
+ - npm (v6 or later)
9
+ - Python backend with FastAPI (for the chess engine)
10
+
11
+ ## Setup Steps
12
+
13
+ ### 1. Install Dependencies
14
+
15
+ Navigate to the web UI directory and install the required dependencies:
16
+
17
+ ```bash
18
+ cd frontend/web
19
+ npm install
20
+ ```
21
+
22
+ ### 2. Copy Assets
23
+
24
+ Copy the chess assets to the public directory:
25
+
26
+ ```bash
27
+ # Create directories if they don't exist
28
+ mkdir -p public/assets/pieces
29
+ mkdir -p public/assets/boards/brown
30
+ mkdir -p public/assets/boards/grey
31
+ mkdir -p public/assets/sounds
32
+
33
+ # Copy assets
34
+ cp ../../assets/pieces/*.png public/assets/pieces/
35
+ cp ../../assets/boards/brown/*.png public/assets/boards/brown/
36
+ cp ../../assets/boards/grey/*.png public/assets/boards/grey/
37
+ ```
38
+
39
+ ### 3. Add Sound Effects (Optional)
40
+
41
+ For a better user experience, add sound effects to the `public/assets/sounds` directory:
42
+
43
+ - `move.mp3` - Sound when a piece moves
44
+ - `capture.mp3` - Sound when a piece is captured
45
+ - `check.mp3` - Sound when a king is in check
46
+ - `castle.mp3` - Sound when castling
47
+ - `promote.mp3` - Sound when a pawn is promoted
48
+ - `game-end.mp3` - Sound when the game ends
49
+ - `illegal.mp3` - Sound when an illegal move is attempted
50
+
51
+ ### 4. Start the Backend
52
+
53
+ Make sure your chess engine backend is running:
54
+
55
+ ```bash
56
+ cd ../.. # Return to project root
57
+ python -m uvicorn chess_engine.api.rest_api:app --reload
58
+ ```
59
+
60
+ ### 5. Start the Web UI
61
+
62
+ In a separate terminal, start the web UI development server:
63
+
64
+ ```bash
65
+ cd frontend/web
66
+ npm run dev
67
+ ```
68
+
69
+ The UI will be available at http://localhost:5173
70
+
71
+ ## Building for Production
72
+
73
+ To create a production build:
74
+
75
+ ```bash
76
+ npm run build
77
+ ```
78
+
79
+ The built files will be in the `dist` directory, which can be served by any static file server.
80
+
81
+ ## Troubleshooting
82
+
83
+ ### TypeScript Errors
84
+
85
+ If you encounter TypeScript errors related to React:
86
+
87
+ 1. Run the fix-dependencies script:
88
+ ```bash
89
+ ./fix-dependencies.bat
90
+ ```
91
+
92
+ 2. Restart your IDE or TypeScript server
93
+
94
+ ### API Connection Issues
95
+
96
+ If the UI can't connect to the backend:
97
+
98
+ 1. Check that the backend is running on port 8000
99
+ 2. Verify the proxy settings in `vite.config.ts`
100
+ 3. Check browser console for CORS or network errors
101
+
102
+ ### Missing Assets
103
+
104
+ If chess pieces or board squares don't appear:
105
+
106
+ 1. Verify that all assets are correctly copied to the public directory
107
+ 2. Check browser console for 404 errors
108
+ 3. Ensure the file paths in the code match your asset directory structure
web/TYPESCRIPT_FIXES.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fixing TypeScript Errors in the Chess UI
2
+
3
+ If you're encountering TypeScript errors related to React modules not being found, follow these steps to resolve them:
4
+
5
+ ## Step 1: Reinstall Dependencies
6
+
7
+ Run the provided script to reinstall React and its type definitions:
8
+
9
+ ```
10
+ fix-dependencies.bat
11
+ ```
12
+
13
+ Or manually run these commands:
14
+
15
+ ```
16
+ npm uninstall react react-dom @types/react @types/react-dom
17
+ npm install --save react react-dom
18
+ npm install --save-dev @types/react @types/react-dom
19
+ ```
20
+
21
+ ## Step 2: Update Import Syntax
22
+
23
+ Make sure all React imports use the following syntax:
24
+
25
+ ```typescript
26
+ import * as React from 'react';
27
+ import { useState, useEffect } from 'react';
28
+ ```
29
+
30
+ Instead of:
31
+
32
+ ```typescript
33
+ import React, { useState, useEffect } from 'react';
34
+ ```
35
+
36
+ ## Step 3: Restart TypeScript Server
37
+
38
+ If you're using VS Code or another IDE, restart the TypeScript server:
39
+
40
+ - VS Code: Press Ctrl+Shift+P (or Cmd+Shift+P on Mac), type "TypeScript: Restart TS Server" and press Enter
41
+
42
+ ## Step 4: Check tsconfig.json
43
+
44
+ Make sure your tsconfig.json has the correct configuration:
45
+
46
+ ```json
47
+ {
48
+ "compilerOptions": {
49
+ "moduleResolution": "node",
50
+ "jsx": "react-jsx",
51
+ "allowSyntheticDefaultImports": true,
52
+ "types": ["vite/client", "node"]
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Additional Tips
58
+
59
+ 1. If you're still having issues, try creating a new Vite project with React and TypeScript template and compare the configurations:
60
+
61
+ ```
62
+ npm create vite@latest my-react-app -- --template react-ts
63
+ ```
64
+
65
+ 2. Make sure your node_modules folder is properly installed and not corrupted.
66
+
67
+ 3. Check for any conflicting TypeScript configurations in your project.
68
+
69
+ 4. If all else fails, you can use JavaScript (.jsx/.js) files temporarily instead of TypeScript until the issue is resolved.
web/WSL_SETUP.md ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Setting Up the Chess UI in WSL
2
+
3
+ This guide provides instructions for setting up and running the Chess UI in a Windows Subsystem for Linux (WSL) environment.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. WSL installed with a Linux distribution (Ubuntu recommended)
8
+ 2. Node.js and npm installed in your WSL environment
9
+
10
+ ## Installation Steps
11
+
12
+ ### 1. Install Node.js and npm (if not already installed)
13
+
14
+ ```bash
15
+ # Update package lists
16
+ sudo apt update
17
+
18
+ # Install Node.js and npm
19
+ sudo apt install nodejs npm
20
+
21
+ # Check installation
22
+ node --version
23
+ npm --version
24
+ ```
25
+
26
+ Alternatively, use nvm for better Node.js version management:
27
+
28
+ ```bash
29
+ # Install nvm
30
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
31
+
32
+ # Reload shell configuration
33
+ source ~/.bashrc
34
+
35
+ # Install latest LTS version of Node.js
36
+ nvm install --lts
37
+
38
+ # Use the installed version
39
+ nvm use --lts
40
+ ```
41
+
42
+ ### 2. Set Up the Project
43
+
44
+ Navigate to the project directory and run the setup script:
45
+
46
+ ```bash
47
+ cd frontend/web
48
+ chmod +x setup.sh
49
+ ./setup.sh
50
+ ```
51
+
52
+ ### 3. Copy Assets
53
+
54
+ Run the asset copy script to copy chess pieces and board images to the public directory:
55
+
56
+ ```bash
57
+ chmod +x copy-assets.sh
58
+ ./copy-assets.sh
59
+ ```
60
+
61
+ ### 4. Fix TypeScript Issues (if needed)
62
+
63
+ If you encounter TypeScript errors related to React:
64
+
65
+ ```bash
66
+ chmod +x fix-dependencies.sh
67
+ ./fix-dependencies.sh
68
+ ```
69
+
70
+ ### 5. Start the Development Server
71
+
72
+ ```bash
73
+ npm run dev
74
+ ```
75
+
76
+ ## Troubleshooting
77
+
78
+ ### TypeScript Errors
79
+
80
+ If you see "Cannot find module 'react'" errors:
81
+
82
+ 1. Make sure all dependencies are installed correctly
83
+ 2. Check that you're using the correct import syntax in your components:
84
+ ```typescript
85
+ import * as React from 'react';
86
+ import { useState, useEffect } from 'react';
87
+ ```
88
+ 3. Restart the TypeScript server in your IDE
89
+
90
+ ### Path Issues
91
+
92
+ WSL uses Linux-style paths. Make sure you're using forward slashes (/) instead of backslashes (\\) in your code.
93
+
94
+ ### Performance Issues
95
+
96
+ If you experience slow performance when running the development server in WSL:
97
+
98
+ 1. Store your project files in the Linux filesystem (e.g., /home/username/projects) rather than the Windows filesystem (/mnt/c/...)
99
+ 2. Use WSL 2 instead of WSL 1 for better performance
100
+ 3. Consider using Visual Studio Code with the Remote - WSL extension for a better development experience
web/copy-assets-fixed.sh ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ echo "Copying chess assets to public directory with correct filenames..."
3
+
4
+ # Create directories if they don't exist
5
+ mkdir -p frontend/web/public/assets/pieces
6
+ mkdir -p frontend/web/public/assets/boards/brown
7
+ mkdir -p frontend/web/public/assets/boards/grey
8
+ mkdir -p frontend/web/public/assets/sounds
9
+
10
+ # Copy pieces
11
+ echo "Copying chess pieces..."
12
+ cp -v frontend/assets/pieces/*.png frontend/web/public/assets/pieces/
13
+
14
+ # Copy brown board
15
+ echo "Copying brown board..."
16
+ cp -v frontend/assets/boards/brown/*.png frontend/web/public/assets/boards/brown/
17
+
18
+ # Copy grey board (with correct filenames)
19
+ echo "Copying grey board..."
20
+ cp -v frontend/assets/boards/grey/*.png frontend/web/public/assets/boards/grey/
21
+
22
+ # Create placeholder sound files if they don't exist
23
+ echo "Creating placeholder sound files..."
24
+ touch frontend/web/public/assets/sounds/move.mp3
25
+ touch frontend/web/public/assets/sounds/capture.mp3
26
+ touch frontend/web/public/assets/sounds/check.mp3
27
+ touch frontend/web/public/assets/sounds/castle.mp3
28
+ touch frontend/web/public/assets/sounds/game-end.mp3
29
+
30
+ echo "Assets copied successfully!"
31
+ echo "Please restart the development server to see the changes."
web/copy-assets.sh ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ echo "Copying chess assets to public directory..."
3
+
4
+ # Create directories if they don't exist
5
+ mkdir -p public/assets/pieces
6
+ mkdir -p public/assets/boards/brown
7
+ mkdir -p public/assets/boards/grey
8
+
9
+ # Copy pieces
10
+ cp -v ../assets/pieces/*.png public/assets/pieces/
11
+
12
+ # Copy board images
13
+ cp -v ../assets/boards/brown/*.png public/assets/boards/brown/
14
+ cp -v ../assets/boards/grey/*.png public/assets/boards/grey/
15
+
16
+ echo "Assets copied successfully!"
web/fix-dependencies.bat ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo Fixing React dependencies for Chess UI...
3
+ npm uninstall react react-dom @types/react @types/react-dom
4
+ npm install --save react react-dom
5
+ npm install --save-dev @types/react @types/react-dom
6
+ echo Dependencies reinstalled successfully!
7
+ echo.
8
+ echo You can now run "npm run dev" to start the development server.
web/fix-dependencies.sh ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ echo "Fixing React dependencies for Chess UI..."
3
+ npm uninstall react react-dom @types/react @types/react-dom
4
+ npm install --save react react-dom
5
+ npm install --save-dev @types/react @types/react-dom
6
+ echo "Dependencies reinstalled successfully!"
7
+ echo ""
8
+ echo "You can now run 'npm run dev' to start the development server."
web/fix-react-typescript.sh ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ echo "Fixing React TypeScript issues..."
3
+
4
+ # Create a temporary package.json backup
5
+ cp package.json package.json.bak
6
+
7
+ # Remove React and its types
8
+ npm uninstall react react-dom @types/react @types/react-dom
9
+
10
+ # Clean npm cache
11
+ npm cache clean --force
12
+
13
+ # Install React and its types with specific versions known to work well together
14
+ npm install --save [email protected] [email protected]
15
+ npm install --save-dev @types/[email protected] @types/[email protected]
16
+
17
+ # Create a proper tsconfig.json if it doesn't exist
18
+ cat > tsconfig.json << EOL
19
+ {
20
+ "compilerOptions": {
21
+ "target": "ES2020",
22
+ "useDefineForClassFields": true,
23
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
24
+ "module": "ESNext",
25
+ "skipLibCheck": true,
26
+ "moduleResolution": "node",
27
+ "allowSyntheticDefaultImports": true,
28
+ "resolveJsonModule": true,
29
+ "isolatedModules": true,
30
+ "noEmit": true,
31
+ "jsx": "react-jsx",
32
+ "strict": true,
33
+ "noUnusedLocals": true,
34
+ "noUnusedParameters": true,
35
+ "noFallthroughCasesInSwitch": true,
36
+ "baseUrl": ".",
37
+ "paths": {
38
+ "@/*": ["src/*"]
39
+ },
40
+ "types": ["vite/client", "node"]
41
+ },
42
+ "include": ["src", "**/*.ts", "**/*.tsx", "src/types/*.d.ts"],
43
+ "references": [{ "path": "./tsconfig.node.json" }]
44
+ }
45
+ EOL
46
+
47
+ # Create a proper tsconfig.node.json if it doesn't exist
48
+ cat > tsconfig.node.json << EOL
49
+ {
50
+ "compilerOptions": {
51
+ "composite": true,
52
+ "skipLibCheck": true,
53
+ "module": "ESNext",
54
+ "moduleResolution": "node",
55
+ "allowSyntheticDefaultImports": true
56
+ },
57
+ "include": ["vite.config.ts"]
58
+ }
59
+ EOL
60
+
61
+ echo "React TypeScript issues fixed! Please restart your IDE or TypeScript server."
62
+ echo "You can now run 'npm run dev' to start the development server."
web/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Chess Engine UI</title>
8
+ </head>
9
+ <body class="bg-gray-100">
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
web/node_modules/.bin/acorn ADDED
@@ -0,0 +1 @@
 
 
1
+ ../acorn/bin/acorn
web/node_modules/.bin/autoprefixer ADDED
@@ -0,0 +1 @@
 
 
1
+ ../autoprefixer/bin/autoprefixer
web/node_modules/.bin/browserslist ADDED
@@ -0,0 +1 @@
 
 
1
+ ../browserslist/cli.js
web/node_modules/.bin/cssesc ADDED
@@ -0,0 +1 @@
 
 
1
+ ../cssesc/bin/cssesc
web/node_modules/.bin/esbuild ADDED
@@ -0,0 +1 @@
 
 
1
+ ../esbuild/bin/esbuild
web/node_modules/.bin/glob ADDED
@@ -0,0 +1 @@
 
 
1
+ ../glob/dist/esm/bin.mjs
web/node_modules/.bin/jiti ADDED
@@ -0,0 +1 @@
 
 
1
+ ../jiti/bin/jiti.js
web/node_modules/.bin/jsesc ADDED
@@ -0,0 +1 @@
 
 
1
+ ../jsesc/bin/jsesc
web/node_modules/.bin/json5 ADDED
@@ -0,0 +1 @@
 
 
1
+ ../json5/lib/cli.js
web/node_modules/.bin/loose-envify ADDED
@@ -0,0 +1 @@
 
 
1
+ ../loose-envify/cli.js
web/node_modules/.bin/nanoid ADDED
@@ -0,0 +1 @@
 
 
1
+ ../nanoid/bin/nanoid.cjs
web/node_modules/.bin/node-which ADDED
@@ -0,0 +1 @@
 
 
1
+ ../which/bin/node-which
web/node_modules/.bin/parser ADDED
@@ -0,0 +1 @@
 
 
1
+ ../@babel/parser/bin/babel-parser.js
web/node_modules/.bin/resolve ADDED
@@ -0,0 +1 @@
 
 
1
+ ../resolve/bin/resolve
web/node_modules/.bin/rollup ADDED
@@ -0,0 +1 @@
 
 
1
+ ../rollup/dist/bin/rollup