ankigammon 1.0.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.
Potentially problematic release.
This version of ankigammon might be problematic. Click here for more details.
- ankigammon/__init__.py +7 -0
- ankigammon/__main__.py +6 -0
- ankigammon/analysis/__init__.py +13 -0
- ankigammon/analysis/score_matrix.py +373 -0
- ankigammon/anki/__init__.py +6 -0
- ankigammon/anki/ankiconnect.py +224 -0
- ankigammon/anki/apkg_exporter.py +123 -0
- ankigammon/anki/card_generator.py +1307 -0
- ankigammon/anki/card_styles.py +1034 -0
- ankigammon/gui/__init__.py +8 -0
- ankigammon/gui/app.py +209 -0
- ankigammon/gui/dialogs/__init__.py +10 -0
- ankigammon/gui/dialogs/export_dialog.py +597 -0
- ankigammon/gui/dialogs/import_options_dialog.py +163 -0
- ankigammon/gui/dialogs/input_dialog.py +776 -0
- ankigammon/gui/dialogs/note_dialog.py +93 -0
- ankigammon/gui/dialogs/settings_dialog.py +384 -0
- ankigammon/gui/format_detector.py +292 -0
- ankigammon/gui/main_window.py +1071 -0
- ankigammon/gui/resources/icon.icns +0 -0
- ankigammon/gui/resources/icon.ico +0 -0
- ankigammon/gui/resources/icon.png +0 -0
- ankigammon/gui/resources/style.qss +394 -0
- ankigammon/gui/resources.py +26 -0
- ankigammon/gui/widgets/__init__.py +8 -0
- ankigammon/gui/widgets/position_list.py +193 -0
- ankigammon/gui/widgets/smart_input.py +268 -0
- ankigammon/models.py +322 -0
- ankigammon/parsers/__init__.py +7 -0
- ankigammon/parsers/gnubg_parser.py +454 -0
- ankigammon/parsers/xg_binary_parser.py +870 -0
- ankigammon/parsers/xg_text_parser.py +729 -0
- ankigammon/renderer/__init__.py +5 -0
- ankigammon/renderer/animation_controller.py +406 -0
- ankigammon/renderer/animation_helper.py +221 -0
- ankigammon/renderer/color_schemes.py +145 -0
- ankigammon/renderer/svg_board_renderer.py +824 -0
- ankigammon/settings.py +239 -0
- ankigammon/thirdparty/__init__.py +7 -0
- ankigammon/thirdparty/xgdatatools/__init__.py +17 -0
- ankigammon/thirdparty/xgdatatools/xgimport.py +160 -0
- ankigammon/thirdparty/xgdatatools/xgstruct.py +1032 -0
- ankigammon/thirdparty/xgdatatools/xgutils.py +118 -0
- ankigammon/thirdparty/xgdatatools/xgzarc.py +260 -0
- ankigammon/utils/__init__.py +13 -0
- ankigammon/utils/gnubg_analyzer.py +431 -0
- ankigammon/utils/gnuid.py +622 -0
- ankigammon/utils/move_parser.py +239 -0
- ankigammon/utils/ogid.py +335 -0
- ankigammon/utils/xgid.py +419 -0
- ankigammon-1.0.0.dist-info/METADATA +370 -0
- ankigammon-1.0.0.dist-info/RECORD +56 -0
- ankigammon-1.0.0.dist-info/WHEEL +5 -0
- ankigammon-1.0.0.dist-info/entry_points.txt +2 -0
- ankigammon-1.0.0.dist-info/licenses/LICENSE +21 -0
- ankigammon-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
"""SVG-based backgammon board renderer for animated cards."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple, List, Dict
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from ankigammon.models import Position, Player, CubeState, Move
|
|
7
|
+
from ankigammon.renderer.color_schemes import ColorScheme, CLASSIC
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SVGBoardRenderer:
|
|
11
|
+
"""Renders backgammon positions as SVG markup."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
width: int = 880,
|
|
16
|
+
height: int = 600,
|
|
17
|
+
point_height_ratio: float = 0.45,
|
|
18
|
+
color_scheme: ColorScheme = CLASSIC,
|
|
19
|
+
orientation: str = "counter-clockwise",
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the SVG board renderer.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
width: SVG viewBox width
|
|
26
|
+
height: SVG viewBox height
|
|
27
|
+
point_height_ratio: Height of points as ratio of board height
|
|
28
|
+
color_scheme: ColorScheme object defining board colors
|
|
29
|
+
orientation: Board orientation ("clockwise" or "counter-clockwise")
|
|
30
|
+
"""
|
|
31
|
+
self.width = width
|
|
32
|
+
self.height = height
|
|
33
|
+
self.point_height_ratio = point_height_ratio
|
|
34
|
+
self.color_scheme = color_scheme
|
|
35
|
+
self.orientation = orientation
|
|
36
|
+
|
|
37
|
+
# Calculate dimensions (same logic as PNG renderer)
|
|
38
|
+
# Use internal width of 900 for calculations, but viewBox can be cropped
|
|
39
|
+
self.internal_width = 900
|
|
40
|
+
self.margin = 20
|
|
41
|
+
self.cube_area_width = 70
|
|
42
|
+
self.bearoff_area_width = 100
|
|
43
|
+
|
|
44
|
+
self.playing_width = self.internal_width - 2 * self.margin - self.cube_area_width - self.bearoff_area_width
|
|
45
|
+
self.board_height = self.height - 2 * self.margin
|
|
46
|
+
|
|
47
|
+
self.bar_width = self.playing_width * 0.08
|
|
48
|
+
self.half_width = (self.playing_width - self.bar_width) / 2
|
|
49
|
+
self.point_width = self.half_width / 6
|
|
50
|
+
self.point_height = self.board_height * point_height_ratio
|
|
51
|
+
|
|
52
|
+
self.checker_radius = min(self.point_width * 0.45, 25)
|
|
53
|
+
|
|
54
|
+
def render_svg(
|
|
55
|
+
self,
|
|
56
|
+
position: Position,
|
|
57
|
+
on_roll: Player = Player.O,
|
|
58
|
+
dice: Optional[Tuple[int, int]] = None,
|
|
59
|
+
dice_opacity: float = 1.0,
|
|
60
|
+
cube_value: int = 1,
|
|
61
|
+
cube_owner: CubeState = CubeState.CENTERED,
|
|
62
|
+
move_data: Optional[Dict] = None,
|
|
63
|
+
score_x: int = 0,
|
|
64
|
+
score_o: int = 0,
|
|
65
|
+
match_length: int = 0,
|
|
66
|
+
) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Render a backgammon position as SVG.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
position: The position to render
|
|
72
|
+
on_roll: Which player is on roll
|
|
73
|
+
dice: Dice values (if any)
|
|
74
|
+
dice_opacity: Opacity for dice (0.0-1.0)
|
|
75
|
+
cube_value: Doubling cube value
|
|
76
|
+
cube_owner: Who owns the cube
|
|
77
|
+
move_data: Optional move animation data (for animated cards)
|
|
78
|
+
score_x: X player's current score
|
|
79
|
+
score_o: O player's current score
|
|
80
|
+
match_length: Match length (0 = money game, > 0 = match play)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
SVG markup string
|
|
84
|
+
"""
|
|
85
|
+
svg_parts = []
|
|
86
|
+
|
|
87
|
+
# Start SVG with viewBox
|
|
88
|
+
svg_parts.append(f'<svg viewBox="0 0 {self.width} {self.height}" '
|
|
89
|
+
f'xmlns="http://www.w3.org/2000/svg" '
|
|
90
|
+
f'class="backgammon-board">')
|
|
91
|
+
|
|
92
|
+
# Add styles
|
|
93
|
+
svg_parts.append(self._generate_styles())
|
|
94
|
+
|
|
95
|
+
# Board coordinates
|
|
96
|
+
board_x = self.margin + self.cube_area_width
|
|
97
|
+
board_y = self.margin
|
|
98
|
+
|
|
99
|
+
# Draw full background (covers entire SVG viewBox)
|
|
100
|
+
svg_parts.append(self._draw_full_background())
|
|
101
|
+
|
|
102
|
+
# Draw board background
|
|
103
|
+
svg_parts.append(self._draw_board_background(board_x, board_y))
|
|
104
|
+
|
|
105
|
+
# Draw bar
|
|
106
|
+
svg_parts.append(self._draw_bar(board_x, board_y))
|
|
107
|
+
|
|
108
|
+
# Draw points
|
|
109
|
+
svg_parts.append(self._draw_points(board_x, board_y))
|
|
110
|
+
|
|
111
|
+
# Draw checkers
|
|
112
|
+
flipped = False
|
|
113
|
+
svg_parts.append(self._draw_checkers(position, board_x, board_y, flipped, move_data))
|
|
114
|
+
|
|
115
|
+
# Draw bear-off trays
|
|
116
|
+
svg_parts.append(self._draw_bearoff(position, board_x, board_y, flipped))
|
|
117
|
+
|
|
118
|
+
# Draw dice
|
|
119
|
+
if dice:
|
|
120
|
+
svg_parts.append(self._draw_dice(dice, on_roll, board_x, board_y, dice_opacity))
|
|
121
|
+
|
|
122
|
+
# Draw cube
|
|
123
|
+
svg_parts.append(self._draw_cube(cube_value, cube_owner, board_x, board_y, flipped))
|
|
124
|
+
|
|
125
|
+
# Draw pip counts
|
|
126
|
+
svg_parts.append(self._draw_pip_counts(position, board_x, board_y, flipped))
|
|
127
|
+
|
|
128
|
+
# Draw scores (for match play only)
|
|
129
|
+
if match_length > 0:
|
|
130
|
+
svg_parts.append(self._draw_scores(score_x, score_o, match_length, board_x, board_y, flipped))
|
|
131
|
+
|
|
132
|
+
# Close SVG
|
|
133
|
+
svg_parts.append('</svg>')
|
|
134
|
+
|
|
135
|
+
return ''.join(svg_parts)
|
|
136
|
+
|
|
137
|
+
def _generate_styles(self) -> str:
|
|
138
|
+
"""Generate CSS styles for the SVG."""
|
|
139
|
+
return f"""
|
|
140
|
+
<defs>
|
|
141
|
+
<style>
|
|
142
|
+
.backgammon-board {{
|
|
143
|
+
max-width: 100%;
|
|
144
|
+
height: auto;
|
|
145
|
+
}}
|
|
146
|
+
.point {{
|
|
147
|
+
stroke: {self.color_scheme.board_dark};
|
|
148
|
+
stroke-width: 1;
|
|
149
|
+
}}
|
|
150
|
+
.checker {{
|
|
151
|
+
stroke: {self.color_scheme.checker_border};
|
|
152
|
+
stroke-width: 2;
|
|
153
|
+
}}
|
|
154
|
+
.checker-x {{
|
|
155
|
+
fill: {self.color_scheme.checker_x};
|
|
156
|
+
}}
|
|
157
|
+
.checker-o {{
|
|
158
|
+
fill: {self.color_scheme.checker_o};
|
|
159
|
+
}}
|
|
160
|
+
.checker-text {{
|
|
161
|
+
font-family: Arial, sans-serif;
|
|
162
|
+
font-weight: bold;
|
|
163
|
+
text-anchor: middle;
|
|
164
|
+
dominant-baseline: middle;
|
|
165
|
+
pointer-events: none;
|
|
166
|
+
}}
|
|
167
|
+
.point-label {{
|
|
168
|
+
font-family: Arial, sans-serif;
|
|
169
|
+
font-size: 10px;
|
|
170
|
+
fill: {self.color_scheme.text};
|
|
171
|
+
text-anchor: middle;
|
|
172
|
+
}}
|
|
173
|
+
.pip-count {{
|
|
174
|
+
font-family: Arial, sans-serif;
|
|
175
|
+
font-size: 12px;
|
|
176
|
+
fill: {self.color_scheme.text};
|
|
177
|
+
}}
|
|
178
|
+
.die {{
|
|
179
|
+
fill: {self.color_scheme.dice_color};
|
|
180
|
+
stroke: #000000;
|
|
181
|
+
stroke-width: 2;
|
|
182
|
+
}}
|
|
183
|
+
.die-pip {{
|
|
184
|
+
fill: #000000;
|
|
185
|
+
}}
|
|
186
|
+
.cube {{
|
|
187
|
+
fill: #FFD700;
|
|
188
|
+
stroke: #000000;
|
|
189
|
+
stroke-width: 2;
|
|
190
|
+
}}
|
|
191
|
+
.cube-text {{
|
|
192
|
+
font-family: Arial, sans-serif;
|
|
193
|
+
font-size: 32px;
|
|
194
|
+
font-weight: bold;
|
|
195
|
+
fill: #000000;
|
|
196
|
+
text-anchor: middle;
|
|
197
|
+
dominant-baseline: middle;
|
|
198
|
+
}}
|
|
199
|
+
/* Animation support */
|
|
200
|
+
.checker-animated {{
|
|
201
|
+
transition: transform 0.8s ease-in-out;
|
|
202
|
+
}}
|
|
203
|
+
</style>
|
|
204
|
+
</defs>
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def _draw_full_background(self) -> str:
|
|
208
|
+
"""Draw the full SVG background (covers entire viewBox)."""
|
|
209
|
+
return f'''
|
|
210
|
+
<rect x="0" y="0" width="{self.width}" height="{self.height}"
|
|
211
|
+
fill="{self.color_scheme.board_light}"/>
|
|
212
|
+
'''
|
|
213
|
+
|
|
214
|
+
def _draw_board_background(self, board_x: float, board_y: float) -> str:
|
|
215
|
+
"""Draw the board background rectangle."""
|
|
216
|
+
return f'''
|
|
217
|
+
<rect x="{board_x}" y="{board_y}"
|
|
218
|
+
width="{self.playing_width}" height="{self.board_height}"
|
|
219
|
+
fill="{self.color_scheme.board_light}"
|
|
220
|
+
stroke="{self.color_scheme.board_dark}"
|
|
221
|
+
stroke-width="3"/>
|
|
222
|
+
'''
|
|
223
|
+
|
|
224
|
+
def _draw_bar(self, board_x: float, board_y: float) -> str:
|
|
225
|
+
"""Draw the center bar."""
|
|
226
|
+
bar_x = board_x + self.half_width
|
|
227
|
+
return f'''
|
|
228
|
+
<rect x="{bar_x}" y="{board_y}"
|
|
229
|
+
width="{self.bar_width}" height="{self.board_height}"
|
|
230
|
+
fill="{self.color_scheme.bar}"
|
|
231
|
+
stroke="{self.color_scheme.board_dark}"
|
|
232
|
+
stroke-width="2"/>
|
|
233
|
+
'''
|
|
234
|
+
|
|
235
|
+
def _get_visual_point_index(self, point_num: int) -> int:
|
|
236
|
+
"""
|
|
237
|
+
Map point number to visual position index based on orientation.
|
|
238
|
+
|
|
239
|
+
Counter-clockwise:
|
|
240
|
+
Top: 13 14 15 16 17 18 | 19 20 21 22 23 24
|
|
241
|
+
Bottom: 12 11 10 9 8 7 | 6 5 4 3 2 1
|
|
242
|
+
|
|
243
|
+
Clockwise (horizontally mirrored):
|
|
244
|
+
Top: 24 23 22 21 20 19 | 18 17 16 15 14 13
|
|
245
|
+
Bottom: 1 2 3 4 5 6 | 7 8 9 10 11 12
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
point_num: Point number (1-24)
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Visual index for rendering (0-23)
|
|
252
|
+
"""
|
|
253
|
+
if self.orientation == "clockwise":
|
|
254
|
+
# Simple horizontal mirror
|
|
255
|
+
if point_num <= 12:
|
|
256
|
+
# Points 1-12: bottom row, mirror horizontally
|
|
257
|
+
# Point 1 → visual 11, Point 12 → visual 0
|
|
258
|
+
return 12 - point_num
|
|
259
|
+
else:
|
|
260
|
+
# Points 13-24: top row, both quadrants need reversal
|
|
261
|
+
# Point 13 → visual 23, Point 18 → visual 18
|
|
262
|
+
# Point 19 → visual 17, Point 24 → visual 12
|
|
263
|
+
return 36 - point_num
|
|
264
|
+
else:
|
|
265
|
+
# Counter-clockwise: standard layout
|
|
266
|
+
return point_num - 1
|
|
267
|
+
|
|
268
|
+
def _draw_points(self, board_x: float, board_y: float) -> str:
|
|
269
|
+
"""Draw the triangular points with numbers."""
|
|
270
|
+
svg_parts = ['<g class="points">']
|
|
271
|
+
|
|
272
|
+
# Iterate through all point numbers (1-24)
|
|
273
|
+
for point_num in range(1, 25):
|
|
274
|
+
# Get visual position for this point
|
|
275
|
+
visual_idx = self._get_visual_point_index(point_num)
|
|
276
|
+
|
|
277
|
+
# Determine point position based on visual index
|
|
278
|
+
if visual_idx < 6:
|
|
279
|
+
# Bottom right quadrant (visual positions 0-5)
|
|
280
|
+
x = board_x + self.half_width + self.bar_width + (5 - visual_idx) * self.point_width
|
|
281
|
+
y_base = board_y + self.board_height
|
|
282
|
+
y_tip = y_base - self.point_height
|
|
283
|
+
color = self.color_scheme.point_dark if point_num % 2 == 1 else self.color_scheme.point_light
|
|
284
|
+
label_y = y_base + 13
|
|
285
|
+
elif visual_idx < 12:
|
|
286
|
+
# Bottom left quadrant (visual positions 6-11)
|
|
287
|
+
x = board_x + (11 - visual_idx) * self.point_width
|
|
288
|
+
y_base = board_y + self.board_height
|
|
289
|
+
y_tip = y_base - self.point_height
|
|
290
|
+
color = self.color_scheme.point_dark if point_num % 2 == 1 else self.color_scheme.point_light
|
|
291
|
+
label_y = y_base + 13
|
|
292
|
+
elif visual_idx < 18:
|
|
293
|
+
# Top left quadrant (visual positions 12-17)
|
|
294
|
+
x = board_x + (visual_idx - 12) * self.point_width
|
|
295
|
+
y_base = board_y
|
|
296
|
+
y_tip = y_base + self.point_height
|
|
297
|
+
color = self.color_scheme.point_dark if point_num % 2 == 1 else self.color_scheme.point_light
|
|
298
|
+
label_y = y_base - 5
|
|
299
|
+
else:
|
|
300
|
+
# Top right quadrant (visual positions 18-23)
|
|
301
|
+
x = board_x + self.half_width + self.bar_width + (visual_idx - 18) * self.point_width
|
|
302
|
+
y_base = board_y
|
|
303
|
+
y_tip = y_base + self.point_height
|
|
304
|
+
color = self.color_scheme.point_dark if point_num % 2 == 1 else self.color_scheme.point_light
|
|
305
|
+
label_y = y_base - 5
|
|
306
|
+
|
|
307
|
+
# Draw triangle
|
|
308
|
+
x_mid = x + self.point_width / 2
|
|
309
|
+
svg_parts.append(f'''
|
|
310
|
+
<polygon class="point" points="{x},{y_base} {x + self.point_width},{y_base} {x_mid},{y_tip}"
|
|
311
|
+
fill="{color}"/>
|
|
312
|
+
''')
|
|
313
|
+
|
|
314
|
+
# Draw point number
|
|
315
|
+
svg_parts.append(f'''
|
|
316
|
+
<text class="point-label" x="{x_mid}" y="{label_y}">{point_num}</text>
|
|
317
|
+
''')
|
|
318
|
+
|
|
319
|
+
svg_parts.append('</g>')
|
|
320
|
+
return ''.join(svg_parts)
|
|
321
|
+
|
|
322
|
+
def _draw_checkers(
|
|
323
|
+
self,
|
|
324
|
+
position: Position,
|
|
325
|
+
board_x: float,
|
|
326
|
+
board_y: float,
|
|
327
|
+
flipped: bool,
|
|
328
|
+
move_data: Optional[Dict] = None
|
|
329
|
+
) -> str:
|
|
330
|
+
"""Draw checkers on the board with optional animation data."""
|
|
331
|
+
svg_parts = ['<g class="checkers">']
|
|
332
|
+
|
|
333
|
+
for point_idx in range(1, 25):
|
|
334
|
+
count = position.points[point_idx]
|
|
335
|
+
if count == 0:
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
player = Player.X if count > 0 else Player.O
|
|
339
|
+
num_checkers = abs(count)
|
|
340
|
+
|
|
341
|
+
# Get point position
|
|
342
|
+
x, y_base, is_top = self._get_point_position(point_idx, board_x, board_y)
|
|
343
|
+
|
|
344
|
+
# Draw checkers
|
|
345
|
+
for checker_num in range(min(num_checkers, 5)):
|
|
346
|
+
if is_top:
|
|
347
|
+
y = y_base + self.checker_radius + checker_num * (self.checker_radius * 2 + 2)
|
|
348
|
+
else:
|
|
349
|
+
y = y_base - self.checker_radius - checker_num * (self.checker_radius * 2 + 2)
|
|
350
|
+
|
|
351
|
+
cx = x + self.point_width / 2
|
|
352
|
+
|
|
353
|
+
# Add animation data attributes
|
|
354
|
+
checker_attrs = f'data-point="{point_idx}" data-checker-index="{checker_num}"'
|
|
355
|
+
if move_data:
|
|
356
|
+
checker_attrs += f' data-move-info=\'{json.dumps(move_data)}\''
|
|
357
|
+
|
|
358
|
+
svg_parts.append(
|
|
359
|
+
self._draw_checker(cx, y, player, checker_attrs)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# If more than 5 checkers, draw a number on the last one
|
|
363
|
+
if num_checkers > 5:
|
|
364
|
+
if is_top:
|
|
365
|
+
y = y_base + self.checker_radius + 4 * (self.checker_radius * 2 + 2)
|
|
366
|
+
else:
|
|
367
|
+
y = y_base - self.checker_radius - 4 * (self.checker_radius * 2 + 2)
|
|
368
|
+
|
|
369
|
+
cx = x + self.point_width / 2
|
|
370
|
+
checker_attrs = f'data-point="{point_idx}" data-checker-index="4"'
|
|
371
|
+
svg_parts.append(
|
|
372
|
+
self._draw_checker_with_number(cx, y, player, num_checkers, checker_attrs)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Draw bar checkers
|
|
376
|
+
svg_parts.append(self._draw_bar_checkers(position, board_x, board_y, flipped))
|
|
377
|
+
|
|
378
|
+
svg_parts.append('</g>')
|
|
379
|
+
return ''.join(svg_parts)
|
|
380
|
+
|
|
381
|
+
def _draw_checker(self, cx: float, cy: float, player: Player, extra_attrs: str = "") -> str:
|
|
382
|
+
"""Draw a single checker."""
|
|
383
|
+
player_class = "checker-x" if player == Player.X else "checker-o"
|
|
384
|
+
return f'''
|
|
385
|
+
<circle class="checker {player_class}" cx="{cx}" cy="{cy}" r="{self.checker_radius}" {extra_attrs}/>
|
|
386
|
+
'''
|
|
387
|
+
|
|
388
|
+
def _draw_checker_with_number(self, cx: float, cy: float, player: Player, number: int, extra_attrs: str = "") -> str:
|
|
389
|
+
"""Draw a checker with a number on it."""
|
|
390
|
+
player_class = "checker-x" if player == Player.X else "checker-o"
|
|
391
|
+
text_color = (self.color_scheme.checker_o if player == Player.X
|
|
392
|
+
else self.color_scheme.checker_x)
|
|
393
|
+
|
|
394
|
+
return f'''
|
|
395
|
+
<circle class="checker {player_class}" cx="{cx}" cy="{cy}" r="{self.checker_radius}" {extra_attrs}/>
|
|
396
|
+
<text class="checker-text" x="{cx}" y="{cy}"
|
|
397
|
+
font-size="{self.checker_radius * 1.2}" fill="{text_color}">{number}</text>
|
|
398
|
+
'''
|
|
399
|
+
|
|
400
|
+
def _draw_bar_checkers(
|
|
401
|
+
self,
|
|
402
|
+
position: Position,
|
|
403
|
+
board_x: float,
|
|
404
|
+
board_y: float,
|
|
405
|
+
flipped: bool
|
|
406
|
+
) -> str:
|
|
407
|
+
"""Draw checkers on the bar."""
|
|
408
|
+
svg_parts = []
|
|
409
|
+
bar_x = board_x + self.half_width
|
|
410
|
+
bar_center_x = bar_x + self.bar_width / 2
|
|
411
|
+
|
|
412
|
+
x_bar_count = max(position.points[0], 0)
|
|
413
|
+
o_bar_count = max(-position.points[25], 0)
|
|
414
|
+
|
|
415
|
+
if not flipped:
|
|
416
|
+
svg_parts.append(
|
|
417
|
+
self._draw_bar_stack(bar_center_x, x_bar_count, Player.X, True, board_y)
|
|
418
|
+
)
|
|
419
|
+
svg_parts.append(
|
|
420
|
+
self._draw_bar_stack(bar_center_x, o_bar_count, Player.O, False, board_y)
|
|
421
|
+
)
|
|
422
|
+
else:
|
|
423
|
+
svg_parts.append(
|
|
424
|
+
self._draw_bar_stack(bar_center_x, x_bar_count, Player.X, False, board_y)
|
|
425
|
+
)
|
|
426
|
+
svg_parts.append(
|
|
427
|
+
self._draw_bar_stack(bar_center_x, o_bar_count, Player.O, True, board_y)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
return ''.join(svg_parts)
|
|
431
|
+
|
|
432
|
+
def _draw_bar_stack(
|
|
433
|
+
self,
|
|
434
|
+
center_x: float,
|
|
435
|
+
count: int,
|
|
436
|
+
player: Player,
|
|
437
|
+
top: bool,
|
|
438
|
+
board_y: float
|
|
439
|
+
) -> str:
|
|
440
|
+
"""Draw stacked checkers on the bar for a single player."""
|
|
441
|
+
if count <= 0:
|
|
442
|
+
return ""
|
|
443
|
+
|
|
444
|
+
svg_parts = []
|
|
445
|
+
max_visible = min(count, 3)
|
|
446
|
+
|
|
447
|
+
# Bar point: 0 for X (top), 25 for O (bottom)
|
|
448
|
+
bar_point = 0 if player == Player.X else 25
|
|
449
|
+
|
|
450
|
+
# Calculate starting Y position - start from center and go outward
|
|
451
|
+
board_center_y = board_y + self.board_height / 2
|
|
452
|
+
# Add extra spacing to separate the two players more
|
|
453
|
+
separation_offset = self.checker_radius * 2 + 10
|
|
454
|
+
|
|
455
|
+
for i in range(max_visible):
|
|
456
|
+
if top:
|
|
457
|
+
# X checkers: start below center, stack toward bottom
|
|
458
|
+
y = board_center_y + separation_offset + i * (self.checker_radius * 2 + 2)
|
|
459
|
+
else:
|
|
460
|
+
# O checkers: start above center, stack toward top
|
|
461
|
+
y = board_center_y - separation_offset - i * (self.checker_radius * 2 + 2)
|
|
462
|
+
|
|
463
|
+
# Add data attributes for animation
|
|
464
|
+
checker_attrs = f'data-point="{bar_point}" data-checker-index="{i}"'
|
|
465
|
+
|
|
466
|
+
if i == max_visible - 1 and count > max_visible:
|
|
467
|
+
# Last visible checker - show count
|
|
468
|
+
svg_parts.append(
|
|
469
|
+
self._draw_checker_with_number(center_x, y, player, count, checker_attrs)
|
|
470
|
+
)
|
|
471
|
+
else:
|
|
472
|
+
svg_parts.append(
|
|
473
|
+
self._draw_checker(center_x, y, player, checker_attrs)
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return ''.join(svg_parts)
|
|
477
|
+
|
|
478
|
+
def _draw_bearoff(
|
|
479
|
+
self,
|
|
480
|
+
position: Position,
|
|
481
|
+
board_x: float,
|
|
482
|
+
board_y: float,
|
|
483
|
+
flipped: bool
|
|
484
|
+
) -> str:
|
|
485
|
+
"""Draw bear-off trays with stacked checker representations."""
|
|
486
|
+
svg_parts = ['<g class="bearoff">']
|
|
487
|
+
|
|
488
|
+
bearoff_x = board_x + self.playing_width + 10
|
|
489
|
+
bearoff_width = self.bearoff_area_width - 20
|
|
490
|
+
|
|
491
|
+
checker_width = 10
|
|
492
|
+
checker_height = 50
|
|
493
|
+
checker_spacing_x = 3
|
|
494
|
+
checker_spacing_y = 4
|
|
495
|
+
checkers_per_row = 5
|
|
496
|
+
|
|
497
|
+
top_player = Player.X if not flipped else Player.O
|
|
498
|
+
bottom_player = Player.O if not flipped else Player.X
|
|
499
|
+
|
|
500
|
+
def get_off_count(player: Player) -> int:
|
|
501
|
+
if player == Player.X:
|
|
502
|
+
return max(position.x_off, 0)
|
|
503
|
+
return max(position.o_off, 0)
|
|
504
|
+
|
|
505
|
+
def get_color(player: Player) -> str:
|
|
506
|
+
return self.color_scheme.checker_x if player == Player.X else self.color_scheme.checker_o
|
|
507
|
+
|
|
508
|
+
# Top tray (reduced height to make room for score display)
|
|
509
|
+
tray_top = board_y + 10
|
|
510
|
+
tray_bottom = board_y + self.board_height / 2 - 70
|
|
511
|
+
|
|
512
|
+
svg_parts.append(f'''
|
|
513
|
+
<rect x="{bearoff_x}" y="{tray_top}"
|
|
514
|
+
width="{bearoff_width}" height="{tray_bottom - tray_top}"
|
|
515
|
+
fill="{self.color_scheme.bearoff}"
|
|
516
|
+
stroke="{self.color_scheme.board_dark}"
|
|
517
|
+
stroke-width="2"/>
|
|
518
|
+
''')
|
|
519
|
+
|
|
520
|
+
# Draw stacked checkers for top player
|
|
521
|
+
top_count = get_off_count(top_player)
|
|
522
|
+
if top_count > 0:
|
|
523
|
+
row_width = checkers_per_row * checker_width + (checkers_per_row - 1) * checker_spacing_x
|
|
524
|
+
start_x = bearoff_x + (bearoff_width - row_width) / 2
|
|
525
|
+
start_y = tray_bottom - 10 - checker_height
|
|
526
|
+
|
|
527
|
+
for i in range(top_count):
|
|
528
|
+
row = i // checkers_per_row
|
|
529
|
+
col = i % checkers_per_row
|
|
530
|
+
x = start_x + col * (checker_width + checker_spacing_x)
|
|
531
|
+
y = start_y - row * (checker_height + checker_spacing_y)
|
|
532
|
+
|
|
533
|
+
svg_parts.append(f'''
|
|
534
|
+
<rect x="{x}" y="{y}"
|
|
535
|
+
width="{checker_width}" height="{checker_height}"
|
|
536
|
+
fill="{get_color(top_player)}"
|
|
537
|
+
stroke="{self.color_scheme.checker_border}"
|
|
538
|
+
stroke-width="1"/>
|
|
539
|
+
''')
|
|
540
|
+
|
|
541
|
+
# Bottom tray (reduced height to make room for score display)
|
|
542
|
+
tray_top = board_y + self.board_height / 2 + 70
|
|
543
|
+
tray_bottom = board_y + self.board_height - 10
|
|
544
|
+
|
|
545
|
+
svg_parts.append(f'''
|
|
546
|
+
<rect x="{bearoff_x}" y="{tray_top}"
|
|
547
|
+
width="{bearoff_width}" height="{tray_bottom - tray_top}"
|
|
548
|
+
fill="{self.color_scheme.bearoff}"
|
|
549
|
+
stroke="{self.color_scheme.board_dark}"
|
|
550
|
+
stroke-width="2"/>
|
|
551
|
+
''')
|
|
552
|
+
|
|
553
|
+
# Draw stacked checkers for bottom player
|
|
554
|
+
bottom_count = get_off_count(bottom_player)
|
|
555
|
+
if bottom_count > 0:
|
|
556
|
+
row_width = checkers_per_row * checker_width + (checkers_per_row - 1) * checker_spacing_x
|
|
557
|
+
start_x = bearoff_x + (bearoff_width - row_width) / 2
|
|
558
|
+
start_y = tray_bottom - 10 - checker_height
|
|
559
|
+
|
|
560
|
+
for i in range(bottom_count):
|
|
561
|
+
row = i // checkers_per_row
|
|
562
|
+
col = i % checkers_per_row
|
|
563
|
+
x = start_x + col * (checker_width + checker_spacing_x)
|
|
564
|
+
y = start_y - row * (checker_height + checker_spacing_y)
|
|
565
|
+
|
|
566
|
+
svg_parts.append(f'''
|
|
567
|
+
<rect x="{x}" y="{y}"
|
|
568
|
+
width="{checker_width}" height="{checker_height}"
|
|
569
|
+
fill="{get_color(bottom_player)}"
|
|
570
|
+
stroke="{self.color_scheme.checker_border}"
|
|
571
|
+
stroke-width="1"/>
|
|
572
|
+
''')
|
|
573
|
+
|
|
574
|
+
svg_parts.append('</g>')
|
|
575
|
+
return ''.join(svg_parts)
|
|
576
|
+
|
|
577
|
+
def _draw_dice(
|
|
578
|
+
self,
|
|
579
|
+
dice: Tuple[int, int],
|
|
580
|
+
on_roll: Player,
|
|
581
|
+
board_x: float,
|
|
582
|
+
board_y: float,
|
|
583
|
+
opacity: float = 1.0
|
|
584
|
+
) -> str:
|
|
585
|
+
"""Draw dice with optional transparency."""
|
|
586
|
+
svg_parts = ['<g class="dice"']
|
|
587
|
+
if opacity < 1.0:
|
|
588
|
+
svg_parts.append(f' opacity="{opacity}"')
|
|
589
|
+
svg_parts.append('>')
|
|
590
|
+
|
|
591
|
+
die_size = 50
|
|
592
|
+
die_spacing = 15
|
|
593
|
+
|
|
594
|
+
# Position dice on the right half of the board
|
|
595
|
+
total_dice_width = 2 * die_size + die_spacing
|
|
596
|
+
right_half_start = board_x + self.half_width + self.bar_width
|
|
597
|
+
die_x = right_half_start + (self.half_width - total_dice_width) / 2
|
|
598
|
+
die_y = board_y + (self.board_height - die_size) / 2
|
|
599
|
+
|
|
600
|
+
svg_parts.append(self._draw_die(die_x, die_y, die_size, dice[0]))
|
|
601
|
+
svg_parts.append(self._draw_die(die_x + die_size + die_spacing, die_y, die_size, dice[1]))
|
|
602
|
+
|
|
603
|
+
svg_parts.append('</g>')
|
|
604
|
+
return ''.join(svg_parts)
|
|
605
|
+
|
|
606
|
+
def _draw_die(self, x: float, y: float, size: float, value: int) -> str:
|
|
607
|
+
"""Draw a single die."""
|
|
608
|
+
svg_parts = [f'''
|
|
609
|
+
<rect class="die" x="{x}" y="{y}" width="{size}" height="{size}" rx="5"/>
|
|
610
|
+
''']
|
|
611
|
+
|
|
612
|
+
# Draw pips
|
|
613
|
+
pip_radius = size / 10
|
|
614
|
+
center = size / 2
|
|
615
|
+
|
|
616
|
+
pip_positions = {
|
|
617
|
+
1: [(center, center)],
|
|
618
|
+
2: [(size / 4, size / 4), (3 * size / 4, 3 * size / 4)],
|
|
619
|
+
3: [(size / 4, size / 4), (center, center), (3 * size / 4, 3 * size / 4)],
|
|
620
|
+
4: [(size / 4, size / 4), (3 * size / 4, size / 4),
|
|
621
|
+
(size / 4, 3 * size / 4), (3 * size / 4, 3 * size / 4)],
|
|
622
|
+
5: [(size / 4, size / 4), (3 * size / 4, size / 4),
|
|
623
|
+
(center, center),
|
|
624
|
+
(size / 4, 3 * size / 4), (3 * size / 4, 3 * size / 4)],
|
|
625
|
+
6: [(size / 4, size / 4), (3 * size / 4, size / 4),
|
|
626
|
+
(size / 4, center), (3 * size / 4, center),
|
|
627
|
+
(size / 4, 3 * size / 4), (3 * size / 4, 3 * size / 4)],
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
for px, py in pip_positions.get(value, []):
|
|
631
|
+
svg_parts.append(f'''
|
|
632
|
+
<circle class="die-pip" cx="{x + px}" cy="{y + py}" r="{pip_radius}"/>
|
|
633
|
+
''')
|
|
634
|
+
|
|
635
|
+
return ''.join(svg_parts)
|
|
636
|
+
|
|
637
|
+
def _draw_cube(
|
|
638
|
+
self,
|
|
639
|
+
cube_value: int,
|
|
640
|
+
cube_owner: CubeState,
|
|
641
|
+
board_x: float,
|
|
642
|
+
board_y: float,
|
|
643
|
+
flipped: bool
|
|
644
|
+
) -> str:
|
|
645
|
+
"""Draw the doubling cube."""
|
|
646
|
+
cube_size = 50
|
|
647
|
+
|
|
648
|
+
# Center the cube in the area from left edge to board start
|
|
649
|
+
# board_x = margin + cube_area_width
|
|
650
|
+
cube_area_center = (self.margin + self.cube_area_width) / 2
|
|
651
|
+
|
|
652
|
+
# Position cube based on owner
|
|
653
|
+
if cube_owner == CubeState.CENTERED:
|
|
654
|
+
cube_x = cube_area_center - cube_size / 2
|
|
655
|
+
cube_y = board_y + (self.board_height - cube_size) / 2
|
|
656
|
+
elif cube_owner == CubeState.O_OWNS:
|
|
657
|
+
cube_x = cube_area_center - cube_size / 2
|
|
658
|
+
cube_y = board_y + self.board_height - cube_size - 10 if not flipped else board_y + 10
|
|
659
|
+
else: # X_OWNS
|
|
660
|
+
cube_x = cube_area_center - cube_size / 2
|
|
661
|
+
cube_y = board_y + 10 if not flipped else board_y + self.board_height - cube_size - 10
|
|
662
|
+
|
|
663
|
+
# Draw cube
|
|
664
|
+
text = "64" if cube_owner == CubeState.CENTERED else str(cube_value)
|
|
665
|
+
|
|
666
|
+
return f'''
|
|
667
|
+
<g class="cube">
|
|
668
|
+
<rect class="cube" x="{cube_x}" y="{cube_y}"
|
|
669
|
+
width="{cube_size}" height="{cube_size}" rx="3"/>
|
|
670
|
+
<text class="cube-text" x="{cube_x + cube_size / 2}" y="{cube_y + cube_size / 2 + 2}">{text}</text>
|
|
671
|
+
</g>
|
|
672
|
+
'''
|
|
673
|
+
|
|
674
|
+
def _draw_pip_counts(
|
|
675
|
+
self,
|
|
676
|
+
position: Position,
|
|
677
|
+
board_x: float,
|
|
678
|
+
board_y: float,
|
|
679
|
+
flipped: bool
|
|
680
|
+
) -> str:
|
|
681
|
+
"""Draw pip counts for both players."""
|
|
682
|
+
x_pips = self._calculate_pip_count(position, Player.X)
|
|
683
|
+
o_pips = self._calculate_pip_count(position, Player.O)
|
|
684
|
+
|
|
685
|
+
bearoff_text_x = board_x + self.playing_width + 15
|
|
686
|
+
x_bearoff_top = board_y + 10 + 15
|
|
687
|
+
o_bearoff_top = board_y + self.board_height / 2 + 70 + 15
|
|
688
|
+
|
|
689
|
+
if flipped:
|
|
690
|
+
return f'''
|
|
691
|
+
<g class="pip-counts">
|
|
692
|
+
<text class="pip-count" x="{bearoff_text_x}" y="{o_bearoff_top}">Pip: {x_pips}</text>
|
|
693
|
+
<text class="pip-count" x="{bearoff_text_x}" y="{x_bearoff_top}">Pip: {o_pips}</text>
|
|
694
|
+
</g>
|
|
695
|
+
'''
|
|
696
|
+
else:
|
|
697
|
+
return f'''
|
|
698
|
+
<g class="pip-counts">
|
|
699
|
+
<text class="pip-count" x="{bearoff_text_x}" y="{x_bearoff_top}">Pip: {x_pips}</text>
|
|
700
|
+
<text class="pip-count" x="{bearoff_text_x}" y="{o_bearoff_top}">Pip: {o_pips}</text>
|
|
701
|
+
</g>
|
|
702
|
+
'''
|
|
703
|
+
|
|
704
|
+
def _draw_scores(
|
|
705
|
+
self,
|
|
706
|
+
score_x: int,
|
|
707
|
+
score_o: int,
|
|
708
|
+
match_length: int,
|
|
709
|
+
board_x: float,
|
|
710
|
+
board_y: float,
|
|
711
|
+
flipped: bool
|
|
712
|
+
) -> str:
|
|
713
|
+
"""Draw match scores on the board (only for match play)."""
|
|
714
|
+
if match_length == 0:
|
|
715
|
+
return ""
|
|
716
|
+
|
|
717
|
+
# Position scores in the middle area between bear-off trays
|
|
718
|
+
bearoff_x = board_x + self.playing_width + 10
|
|
719
|
+
bearoff_width = self.bearoff_area_width - 20
|
|
720
|
+
center_x = bearoff_x + bearoff_width / 2
|
|
721
|
+
|
|
722
|
+
# Center vertically in the board
|
|
723
|
+
center_y = board_y + self.board_height / 2
|
|
724
|
+
|
|
725
|
+
# Box dimensions
|
|
726
|
+
box_width = 60
|
|
727
|
+
box_height = 35
|
|
728
|
+
box_spacing = 5
|
|
729
|
+
|
|
730
|
+
# Calculate positions for 3 boxes stacked vertically
|
|
731
|
+
total_height = 3 * box_height + 2 * box_spacing
|
|
732
|
+
start_y = center_y - total_height / 2
|
|
733
|
+
|
|
734
|
+
return f'''
|
|
735
|
+
<g class="match-scores">
|
|
736
|
+
<!-- Top box: X player score -->
|
|
737
|
+
<rect x="{center_x - box_width/2}" y="{start_y}"
|
|
738
|
+
width="{box_width}" height="{box_height}"
|
|
739
|
+
fill="{self.color_scheme.point_dark}"
|
|
740
|
+
stroke="{self.color_scheme.bearoff}"
|
|
741
|
+
stroke-width="2"/>
|
|
742
|
+
<text x="{center_x}" y="{start_y + box_height/2 + 8}"
|
|
743
|
+
text-anchor="middle" font-family="Arial, sans-serif"
|
|
744
|
+
font-size="22px" font-weight="bold" fill="{self.color_scheme.text}">{score_x}</text>
|
|
745
|
+
|
|
746
|
+
<!-- Middle box: Match length -->
|
|
747
|
+
<rect x="{center_x - box_width/2}" y="{start_y + box_height + box_spacing}"
|
|
748
|
+
width="{box_width}" height="{box_height}"
|
|
749
|
+
fill="{self.color_scheme.point_dark}"
|
|
750
|
+
stroke="{self.color_scheme.bearoff}"
|
|
751
|
+
stroke-width="2"/>
|
|
752
|
+
<text x="{center_x}" y="{start_y + box_height + box_spacing + box_height/2 + 7}"
|
|
753
|
+
text-anchor="middle" font-family="Arial, sans-serif"
|
|
754
|
+
font-size="16px" font-weight="bold" fill="{self.color_scheme.text}">{match_length}pt</text>
|
|
755
|
+
|
|
756
|
+
<!-- Bottom box: O player score -->
|
|
757
|
+
<rect x="{center_x - box_width/2}" y="{start_y + 2*box_height + 2*box_spacing}"
|
|
758
|
+
width="{box_width}" height="{box_height}"
|
|
759
|
+
fill="{self.color_scheme.point_dark}"
|
|
760
|
+
stroke="{self.color_scheme.bearoff}"
|
|
761
|
+
stroke-width="2"/>
|
|
762
|
+
<text x="{center_x}" y="{start_y + 2*box_height + 2*box_spacing + box_height/2 + 8}"
|
|
763
|
+
text-anchor="middle" font-family="Arial, sans-serif"
|
|
764
|
+
font-size="22px" font-weight="bold" fill="{self.color_scheme.text}">{score_o}</text>
|
|
765
|
+
</g>
|
|
766
|
+
'''
|
|
767
|
+
|
|
768
|
+
def _get_point_position(self, point_idx: int, board_x: float, board_y: float) -> Tuple[float, float, bool]:
|
|
769
|
+
"""
|
|
770
|
+
Get the x, y position and orientation of a point.
|
|
771
|
+
|
|
772
|
+
Returns:
|
|
773
|
+
(x, y_base, is_top) where is_top indicates if point extends from top
|
|
774
|
+
"""
|
|
775
|
+
if point_idx < 1 or point_idx > 24:
|
|
776
|
+
raise ValueError(f"Invalid point index: {point_idx}")
|
|
777
|
+
|
|
778
|
+
# Get visual position based on orientation
|
|
779
|
+
visual_idx = self._get_visual_point_index(point_idx)
|
|
780
|
+
|
|
781
|
+
# Calculate position based on visual index
|
|
782
|
+
if visual_idx < 6:
|
|
783
|
+
# Bottom right quadrant (visual positions 0-5)
|
|
784
|
+
x = board_x + self.half_width + self.bar_width + (5 - visual_idx) * self.point_width
|
|
785
|
+
y_base = board_y + self.board_height
|
|
786
|
+
is_top = False
|
|
787
|
+
elif visual_idx < 12:
|
|
788
|
+
# Bottom left quadrant (visual positions 6-11)
|
|
789
|
+
x = board_x + (11 - visual_idx) * self.point_width
|
|
790
|
+
y_base = board_y + self.board_height
|
|
791
|
+
is_top = False
|
|
792
|
+
elif visual_idx < 18:
|
|
793
|
+
# Top left quadrant (visual positions 12-17)
|
|
794
|
+
x = board_x + (visual_idx - 12) * self.point_width
|
|
795
|
+
y_base = board_y
|
|
796
|
+
is_top = True
|
|
797
|
+
else:
|
|
798
|
+
# Top right quadrant (visual positions 18-23)
|
|
799
|
+
x = board_x + self.half_width + self.bar_width + (visual_idx - 18) * self.point_width
|
|
800
|
+
y_base = board_y
|
|
801
|
+
is_top = True
|
|
802
|
+
|
|
803
|
+
return x, y_base, is_top
|
|
804
|
+
|
|
805
|
+
def _calculate_pip_count(self, position: Position, player: Player) -> int:
|
|
806
|
+
"""Calculate pip count for a player."""
|
|
807
|
+
pip_count = 0
|
|
808
|
+
|
|
809
|
+
if player == Player.X:
|
|
810
|
+
for point_idx in range(1, 25):
|
|
811
|
+
if position.points[point_idx] > 0:
|
|
812
|
+
x_pip_distance = 25 - point_idx
|
|
813
|
+
pip_count += x_pip_distance * position.points[point_idx]
|
|
814
|
+
if position.points[0] > 0:
|
|
815
|
+
pip_count += 25 * position.points[0]
|
|
816
|
+
else:
|
|
817
|
+
for point_idx in range(1, 25):
|
|
818
|
+
if position.points[point_idx] < 0:
|
|
819
|
+
o_pip_distance = point_idx
|
|
820
|
+
pip_count += o_pip_distance * abs(position.points[point_idx])
|
|
821
|
+
if position.points[25] < 0:
|
|
822
|
+
pip_count += 25 * abs(position.points[25])
|
|
823
|
+
|
|
824
|
+
return pip_count
|