Commit
Β·
d76d581
0
Parent(s):
save changes
Browse files- .gitignore +13 -0
- README.md +15 -0
- pyproject.toml +29 -0
- requirements-dev.lock +82 -0
- requirements.lock +82 -0
- src/chessli2/__init__.py +0 -0
- src/chessli2/games.py +68 -0
- src/chessli2/gui.py +125 -0
- src/chessli2/main.py +5 -0
- src/chessli2/mistakes.py +84 -0
.gitignore
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
exploration
|
2 |
+
data/
|
3 |
+
|
4 |
+
# python generated files
|
5 |
+
__pycache__/
|
6 |
+
*.py[oc]
|
7 |
+
build/
|
8 |
+
dist/
|
9 |
+
wheels/
|
10 |
+
*.egg-info
|
11 |
+
|
12 |
+
# venv
|
13 |
+
.venv
|
README.md
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Welcome to Chessli2 π°
|
2 |
+
|
3 |
+
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/).
|
4 |
+
|
5 |
+
## Features π
|
6 |
+
|
7 |
+
- **Automatically fetch your games and played tactics puzzles** from lichess via the [berserk python client](https://github.com/lichess-org/berserk) for the Lichess API! π
|
8 |
+
- **Find your mistakes** by parsing and analyzing your games with [python-chess](https://github.com/niklasf/python-chess) π
|
9 |
+
- **Leverage the power of spaced repetition** using [Anki](https://apps.ankiweb.net/) with this amazing interactive chess template: [Anki-Chess-2.0](https://github.com/TowelSniffer/Anki-Chess-2.0) π§
|
10 |
+
|
11 |
+
Chessli2 is here to support your journey to becoming a chess master. Dive in and start enhancing your skills today! π
|
12 |
+
|
13 |
+
## How to
|
14 |
+
|
15 |
+
Coming soon...
|
pyproject.toml
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "chessli2"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Free and open-source chess trainer"
|
5 |
+
authors = [
|
6 |
+
{ name = "Pascal Wenker", email = "[email protected]" }
|
7 |
+
]
|
8 |
+
dependencies = [
|
9 |
+
"berserk>=0.13.2",
|
10 |
+
"pydantic-settings>=2.1.0",
|
11 |
+
"pydantic>=2.5.3",
|
12 |
+
"chess>=1.10.0",
|
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.8"
|
19 |
+
|
20 |
+
[build-system]
|
21 |
+
requires = ["hatchling"]
|
22 |
+
build-backend = "hatchling.build"
|
23 |
+
|
24 |
+
[tool.rye]
|
25 |
+
managed = true
|
26 |
+
dev-dependencies = []
|
27 |
+
|
28 |
+
[tool.hatch.metadata]
|
29 |
+
allow-direct-references = true
|
requirements-dev.lock
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# generated by rye
|
2 |
+
# use `rye lock` or `rye sync` to update this lockfile
|
3 |
+
#
|
4 |
+
# last locked with the following flags:
|
5 |
+
# pre: false
|
6 |
+
# features: []
|
7 |
+
# all-features: false
|
8 |
+
|
9 |
+
-e file:.
|
10 |
+
aiofiles==23.2.1
|
11 |
+
altair==5.2.0
|
12 |
+
annotated-types==0.6.0
|
13 |
+
anyio==4.2.0
|
14 |
+
attrs==23.2.0
|
15 |
+
berserk==0.13.2
|
16 |
+
certifi==2023.11.17
|
17 |
+
charset-normalizer==3.3.2
|
18 |
+
chess==1.10.0
|
19 |
+
click==8.1.7
|
20 |
+
contourpy==1.2.0
|
21 |
+
cycler==0.12.1
|
22 |
+
deprecated==1.2.14
|
23 |
+
fastapi==0.109.0
|
24 |
+
ffmpy==0.3.1
|
25 |
+
filelock==3.13.1
|
26 |
+
fonttools==4.47.2
|
27 |
+
fsspec==2023.12.2
|
28 |
+
gradio==4.29.0
|
29 |
+
gradio-calendar==0.0.4
|
30 |
+
gradio-client==0.16.1
|
31 |
+
h11==0.14.0
|
32 |
+
httpcore==1.0.2
|
33 |
+
httpx==0.26.0
|
34 |
+
huggingface-hub==0.20.2
|
35 |
+
idna==3.6
|
36 |
+
importlib-resources==6.1.1
|
37 |
+
jinja2==3.1.3
|
38 |
+
jsonschema==4.20.0
|
39 |
+
jsonschema-specifications==2023.12.1
|
40 |
+
kiwisolver==1.4.5
|
41 |
+
markdown-it-py==3.0.0
|
42 |
+
markupsafe==2.1.3
|
43 |
+
matplotlib==3.8.2
|
44 |
+
mdurl==0.1.2
|
45 |
+
ndjson==0.3.1
|
46 |
+
numpy==1.26.3
|
47 |
+
orjson==3.9.10
|
48 |
+
packaging==23.2
|
49 |
+
pandas==2.1.4
|
50 |
+
pillow==10.2.0
|
51 |
+
pydantic==2.5.3
|
52 |
+
pydantic-core==2.14.6
|
53 |
+
pydantic-settings==2.1.0
|
54 |
+
pydub==0.25.1
|
55 |
+
pygments==2.17.2
|
56 |
+
pyparsing==3.1.1
|
57 |
+
python-dateutil==2.8.2
|
58 |
+
python-dotenv==1.0.0
|
59 |
+
python-multipart==0.0.9
|
60 |
+
pytz==2023.3.post1
|
61 |
+
pyyaml==6.0.1
|
62 |
+
referencing==0.32.1
|
63 |
+
requests==2.31.0
|
64 |
+
rich==13.7.0
|
65 |
+
rpds-py==0.17.1
|
66 |
+
ruff==0.4.3
|
67 |
+
semantic-version==2.10.0
|
68 |
+
shellingham==1.5.4
|
69 |
+
six==1.16.0
|
70 |
+
sniffio==1.3.0
|
71 |
+
starlette==0.35.1
|
72 |
+
tabulate==0.9.0
|
73 |
+
tomlkit==0.12.0
|
74 |
+
toolz==0.12.0
|
75 |
+
tqdm==4.66.1
|
76 |
+
typer==0.12.3
|
77 |
+
typing-extensions==4.9.0
|
78 |
+
tzdata==2023.4
|
79 |
+
urllib3==2.1.0
|
80 |
+
uvicorn==0.25.0
|
81 |
+
websockets==11.0.3
|
82 |
+
wrapt==1.16.0
|
requirements.lock
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# generated by rye
|
2 |
+
# use `rye lock` or `rye sync` to update this lockfile
|
3 |
+
#
|
4 |
+
# last locked with the following flags:
|
5 |
+
# pre: false
|
6 |
+
# features: []
|
7 |
+
# all-features: false
|
8 |
+
|
9 |
+
-e file:.
|
10 |
+
aiofiles==23.2.1
|
11 |
+
altair==5.2.0
|
12 |
+
annotated-types==0.6.0
|
13 |
+
anyio==4.2.0
|
14 |
+
attrs==23.2.0
|
15 |
+
berserk==0.13.2
|
16 |
+
certifi==2023.11.17
|
17 |
+
charset-normalizer==3.3.2
|
18 |
+
chess==1.10.0
|
19 |
+
click==8.1.7
|
20 |
+
contourpy==1.2.0
|
21 |
+
cycler==0.12.1
|
22 |
+
deprecated==1.2.14
|
23 |
+
fastapi==0.109.0
|
24 |
+
ffmpy==0.3.1
|
25 |
+
filelock==3.13.1
|
26 |
+
fonttools==4.47.2
|
27 |
+
fsspec==2023.12.2
|
28 |
+
gradio==4.29.0
|
29 |
+
gradio-calendar==0.0.4
|
30 |
+
gradio-client==0.16.1
|
31 |
+
h11==0.14.0
|
32 |
+
httpcore==1.0.2
|
33 |
+
httpx==0.26.0
|
34 |
+
huggingface-hub==0.20.2
|
35 |
+
idna==3.6
|
36 |
+
importlib-resources==6.1.1
|
37 |
+
jinja2==3.1.3
|
38 |
+
jsonschema==4.20.0
|
39 |
+
jsonschema-specifications==2023.12.1
|
40 |
+
kiwisolver==1.4.5
|
41 |
+
markdown-it-py==3.0.0
|
42 |
+
markupsafe==2.1.3
|
43 |
+
matplotlib==3.8.2
|
44 |
+
mdurl==0.1.2
|
45 |
+
ndjson==0.3.1
|
46 |
+
numpy==1.26.3
|
47 |
+
orjson==3.9.10
|
48 |
+
packaging==23.2
|
49 |
+
pandas==2.1.4
|
50 |
+
pillow==10.2.0
|
51 |
+
pydantic==2.5.3
|
52 |
+
pydantic-core==2.14.6
|
53 |
+
pydantic-settings==2.1.0
|
54 |
+
pydub==0.25.1
|
55 |
+
pygments==2.17.2
|
56 |
+
pyparsing==3.1.1
|
57 |
+
python-dateutil==2.8.2
|
58 |
+
python-dotenv==1.0.0
|
59 |
+
python-multipart==0.0.9
|
60 |
+
pytz==2023.3.post1
|
61 |
+
pyyaml==6.0.1
|
62 |
+
referencing==0.32.1
|
63 |
+
requests==2.31.0
|
64 |
+
rich==13.7.0
|
65 |
+
rpds-py==0.17.1
|
66 |
+
ruff==0.4.3
|
67 |
+
semantic-version==2.10.0
|
68 |
+
shellingham==1.5.4
|
69 |
+
six==1.16.0
|
70 |
+
sniffio==1.3.0
|
71 |
+
starlette==0.35.1
|
72 |
+
tabulate==0.9.0
|
73 |
+
tomlkit==0.12.0
|
74 |
+
toolz==0.12.0
|
75 |
+
tqdm==4.66.1
|
76 |
+
typer==0.12.3
|
77 |
+
typing-extensions==4.9.0
|
78 |
+
tzdata==2023.4
|
79 |
+
urllib3==2.1.0
|
80 |
+
uvicorn==0.25.0
|
81 |
+
websockets==11.0.3
|
82 |
+
wrapt==1.16.0
|
src/chessli2/__init__.py
ADDED
File without changes
|
src/chessli2/games.py
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 chessli2.mistakes import validated_get_mistakes
|
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 demo:
|
22 |
+
with gr.Tab("Welcome"):
|
23 |
+
gr.Markdown(readme_file.read_text())
|
24 |
+
with gr.Tab("Games & Mistakes"):
|
25 |
+
gr.Markdown("Fetch your games and download a CSV with your mistakes")
|
26 |
+
|
27 |
+
lichess_api_token = gr.Textbox(
|
28 |
+
placeholder="Paste your Lichess API key",
|
29 |
+
label="Lichess API Token",
|
30 |
+
lines=1,
|
31 |
+
type="password",
|
32 |
+
)
|
33 |
+
user_name = gr.Textbox(
|
34 |
+
label="User Name",
|
35 |
+
placeholder="Enter your user name",
|
36 |
+
info="Type in your lichess user name",
|
37 |
+
)
|
38 |
+
with gr.Row():
|
39 |
+
start_date = Calendar(
|
40 |
+
type="datetime",
|
41 |
+
value=datetime.now(),
|
42 |
+
label="Select a start date",
|
43 |
+
info="Click the calendar icon to bring up the calendar.",
|
44 |
+
)
|
45 |
+
end_date = Calendar(
|
46 |
+
type="datetime",
|
47 |
+
value=datetime.now(),
|
48 |
+
label="Select an end date",
|
49 |
+
info="Click the calendar icon to bring up the calendar.",
|
50 |
+
)
|
51 |
+
nags_checkbox = gr.CheckboxGroup(
|
52 |
+
label="Mistake Types",
|
53 |
+
value=[2, 4], # Mistakes and Blundes per default
|
54 |
+
info="Select which types of mistakes should be detected",
|
55 |
+
choices=[(k, v) for k, v in nags.items()],
|
56 |
+
)
|
57 |
+
get_mistakes_btn = gr.Button("Get Mistakes", variant="primary")
|
58 |
+
|
59 |
+
gr.Markdown("### Mistakes")
|
60 |
+
mistakes_df = gr.Dataframe(
|
61 |
+
headers=["PGN"],
|
62 |
+
visible=False,
|
63 |
+
interactive=False,
|
64 |
+
)
|
65 |
+
mistakes_md = gr.Markdown()
|
66 |
+
|
67 |
+
def export_csv(df, user_name):
|
68 |
+
gr.Info("Preparing downloadable CSV file πΎ")
|
69 |
+
file_name = f"mistakes_{user_name}.csv"
|
70 |
+
df.to_csv(
|
71 |
+
file_name,
|
72 |
+
index=False,
|
73 |
+
header=False,
|
74 |
+
)
|
75 |
+
return gr.DownloadButton(value=file_name, visible=True)
|
76 |
+
|
77 |
+
download_button = gr.DownloadButton(
|
78 |
+
"Download CSV", variant="primary", visible=False
|
79 |
+
)
|
80 |
+
|
81 |
+
def df_to_md(df):
|
82 |
+
gr.Info("Visualizing mistakes as markdown table ππ")
|
83 |
+
|
84 |
+
def parse_pgn(pgn_text):
|
85 |
+
pgn_io = StringIO(pgn_text)
|
86 |
+
game = chess.pgn.read_game(pgn_io)
|
87 |
+
headers = game.headers
|
88 |
+
moves = pgn_text.split("\n\n")[1]
|
89 |
+
mistake, *correct_variation = moves.split("\n")
|
90 |
+
result_emoji = (
|
91 |
+
"β
"
|
92 |
+
if headers["Result"] == "1-0"
|
93 |
+
else "β"
|
94 |
+
if headers["Result"] == "0-1"
|
95 |
+
else "β"
|
96 |
+
)
|
97 |
+
return {
|
98 |
+
"White": headers["White"],
|
99 |
+
"Black": headers["Black"],
|
100 |
+
"Result": result_emoji,
|
101 |
+
"White Elo": headers["WhiteElo"],
|
102 |
+
"Black Elo": headers["BlackElo"],
|
103 |
+
"Opening": headers["Opening"],
|
104 |
+
"Mistake β": mistake,
|
105 |
+
"Correct Variation β
": correct_variation,
|
106 |
+
}
|
107 |
+
|
108 |
+
df_info = df["PGN"].apply(parse_pgn).apply(pd.Series)
|
109 |
+
|
110 |
+
return df_info.to_markdown(index=False)
|
111 |
+
|
112 |
+
get_mistakes_btn.click(
|
113 |
+
fn=validated_get_mistakes,
|
114 |
+
inputs=[lichess_api_token, user_name, start_date, end_date, nags_checkbox],
|
115 |
+
outputs=mistakes_df,
|
116 |
+
api_name="get_mistakes",
|
117 |
+
).success(
|
118 |
+
fn=export_csv,
|
119 |
+
inputs=[mistakes_df, user_name],
|
120 |
+
outputs=download_button,
|
121 |
+
).success(
|
122 |
+
fn=df_to_md,
|
123 |
+
inputs=mistakes_df,
|
124 |
+
outputs=mistakes_md,
|
125 |
+
)
|
src/chessli2/main.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from chessli2.gui import demo
|
2 |
+
|
3 |
+
|
4 |
+
if __name__ == "__main__":
|
5 |
+
demo.launch(show_error=True)
|
src/chessli2/mistakes.py
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import io
|
2 |
+
from enum import Enum
|
3 |
+
|
4 |
+
import chess
|
5 |
+
import chess.pgn
|
6 |
+
import gradio as gr
|
7 |
+
|
8 |
+
from chessli2.games import fetch_games
|
9 |
+
|
10 |
+
|
11 |
+
class Color(Enum):
|
12 |
+
white = 1
|
13 |
+
black = 0
|
14 |
+
|
15 |
+
|
16 |
+
def create_mistake_pgn(game_node):
|
17 |
+
parent = game_node.parent
|
18 |
+
game_root = game_node.game()
|
19 |
+
|
20 |
+
mistake_pgn = chess.pgn.Game()
|
21 |
+
mistake_pgn.headers = game_root.headers.copy()
|
22 |
+
mistake_pgn.setup(parent.parent.board())
|
23 |
+
|
24 |
+
mainline, *variations = parent.variations
|
25 |
+
assert len(variations) == 1
|
26 |
+
assert mainline.is_mainline()
|
27 |
+
|
28 |
+
vr = variations[0]
|
29 |
+
|
30 |
+
moves = [vr.parent.move, vr.move, *[n.move for n in vr.mainline()]]
|
31 |
+
mistake_pgn.add_line(
|
32 |
+
moves=moves,
|
33 |
+
starting_comment=f"{game_node.comment} (player's move was {game_node.san()})",
|
34 |
+
)
|
35 |
+
|
36 |
+
exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
|
37 |
+
pgn_string = mistake_pgn.accept(exporter)
|
38 |
+
return pgn_string
|
39 |
+
|
40 |
+
|
41 |
+
def get_mistakes(lichess_api_token, user_name, start_date, end_date, nags):
|
42 |
+
games, _ = fetch_games(user_name, start_date, end_date, lichess_api_token)
|
43 |
+
|
44 |
+
gr.Info("Finding mistakes πβ")
|
45 |
+
mistake_pgns = []
|
46 |
+
for game_pgn in games:
|
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 |
+
def was_players_move(game_node):
|
55 |
+
return player.value != game_node.turn()
|
56 |
+
|
57 |
+
def relevant_mistakes_were_made(game_node, nags):
|
58 |
+
return game_node.nags and game_node.nags.issubset(set(nags))
|
59 |
+
|
60 |
+
while game_node is not None:
|
61 |
+
if was_players_move(game_node) and relevant_mistakes_were_made(
|
62 |
+
game_node, nags
|
63 |
+
):
|
64 |
+
mistake_pgn = create_mistake_pgn(game_node)
|
65 |
+
|
66 |
+
mistake_pgns.append(mistake_pgn)
|
67 |
+
game_node = game_node.next()
|
68 |
+
|
69 |
+
return [[mp] for mp in mistake_pgns]
|
70 |
+
|
71 |
+
|
72 |
+
def validate_user_input(user_name, start_date, end_date):
|
73 |
+
if not user_name.strip():
|
74 |
+
raise gr.Error("User name cannot be empty.")
|
75 |
+
if start_date > end_date:
|
76 |
+
raise gr.Error("Start date must be before end date.")
|
77 |
+
return user_name, start_date, end_date
|
78 |
+
|
79 |
+
|
80 |
+
def validated_get_mistakes(lichess_api_token, user_name, start_date, end_date, nags):
|
81 |
+
user_name, start_date, end_date = validate_user_input(
|
82 |
+
user_name, start_date, end_date
|
83 |
+
)
|
84 |
+
return get_mistakes(lichess_api_token, user_name, start_date, end_date, nags)
|