Update
Browse files- README.md +43 -3
- app.py +0 -5
- docs/puzzle_themes.md +65 -0
- pyproject.toml +7 -3
- requirements-dev.lock +200 -45
- requirements.lock +189 -45
- requirements.txt +1 -75
- src/chessli2/app.py +3 -0
- src/chessli2/choices.py +92 -0
- src/chessli2/cli.py +122 -0
- src/chessli2/games.py +0 -68
- src/chessli2/gui.py +101 -81
- src/chessli2/main.py +0 -5
- src/chessli2/mistakes.py +80 -34
- src/chessli2/rich_logging.py +33 -0
- src/chessli2/settings.py +18 -0
- src/chessli2/tactics.py +136 -0
- src/chessli2/writer.py +58 -0
- tests/test_cli.py +63 -0
README.md
CHANGED
@@ -11,7 +11,7 @@ pinned: True
|
|
11 |
|
12 |
# Welcome to Chessli2 🏰
|
13 |
|
14 |
-
Chessli2 is your **always free** and **open-source** chess trainer 🛡️, designed to elevate your game by allowing you to analyze games, identify mistakes, and sharpen your tactics, all sourced directly from [lichess.org](https://lichess.org/).
|
15 |
|
16 |
## Why a second version?
|
17 |
|
@@ -27,10 +27,50 @@ This overwhelming support has inspired me to develop a sleek new version of Ches
|
|
27 |
|
28 |
Chessli2 is here to support your journey to becoming a chess master. Dive in and start enhancing your skills today! 🚀
|
29 |
|
30 |
-
## Quickstart
|
31 |
|
32 |
-
|
|
|
33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
|
35 |
## FAQ
|
36 |
|
|
|
11 |
|
12 |
# Welcome to Chessli2 🏰
|
13 |
|
14 |
+
[Chessli2](https://github.com/pwenker/chessli2) is your **always free** and **open-source** chess trainer 🛡️, designed to elevate your game by allowing you to analyze games, identify mistakes, and sharpen your tactics, all sourced directly from [lichess.org](https://lichess.org/).
|
15 |
|
16 |
## Why a second version?
|
17 |
|
|
|
27 |
|
28 |
Chessli2 is here to support your journey to becoming a chess master. Dive in and start enhancing your skills today! 🚀
|
29 |
|
30 |
+
## Quickstart 🚀
|
31 |
|
32 |
+
### 👉 Click here to try out the app directly:
|
33 |
+
[**Chessli2**](https://pwenker-chessli2.hf.space/)
|
34 |
|
35 |
+
### 🔍 Inspect code at:
|
36 |
+
- **GitHub:** [pwenker/chessli2](https://github.com/pwenker/chessli2)
|
37 |
+
- **Hugging Face Spaces:** [pwenker/chessli2](https://huggingface.co/spaces/pwenker/chessli2)
|
38 |
+
|
39 |
+
|
40 |
+
## Local Deployment 🏠
|
41 |
+
|
42 |
+
### Prerequisites 📋
|
43 |
+
|
44 |
+
#### Rye 🌾
|
45 |
+
[Install `Rye`](https://rye-up.com/guide/installation/#installing-rye)
|
46 |
+
> Rye is a comprehensive tool designed for Python developers. It simplifies your workflow by managing Python installations and dependencies. Simply install Rye, and it takes care of the rest.
|
47 |
+
|
48 |
+
### Set-Up 🛠️
|
49 |
+
|
50 |
+
Clone the repository, e.g. with:
|
51 |
+
```
|
52 |
+
git clone https://github.com/pwenker/chessli2.git
|
53 |
+
```
|
54 |
+
Navigate to the directory:
|
55 |
+
```
|
56 |
+
cd chessli2
|
57 |
+
```
|
58 |
+
And execute:
|
59 |
+
```
|
60 |
+
rye sync
|
61 |
+
```
|
62 |
+
This creates a virtual environment in `.venv` and synchronizes the repo.
|
63 |
+
|
64 |
+
For more details, visit: [Basics - Rye](https://rye-up.com/guide/basics/)
|
65 |
+
|
66 |
+
### Start the App 🌟
|
67 |
+
|
68 |
+
Launch the app using:
|
69 |
+
```
|
70 |
+
rye run python src/chessli2/app.py
|
71 |
+
```
|
72 |
+
|
73 |
+
Finally, open your browser and visit [http://localhost:7860](http://localhost:7860/) to start practicing!
|
74 |
|
75 |
## FAQ
|
76 |
|
app.py
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
from src.chessli2.gui import demo
|
2 |
-
|
3 |
-
|
4 |
-
if __name__ == "__main__":
|
5 |
-
demo.launch(show_error=True)
|
|
|
|
|
|
|
|
|
|
|
|
docs/puzzle_themes.md
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
| Name | Description |
|
2 |
+
|-------------------------|---------------------------------------------------------------------------------------------------------------------|
|
3 |
+
| Advanced pawn | One of your pawns is deep into the opponent position, maybe threatening to promote. |
|
4 |
+
| Advantage | Seize your chance to get a decisive advantage. (200cp ≤ eval ≤ 600cp) |
|
5 |
+
| Anastasia's mate | A knight and rook or queen team up to trap the opposing king between the side of the board and a friendly piece. |
|
6 |
+
| Arabian mate | A knight and a rook team up to trap the opposing king on a corner of the board. |
|
7 |
+
| Attacking f2 or f7 | An attack focusing on the f2 or f7 pawn, such as in the fried liver opening. |
|
8 |
+
| Attraction | An exchange or sacrifice encouraging or forcing an opponent piece to a square that allows a follow-up tactic. |
|
9 |
+
| Back rank mate | Checkmate the king on the home rank, when it is trapped there by its own pieces. |
|
10 |
+
| Bishop endgame | An endgame with only bishops and pawns. |
|
11 |
+
| Boden's mate | Two attacking bishops on criss-crossing diagonals deliver mate to a king obstructed by friendly pieces. |
|
12 |
+
| Castling | Bring the king to safety, and deploy the rook for attack. |
|
13 |
+
| Capture the defender | Removing a piece that is critical to defence of another piece, allowing the now undefended piece to be captured on a following move. |
|
14 |
+
| Crushing | Spot the opponent blunder to obtain a crushing advantage. (eval ≥ 600cp) |
|
15 |
+
| Double bishop mate | Two attacking bishops on adjacent diagonals deliver mate to a king obstructed by friendly pieces. |
|
16 |
+
| Dovetail mate | A queen delivers mate to an adjacent king, whose only two escape squares are obstructed by friendly pieces. |
|
17 |
+
| Equality | Come back from a losing position, and secure a draw or a balanced position. (eval ≤ 200cp) |
|
18 |
+
| Kingside attack | An attack of the opponent's king, after they castled on the king side. |
|
19 |
+
| Clearance | A move, often with tempo, that clears a square, file or diagonal for a follow-up tactical idea. |
|
20 |
+
| Defensive move | A precise move or sequence of moves that is needed to avoid losing material or another advantage. |
|
21 |
+
| Deflection | A move that distracts an opponent piece from another duty that it performs, such as guarding a key square. Sometimes also called "overloading". |
|
22 |
+
| Discovered attack | Moving a piece (such as a knight), that previously blocked an attack by a long range piece (such as a rook), out of the way of that piece. |
|
23 |
+
| Double check | Checking with two pieces at once, as a result of a discovered attack where both the moving piece and the unveiled piece attack the opponent's king. |
|
24 |
+
| Endgame | A tactic during the last phase of the game. |
|
25 |
+
| En passant | A tactic involving the en passant rule, where a pawn can capture an opponent pawn that has bypassed it using its initial two-square move. |
|
26 |
+
| Exposed king | A tactic involving a king with few defenders around it, often leading to checkmate. |
|
27 |
+
| Fork | A move where the moved piece attacks two opponent pieces at once. |
|
28 |
+
| Hanging piece | A tactic involving an opponent piece being undefended or insufficiently defended and free to capture. |
|
29 |
+
| Hook mate | Checkmate with a rook, knight, and pawn along with one enemy pawn to limit the enemy king's escape. |
|
30 |
+
| Interference | Moving a piece between two opponent pieces to leave one or both opponent pieces undefended, such as a knight on a defended square between two rooks. |
|
31 |
+
| Intermezzo | Instead of playing the expected move, first interpose another move posing an immediate threat that the opponent must answer. Also known as "Zwischenzug" or "In between". |
|
32 |
+
| Knight endgame | An endgame with only knights and pawns. |
|
33 |
+
| Long | Three moves to win. |
|
34 |
+
| Master games | Puzzles from games played by titled players. |
|
35 |
+
| Master vs Master games | Puzzles from games between two titled players. |
|
36 |
+
| Checkmate | Win the game with style. |
|
37 |
+
| Mate in 1 | Deliver checkmate in one move. |
|
38 |
+
| Mate in 2 | Deliver checkmate in two moves. |
|
39 |
+
| Mate in 3 | Deliver checkmate in three moves. |
|
40 |
+
| Mate in 4 | Deliver checkmate in four moves. |
|
41 |
+
| Mate in 5 or more | Figure out a long mating sequence. |
|
42 |
+
| Middlegame | A tactic during the second phase of the game. |
|
43 |
+
| One-move puzzle | A puzzle that is only one move long. |
|
44 |
+
| Opening | A tactic during the first phase of the game. |
|
45 |
+
| Pawn endgame | An endgame with only pawns. |
|
46 |
+
| Pin | A tactic involving pins, where a piece is unable to move without revealing an attack on a higher value piece. |
|
47 |
+
| Promotion | Promote one of your pawn to a queen or minor piece. |
|
48 |
+
| Queen endgame | An endgame with only queens and pawns. |
|
49 |
+
| Queen and Rook | An endgame with only queens, rooks and pawns. |
|
50 |
+
| Queenside attack | An attack of the opponent's king, after they castled on the queen side. |
|
51 |
+
| Quiet move | A move that does neither make a check or capture, nor an immediate threat to capture, but does prepare a more hidden unavoidable threat for a later move. |
|
52 |
+
| Rook endgame | An endgame with only rooks and pawns. |
|
53 |
+
| Sacrifice | A tactic involving giving up material in the short-term, to gain an advantage again after a forced sequence of moves. |
|
54 |
+
| Short | Two moves to win. |
|
55 |
+
| Skewer | A motif involving a high value piece being attacked, moving out the way, and allowing a lower value piece behind it to be captured or attacked, the inverse of a pin. |
|
56 |
+
| Smothered mate | A checkmate delivered by a knight in which the mated king is unable to move because it is surrounded (or smothered) by its own pieces. |
|
57 |
+
| Super GM games | Puzzles from games played by the best players in the world. |
|
58 |
+
| Trapped piece | A piece is unable to escape capture as it has limited moves. |
|
59 |
+
| Underpromotion | Promotion to a knight, bishop, or rook. |
|
60 |
+
| Very long | Four moves or more to win. |
|
61 |
+
| X-Ray attack | A piece attacks or defends a square, through an enemy piece. |
|
62 |
+
| Zugzwang | The opponent is limited in the moves they can make, and all moves worsen their position. |
|
63 |
+
| Healthy mix | A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games. |
|
64 |
+
| Player games | Lookup puzzles generated from your games, or from another player's games. |
|
65 |
+
| Puzzle download information | These puzzles are in the public domain, and can be downloaded from %s. |
|
pyproject.toml
CHANGED
@@ -13,12 +13,13 @@ dependencies = [
|
|
13 |
"gradio_calendar>=0.0.4",
|
14 |
"gradio>=4.29.0",
|
15 |
"tabulate>=0.9.0",
|
|
|
16 |
]
|
17 |
readme = "README.md"
|
18 |
-
requires-python = ">= 3.
|
19 |
|
20 |
[project.scripts]
|
21 |
-
chessli2 = "chessli2.
|
22 |
|
23 |
[build-system]
|
24 |
requires = ["hatchling"]
|
@@ -26,7 +27,10 @@ build-backend = "hatchling.build"
|
|
26 |
|
27 |
[tool.rye]
|
28 |
managed = true
|
29 |
-
dev-dependencies = [
|
|
|
|
|
|
|
30 |
|
31 |
[tool.hatch.metadata]
|
32 |
allow-direct-references = true
|
|
|
13 |
"gradio_calendar>=0.0.4",
|
14 |
"gradio>=4.29.0",
|
15 |
"tabulate>=0.9.0",
|
16 |
+
"typer>=0.12.3",
|
17 |
]
|
18 |
readme = "README.md"
|
19 |
+
requires-python = ">= 3.11"
|
20 |
|
21 |
[project.scripts]
|
22 |
+
chessli2 = "chessli2.cli:app"
|
23 |
|
24 |
[build-system]
|
25 |
requires = ["hatchling"]
|
|
|
27 |
|
28 |
[tool.rye]
|
29 |
managed = true
|
30 |
+
dev-dependencies = [
|
31 |
+
"pytest>=8.2.0",
|
32 |
+
"pytest-sugar>=1.0.0",
|
33 |
+
]
|
34 |
|
35 |
[tool.hatch.metadata]
|
36 |
allow-direct-references = true
|
requirements-dev.lock
CHANGED
@@ -5,78 +5,233 @@
|
|
5 |
# pre: false
|
6 |
# features: []
|
7 |
# all-features: false
|
|
|
8 |
|
9 |
-e file:.
|
10 |
aiofiles==23.2.1
|
11 |
-
|
|
|
|
|
12 |
annotated-types==0.6.0
|
13 |
-
|
|
|
|
|
|
|
|
|
14 |
attrs==23.2.0
|
|
|
|
|
15 |
berserk==0.13.2
|
16 |
-
|
|
|
|
|
|
|
|
|
17 |
charset-normalizer==3.3.2
|
|
|
18 |
chess==1.10.0
|
|
|
19 |
click==8.1.7
|
|
|
|
|
|
|
|
|
20 |
cycler==0.12.1
|
|
|
21 |
deprecated==1.2.14
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
gradio-calendar==0.0.4
|
30 |
-
|
|
|
|
|
31 |
h11==0.14.0
|
32 |
-
httpcore
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
jsonschema-specifications==2023.12.1
|
|
|
40 |
kiwisolver==1.4.5
|
|
|
41 |
markdown-it-py==3.0.0
|
42 |
-
|
43 |
-
|
|
|
|
|
|
|
|
|
44 |
mdurl==0.1.2
|
|
|
45 |
ndjson==0.3.1
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
pydub==0.25.1
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
python-multipart==0.0.9
|
60 |
-
|
|
|
|
|
|
|
61 |
pyyaml==6.0.1
|
62 |
-
|
|
|
|
|
|
|
|
|
|
|
63 |
requests==2.31.0
|
64 |
-
|
65 |
-
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
semantic-version==2.10.0
|
|
|
68 |
shellingham==1.5.4
|
|
|
69 |
six==1.16.0
|
70 |
-
|
71 |
-
|
|
|
|
|
|
|
|
|
72 |
tabulate==0.9.0
|
|
|
|
|
|
|
73 |
tomlkit==0.12.0
|
74 |
-
|
75 |
-
|
|
|
|
|
|
|
76 |
typer==0.12.3
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
websockets==11.0.3
|
|
|
|
|
82 |
wrapt==1.16.0
|
|
|
|
5 |
# pre: false
|
6 |
# features: []
|
7 |
# all-features: false
|
8 |
+
# with-sources: false
|
9 |
|
10 |
-e file:.
|
11 |
aiofiles==23.2.1
|
12 |
+
# via gradio
|
13 |
+
altair==5.3.0
|
14 |
+
# via gradio
|
15 |
annotated-types==0.6.0
|
16 |
+
# via pydantic
|
17 |
+
anyio==4.3.0
|
18 |
+
# via httpx
|
19 |
+
# via starlette
|
20 |
+
# via watchfiles
|
21 |
attrs==23.2.0
|
22 |
+
# via jsonschema
|
23 |
+
# via referencing
|
24 |
berserk==0.13.2
|
25 |
+
# via chessli2
|
26 |
+
certifi==2024.2.2
|
27 |
+
# via httpcore
|
28 |
+
# via httpx
|
29 |
+
# via requests
|
30 |
charset-normalizer==3.3.2
|
31 |
+
# via requests
|
32 |
chess==1.10.0
|
33 |
+
# via chessli2
|
34 |
click==8.1.7
|
35 |
+
# via typer
|
36 |
+
# via uvicorn
|
37 |
+
contourpy==1.2.1
|
38 |
+
# via matplotlib
|
39 |
cycler==0.12.1
|
40 |
+
# via matplotlib
|
41 |
deprecated==1.2.14
|
42 |
+
# via berserk
|
43 |
+
dnspython==2.6.1
|
44 |
+
# via email-validator
|
45 |
+
email-validator==2.1.1
|
46 |
+
# via fastapi
|
47 |
+
fastapi==0.111.0
|
48 |
+
# via fastapi-cli
|
49 |
+
# via gradio
|
50 |
+
fastapi-cli==0.0.3
|
51 |
+
# via fastapi
|
52 |
+
ffmpy==0.3.2
|
53 |
+
# via gradio
|
54 |
+
filelock==3.14.0
|
55 |
+
# via huggingface-hub
|
56 |
+
fonttools==4.51.0
|
57 |
+
# via matplotlib
|
58 |
+
fsspec==2024.3.1
|
59 |
+
# via gradio-client
|
60 |
+
# via huggingface-hub
|
61 |
+
gradio==4.31.0
|
62 |
+
# via chessli2
|
63 |
+
# via gradio-calendar
|
64 |
gradio-calendar==0.0.4
|
65 |
+
# via chessli2
|
66 |
+
gradio-client==0.16.2
|
67 |
+
# via gradio
|
68 |
h11==0.14.0
|
69 |
+
# via httpcore
|
70 |
+
# via uvicorn
|
71 |
+
httpcore==1.0.5
|
72 |
+
# via httpx
|
73 |
+
httptools==0.6.1
|
74 |
+
# via uvicorn
|
75 |
+
httpx==0.27.0
|
76 |
+
# via fastapi
|
77 |
+
# via gradio
|
78 |
+
# via gradio-client
|
79 |
+
huggingface-hub==0.23.0
|
80 |
+
# via gradio
|
81 |
+
# via gradio-client
|
82 |
+
idna==3.7
|
83 |
+
# via anyio
|
84 |
+
# via email-validator
|
85 |
+
# via httpx
|
86 |
+
# via requests
|
87 |
+
importlib-resources==6.4.0
|
88 |
+
# via gradio
|
89 |
+
iniconfig==2.0.0
|
90 |
+
# via pytest
|
91 |
+
jinja2==3.1.4
|
92 |
+
# via altair
|
93 |
+
# via fastapi
|
94 |
+
# via gradio
|
95 |
+
jsonschema==4.22.0
|
96 |
+
# via altair
|
97 |
jsonschema-specifications==2023.12.1
|
98 |
+
# via jsonschema
|
99 |
kiwisolver==1.4.5
|
100 |
+
# via matplotlib
|
101 |
markdown-it-py==3.0.0
|
102 |
+
# via rich
|
103 |
+
markupsafe==2.1.5
|
104 |
+
# via gradio
|
105 |
+
# via jinja2
|
106 |
+
matplotlib==3.8.4
|
107 |
+
# via gradio
|
108 |
mdurl==0.1.2
|
109 |
+
# via markdown-it-py
|
110 |
ndjson==0.3.1
|
111 |
+
# via berserk
|
112 |
+
numpy==1.26.4
|
113 |
+
# via altair
|
114 |
+
# via contourpy
|
115 |
+
# via gradio
|
116 |
+
# via matplotlib
|
117 |
+
# via pandas
|
118 |
+
orjson==3.10.3
|
119 |
+
# via fastapi
|
120 |
+
# via gradio
|
121 |
+
packaging==24.0
|
122 |
+
# via altair
|
123 |
+
# via gradio
|
124 |
+
# via gradio-client
|
125 |
+
# via huggingface-hub
|
126 |
+
# via matplotlib
|
127 |
+
# via pytest
|
128 |
+
# via pytest-sugar
|
129 |
+
pandas==2.2.2
|
130 |
+
# via altair
|
131 |
+
# via gradio
|
132 |
+
pillow==10.3.0
|
133 |
+
# via gradio
|
134 |
+
# via matplotlib
|
135 |
+
pluggy==1.5.0
|
136 |
+
# via pytest
|
137 |
+
pydantic==2.7.1
|
138 |
+
# via chessli2
|
139 |
+
# via fastapi
|
140 |
+
# via gradio
|
141 |
+
# via pydantic-settings
|
142 |
+
pydantic-core==2.18.2
|
143 |
+
# via pydantic
|
144 |
+
pydantic-settings==2.2.1
|
145 |
+
# via chessli2
|
146 |
pydub==0.25.1
|
147 |
+
# via gradio
|
148 |
+
pygments==2.18.0
|
149 |
+
# via rich
|
150 |
+
pyparsing==3.1.2
|
151 |
+
# via matplotlib
|
152 |
+
pytest==8.2.0
|
153 |
+
# via pytest-sugar
|
154 |
+
pytest-sugar==1.0.0
|
155 |
+
python-dateutil==2.9.0.post0
|
156 |
+
# via berserk
|
157 |
+
# via matplotlib
|
158 |
+
# via pandas
|
159 |
+
python-dotenv==1.0.1
|
160 |
+
# via pydantic-settings
|
161 |
+
# via uvicorn
|
162 |
python-multipart==0.0.9
|
163 |
+
# via fastapi
|
164 |
+
# via gradio
|
165 |
+
pytz==2024.1
|
166 |
+
# via pandas
|
167 |
pyyaml==6.0.1
|
168 |
+
# via gradio
|
169 |
+
# via huggingface-hub
|
170 |
+
# via uvicorn
|
171 |
+
referencing==0.35.1
|
172 |
+
# via jsonschema
|
173 |
+
# via jsonschema-specifications
|
174 |
requests==2.31.0
|
175 |
+
# via berserk
|
176 |
+
# via huggingface-hub
|
177 |
+
rich==13.7.1
|
178 |
+
# via typer
|
179 |
+
rpds-py==0.18.1
|
180 |
+
# via jsonschema
|
181 |
+
# via referencing
|
182 |
+
ruff==0.4.4
|
183 |
+
# via gradio
|
184 |
semantic-version==2.10.0
|
185 |
+
# via gradio
|
186 |
shellingham==1.5.4
|
187 |
+
# via typer
|
188 |
six==1.16.0
|
189 |
+
# via python-dateutil
|
190 |
+
sniffio==1.3.1
|
191 |
+
# via anyio
|
192 |
+
# via httpx
|
193 |
+
starlette==0.37.2
|
194 |
+
# via fastapi
|
195 |
tabulate==0.9.0
|
196 |
+
# via chessli2
|
197 |
+
termcolor==2.4.0
|
198 |
+
# via pytest-sugar
|
199 |
tomlkit==0.12.0
|
200 |
+
# via gradio
|
201 |
+
toolz==0.12.1
|
202 |
+
# via altair
|
203 |
+
tqdm==4.66.4
|
204 |
+
# via huggingface-hub
|
205 |
typer==0.12.3
|
206 |
+
# via chessli2
|
207 |
+
# via fastapi-cli
|
208 |
+
# via gradio
|
209 |
+
typing-extensions==4.11.0
|
210 |
+
# via berserk
|
211 |
+
# via fastapi
|
212 |
+
# via gradio
|
213 |
+
# via gradio-client
|
214 |
+
# via huggingface-hub
|
215 |
+
# via pydantic
|
216 |
+
# via pydantic-core
|
217 |
+
# via typer
|
218 |
+
tzdata==2024.1
|
219 |
+
# via pandas
|
220 |
+
ujson==5.9.0
|
221 |
+
# via fastapi
|
222 |
+
urllib3==2.2.1
|
223 |
+
# via gradio
|
224 |
+
# via requests
|
225 |
+
uvicorn==0.29.0
|
226 |
+
# via fastapi
|
227 |
+
# via fastapi-cli
|
228 |
+
# via gradio
|
229 |
+
uvloop==0.19.0
|
230 |
+
# via uvicorn
|
231 |
+
watchfiles==0.21.0
|
232 |
+
# via uvicorn
|
233 |
websockets==11.0.3
|
234 |
+
# via gradio-client
|
235 |
+
# via uvicorn
|
236 |
wrapt==1.16.0
|
237 |
+
# via deprecated
|
requirements.lock
CHANGED
@@ -5,78 +5,222 @@
|
|
5 |
# pre: false
|
6 |
# features: []
|
7 |
# all-features: false
|
|
|
8 |
|
9 |
-e file:.
|
10 |
aiofiles==23.2.1
|
11 |
-
|
|
|
|
|
12 |
annotated-types==0.6.0
|
13 |
-
|
|
|
|
|
|
|
|
|
14 |
attrs==23.2.0
|
|
|
|
|
15 |
berserk==0.13.2
|
16 |
-
|
|
|
|
|
|
|
|
|
17 |
charset-normalizer==3.3.2
|
|
|
18 |
chess==1.10.0
|
|
|
19 |
click==8.1.7
|
|
|
|
|
|
|
|
|
20 |
cycler==0.12.1
|
|
|
21 |
deprecated==1.2.14
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
gradio-calendar==0.0.4
|
30 |
-
|
|
|
|
|
31 |
h11==0.14.0
|
32 |
-
httpcore
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
jsonschema-specifications==2023.12.1
|
|
|
40 |
kiwisolver==1.4.5
|
|
|
41 |
markdown-it-py==3.0.0
|
42 |
-
|
43 |
-
|
|
|
|
|
|
|
|
|
44 |
mdurl==0.1.2
|
|
|
45 |
ndjson==0.3.1
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
pydub==0.25.1
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
python-multipart==0.0.9
|
60 |
-
|
|
|
|
|
|
|
61 |
pyyaml==6.0.1
|
62 |
-
|
|
|
|
|
|
|
|
|
|
|
63 |
requests==2.31.0
|
64 |
-
|
65 |
-
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
semantic-version==2.10.0
|
|
|
68 |
shellingham==1.5.4
|
|
|
69 |
six==1.16.0
|
70 |
-
|
71 |
-
|
|
|
|
|
|
|
|
|
72 |
tabulate==0.9.0
|
|
|
73 |
tomlkit==0.12.0
|
74 |
-
|
75 |
-
|
|
|
|
|
|
|
76 |
typer==0.12.3
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
websockets==11.0.3
|
|
|
|
|
82 |
wrapt==1.16.0
|
|
|
|
5 |
# pre: false
|
6 |
# features: []
|
7 |
# all-features: false
|
8 |
+
# with-sources: false
|
9 |
|
10 |
-e file:.
|
11 |
aiofiles==23.2.1
|
12 |
+
# via gradio
|
13 |
+
altair==5.3.0
|
14 |
+
# via gradio
|
15 |
annotated-types==0.6.0
|
16 |
+
# via pydantic
|
17 |
+
anyio==4.3.0
|
18 |
+
# via httpx
|
19 |
+
# via starlette
|
20 |
+
# via watchfiles
|
21 |
attrs==23.2.0
|
22 |
+
# via jsonschema
|
23 |
+
# via referencing
|
24 |
berserk==0.13.2
|
25 |
+
# via chessli2
|
26 |
+
certifi==2024.2.2
|
27 |
+
# via httpcore
|
28 |
+
# via httpx
|
29 |
+
# via requests
|
30 |
charset-normalizer==3.3.2
|
31 |
+
# via requests
|
32 |
chess==1.10.0
|
33 |
+
# via chessli2
|
34 |
click==8.1.7
|
35 |
+
# via typer
|
36 |
+
# via uvicorn
|
37 |
+
contourpy==1.2.1
|
38 |
+
# via matplotlib
|
39 |
cycler==0.12.1
|
40 |
+
# via matplotlib
|
41 |
deprecated==1.2.14
|
42 |
+
# via berserk
|
43 |
+
dnspython==2.6.1
|
44 |
+
# via email-validator
|
45 |
+
email-validator==2.1.1
|
46 |
+
# via fastapi
|
47 |
+
fastapi==0.111.0
|
48 |
+
# via fastapi-cli
|
49 |
+
# via gradio
|
50 |
+
fastapi-cli==0.0.3
|
51 |
+
# via fastapi
|
52 |
+
ffmpy==0.3.2
|
53 |
+
# via gradio
|
54 |
+
filelock==3.14.0
|
55 |
+
# via huggingface-hub
|
56 |
+
fonttools==4.51.0
|
57 |
+
# via matplotlib
|
58 |
+
fsspec==2024.3.1
|
59 |
+
# via gradio-client
|
60 |
+
# via huggingface-hub
|
61 |
+
gradio==4.31.0
|
62 |
+
# via chessli2
|
63 |
+
# via gradio-calendar
|
64 |
gradio-calendar==0.0.4
|
65 |
+
# via chessli2
|
66 |
+
gradio-client==0.16.2
|
67 |
+
# via gradio
|
68 |
h11==0.14.0
|
69 |
+
# via httpcore
|
70 |
+
# via uvicorn
|
71 |
+
httpcore==1.0.5
|
72 |
+
# via httpx
|
73 |
+
httptools==0.6.1
|
74 |
+
# via uvicorn
|
75 |
+
httpx==0.27.0
|
76 |
+
# via fastapi
|
77 |
+
# via gradio
|
78 |
+
# via gradio-client
|
79 |
+
huggingface-hub==0.23.0
|
80 |
+
# via gradio
|
81 |
+
# via gradio-client
|
82 |
+
idna==3.7
|
83 |
+
# via anyio
|
84 |
+
# via email-validator
|
85 |
+
# via httpx
|
86 |
+
# via requests
|
87 |
+
importlib-resources==6.4.0
|
88 |
+
# via gradio
|
89 |
+
jinja2==3.1.4
|
90 |
+
# via altair
|
91 |
+
# via fastapi
|
92 |
+
# via gradio
|
93 |
+
jsonschema==4.22.0
|
94 |
+
# via altair
|
95 |
jsonschema-specifications==2023.12.1
|
96 |
+
# via jsonschema
|
97 |
kiwisolver==1.4.5
|
98 |
+
# via matplotlib
|
99 |
markdown-it-py==3.0.0
|
100 |
+
# via rich
|
101 |
+
markupsafe==2.1.5
|
102 |
+
# via gradio
|
103 |
+
# via jinja2
|
104 |
+
matplotlib==3.8.4
|
105 |
+
# via gradio
|
106 |
mdurl==0.1.2
|
107 |
+
# via markdown-it-py
|
108 |
ndjson==0.3.1
|
109 |
+
# via berserk
|
110 |
+
numpy==1.26.4
|
111 |
+
# via altair
|
112 |
+
# via contourpy
|
113 |
+
# via gradio
|
114 |
+
# via matplotlib
|
115 |
+
# via pandas
|
116 |
+
orjson==3.10.3
|
117 |
+
# via fastapi
|
118 |
+
# via gradio
|
119 |
+
packaging==24.0
|
120 |
+
# via altair
|
121 |
+
# via gradio
|
122 |
+
# via gradio-client
|
123 |
+
# via huggingface-hub
|
124 |
+
# via matplotlib
|
125 |
+
pandas==2.2.2
|
126 |
+
# via altair
|
127 |
+
# via gradio
|
128 |
+
pillow==10.3.0
|
129 |
+
# via gradio
|
130 |
+
# via matplotlib
|
131 |
+
pydantic==2.7.1
|
132 |
+
# via chessli2
|
133 |
+
# via fastapi
|
134 |
+
# via gradio
|
135 |
+
# via pydantic-settings
|
136 |
+
pydantic-core==2.18.2
|
137 |
+
# via pydantic
|
138 |
+
pydantic-settings==2.2.1
|
139 |
+
# via chessli2
|
140 |
pydub==0.25.1
|
141 |
+
# via gradio
|
142 |
+
pygments==2.18.0
|
143 |
+
# via rich
|
144 |
+
pyparsing==3.1.2
|
145 |
+
# via matplotlib
|
146 |
+
python-dateutil==2.9.0.post0
|
147 |
+
# via berserk
|
148 |
+
# via matplotlib
|
149 |
+
# via pandas
|
150 |
+
python-dotenv==1.0.1
|
151 |
+
# via pydantic-settings
|
152 |
+
# via uvicorn
|
153 |
python-multipart==0.0.9
|
154 |
+
# via fastapi
|
155 |
+
# via gradio
|
156 |
+
pytz==2024.1
|
157 |
+
# via pandas
|
158 |
pyyaml==6.0.1
|
159 |
+
# via gradio
|
160 |
+
# via huggingface-hub
|
161 |
+
# via uvicorn
|
162 |
+
referencing==0.35.1
|
163 |
+
# via jsonschema
|
164 |
+
# via jsonschema-specifications
|
165 |
requests==2.31.0
|
166 |
+
# via berserk
|
167 |
+
# via huggingface-hub
|
168 |
+
rich==13.7.1
|
169 |
+
# via typer
|
170 |
+
rpds-py==0.18.1
|
171 |
+
# via jsonschema
|
172 |
+
# via referencing
|
173 |
+
ruff==0.4.4
|
174 |
+
# via gradio
|
175 |
semantic-version==2.10.0
|
176 |
+
# via gradio
|
177 |
shellingham==1.5.4
|
178 |
+
# via typer
|
179 |
six==1.16.0
|
180 |
+
# via python-dateutil
|
181 |
+
sniffio==1.3.1
|
182 |
+
# via anyio
|
183 |
+
# via httpx
|
184 |
+
starlette==0.37.2
|
185 |
+
# via fastapi
|
186 |
tabulate==0.9.0
|
187 |
+
# via chessli2
|
188 |
tomlkit==0.12.0
|
189 |
+
# via gradio
|
190 |
+
toolz==0.12.1
|
191 |
+
# via altair
|
192 |
+
tqdm==4.66.4
|
193 |
+
# via huggingface-hub
|
194 |
typer==0.12.3
|
195 |
+
# via chessli2
|
196 |
+
# via fastapi-cli
|
197 |
+
# via gradio
|
198 |
+
typing-extensions==4.11.0
|
199 |
+
# via berserk
|
200 |
+
# via fastapi
|
201 |
+
# via gradio
|
202 |
+
# via gradio-client
|
203 |
+
# via huggingface-hub
|
204 |
+
# via pydantic
|
205 |
+
# via pydantic-core
|
206 |
+
# via typer
|
207 |
+
tzdata==2024.1
|
208 |
+
# via pandas
|
209 |
+
ujson==5.9.0
|
210 |
+
# via fastapi
|
211 |
+
urllib3==2.2.1
|
212 |
+
# via gradio
|
213 |
+
# via requests
|
214 |
+
uvicorn==0.29.0
|
215 |
+
# via fastapi
|
216 |
+
# via fastapi-cli
|
217 |
+
# via gradio
|
218 |
+
uvloop==0.19.0
|
219 |
+
# via uvicorn
|
220 |
+
watchfiles==0.21.0
|
221 |
+
# via uvicorn
|
222 |
websockets==11.0.3
|
223 |
+
# via gradio-client
|
224 |
+
# via uvicorn
|
225 |
wrapt==1.16.0
|
226 |
+
# via deprecated
|
requirements.txt
CHANGED
@@ -1,75 +1 @@
|
|
1 |
-
|
2 |
-
altair==5.2.0
|
3 |
-
annotated-types==0.6.0
|
4 |
-
anyio==4.2.0
|
5 |
-
attrs==23.2.0
|
6 |
-
berserk==0.13.2
|
7 |
-
certifi==2023.11.17
|
8 |
-
charset-normalizer==3.3.2
|
9 |
-
chess==1.10.0
|
10 |
-
click==8.1.7
|
11 |
-
cycler==0.12.1
|
12 |
-
deprecated==1.2.14
|
13 |
-
exceptiongroup==1.2.1
|
14 |
-
fastapi==0.109.0
|
15 |
-
ffmpy==0.3.1
|
16 |
-
filelock==3.13.1
|
17 |
-
fonttools==4.47.2
|
18 |
-
fsspec==2023.12.2
|
19 |
-
gradio==4.29.0
|
20 |
-
gradio-calendar==0.0.4
|
21 |
-
gradio-client==0.16.1
|
22 |
-
h11==0.14.0
|
23 |
-
httpcore==1.0.2
|
24 |
-
httpx==0.26.0
|
25 |
-
huggingface-hub==0.20.2
|
26 |
-
idna==3.6
|
27 |
-
importlib-resources==6.1.1
|
28 |
-
jinja2==3.1.3
|
29 |
-
jsonschema==4.20.0
|
30 |
-
jsonschema-specifications==2023.12.1
|
31 |
-
kiwisolver==1.4.5
|
32 |
-
markdown-it-py==3.0.0
|
33 |
-
markupsafe==2.1.3
|
34 |
-
matplotlib==3.5.3
|
35 |
-
mdurl==0.1.2
|
36 |
-
ndjson==0.3.1
|
37 |
-
numpy==1.24.4
|
38 |
-
orjson==3.9.10
|
39 |
-
packaging==23.2
|
40 |
-
pandas==2.0.3
|
41 |
-
pillow==10.2.0
|
42 |
-
pkgutil-resolve-name==1.3.10
|
43 |
-
pydantic==2.5.3
|
44 |
-
pydantic-core==2.14.6
|
45 |
-
pydantic-settings==2.1.0
|
46 |
-
pydub==0.25.1
|
47 |
-
pygments==2.17.2
|
48 |
-
pyparsing==3.1.1
|
49 |
-
python-dateutil==2.8.2
|
50 |
-
python-dotenv==1.0.0
|
51 |
-
python-multipart==0.0.9
|
52 |
-
pytz==2023.3.post1
|
53 |
-
pyyaml==6.0.1
|
54 |
-
referencing==0.32.1
|
55 |
-
requests==2.31.0
|
56 |
-
rich==13.7.0
|
57 |
-
rpds-py==0.17.1
|
58 |
-
ruff==0.4.3
|
59 |
-
semantic-version==2.10.0
|
60 |
-
shellingham==1.5.4
|
61 |
-
six==1.16.0
|
62 |
-
sniffio==1.3.0
|
63 |
-
starlette==0.35.1
|
64 |
-
tabulate==0.9.0
|
65 |
-
tomlkit==0.12.0
|
66 |
-
toolz==0.12.0
|
67 |
-
tqdm==4.66.1
|
68 |
-
typer==0.12.3
|
69 |
-
typing-extensions==4.9.0
|
70 |
-
tzdata==2023.4
|
71 |
-
urllib3==2.1.0
|
72 |
-
uvicorn==0.25.0
|
73 |
-
websockets==11.0.3
|
74 |
-
wrapt==1.16.0
|
75 |
-
zipp==3.18.1
|
|
|
1 |
+
chessli2 @ git+https://github.com/pwenker/chessli2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/chessli2/app.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
if __name__ == "__main__":
|
2 |
+
from chessli2.gui import chessli2_gradio_app
|
3 |
+
chessli2_gradio_app.launch()
|
src/chessli2/choices.py
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from enum import Enum
|
2 |
+
|
3 |
+
|
4 |
+
class Output(str, Enum):
|
5 |
+
file = "file"
|
6 |
+
info = "info"
|
7 |
+
pgn = "pgn"
|
8 |
+
|
9 |
+
|
10 |
+
class Color(Enum):
|
11 |
+
white = 1
|
12 |
+
black = 0
|
13 |
+
|
14 |
+
|
15 |
+
class MistakeNag(Enum):
|
16 |
+
mistake = 2
|
17 |
+
blunder = 4
|
18 |
+
speculative = 5
|
19 |
+
dubious = 6
|
20 |
+
|
21 |
+
|
22 |
+
class TimeControl(str, Enum):
|
23 |
+
bullet = "Bullet"
|
24 |
+
blitz = "Blitz"
|
25 |
+
rapid = "Rapid"
|
26 |
+
classical = "Classical"
|
27 |
+
ultraBullet = "UltraBullet"
|
28 |
+
all = "All Time Controls"
|
29 |
+
|
30 |
+
|
31 |
+
class PuzzleTheme(str, Enum):
|
32 |
+
advancedPawn = "Advanced pawn"
|
33 |
+
advantage = "Advantage"
|
34 |
+
anastasiaMate = "Anastasia's mate"
|
35 |
+
arabianMate = "Arabian mate"
|
36 |
+
attackingF2F7 = "Attacking f2 or f7"
|
37 |
+
attraction = "Attraction"
|
38 |
+
backRankMate = "Back rank mate"
|
39 |
+
bishopEndgame = "Bishop endgame"
|
40 |
+
bodenMate = "Boden's mate"
|
41 |
+
castling = "Castling"
|
42 |
+
capturingDefender = "Capture the defender"
|
43 |
+
crushing = "Crushing"
|
44 |
+
doubleBishopMate = "Double bishop mate"
|
45 |
+
dovetailMate = "Dovetail mate"
|
46 |
+
equality = "Equality"
|
47 |
+
kingsideAttack = "Kingside attack"
|
48 |
+
clearance = "Clearance"
|
49 |
+
defensiveMove = "Defensive move"
|
50 |
+
deflection = "Deflection"
|
51 |
+
discoveredAttack = "Discovered attack"
|
52 |
+
doubleCheck = "Double check"
|
53 |
+
endgame = "Endgame"
|
54 |
+
exposedKing = "Exposed king"
|
55 |
+
fork = "Fork"
|
56 |
+
hangingPiece = "Hanging piece"
|
57 |
+
hookMate = "Hook mate"
|
58 |
+
interference = "Interference"
|
59 |
+
intermezzo = "Intermezzo"
|
60 |
+
knightEndgame = "Knight endgame"
|
61 |
+
long = "Long puzzle"
|
62 |
+
master = "Master games"
|
63 |
+
masterVsMaster = "Master vs Master games"
|
64 |
+
mate = "Checkmate"
|
65 |
+
mateIn1 = "Mate in 1"
|
66 |
+
mateIn2 = "Mate in 2"
|
67 |
+
mateIn3 = "Mate in 3"
|
68 |
+
mateIn4 = "Mate in 4"
|
69 |
+
mateIn5 = "Mate in 5 or more"
|
70 |
+
middlegame = "Middlegame"
|
71 |
+
oneMove = "One-move puzzle"
|
72 |
+
opening = "Opening"
|
73 |
+
pawnEndgame = "Pawn endgame"
|
74 |
+
pin = "Pin"
|
75 |
+
promotion = "Promotion"
|
76 |
+
queenEndgame = "Queen endgame"
|
77 |
+
queenRookEndgame = "Queen and Rook"
|
78 |
+
queensideAttack = "Queenside attack"
|
79 |
+
quietMove = "Quiet move"
|
80 |
+
rookEndgame = "Rook endgame"
|
81 |
+
sacrifice = "Sacrifice"
|
82 |
+
short = "Short puzzle"
|
83 |
+
skewer = "Skewer"
|
84 |
+
smotheredMate = "Smothered mate"
|
85 |
+
superGM = "Super GM games"
|
86 |
+
trappedPiece = "Trapped piece"
|
87 |
+
underPromotion = "Underpromotion"
|
88 |
+
veryLong = "Very long puzzle"
|
89 |
+
xRayAttack = "X-Ray attack"
|
90 |
+
zugzwang = "Zugzwang"
|
91 |
+
healthyMix = "Healthy mix"
|
92 |
+
playerGames = "Player games"
|
src/chessli2/cli.py
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
|
3 |
+
import typer
|
4 |
+
from gradio_client import Client
|
5 |
+
from rich import print
|
6 |
+
|
7 |
+
from chessli2.choices import MistakeNag, Output, PuzzleTheme, TimeControl
|
8 |
+
from chessli2.settings import settings
|
9 |
+
|
10 |
+
app = typer.Typer()
|
11 |
+
|
12 |
+
|
13 |
+
@app.command()
|
14 |
+
def serve(
|
15 |
+
server_name: str = typer.Option(
|
16 |
+
"127.0.0.1",
|
17 |
+
help="To make app accessible on local network, set this to '0.0.0.0'. Can be set by environment variable GRADIO_SERVER_NAME. If None, will use '127.0.0.1",
|
18 |
+
),
|
19 |
+
server_port: int = typer.Option(
|
20 |
+
7860,
|
21 |
+
help="Will start gradio app on this port (if available). Can be set by environment variable GRADIO_SERVER_PORT. If None, will search for an available port starting at 7860.",
|
22 |
+
),
|
23 |
+
):
|
24 |
+
"""
|
25 |
+
Starts the app and serves it given specified `server_name` and `server_port`.
|
26 |
+
"""
|
27 |
+
from chessli2.gui import chessli2_gradio_app
|
28 |
+
|
29 |
+
chessli2_gradio_app.launch(server_port=server_port, server_name=server_name)
|
30 |
+
|
31 |
+
|
32 |
+
@app.command()
|
33 |
+
def mistakes(
|
34 |
+
src: str = typer.Option(
|
35 |
+
"pwenker/chessli2",
|
36 |
+
help='Select either the name of the Hugging Face Space to load, (e.g. "pwenker/chessli2") or the full URL (including "http" or "https") of the hosted Gradio app to load (e.g. "http://localhost:7860/" or "https://bec81a83-5b5c-471e.gradio.live/',
|
37 |
+
),
|
38 |
+
lichess_api_token: str = typer.Option(
|
39 |
+
settings.lichess_api_token, help="Select lichess API token"
|
40 |
+
),
|
41 |
+
user_name: str = typer.Option("pwenker", help="Select user name"),
|
42 |
+
start_date: str = typer.Option("2017-05-14", help="Select start date (YYYY-MM-DD)"),
|
43 |
+
end_date: str = typer.Option("2024-05-14", help="Select end date (YYYY-MM-DD)"),
|
44 |
+
nags: list[int] = typer.Option(
|
45 |
+
[MistakeNag.blunder.value, MistakeNag.mistake.value],
|
46 |
+
help="Filter by mistake nag type (blunder=4, mistake=2, speculative=5, dubious=6)",
|
47 |
+
),
|
48 |
+
time_control: TimeControl = typer.Option(
|
49 |
+
TimeControl.all, help="Filter by time control"
|
50 |
+
),
|
51 |
+
output: Output = typer.Option(
|
52 |
+
Output.file,
|
53 |
+
help="""Select whether to output mistakes as markdown table, as PGN string, or file path of CSV file containing PGNS""",
|
54 |
+
),
|
55 |
+
):
|
56 |
+
"""
|
57 |
+
Fetches mistakes from the API based on the specified parameters.
|
58 |
+
"""
|
59 |
+
client = Client(src)
|
60 |
+
mistake_pgns, mistake_md, mistake_csv = client.predict(
|
61 |
+
lichess_api_token=lichess_api_token,
|
62 |
+
user_name=user_name,
|
63 |
+
start_date=start_date,
|
64 |
+
end_date=end_date,
|
65 |
+
nags=nags,
|
66 |
+
time_control=time_control,
|
67 |
+
api_name="/get_mistakes",
|
68 |
+
)
|
69 |
+
|
70 |
+
if output == Output.info:
|
71 |
+
print(mistake_md)
|
72 |
+
elif output == Output.pgn:
|
73 |
+
print(mistake_pgns)
|
74 |
+
elif output == Output.file:
|
75 |
+
print(mistake_csv["value"])
|
76 |
+
|
77 |
+
|
78 |
+
@app.command()
|
79 |
+
def puzzles(
|
80 |
+
src: str = typer.Option(
|
81 |
+
"http://pwenker.github.io/chessli2/",
|
82 |
+
help="Select either the name of the Hugging Face Space to load, (e.g. 'pwenker/chessli2') or the full URL (including 'http' or 'https') of the hosted Gradio app to load (e.g. 'http://localhost:7860/' or 'https://bec81a83-5b5c-471e.gradio.live/')",
|
83 |
+
),
|
84 |
+
lichess_api_token: str = typer.Option(
|
85 |
+
settings.lichess_api_token, help="Select lichess API token"
|
86 |
+
),
|
87 |
+
before: str = typer.Option(
|
88 |
+
"2024-05-15",
|
89 |
+
help="Only get puzzle activity before this date (format YYYY-MM-DD)",
|
90 |
+
),
|
91 |
+
max: int = typer.Option(100, help="Select maxmimum number of puzzles"),
|
92 |
+
themes: list[PuzzleTheme] = typer.Option(
|
93 |
+
[pt for pt in PuzzleTheme],
|
94 |
+
help="Filter by puzzle themes",
|
95 |
+
),
|
96 |
+
output: Output = typer.Option(
|
97 |
+
Output.file,
|
98 |
+
help="Select whether to output info about mistakes, mistakes as PGN string, or a file path of CSV file containing PGNs",
|
99 |
+
),
|
100 |
+
):
|
101 |
+
"""
|
102 |
+
Fetches puzzles from the API based on the specified parameters.
|
103 |
+
"""
|
104 |
+
logging.disable(logging.CRITICAL)
|
105 |
+
client = Client(src)
|
106 |
+
puzzle_pgns, puzzle_theme_counter, puzzle_csv = client.predict(
|
107 |
+
before=before,
|
108 |
+
max=max,
|
109 |
+
themes_selection=themes,
|
110 |
+
api_name="/get_puzzles",
|
111 |
+
)
|
112 |
+
|
113 |
+
if output == Output.info:
|
114 |
+
print(puzzle_theme_counter)
|
115 |
+
elif output == Output.pgn:
|
116 |
+
print(puzzle_pgns)
|
117 |
+
elif output == Output.file:
|
118 |
+
print(puzzle_csv["value"])
|
119 |
+
|
120 |
+
|
121 |
+
if __name__ == "__main__":
|
122 |
+
app()
|
src/chessli2/games.py
DELETED
@@ -1,68 +0,0 @@
|
|
1 |
-
import io
|
2 |
-
|
3 |
-
import berserk
|
4 |
-
import chess.pgn
|
5 |
-
import gradio as gr
|
6 |
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
7 |
-
|
8 |
-
|
9 |
-
class Settings(BaseSettings):
|
10 |
-
model_config = SettingsConfigDict(env_file=".env")
|
11 |
-
lichess_api_token: str = ''
|
12 |
-
|
13 |
-
|
14 |
-
settings = Settings()
|
15 |
-
|
16 |
-
def generate_grid_html(games_list, row_length=2):
|
17 |
-
game_ids = [
|
18 |
-
chess.pgn.read_headers(io.StringIO(game_pgn))["Site"].rpartition("/")[-1]
|
19 |
-
for game_pgn in games_list
|
20 |
-
]
|
21 |
-
if row_length == 1:
|
22 |
-
return "\n".join(
|
23 |
-
[
|
24 |
-
f'<iframe src="https://lichess.org/embed/game/{game_id}?theme=auto&bg=auto" width=600 height=397 frameborder=0></iframe>'
|
25 |
-
for game_id in game_ids
|
26 |
-
]
|
27 |
-
)
|
28 |
-
else:
|
29 |
-
html_rows = []
|
30 |
-
for i in range(0, len(game_ids), row_length):
|
31 |
-
row_html = "".join(
|
32 |
-
[
|
33 |
-
f'<iframe src="https://lichess.org/embed/game/{id}?theme=auto&bg=auto" width="600" height="397" frameborder="0"></iframe>'
|
34 |
-
for id in game_ids[i : i + row_length]
|
35 |
-
]
|
36 |
-
)
|
37 |
-
# Wrap each row in a div for grid formatting
|
38 |
-
html_rows.append(
|
39 |
-
f'<div style="display: flex; justify-content: space-around;">{row_html}</div>'
|
40 |
-
)
|
41 |
-
return "\n".join(html_rows)
|
42 |
-
|
43 |
-
def fetch_games(
|
44 |
-
user_name, start_date, end_date, lichess_api_token=settings.lichess_api_token
|
45 |
-
):
|
46 |
-
gr.Info("Fetching chess games 🔄♟️")
|
47 |
-
|
48 |
-
start = berserk.utils.to_millis(start_date)
|
49 |
-
end = berserk.utils.to_millis(end_date)
|
50 |
-
session = berserk.TokenSession(lichess_api_token)
|
51 |
-
client = berserk.Client(session=session)
|
52 |
-
games = client.games.export_by_player(
|
53 |
-
user_name,
|
54 |
-
as_pgn=True,
|
55 |
-
color="black",
|
56 |
-
evals=True,
|
57 |
-
analysed=True,
|
58 |
-
literate=True,
|
59 |
-
since=start,
|
60 |
-
until=end,
|
61 |
-
opening=True,
|
62 |
-
)
|
63 |
-
games_list = list(games)
|
64 |
-
|
65 |
-
|
66 |
-
game_ids_html = generate_grid_html(games_list)
|
67 |
-
|
68 |
-
return games_list, gr.HTML(game_ids_html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/chessli2/gui.py
CHANGED
@@ -1,42 +1,44 @@
|
|
1 |
from datetime import datetime
|
2 |
-
from io import StringIO
|
3 |
from pathlib import Path
|
4 |
|
5 |
-
import chess.pgn
|
6 |
import gradio as gr
|
7 |
-
import pandas as pd
|
8 |
from gradio_calendar import Calendar
|
9 |
|
10 |
-
from
|
|
|
|
|
11 |
|
12 |
readme_file = Path("README.md")
|
|
|
13 |
|
14 |
-
nags = {
|
15 |
-
"Mistake (?)": 2,
|
16 |
-
"Blunder (??)": 4,
|
17 |
-
"Speculative Move (!?)": 5,
|
18 |
-
"Dubious Move (?!)": 6,
|
19 |
-
}
|
20 |
|
21 |
-
with gr.Blocks() as
|
22 |
with gr.Tab("Welcome"):
|
23 |
readme = readme_file.read_text()
|
24 |
-
gr_readme = readme.split(
|
25 |
gr.Markdown(gr_readme)
|
26 |
-
with gr.Tab("Games & Mistakes"):
|
27 |
-
gr.Markdown("Fetch your games and download a CSV with your mistakes")
|
28 |
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
|
|
|
|
|
|
34 |
)
|
|
|
35 |
user_name = gr.Textbox(
|
36 |
label="User Name",
|
37 |
placeholder="Enter your user name",
|
38 |
info="Type in your lichess user name",
|
39 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
with gr.Row():
|
41 |
start_date = Calendar(
|
42 |
type="datetime",
|
@@ -52,76 +54,94 @@ with gr.Blocks() as demo:
|
|
52 |
)
|
53 |
nags_checkbox = gr.CheckboxGroup(
|
54 |
label="Mistake Types",
|
55 |
-
value=[
|
56 |
info="Select which types of mistakes should be detected",
|
57 |
-
choices=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
)
|
59 |
get_mistakes_btn = gr.Button("Get Mistakes", variant="primary")
|
60 |
|
61 |
-
gr.
|
62 |
-
|
63 |
-
headers=["PGN"],
|
64 |
-
visible=False,
|
65 |
-
interactive=False,
|
66 |
-
)
|
67 |
-
mistakes_md = gr.Markdown()
|
68 |
|
69 |
-
|
70 |
-
gr.Info("Preparing downloadable CSV file 💾")
|
71 |
-
file_name = f"mistakes_{user_name}.csv"
|
72 |
-
df.to_csv(
|
73 |
-
file_name,
|
74 |
-
index=False,
|
75 |
-
header=False,
|
76 |
-
)
|
77 |
-
return gr.DownloadButton(value=file_name, visible=True)
|
78 |
-
|
79 |
-
download_button = gr.DownloadButton(
|
80 |
"Download CSV", variant="primary", visible=False
|
81 |
)
|
82 |
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
"✅"
|
94 |
-
if headers["Result"] == "1-0"
|
95 |
-
else "❌"
|
96 |
-
if headers["Result"] == "0-1"
|
97 |
-
else "➖"
|
98 |
-
)
|
99 |
-
return {
|
100 |
-
"White": headers["White"],
|
101 |
-
"Black": headers["Black"],
|
102 |
-
"Result": result_emoji,
|
103 |
-
"White Elo": headers["WhiteElo"],
|
104 |
-
"Black Elo": headers["BlackElo"],
|
105 |
-
"Opening": headers["Opening"],
|
106 |
-
"Mistake ❌": mistake,
|
107 |
-
"Correct Variation ✅": correct_variation,
|
108 |
-
}
|
109 |
-
|
110 |
-
df_info = df["PGN"].apply(parse_pgn).apply(pd.Series)
|
111 |
|
112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from datetime import datetime
|
|
|
2 |
from pathlib import Path
|
3 |
|
|
|
4 |
import gradio as gr
|
|
|
5 |
from gradio_calendar import Calendar
|
6 |
|
7 |
+
from chessli2 import choices as ch
|
8 |
+
from chessli2.mistakes import validated_get_mistakes
|
9 |
+
from chessli2.tactics import get_puzzles
|
10 |
|
11 |
readme_file = Path("README.md")
|
12 |
+
puzzle_themes_file = Path("docs/puzzle_themes.md")
|
13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
+
with gr.Blocks() as chessli2_gradio_app:
|
16 |
with gr.Tab("Welcome"):
|
17 |
readme = readme_file.read_text()
|
18 |
+
gr_readme = readme.split("---")[2].strip()
|
19 |
gr.Markdown(gr_readme)
|
|
|
|
|
20 |
|
21 |
+
with gr.Tab("Games & Mistakes"):
|
22 |
+
gr.Markdown(
|
23 |
+
"""
|
24 |
+
### Games & Mistakes
|
25 |
+
|
26 |
+
Here you can fetch your games and mistakes from your **game history** 🎮. You can filter by time control, type of mistakes, and date range.
|
27 |
+
|
28 |
+
When you are happy, you can download a CSV file with the selected mistakes' PGNs to practice them with Anki 📥."""
|
29 |
)
|
30 |
+
|
31 |
user_name = gr.Textbox(
|
32 |
label="User Name",
|
33 |
placeholder="Enter your user name",
|
34 |
info="Type in your lichess user name",
|
35 |
)
|
36 |
+
lichess_api_token_mistakes = gr.Textbox(
|
37 |
+
placeholder="Paste your Lichess API key",
|
38 |
+
label="Lichess API Token",
|
39 |
+
lines=1,
|
40 |
+
type="password",
|
41 |
+
)
|
42 |
with gr.Row():
|
43 |
start_date = Calendar(
|
44 |
type="datetime",
|
|
|
54 |
)
|
55 |
nags_checkbox = gr.CheckboxGroup(
|
56 |
label="Mistake Types",
|
57 |
+
value=[ch.MistakeNag.mistake.value, ch.MistakeNag.blunder.value],
|
58 |
info="Select which types of mistakes should be detected",
|
59 |
+
choices=[
|
60 |
+
("Mistake (?)", ch.MistakeNag.mistake.value),
|
61 |
+
("Blunder (??)", ch.MistakeNag.blunder.value),
|
62 |
+
("Speculative Move (!?)", ch.MistakeNag.speculative.value),
|
63 |
+
("Dubious Move (?!)", ch.MistakeNag.dubious.value),
|
64 |
+
],
|
65 |
+
)
|
66 |
+
time_control = gr.Dropdown(
|
67 |
+
label="Filter by Time Control",
|
68 |
+
value="All Time Controls",
|
69 |
+
allow_custom_value=True,
|
70 |
+
choices=[(tc.value, tc.name) for tc in ch.TimeControl],
|
71 |
)
|
72 |
get_mistakes_btn = gr.Button("Get Mistakes", variant="primary")
|
73 |
|
74 |
+
mistake_pgns = gr.Textbox(visible=False)
|
75 |
+
mistakes_md = gr.Markdown(label="Mistakes")
|
|
|
|
|
|
|
|
|
|
|
76 |
|
77 |
+
mistakes_download_button = gr.DownloadButton(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
"Download CSV", variant="primary", visible=False
|
79 |
)
|
80 |
|
81 |
+
with gr.Tab("Tactics"):
|
82 |
+
gr.Markdown(
|
83 |
+
"""
|
84 |
+
### Tactics & Puzzles
|
85 |
+
|
86 |
+
Here you can fetch your puzzles from your [puzzle history](https://lichess.org/training/history) 🧩.
|
87 |
+
|
88 |
+
You can filter by [puzzle themes](https://lichess.org/training/themes) 🎯 and also select a maximum number of puzzles.
|
89 |
+
When you are happy, you can download a CSV file with the selected puzzles' PGNs to practice them with Anki 📥."""
|
90 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
|
92 |
+
lichess_api_token_tactics = gr.Textbox(
|
93 |
+
placeholder="Paste your Lichess API key",
|
94 |
+
label="Lichess API Token",
|
95 |
+
lines=1,
|
96 |
+
type="password",
|
97 |
+
)
|
98 |
+
with gr.Accordion("Info about puzzle themes 🧩", open=False):
|
99 |
+
gr.Markdown(puzzle_themes_file.read_text())
|
100 |
|
101 |
+
puzzle_themes = gr.Dropdown(
|
102 |
+
label="Puzzle Themes",
|
103 |
+
info="Filter puzzles by selecting themes. Per default, all available themes are selected",
|
104 |
+
multiselect=True,
|
105 |
+
value=[th.name for th in ch.PuzzleTheme],
|
106 |
+
choices=[(th.value, th.name) for th in ch.PuzzleTheme],
|
107 |
+
)
|
108 |
+
max_puzzles = gr.Slider(
|
109 |
+
label="Maximum number of puzzles",
|
110 |
+
info="Since Puzzle activity is sorted by reverse chronological order (most recent first), this selects the last n puzzles",
|
111 |
+
minimum=1,
|
112 |
+
value=100,
|
113 |
+
maximum=100000,
|
114 |
+
)
|
115 |
+
until_date = Calendar(
|
116 |
+
type="datetime",
|
117 |
+
value=datetime.now(),
|
118 |
+
label="Only get puzzle activity before this time",
|
119 |
+
info="Click the calendar icon to bring up the calendar.",
|
120 |
)
|
121 |
+
tactics_btn = gr.Button("Get tactics", variant="primary")
|
122 |
+
puzzle_pgns = gr.Textbox(visible=False)
|
123 |
+
puzzle_info = gr.JSON(label="Number of puzzles found organized by themes")
|
124 |
+
puzzle_md_table = gr.Markdown()
|
125 |
+
puzzle_download_button = gr.DownloadButton(
|
126 |
+
"Download CSV", variant="primary", visible=False
|
127 |
+
)
|
128 |
+
|
129 |
+
# Event Handler
|
130 |
+
get_mistakes_btn.click(
|
131 |
+
fn=validated_get_mistakes,
|
132 |
+
inputs=[
|
133 |
+
lichess_api_token_mistakes,
|
134 |
+
user_name,
|
135 |
+
start_date,
|
136 |
+
end_date,
|
137 |
+
nags_checkbox,
|
138 |
+
time_control,
|
139 |
+
],
|
140 |
+
outputs=[mistake_pgns, mistakes_md, mistakes_download_button],
|
141 |
+
api_name="get_mistakes",
|
142 |
+
)
|
143 |
+
tactics_btn.click(
|
144 |
+
fn=get_puzzles,
|
145 |
+
inputs=[until_date, max_puzzles, puzzle_themes, lichess_api_token_tactics],
|
146 |
+
outputs=[puzzle_pgns, puzzle_info, puzzle_download_button],
|
147 |
+
)
|
src/chessli2/main.py
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
from src.chessli2.gui import demo
|
2 |
-
|
3 |
-
|
4 |
-
if __name__ == "__main__":
|
5 |
-
demo.launch(show_error=True)
|
|
|
|
|
|
|
|
|
|
|
|
src/chessli2/mistakes.py
CHANGED
@@ -1,16 +1,13 @@
|
|
1 |
import io
|
2 |
-
from enum import Enum
|
3 |
|
|
|
4 |
import chess
|
5 |
import chess.pgn
|
6 |
import gradio as gr
|
7 |
|
8 |
-
from
|
9 |
-
|
10 |
-
|
11 |
-
class Color(Enum):
|
12 |
-
white = 1
|
13 |
-
black = 0
|
14 |
|
15 |
|
16 |
def create_mistake_pgn(game_node):
|
@@ -38,35 +35,80 @@ def create_mistake_pgn(game_node):
|
|
38 |
return pgn_string
|
39 |
|
40 |
|
41 |
-
def get_mistakes(
|
42 |
-
|
|
|
|
|
43 |
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
game_pgn = io.StringIO(game_pgn)
|
48 |
-
game_node = chess.pgn.read_game(game_pgn)
|
49 |
-
|
50 |
-
player: Color = (
|
51 |
-
Color.white if game_node.headers["White"] == user_name else Color.black
|
52 |
)
|
53 |
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
|
71 |
|
72 |
def validate_user_input(user_name, start_date, end_date):
|
@@ -77,8 +119,12 @@ def validate_user_input(user_name, start_date, end_date):
|
|
77 |
return user_name, start_date, end_date
|
78 |
|
79 |
|
80 |
-
def validated_get_mistakes(
|
|
|
|
|
81 |
user_name, start_date, end_date = validate_user_input(
|
82 |
user_name, start_date, end_date
|
83 |
)
|
84 |
-
|
|
|
|
|
|
1 |
import io
|
|
|
2 |
|
3 |
+
import berserk
|
4 |
import chess
|
5 |
import chess.pgn
|
6 |
import gradio as gr
|
7 |
|
8 |
+
from chessli2.choices import Color
|
9 |
+
from chessli2.settings import get_client
|
10 |
+
from chessli2.writer import mistake_pgns_to_md, write_pgn_to_csv
|
|
|
|
|
|
|
11 |
|
12 |
|
13 |
def create_mistake_pgn(game_node):
|
|
|
35 |
return pgn_string
|
36 |
|
37 |
|
38 |
+
def get_mistakes(
|
39 |
+
lichess_api_token, user_name, start_date, end_date, nags, time_control
|
40 |
+
):
|
41 |
+
client, lichess_api_token = get_client(lichess_api_token=lichess_api_token)
|
42 |
|
43 |
+
if not lichess_api_token:
|
44 |
+
gr.Info(
|
45 |
+
"If you authenticate with your lichess API token you can increase requests limit from 20 to 60 games per second!"
|
|
|
|
|
|
|
|
|
|
|
46 |
)
|
47 |
|
48 |
+
games = client.games.export_by_player(
|
49 |
+
user_name,
|
50 |
+
as_pgn=True,
|
51 |
+
evals=True,
|
52 |
+
analysed=True,
|
53 |
+
literate=True,
|
54 |
+
since=berserk.utils.to_millis(start_date),
|
55 |
+
perf_type=None if time_control == "all" else time_control,
|
56 |
+
until=berserk.utils.to_millis(end_date),
|
57 |
+
opening=True,
|
58 |
+
)
|
59 |
|
60 |
+
mistake_pgns = []
|
61 |
+
n_games = 0
|
62 |
+
try:
|
63 |
+
for n, game_pgn in enumerate(games):
|
64 |
+
game_pgn = io.StringIO(game_pgn)
|
65 |
+
game_node = chess.pgn.read_game(game_pgn)
|
66 |
+
|
67 |
+
player: Color = (
|
68 |
+
Color.white if game_node.headers["White"] == user_name else Color.black
|
69 |
+
)
|
70 |
+
|
71 |
+
def was_players_move(game_node):
|
72 |
+
return player.value != game_node.turn()
|
73 |
+
|
74 |
+
def relevant_mistakes_were_made(game_node, nags):
|
75 |
+
return game_node.nags and game_node.nags.issubset(set(nags))
|
76 |
+
|
77 |
+
while game_node is not None:
|
78 |
+
if was_players_move(game_node) and relevant_mistakes_were_made(
|
79 |
+
game_node, nags
|
80 |
+
):
|
81 |
+
mistake_pgn = create_mistake_pgn(game_node)
|
82 |
+
|
83 |
+
mistake_pgns.append(mistake_pgn)
|
84 |
+
game_node = game_node.next()
|
85 |
+
|
86 |
+
n_games = n
|
87 |
+
info = f"Fetched {n} games(s) with {len(mistake_pgns)} mistakes..."
|
88 |
+
yield mistake_pgns, "", gr.DownloadButton(
|
89 |
+
label=info, variant="secondary", visible=True
|
90 |
+
)
|
91 |
+
|
92 |
+
if mistake_pgns:
|
93 |
+
filename = f"{user_name}_mistakes.csv"
|
94 |
+
write_pgn_to_csv(mistake_pgns, filename=filename)
|
95 |
+
mistakes_md = mistake_pgns_to_md(mistake_pgns=mistake_pgns)
|
96 |
+
info = f"Download CSV file with PGNs of {len(mistake_pgns)} mistakes in {n_games} games"
|
97 |
+
yield mistake_pgns, mistakes_md, gr.DownloadButton(
|
98 |
+
value=filename, label=info, variant="primary"
|
99 |
+
)
|
100 |
+
else:
|
101 |
+
mistakes_md = ""
|
102 |
+
yield mistake_pgns, mistakes_md, gr.DownloadButton(
|
103 |
+
label="No mistakes found", variant="secondary"
|
104 |
+
)
|
105 |
+
except berserk.exceptions.ResponseError:
|
106 |
+
gr.Warning("Your lichess API token is invalid. Did you misspell it?")
|
107 |
+
yield "", "", gr.DownloadButton(
|
108 |
+
label="Please insert a correct lichess API token into the textbox at the top of the interface or leave it empty",
|
109 |
+
visible=True,
|
110 |
+
variant="stop",
|
111 |
+
)
|
112 |
|
113 |
|
114 |
def validate_user_input(user_name, start_date, end_date):
|
|
|
119 |
return user_name, start_date, end_date
|
120 |
|
121 |
|
122 |
+
def validated_get_mistakes(
|
123 |
+
lichess_api_token, user_name, start_date, end_date, nags, time_control
|
124 |
+
):
|
125 |
user_name, start_date, end_date = validate_user_input(
|
126 |
user_name, start_date, end_date
|
127 |
)
|
128 |
+
yield from get_mistakes(
|
129 |
+
lichess_api_token, user_name, start_date, end_date, nags, time_control
|
130 |
+
)
|
src/chessli2/rich_logging.py
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This file contains the logging configuration for the project.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import logging
|
6 |
+
|
7 |
+
from rich.console import Console
|
8 |
+
from rich.logging import RichHandler
|
9 |
+
|
10 |
+
FORMAT = "%(message)s"
|
11 |
+
logging.basicConfig(
|
12 |
+
level="INFO",
|
13 |
+
format=FORMAT,
|
14 |
+
datefmt="[%X]",
|
15 |
+
handlers=[RichHandler(rich_tracebacks=True)],
|
16 |
+
)
|
17 |
+
|
18 |
+
console = Console()
|
19 |
+
log = logging.getLogger("rich")
|
20 |
+
|
21 |
+
|
22 |
+
def set_log_level(verbosity: int):
|
23 |
+
if verbosity == 0:
|
24 |
+
log.setLevel(logging.CRITICAL)
|
25 |
+
elif verbosity == 1:
|
26 |
+
log.setLevel(logging.ERROR)
|
27 |
+
if verbosity == 2:
|
28 |
+
log.setLevel(logging.WARNING)
|
29 |
+
elif verbosity == 3:
|
30 |
+
log.setLevel(logging.INFO)
|
31 |
+
elif verbosity == 4:
|
32 |
+
log.setLevel(logging.DEBUG)
|
33 |
+
return log
|
src/chessli2/settings.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import berserk
|
2 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
3 |
+
|
4 |
+
|
5 |
+
class Settings(BaseSettings):
|
6 |
+
model_config = SettingsConfigDict(env_file=".env")
|
7 |
+
lichess_api_token: str = ""
|
8 |
+
|
9 |
+
|
10 |
+
settings = Settings()
|
11 |
+
|
12 |
+
|
13 |
+
def get_client(lichess_api_token):
|
14 |
+
if not lichess_api_token:
|
15 |
+
lichess_api_token = settings.lichess_api_token
|
16 |
+
session = berserk.TokenSession(lichess_api_token)
|
17 |
+
client = berserk.Client(session=session)
|
18 |
+
return client, lichess_api_token
|
src/chessli2/tactics.py
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import datetime
|
2 |
+
from collections import Counter
|
3 |
+
|
4 |
+
import berserk
|
5 |
+
import chess
|
6 |
+
import gradio as gr
|
7 |
+
|
8 |
+
from chessli2.settings import get_client
|
9 |
+
from chessli2.writer import write_pgn_to_csv
|
10 |
+
|
11 |
+
|
12 |
+
def puzzle_pgn(puzzle_activity):
|
13 |
+
game = puzzle_activity["game"]
|
14 |
+
puzzle = puzzle_activity["puzzle"]
|
15 |
+
themes = puzzle["themes"]
|
16 |
+
|
17 |
+
board = chess.Board()
|
18 |
+
moves = game["pgn"].split()
|
19 |
+
|
20 |
+
for move in moves[:-1]:
|
21 |
+
board.push_san(move)
|
22 |
+
last_move = board.parse_san(moves[-1])
|
23 |
+
|
24 |
+
pgn = chess.pgn.Game(
|
25 |
+
headers={
|
26 |
+
"Event": "Puzzle",
|
27 |
+
"Site": f"https://www.lichess.org/training/{puzzle['id']}",
|
28 |
+
"Themes": " ".join(themes),
|
29 |
+
}
|
30 |
+
)
|
31 |
+
pgn.setup(board)
|
32 |
+
puzzle_moves = [chess.Move.from_uci(m) for m in puzzle["solution"]]
|
33 |
+
|
34 |
+
pgn.add_line(
|
35 |
+
moves=[last_move] + puzzle_moves,
|
36 |
+
starting_comment=f"Puzzle {puzzle['id']} with themes: {' '.join(puzzle['themes'])}",
|
37 |
+
)
|
38 |
+
|
39 |
+
exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
|
40 |
+
pgn_string = pgn.accept(exporter)
|
41 |
+
return pgn_string, themes
|
42 |
+
|
43 |
+
|
44 |
+
def get_puzzles(before, max, themes_selection, lichess_api_token):
|
45 |
+
client, lichess_api_token = get_client(lichess_api_token=lichess_api_token)
|
46 |
+
|
47 |
+
puzzle_activity = client.puzzles.get_puzzle_activity(
|
48 |
+
before=berserk.utils.to_millis(before), max=max
|
49 |
+
)
|
50 |
+
n_puzzles = 0
|
51 |
+
skipped_puzzles = 0
|
52 |
+
puzzle_pgns = []
|
53 |
+
# Create a counter object that tracks the number of puzzles per theme
|
54 |
+
theme_counter = Counter()
|
55 |
+
|
56 |
+
try:
|
57 |
+
for pa in puzzle_activity:
|
58 |
+
ppgn, themes = puzzle_pgn(client.puzzles.get(pa["puzzle"]["id"]))
|
59 |
+
# check if any of the selected themes are in the puzzle themes
|
60 |
+
if not set(themes_selection).isdisjoint(themes):
|
61 |
+
n_puzzles += 1
|
62 |
+
theme_counter.update(themes)
|
63 |
+
puzzle_pgns.append(ppgn)
|
64 |
+
else:
|
65 |
+
skipped_puzzles += 1
|
66 |
+
download_info = (
|
67 |
+
f"Fetched {n_puzzles}, skipped {skipped_puzzles} puzzle(s)..."
|
68 |
+
)
|
69 |
+
yield puzzle_pgns, theme_counter, gr.DownloadButton(
|
70 |
+
label=download_info, variant="secondary", visible=True
|
71 |
+
)
|
72 |
+
|
73 |
+
if n_puzzles > 0:
|
74 |
+
filename = "puzzles.csv"
|
75 |
+
write_pgn_to_csv(puzzle_pgns, filename=filename)
|
76 |
+
download_info = f"Download CSV file with PGNs of {n_puzzles} puzzles"
|
77 |
+
yield puzzle_pgns, theme_counter, gr.DownloadButton(
|
78 |
+
value=filename, label=download_info, variant="primary"
|
79 |
+
)
|
80 |
+
else:
|
81 |
+
yield puzzle_pgns, {"Puzzles": 0}, gr.DownloadButton(
|
82 |
+
label="No puzzles found", variant="secondary"
|
83 |
+
)
|
84 |
+
except berserk.exceptions.ResponseError as e:
|
85 |
+
if "No such token" in e.message:
|
86 |
+
gr.Warning("Your lichess API token is invalid. Did you misspell it?")
|
87 |
+
warning = "Please insert a correct lichess API token into the textbox at the top of the interface"
|
88 |
+
else:
|
89 |
+
gr.Warning(
|
90 |
+
"To fetch your puzzle activity, you need to authenticate with your lichess API token!"
|
91 |
+
)
|
92 |
+
warning = "Please paste your lichess API token into the corresponding textbox at the top of the interface to fetch your puzzles"
|
93 |
+
yield "", {}, gr.DownloadButton(
|
94 |
+
label=warning,
|
95 |
+
visible=True,
|
96 |
+
variant="stop",
|
97 |
+
)
|
98 |
+
|
99 |
+
|
100 |
+
def create_markdown_table(entries):
|
101 |
+
if not entries:
|
102 |
+
return "No data provided"
|
103 |
+
|
104 |
+
if not isinstance(entries, list) or not all(
|
105 |
+
isinstance(item, dict) for item in entries
|
106 |
+
):
|
107 |
+
return "Invalid input format. Please provide a list of dictionaries."
|
108 |
+
|
109 |
+
# Extract headers from the first dictionary
|
110 |
+
headers = entries[0].keys()
|
111 |
+
|
112 |
+
# Create the header row
|
113 |
+
header_row = "| " + " | ".join(headers) + " |"
|
114 |
+
|
115 |
+
# Create the separator row
|
116 |
+
separator_row = "| " + " | ".join(["---"] * len(headers)) + " |"
|
117 |
+
|
118 |
+
# Initialize the table with the header and separator
|
119 |
+
table = [header_row, separator_row]
|
120 |
+
|
121 |
+
# Fill in the data rows
|
122 |
+
for entry in entries:
|
123 |
+
row = []
|
124 |
+
for header in headers:
|
125 |
+
if header in entry:
|
126 |
+
value = entry[header]
|
127 |
+
if isinstance(value, datetime.datetime):
|
128 |
+
value = value.strftime("%Y-%m-%d %H:%M:%S %Z")
|
129 |
+
elif isinstance(value, list):
|
130 |
+
value = ", ".join(map(str, value))
|
131 |
+
row.append(str(value))
|
132 |
+
else:
|
133 |
+
row.append("N/A")
|
134 |
+
table.append("| " + " | ".join(row) + " |")
|
135 |
+
|
136 |
+
return "\n".join(table)
|
src/chessli2/writer.py
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import csv
|
2 |
+
from io import StringIO
|
3 |
+
|
4 |
+
import chess
|
5 |
+
import gradio as gr
|
6 |
+
import pandas as pd
|
7 |
+
|
8 |
+
|
9 |
+
def write_pgn_to_csv(pgns, filename="pgns.csv"):
|
10 |
+
"""
|
11 |
+
Writes a list of PGN strings to a CSV file with a single header 'PGN'.
|
12 |
+
|
13 |
+
Args:
|
14 |
+
pgns (list of str): List containing PGN strings.
|
15 |
+
filename (str): Name of the CSV file to be created.
|
16 |
+
"""
|
17 |
+
# Open the file in write mode
|
18 |
+
with open(filename, mode="w", newline="", encoding="utf-8") as file:
|
19 |
+
# Create a CSV writer object
|
20 |
+
writer = csv.writer(file)
|
21 |
+
|
22 |
+
# Write each PGN as a new row
|
23 |
+
for pgn in pgns:
|
24 |
+
writer.writerow([pgn])
|
25 |
+
|
26 |
+
|
27 |
+
def mistake_pgns_to_md(mistake_pgns):
|
28 |
+
gr.Info("Visualizing mistakes as markdown table 📉📝")
|
29 |
+
|
30 |
+
def parse_pgn(pgn_text):
|
31 |
+
pgn_io = StringIO(pgn_text)
|
32 |
+
game = chess.pgn.read_game(pgn_io)
|
33 |
+
headers = game.headers
|
34 |
+
moves = pgn_text.split("\n\n")[1]
|
35 |
+
mistake, *correct_variation = moves.split("\n")
|
36 |
+
result_emoji = (
|
37 |
+
"✅"
|
38 |
+
if headers["Result"] == "1-0"
|
39 |
+
else "❌"
|
40 |
+
if headers["Result"] == "0-1"
|
41 |
+
else "➖"
|
42 |
+
)
|
43 |
+
return {
|
44 |
+
"White": headers["White"],
|
45 |
+
"Black": headers["Black"],
|
46 |
+
"Result": result_emoji,
|
47 |
+
"White Elo": headers["WhiteElo"],
|
48 |
+
"Black Elo": headers["BlackElo"],
|
49 |
+
"Opening": headers["Opening"],
|
50 |
+
"Time Control": headers["TimeControl"],
|
51 |
+
"Mistake ❌": mistake,
|
52 |
+
"Correct Variation ✅": correct_variation,
|
53 |
+
}
|
54 |
+
|
55 |
+
df = pd.DataFrame(data=mistake_pgns, columns=["PGN"])
|
56 |
+
df_info = df["PGN"].apply(parse_pgn).apply(pd.Series)
|
57 |
+
|
58 |
+
return df_info.to_markdown(index=False)
|
tests/test_cli.py
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pytest
|
2 |
+
from typer.testing import CliRunner
|
3 |
+
|
4 |
+
from chessli2.cli import app
|
5 |
+
from chessli2.settings import settings
|
6 |
+
|
7 |
+
runner = CliRunner()
|
8 |
+
|
9 |
+
sources = ["pwenker/chessli2", "http://localhost:7860"]
|
10 |
+
|
11 |
+
|
12 |
+
@pytest.mark.parametrize("src", ["http://localhost:7860"])
|
13 |
+
def test_mistakes_with_options(src):
|
14 |
+
result = runner.invoke(
|
15 |
+
app,
|
16 |
+
[
|
17 |
+
"mistakes",
|
18 |
+
"--src",
|
19 |
+
src,
|
20 |
+
"--lichess-api-token",
|
21 |
+
settings.lichess_api_token,
|
22 |
+
"--user-name",
|
23 |
+
"pwenker",
|
24 |
+
"--start-date",
|
25 |
+
"2017-05-14",
|
26 |
+
"--end-date",
|
27 |
+
"2024-05-14",
|
28 |
+
"--nags",
|
29 |
+
"4",
|
30 |
+
"--nags",
|
31 |
+
"2",
|
32 |
+
"--time-control",
|
33 |
+
"All Time Controls",
|
34 |
+
"--output",
|
35 |
+
"file",
|
36 |
+
],
|
37 |
+
)
|
38 |
+
assert result.exit_code == 0
|
39 |
+
|
40 |
+
|
41 |
+
@pytest.mark.parametrize("src", ["http://localhost:7860"])
|
42 |
+
def test_puzzles_with_options(src):
|
43 |
+
"""
|
44 |
+
chessli puzzles --src http://localhost:7860 --user-name pwenker --before 2024-05-14 --max 100 --output file
|
45 |
+
"""
|
46 |
+
result = runner.invoke(
|
47 |
+
app,
|
48 |
+
[
|
49 |
+
"puzzles",
|
50 |
+
"--src",
|
51 |
+
src,
|
52 |
+
"--lichess-api-token",
|
53 |
+
settings.lichess_api_token,
|
54 |
+
"--before",
|
55 |
+
"2024-05-14",
|
56 |
+
"--max",
|
57 |
+
'10',
|
58 |
+
"--output",
|
59 |
+
"file",
|
60 |
+
],
|
61 |
+
)
|
62 |
+
__import__('pdb').set_trace()
|
63 |
+
assert result.exit_code == 0
|