Spaces:
Sleeping
Sleeping
Commit
·
100a6dd
0
Parent(s):
first commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +10 -0
- .gitignore +24 -0
- Dockerfile +55 -0
- LICENSE-ASSETS.md +29 -0
- README.md +90 -0
- app.py +9 -0
- chess_engine/__init__.py +13 -0
- chess_engine/ai/__init__.py +4 -0
- chess_engine/ai/evaluation.py +535 -0
- chess_engine/ai/stockfish_wrapper.py +422 -0
- chess_engine/api/__init__.py +3 -0
- chess_engine/api/cli.py +223 -0
- chess_engine/api/game_controller.py +402 -0
- chess_engine/api/rest_api.py +271 -0
- chess_engine/board.py +341 -0
- chess_engine/pieces.py +316 -0
- chess_engine/promotion.py +306 -0
- docker-entrypoint.sh +19 -0
- main.py +62 -0
- requirements.api.txt +14 -0
- web/.npmrc +2 -0
- web/IMPLEMENTATION_DETAILS.md +82 -0
- web/IMPLEMENTATION_SUMMARY.md +88 -0
- web/OVERVIEW.md +91 -0
- web/README.md +125 -0
- web/SETUP_GUIDE.md +123 -0
- web/SETUP_INSTRUCTIONS.md +108 -0
- web/TYPESCRIPT_FIXES.md +69 -0
- web/WSL_SETUP.md +100 -0
- web/copy-assets-fixed.sh +31 -0
- web/copy-assets.sh +16 -0
- web/fix-dependencies.bat +8 -0
- web/fix-dependencies.sh +8 -0
- web/fix-react-typescript.sh +62 -0
- web/index.html +13 -0
- web/node_modules/.bin/acorn +1 -0
- web/node_modules/.bin/autoprefixer +1 -0
- web/node_modules/.bin/browserslist +1 -0
- web/node_modules/.bin/cssesc +1 -0
- web/node_modules/.bin/esbuild +1 -0
- web/node_modules/.bin/glob +1 -0
- web/node_modules/.bin/jiti +1 -0
- web/node_modules/.bin/jsesc +1 -0
- web/node_modules/.bin/json5 +1 -0
- web/node_modules/.bin/loose-envify +1 -0
- web/node_modules/.bin/nanoid +1 -0
- web/node_modules/.bin/node-which +1 -0
- web/node_modules/.bin/parser +1 -0
- web/node_modules/.bin/resolve +1 -0
- 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
|