player-prepper 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- player_prepper/__init__.py +3 -0
- player_prepper/board.py +223 -0
- player_prepper/book.py +507 -0
- player_prepper/bridge.py +110 -0
- player_prepper/cli.py +494 -0
- player_prepper/engine.py +306 -0
- player_prepper/exploit.py +199 -0
- player_prepper/export.py +279 -0
- player_prepper/fetch.py +407 -0
- player_prepper/jobs.py +155 -0
- player_prepper/openings.py +148 -0
- player_prepper/paths.py +82 -0
- player_prepper/pipeline.py +200 -0
- player_prepper/scout.py +454 -0
- player_prepper/server.py +547 -0
- player_prepper/store.py +197 -0
- player_prepper/tree.py +363 -0
- player_prepper/web/app.js +1432 -0
- player_prepper/web/index.html +303 -0
- player_prepper/web/style.css +425 -0
- player_prepper-0.1.0.dist-info/METADATA +637 -0
- player_prepper-0.1.0.dist-info/RECORD +26 -0
- player_prepper-0.1.0.dist-info/WHEEL +5 -0
- player_prepper-0.1.0.dist-info/entry_points.txt +2 -0
- player_prepper-0.1.0.dist-info/licenses/LICENSE +21 -0
- player_prepper-0.1.0.dist-info/top_level.txt +1 -0
player_prepper/board.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Board images, rendered server-side as SVG.
|
|
2
|
+
|
|
3
|
+
python-chess draws the board itself, so a diagram needs nothing beyond the
|
|
4
|
+
core dependency. That matters here because a gap is only understandable as a
|
|
5
|
+
picture: "you have no answer after 1.e4 c5 2.Nf3 d6 3.d4 cxd4 4.Nxd4 Nf6
|
|
6
|
+
5.Nc3 a6" is a sentence nobody can hold in their head, and the same thing as
|
|
7
|
+
a diagram is obvious.
|
|
8
|
+
|
|
9
|
+
The palette matches the sibling apps so a position looks the same here, in the
|
|
10
|
+
repertoire editor and in the printed book.
|
|
11
|
+
|
|
12
|
+
The board is also playable: you can carry on from any position the report puts
|
|
13
|
+
in front of you. Legality is decided here rather than in the browser, which
|
|
14
|
+
keeps the page free of a chess library -- the same choice both sibling apps
|
|
15
|
+
made. That costs a request per move and buys one source of truth about what
|
|
16
|
+
is legal.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
import chess
|
|
24
|
+
import chess.svg
|
|
25
|
+
|
|
26
|
+
BOARD_COLORS = {
|
|
27
|
+
"square light": "#f0d9b5",
|
|
28
|
+
"square dark": "#b58863",
|
|
29
|
+
"square light lastmove": "#cdd26a",
|
|
30
|
+
"square dark lastmove": "#aaa23a",
|
|
31
|
+
"margin": "#f7f2e8",
|
|
32
|
+
"coord": "#5c4a33",
|
|
33
|
+
"arrow green": "#15781baa",
|
|
34
|
+
"arrow red": "#882020aa",
|
|
35
|
+
"arrow yellow": "#e68f00aa",
|
|
36
|
+
"arrow blue": "#003088aa",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _arrow(uci: str, color: str):
|
|
41
|
+
"""One ``e2e4`` into a python-chess arrow, or None if it is nonsense."""
|
|
42
|
+
if not uci or len(uci) < 4:
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
move = chess.Move.from_uci(uci[:5])
|
|
46
|
+
except ValueError:
|
|
47
|
+
return None
|
|
48
|
+
return chess.svg.Arrow(move.from_square, move.to_square, color=color)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def board_svg(
|
|
52
|
+
fen: str,
|
|
53
|
+
*,
|
|
54
|
+
size: int = 360,
|
|
55
|
+
flipped: bool = False,
|
|
56
|
+
last_move: str = "",
|
|
57
|
+
arrows: str = "",
|
|
58
|
+
coordinates: bool = True,
|
|
59
|
+
) -> str:
|
|
60
|
+
"""A diagram of one position.
|
|
61
|
+
|
|
62
|
+
``arrows`` is the compact query form the browser sends:
|
|
63
|
+
``blue:e2e4,yellow:g1f3``. The colours mean one thing each, everywhere:
|
|
64
|
+
**blue** is what the engine suggests, **yellow** is the next move of the
|
|
65
|
+
line you are looking at, and **green** is a move of your own.
|
|
66
|
+
"""
|
|
67
|
+
board = chess.Board(fen)
|
|
68
|
+
|
|
69
|
+
shapes = []
|
|
70
|
+
for item in filter(None, (arrows or "").split(",")):
|
|
71
|
+
color, _, uci = item.partition(":")
|
|
72
|
+
arrow = _arrow(uci, color or "green")
|
|
73
|
+
if arrow is not None:
|
|
74
|
+
shapes.append(arrow)
|
|
75
|
+
|
|
76
|
+
move = None
|
|
77
|
+
if last_move and len(last_move) >= 4:
|
|
78
|
+
try:
|
|
79
|
+
move = chess.Move.from_uci(last_move[:5])
|
|
80
|
+
except ValueError:
|
|
81
|
+
move = None
|
|
82
|
+
|
|
83
|
+
return chess.svg.board(
|
|
84
|
+
board,
|
|
85
|
+
size=size,
|
|
86
|
+
orientation=chess.BLACK if flipped else chess.WHITE,
|
|
87
|
+
lastmove=move,
|
|
88
|
+
arrows=shapes,
|
|
89
|
+
coordinates=coordinates,
|
|
90
|
+
check=board.king(board.turn) if board.is_check() else None,
|
|
91
|
+
colors=BOARD_COLORS,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
_MARGIN_CACHE: dict = {}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def margin_fraction(coordinates: bool = True) -> float:
|
|
99
|
+
"""How much of the image is coordinate margin, as a fraction of one side.
|
|
100
|
+
|
|
101
|
+
With coordinates on, python-chess draws the rank and file labels *inside*
|
|
102
|
+
the SVG: the image is 390 units across but the 8x8 board only occupies
|
|
103
|
+
15..375 of it. A click overlay stretched across the whole image is
|
|
104
|
+
therefore off by that margin on every square -- a bug you see rather than
|
|
105
|
+
read about, as a piece that picks up the wrong square near the edge.
|
|
106
|
+
|
|
107
|
+
Measured from a real SVG rather than taken from ``chess.svg.MARGIN``,
|
|
108
|
+
because that constant does not match what the library actually emits and
|
|
109
|
+
either one could change in a future version. Same approach, and the same
|
|
110
|
+
reason, as Repertoire-Creator's copy.
|
|
111
|
+
"""
|
|
112
|
+
if coordinates in _MARGIN_CACHE:
|
|
113
|
+
return _MARGIN_CACHE[coordinates]
|
|
114
|
+
|
|
115
|
+
svg = chess.svg.board(chess.Board(), size=400, coordinates=coordinates)
|
|
116
|
+
fraction = 0.0
|
|
117
|
+
view_box = re.search(r'viewBox="[\d.]+ [\d.]+ ([\d.]+) ', svg)
|
|
118
|
+
# Must be a *square* rect: the first rect in the document is the board
|
|
119
|
+
# background, which spans the margin too and would measure zero.
|
|
120
|
+
square = re.search(
|
|
121
|
+
r'<rect x="([\d.]+)" y="[\d.]+" width="([\d.]+)"[^>]*class="square', svg)
|
|
122
|
+
if view_box and square:
|
|
123
|
+
total = float(view_box.group(1))
|
|
124
|
+
board_size = float(square.group(2)) * 8
|
|
125
|
+
if total > 0 < board_size <= total:
|
|
126
|
+
fraction = (total - board_size) / 2.0 / total
|
|
127
|
+
|
|
128
|
+
_MARGIN_CACHE[coordinates] = fraction
|
|
129
|
+
return fraction
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def legal_moves(fen: str) -> dict:
|
|
133
|
+
"""Legal destinations per origin square, for click-to-move.
|
|
134
|
+
|
|
135
|
+
The browser has no chess library -- deliberately, as in both sibling apps
|
|
136
|
+
-- so legality is decided here and the page only ever draws what it is
|
|
137
|
+
told.
|
|
138
|
+
"""
|
|
139
|
+
board = chess.Board(fen)
|
|
140
|
+
out: dict = {}
|
|
141
|
+
for move in board.legal_moves:
|
|
142
|
+
out.setdefault(chess.square_name(move.from_square), set()).add(
|
|
143
|
+
chess.square_name(move.to_square))
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"moves": {key: sorted(value) for key, value in out.items()},
|
|
147
|
+
"turn": "white" if board.turn == chess.WHITE else "black",
|
|
148
|
+
"check": board.is_check(),
|
|
149
|
+
"gameOver": board.is_game_over(),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def play_move(fen: str, uci: str) -> dict:
|
|
154
|
+
"""Play one move and report the position it reaches.
|
|
155
|
+
|
|
156
|
+
Promotions are filled in as a queen when the browser sends a bare
|
|
157
|
+
``e7e8``: offering an under-promotion picker in a scouting tool is UI
|
|
158
|
+
nobody would use, and refusing the move outright would look broken.
|
|
159
|
+
"""
|
|
160
|
+
board = chess.Board(fen)
|
|
161
|
+
try:
|
|
162
|
+
move = chess.Move.from_uci(uci)
|
|
163
|
+
except (ValueError, AssertionError) as exc:
|
|
164
|
+
raise ValueError(f"{uci} is not a move.") from exc
|
|
165
|
+
|
|
166
|
+
if move not in board.legal_moves and move.promotion is None:
|
|
167
|
+
promoted = chess.Move(move.from_square, move.to_square,
|
|
168
|
+
promotion=chess.QUEEN)
|
|
169
|
+
if promoted in board.legal_moves:
|
|
170
|
+
move = promoted
|
|
171
|
+
|
|
172
|
+
if move not in board.legal_moves:
|
|
173
|
+
raise ValueError(f"{uci} is not legal in that position.")
|
|
174
|
+
|
|
175
|
+
san = board.san(move)
|
|
176
|
+
board.push(move)
|
|
177
|
+
return {
|
|
178
|
+
"fen": board.fen(),
|
|
179
|
+
"uci": move.uci(),
|
|
180
|
+
"san": san,
|
|
181
|
+
"turn": "white" if board.turn == chess.WHITE else "black",
|
|
182
|
+
"check": board.is_check(),
|
|
183
|
+
"gameOver": board.is_game_over(),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def line_positions(ucis) -> dict:
|
|
188
|
+
"""Every position along a line, so the board can be stepped through.
|
|
189
|
+
|
|
190
|
+
One request per line rather than one per ply: the wheel has to feel
|
|
191
|
+
instant, and a round trip for each notch would not. Stops at the first
|
|
192
|
+
move that will not play and says how far it got, which keeps a stale
|
|
193
|
+
cached line usable instead of erroring.
|
|
194
|
+
"""
|
|
195
|
+
board = chess.Board()
|
|
196
|
+
fens = [board.fen()]
|
|
197
|
+
sans = []
|
|
198
|
+
played = []
|
|
199
|
+
|
|
200
|
+
for uci in ucis or []:
|
|
201
|
+
try:
|
|
202
|
+
move = chess.Move.from_uci(uci)
|
|
203
|
+
except (ValueError, AssertionError):
|
|
204
|
+
break
|
|
205
|
+
if move not in board.legal_moves:
|
|
206
|
+
break
|
|
207
|
+
sans.append(board.san(move))
|
|
208
|
+
board.push(move)
|
|
209
|
+
fens.append(board.fen())
|
|
210
|
+
played.append(uci)
|
|
211
|
+
|
|
212
|
+
return {"fens": fens, "sans": sans, "uci": played,
|
|
213
|
+
"complete": len(played) == len(list(ucis or []))}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
__all__ = [
|
|
217
|
+
"BOARD_COLORS",
|
|
218
|
+
"board_svg",
|
|
219
|
+
"legal_moves",
|
|
220
|
+
"line_positions",
|
|
221
|
+
"margin_fraction",
|
|
222
|
+
"play_move",
|
|
223
|
+
]
|