pwenker commited on
Commit
d76d581
Β·
0 Parent(s):

save changes

Browse files
.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)