Skip to content

Commit 354bfbc

Browse files
authored
Merge pull request #70 from olivierphi/better-lichess-import-II
[database] Slight improvement of the Lichess puzzles import, episode II
2 parents 0d1988a + 607228c commit 354bfbc

File tree

4 files changed

+114
-16
lines changed

4 files changed

+114
-16
lines changed

src/apps/daily_challenge/management/commands/dailychallenge_create_from_lichess_puzzles_csv.py

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import csv
22
import re
33
from pathlib import Path
4+
from typing import NamedTuple
45

56
import chess
67
from django.core.management import BaseCommand
@@ -55,6 +56,7 @@ def handle(
5556
min_popularity: int,
5657
rating_min_max: tuple[int, int],
5758
stop_after: int | None,
59+
verbosity: int,
5860
**options,
5961
):
6062
rating_min, rating_max = rating_min_max
@@ -63,48 +65,56 @@ def handle(
6365
Substr("source", 9), flat=True
6466
)
6567
)
66-
print(f"already_imported_ids: {already_imported_ids}")
68+
if verbosity >= 2:
69+
print(f"already_imported_ids: {already_imported_ids}")
6770

6871
created_count = 0
6972
current_batch: list[DailyChallenge] = []
7073
with csv_file_path.open(newline="") as csv_file:
7174
reader = csv.DictReader(csv_file)
72-
for row in reader:
75+
for row in reader: # type: dict[str,str]
7376
themes: set[str] = set(row.get("Themes", "").split())
7477
if themes & THEMES_TO_IGNORE:
75-
self.stdout.write("Skipping too short puzzle")
78+
if verbosity >= 2:
79+
self.stdout.write("Skipping puzzle with ignored theme")
7680
continue
7781
if (popularity := int(row["Popularity"])) < min_popularity:
78-
self.stdout.write(f"Skipping puzzle with Popularity {popularity}")
82+
if verbosity >= 2:
83+
self.stdout.write(
84+
f"Skipping puzzle with Popularity {popularity}"
85+
)
7986
continue
8087
if (rating := int(row["Rating"])) < rating_min or rating > rating_max:
81-
self.stdout.write(f"Skipping puzzle with Rating {rating}")
88+
if verbosity >= 2:
89+
self.stdout.write(f"Skipping puzzle with Rating {rating}")
8290
continue
8391

8492
puzzle_id = row["PuzzleId"]
8593
if puzzle_id in already_imported_ids:
86-
self.stdout.write(f"Skipping already imported puzzle '{puzzle_id}'")
94+
if verbosity >= 2:
95+
self.stdout.write(
96+
f"Skipping already imported puzzle '{puzzle_id}'"
97+
)
8798
continue
88-
fen = row["FEN"]
8999

90-
self.stdout.write(
91-
f"Creating DailyChallenge for puzzle '{puzzle_id}' with Popularity {popularity}, rating {rating}, FEN '{fen}'"
92-
)
93-
94-
if " b " in fen: # quick and dirty way to detect if black is to move
95-
self.stdout.write("Mirroring puzzle with black to move")
96-
fen = chess.Board(fen).mirror().fen()
100+
bot_first_move, fen = get_bot_first_move_and_resulting_fen(row)
101+
if verbosity >= 2:
102+
self.stdout.write(
103+
f"Creating DailyChallenge for puzzle '{puzzle_id}' with Popularity {popularity}, rating {rating}."
104+
)
97105

98106
current_batch.append(
99107
DailyChallenge(
100108
source=f"lichess-{puzzle_id}",
101109
fen=fen,
110+
bot_first_move=bot_first_move,
102111
)
103112
)
104113

105114
if len(current_batch) == batch_size:
106115
DailyChallenge.objects.bulk_create(current_batch)
107-
self.stdout.write(f"Created {batch_size} puzzles.")
116+
if verbosity >= 2:
117+
self.stdout.write(f"Created batch of {batch_size} puzzles.")
108118
current_batch = []
109119

110120
created_count += 1
@@ -114,7 +124,55 @@ def handle(
114124

115125
if current_batch:
116126
DailyChallenge.objects.bulk_create(current_batch)
117-
self.stdout.write(f"Created {len(current_batch)} puzzles.")
127+
if verbosity >= 2:
128+
self.stdout.write(f"Created batch of {batch_size} puzzles.")
129+
130+
self.stdout.write(
131+
f"Imported {self.style.SUCCESS(created_count)} Lichess puzzles."
132+
)
133+
134+
135+
def get_bot_first_move_and_resulting_fen(
136+
csv_row: dict,
137+
) -> "BotFirstMoveAndResultingFen":
138+
fen_before_bot_first_move = csv_row["FEN"]
139+
bot_first_move_uci = csv_row["Moves"][0:4]
140+
141+
# The Lichess puzzles FEN is always the one *before* the bot's move,
142+
# so we're going to have to adapt this to our own models.
143+
board = chess.Board(fen_before_bot_first_move)
144+
145+
# Quick and dirty way to detect if white is to move from the FEN:
146+
have_to_mirror_board = " w " in fen_before_bot_first_move
147+
148+
if have_to_mirror_board:
149+
# If this is the white to play, we're playing black.
150+
# (the FEN is always the one before the bot's move)
151+
# As we always play white in the Zakuchess daily challenge, for simplicity,
152+
# we have to mirror the board when that's the case.
153+
board.apply_mirror()
154+
155+
# Ok, let's calculate the FEN after the bot's move:
156+
if have_to_mirror_board:
157+
# If we mirrored the board, we have to mirror the move too:
158+
bot_first_move = chess.Move.from_uci(bot_first_move_uci)
159+
bot_first_move_uci = "".join(
160+
chess.square_name(chess.square_mirror(sq))
161+
for sq in (bot_first_move.from_square, bot_first_move.to_square)
162+
)
163+
164+
board.push(chess.Move.from_uci(bot_first_move_uci))
165+
starting_fen_after_bot_first_move = board.fen()
166+
167+
return BotFirstMoveAndResultingFen(
168+
first_move=bot_first_move_uci,
169+
fen=starting_fen_after_bot_first_move,
170+
)
171+
172+
173+
class BotFirstMoveAndResultingFen(NamedTuple):
174+
first_move: str
175+
fen: str
118176

119177

120178
def existing_path(value: str) -> Path:

src/apps/daily_challenge/tests/management/__init__.py

Whitespace-only changes.

src/apps/daily_challenge/tests/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import pytest
2+
3+
from apps.daily_challenge.management.commands.dailychallenge_create_from_lichess_puzzles_csv import (
4+
BotFirstMoveAndResultingFen,
5+
get_bot_first_move_and_resulting_fen,
6+
)
7+
8+
9+
@pytest.mark.parametrize(
10+
("lichess_puzzle_fen", "lichess_puzzle_moves", "expected_result"),
11+
[
12+
(
13+
# Black to move: simple case
14+
"8/3b2kp/8/8/p1pp4/P7/K4R1b/2B5 b - - 1 39",
15+
"h2g3 f2g2 g7f6 g2g3",
16+
BotFirstMoveAndResultingFen(
17+
"h2g3", "8/3b2kp/8/8/p1pp4/P5b1/K4R2/2B5 w - - 2 40"
18+
),
19+
),
20+
(
21+
# White to move: the board will have to be mirrored, so *we*
22+
# start on the white side
23+
"r1r3k1/4qpbp/1B2p1p1/p2b4/8/PP2PN2/4QPPP/2R2RK1 w - - 1 21",
24+
"b6c5 c8c5 c1c5 e7c5",
25+
BotFirstMoveAndResultingFen(
26+
# First move was mirrored too ("b6c5" -> "b3c4")
27+
"b3c4",
28+
"2r2rk1/4qppp/pp2pn2/8/P1bB4/4P1P1/4QPBP/R1R3K1 w - - 2 22",
29+
),
30+
),
31+
],
32+
)
33+
def test_get_bot_first_move_and_resulting_fen(
34+
lichess_puzzle_fen: str,
35+
lichess_puzzle_moves: str,
36+
expected_result: BotFirstMoveAndResultingFen,
37+
):
38+
lichess_csv_row = {"FEN": lichess_puzzle_fen, "Moves": lichess_puzzle_moves}
39+
result = get_bot_first_move_and_resulting_fen(lichess_csv_row)
40+
assert result == expected_result

0 commit comments

Comments
 (0)