ankigammon 1.0.6__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.
- ankigammon/__init__.py +7 -0
- ankigammon/__main__.py +6 -0
- ankigammon/analysis/__init__.py +13 -0
- ankigammon/analysis/score_matrix.py +391 -0
- ankigammon/anki/__init__.py +6 -0
- ankigammon/anki/ankiconnect.py +216 -0
- ankigammon/anki/apkg_exporter.py +111 -0
- ankigammon/anki/card_generator.py +1325 -0
- ankigammon/anki/card_styles.py +1054 -0
- ankigammon/gui/__init__.py +8 -0
- ankigammon/gui/app.py +192 -0
- ankigammon/gui/dialogs/__init__.py +10 -0
- ankigammon/gui/dialogs/export_dialog.py +594 -0
- ankigammon/gui/dialogs/import_options_dialog.py +201 -0
- ankigammon/gui/dialogs/input_dialog.py +762 -0
- ankigammon/gui/dialogs/note_dialog.py +93 -0
- ankigammon/gui/dialogs/settings_dialog.py +420 -0
- ankigammon/gui/dialogs/update_dialog.py +373 -0
- ankigammon/gui/format_detector.py +377 -0
- ankigammon/gui/main_window.py +1611 -0
- ankigammon/gui/resources/down-arrow.svg +3 -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 +402 -0
- ankigammon/gui/resources.py +26 -0
- ankigammon/gui/update_checker.py +259 -0
- ankigammon/gui/widgets/__init__.py +8 -0
- ankigammon/gui/widgets/position_list.py +166 -0
- ankigammon/gui/widgets/smart_input.py +268 -0
- ankigammon/models.py +356 -0
- ankigammon/parsers/__init__.py +7 -0
- ankigammon/parsers/gnubg_match_parser.py +1094 -0
- ankigammon/parsers/gnubg_parser.py +468 -0
- ankigammon/parsers/sgf_parser.py +290 -0
- ankigammon/parsers/xg_binary_parser.py +1097 -0
- ankigammon/parsers/xg_text_parser.py +688 -0
- ankigammon/renderer/__init__.py +5 -0
- ankigammon/renderer/animation_controller.py +391 -0
- ankigammon/renderer/animation_helper.py +191 -0
- ankigammon/renderer/color_schemes.py +145 -0
- ankigammon/renderer/svg_board_renderer.py +791 -0
- ankigammon/settings.py +315 -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 +590 -0
- ankigammon/utils/gnuid.py +577 -0
- ankigammon/utils/move_parser.py +204 -0
- ankigammon/utils/ogid.py +326 -0
- ankigammon/utils/xgid.py +387 -0
- ankigammon-1.0.6.dist-info/METADATA +352 -0
- ankigammon-1.0.6.dist-info/RECORD +61 -0
- ankigammon-1.0.6.dist-info/WHEEL +5 -0
- ankigammon-1.0.6.dist-info/entry_points.txt +2 -0
- ankigammon-1.0.6.dist-info/licenses/LICENSE +21 -0
- ankigammon-1.0.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Animation controller for backgammon checker movements.
|
|
3
|
+
|
|
4
|
+
Generates JavaScript code for animating checker movements using various strategies:
|
|
5
|
+
- Cross-fade: Simple opacity transition between positions
|
|
6
|
+
- Arc movement: GSAP-based arc animation of individual checkers
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import List, Tuple, Dict, Optional
|
|
11
|
+
from ankigammon.models import Position, Move, Player
|
|
12
|
+
from ankigammon.renderer.animation_helper import AnimationHelper
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AnimationController:
|
|
16
|
+
"""
|
|
17
|
+
Orchestrates checker movement animations for Anki cards.
|
|
18
|
+
|
|
19
|
+
This controller generates JavaScript code that animates transitions
|
|
20
|
+
between backgammon positions using either simple cross-fade or
|
|
21
|
+
sophisticated GSAP-based arc movements.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
board_width: int = 900,
|
|
27
|
+
board_height: int = 600,
|
|
28
|
+
point_height_ratio: float = 0.45
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the animation controller.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
board_width: SVG viewBox width (must match renderer)
|
|
35
|
+
board_height: SVG viewBox height (must match renderer)
|
|
36
|
+
point_height_ratio: Height of points as ratio of board height
|
|
37
|
+
"""
|
|
38
|
+
self.board_width = board_width
|
|
39
|
+
self.board_height = board_height
|
|
40
|
+
self.point_height_ratio = point_height_ratio
|
|
41
|
+
|
|
42
|
+
# Board dimensions matching SVGBoardRenderer
|
|
43
|
+
self.margin = 20
|
|
44
|
+
self.cube_area_width = 70
|
|
45
|
+
self.bearoff_area_width = 100
|
|
46
|
+
|
|
47
|
+
self.playing_width = (
|
|
48
|
+
self.board_width - 2 * self.margin -
|
|
49
|
+
self.cube_area_width - self.bearoff_area_width
|
|
50
|
+
)
|
|
51
|
+
self.board_height_inner = self.board_height - 2 * self.margin
|
|
52
|
+
|
|
53
|
+
self.bar_width = self.playing_width * 0.08
|
|
54
|
+
self.half_width = (self.playing_width - self.bar_width) / 2
|
|
55
|
+
self.point_width = self.half_width / 6
|
|
56
|
+
self.point_height = self.board_height_inner * point_height_ratio
|
|
57
|
+
|
|
58
|
+
self.checker_radius = min(self.point_width * 0.45, 25)
|
|
59
|
+
|
|
60
|
+
self.board_x = self.margin + self.cube_area_width
|
|
61
|
+
self.board_y = self.margin
|
|
62
|
+
|
|
63
|
+
def get_point_coordinates(self, point_num: int, checker_index: int = 0, player: Optional[Player] = None) -> Tuple[float, float]:
|
|
64
|
+
"""
|
|
65
|
+
Calculate SVG coordinates for a checker on a specific point.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
point_num: Point number (0=X bar, 1-24=board points, 25=O bar, -1=bear off)
|
|
69
|
+
checker_index: Index of checker in stack (0=bottom, 1=next up, etc.)
|
|
70
|
+
player: Player bearing off (required when point_num == -1)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
(x, y) coordinates in SVG space
|
|
74
|
+
"""
|
|
75
|
+
# Handle bar positions
|
|
76
|
+
if point_num == 0: # X's bar (top)
|
|
77
|
+
bar_x = self.board_x + self.half_width
|
|
78
|
+
bar_center_x = bar_x + self.bar_width / 2
|
|
79
|
+
y = self.board_y + self.point_height + checker_index * (self.checker_radius * 2 + 2)
|
|
80
|
+
return (bar_center_x, y)
|
|
81
|
+
|
|
82
|
+
if point_num == 25: # O's bar (bottom)
|
|
83
|
+
bar_x = self.board_x + self.half_width
|
|
84
|
+
bar_center_x = bar_x + self.bar_width / 2
|
|
85
|
+
y = (self.board_y + self.board_height_inner - self.point_height -
|
|
86
|
+
checker_index * (self.checker_radius * 2 + 2))
|
|
87
|
+
return (bar_center_x, y)
|
|
88
|
+
|
|
89
|
+
# Handle bear-off
|
|
90
|
+
if point_num == -1:
|
|
91
|
+
bearoff_x = self.board_x + self.playing_width + 50
|
|
92
|
+
checker_height = 50
|
|
93
|
+
|
|
94
|
+
if player == Player.X:
|
|
95
|
+
tray_bottom = self.board_y + self.board_height_inner / 2 - 10
|
|
96
|
+
bearoff_y = tray_bottom - 10 - checker_height
|
|
97
|
+
else:
|
|
98
|
+
tray_bottom = self.board_y + self.board_height_inner - 10
|
|
99
|
+
bearoff_y = tray_bottom - 10 - checker_height
|
|
100
|
+
|
|
101
|
+
return (bearoff_x, bearoff_y)
|
|
102
|
+
|
|
103
|
+
if point_num < 1 or point_num > 24:
|
|
104
|
+
raise ValueError(f"Invalid point number: {point_num}")
|
|
105
|
+
|
|
106
|
+
x, y_base, is_top = self._get_point_position(point_num)
|
|
107
|
+
|
|
108
|
+
if is_top:
|
|
109
|
+
y = y_base + self.checker_radius + checker_index * (self.checker_radius * 2 + 2)
|
|
110
|
+
else:
|
|
111
|
+
y = y_base - self.checker_radius - checker_index * (self.checker_radius * 2 + 2)
|
|
112
|
+
|
|
113
|
+
cx = x + self.point_width / 2
|
|
114
|
+
|
|
115
|
+
return (cx, y)
|
|
116
|
+
|
|
117
|
+
def _get_point_position(self, point_idx: int) -> Tuple[float, float, bool]:
|
|
118
|
+
"""
|
|
119
|
+
Get the x, y position and orientation of a point.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
(x, y_base, is_top) where is_top indicates if point extends from top
|
|
123
|
+
"""
|
|
124
|
+
if point_idx <= 6:
|
|
125
|
+
x = self.board_x + self.half_width + self.bar_width + (6 - point_idx) * self.point_width
|
|
126
|
+
y_base = self.board_y + self.board_height_inner
|
|
127
|
+
is_top = False
|
|
128
|
+
elif point_idx <= 12:
|
|
129
|
+
x = self.board_x + (12 - point_idx) * self.point_width
|
|
130
|
+
y_base = self.board_y + self.board_height_inner
|
|
131
|
+
is_top = False
|
|
132
|
+
elif point_idx <= 18:
|
|
133
|
+
x = self.board_x + (point_idx - 13) * self.point_width
|
|
134
|
+
y_base = self.board_y
|
|
135
|
+
is_top = True
|
|
136
|
+
else:
|
|
137
|
+
x = self.board_x + self.half_width + self.bar_width + (point_idx - 19) * self.point_width
|
|
138
|
+
y_base = self.board_y
|
|
139
|
+
is_top = True
|
|
140
|
+
|
|
141
|
+
return x, y_base, is_top
|
|
142
|
+
|
|
143
|
+
def generate_animation_script(
|
|
144
|
+
self,
|
|
145
|
+
from_position: Position,
|
|
146
|
+
to_position: Position,
|
|
147
|
+
move_notation: str,
|
|
148
|
+
on_roll: Player,
|
|
149
|
+
animation_style: str = "arc",
|
|
150
|
+
duration: float = 0.8,
|
|
151
|
+
svg_id_original: str = "original-svg",
|
|
152
|
+
svg_id_result: str = "result-svg"
|
|
153
|
+
) -> str:
|
|
154
|
+
"""
|
|
155
|
+
Generate JavaScript for animating a move.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
from_position: Starting position
|
|
159
|
+
to_position: Ending position
|
|
160
|
+
move_notation: Move notation (e.g., "24/23 24/23")
|
|
161
|
+
on_roll: Player making the move
|
|
162
|
+
animation_style: 'fade', 'arc', or 'none'
|
|
163
|
+
duration: Animation duration in seconds
|
|
164
|
+
svg_id_original: ID of original position SVG container
|
|
165
|
+
svg_id_result: ID of result position SVG container
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
JavaScript code as string
|
|
169
|
+
"""
|
|
170
|
+
if animation_style == "none":
|
|
171
|
+
return ""
|
|
172
|
+
|
|
173
|
+
if animation_style == "fade":
|
|
174
|
+
return self._generate_fade_animation(
|
|
175
|
+
svg_id_original, svg_id_result, duration
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if animation_style == "arc":
|
|
179
|
+
return self._generate_arc_animation(
|
|
180
|
+
from_position, to_position, move_notation, on_roll,
|
|
181
|
+
duration, svg_id_original, svg_id_result
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return ""
|
|
185
|
+
|
|
186
|
+
def _generate_fade_animation(
|
|
187
|
+
self,
|
|
188
|
+
svg_id_original: str,
|
|
189
|
+
svg_id_result: str,
|
|
190
|
+
duration: float
|
|
191
|
+
) -> str:
|
|
192
|
+
"""Generate simple cross-fade animation JavaScript."""
|
|
193
|
+
return f"""
|
|
194
|
+
// Cross-fade animation
|
|
195
|
+
function animatePositionTransition() {{
|
|
196
|
+
const originalSvg = document.getElementById('{svg_id_original}');
|
|
197
|
+
const resultSvg = document.getElementById('{svg_id_result}');
|
|
198
|
+
|
|
199
|
+
if (!originalSvg || !resultSvg) return;
|
|
200
|
+
|
|
201
|
+
// Set up initial state
|
|
202
|
+
originalSvg.style.opacity = '1';
|
|
203
|
+
originalSvg.style.transition = 'opacity {duration}s ease-in-out';
|
|
204
|
+
resultSvg.style.opacity = '0';
|
|
205
|
+
resultSvg.style.transition = 'opacity {duration}s ease-in-out';
|
|
206
|
+
resultSvg.style.display = 'block';
|
|
207
|
+
|
|
208
|
+
// Trigger fade
|
|
209
|
+
setTimeout(() => {{
|
|
210
|
+
originalSvg.style.opacity = '0';
|
|
211
|
+
resultSvg.style.opacity = '1';
|
|
212
|
+
}}, 50);
|
|
213
|
+
|
|
214
|
+
// Clean up
|
|
215
|
+
setTimeout(() => {{
|
|
216
|
+
originalSvg.style.display = 'none';
|
|
217
|
+
}}, {duration * 1000 + 100});
|
|
218
|
+
}}
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def _generate_arc_animation(
|
|
222
|
+
self,
|
|
223
|
+
from_position: Position,
|
|
224
|
+
to_position: Position,
|
|
225
|
+
move_notation: str,
|
|
226
|
+
on_roll: Player,
|
|
227
|
+
duration: float,
|
|
228
|
+
svg_id_original: str,
|
|
229
|
+
svg_id_result: str
|
|
230
|
+
) -> str:
|
|
231
|
+
"""Generate GSAP-based arc animation JavaScript."""
|
|
232
|
+
moves = AnimationHelper.parse_move_notation(move_notation, on_roll)
|
|
233
|
+
|
|
234
|
+
if not moves:
|
|
235
|
+
return self._generate_fade_animation(svg_id_original, svg_id_result, duration)
|
|
236
|
+
|
|
237
|
+
# Calculate movement coordinates
|
|
238
|
+
movements = []
|
|
239
|
+
for from_point, to_point in moves:
|
|
240
|
+
checker_index = 0
|
|
241
|
+
|
|
242
|
+
start_x, start_y = self.get_point_coordinates(from_point, checker_index)
|
|
243
|
+
end_x, end_y = self.get_point_coordinates(to_point, checker_index, player=on_roll if to_point == -1 else None)
|
|
244
|
+
|
|
245
|
+
# Calculate arc control point
|
|
246
|
+
mid_x = (start_x + end_x) / 2
|
|
247
|
+
mid_y = min(start_y, end_y) - 80
|
|
248
|
+
|
|
249
|
+
movements.append({
|
|
250
|
+
'from_point': from_point,
|
|
251
|
+
'to_point': to_point,
|
|
252
|
+
'start': [start_x, start_y],
|
|
253
|
+
'control': [mid_x, mid_y],
|
|
254
|
+
'end': [end_x, end_y],
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
movements_json = json.dumps(movements)
|
|
258
|
+
stagger = 0.05 # Delay between multiple checker animations
|
|
259
|
+
|
|
260
|
+
return f"""
|
|
261
|
+
// GSAP arc animation
|
|
262
|
+
if (typeof gsap !== 'undefined') {{
|
|
263
|
+
gsap.registerPlugin(MotionPathPlugin);
|
|
264
|
+
|
|
265
|
+
const movements = {movements_json};
|
|
266
|
+
const duration = {duration};
|
|
267
|
+
const stagger = {stagger};
|
|
268
|
+
|
|
269
|
+
function animatePositionTransition() {{
|
|
270
|
+
const originalSvg = document.getElementById('{svg_id_original}');
|
|
271
|
+
const resultSvg = document.getElementById('{svg_id_result}');
|
|
272
|
+
|
|
273
|
+
if (!originalSvg || !resultSvg) return;
|
|
274
|
+
|
|
275
|
+
// Clone the original SVG for animation
|
|
276
|
+
const animSvg = originalSvg.cloneNode(true);
|
|
277
|
+
animSvg.id = 'animation-svg';
|
|
278
|
+
animSvg.style.position = 'absolute';
|
|
279
|
+
animSvg.style.top = '0';
|
|
280
|
+
animSvg.style.left = '0';
|
|
281
|
+
animSvg.style.width = '100%';
|
|
282
|
+
animSvg.style.height = '100%';
|
|
283
|
+
|
|
284
|
+
originalSvg.parentNode.style.position = 'relative';
|
|
285
|
+
originalSvg.parentNode.appendChild(animSvg);
|
|
286
|
+
|
|
287
|
+
// Hide result initially
|
|
288
|
+
resultSvg.style.opacity = '0';
|
|
289
|
+
resultSvg.style.display = 'block';
|
|
290
|
+
|
|
291
|
+
// Animate each checker movement
|
|
292
|
+
const timeline = gsap.timeline({{
|
|
293
|
+
onComplete: function() {{
|
|
294
|
+
// Show final position
|
|
295
|
+
resultSvg.style.transition = 'opacity 0.2s';
|
|
296
|
+
resultSvg.style.opacity = '1';
|
|
297
|
+
|
|
298
|
+
// Remove animation SVG
|
|
299
|
+
setTimeout(() => {{
|
|
300
|
+
if (animSvg.parentNode) {{
|
|
301
|
+
animSvg.parentNode.removeChild(animSvg);
|
|
302
|
+
}}
|
|
303
|
+
originalSvg.style.display = 'none';
|
|
304
|
+
}}, 200);
|
|
305
|
+
}}
|
|
306
|
+
}});
|
|
307
|
+
|
|
308
|
+
movements.forEach((movement, index) => {{
|
|
309
|
+
// Find checkers at from_point in the animation SVG
|
|
310
|
+
const checkers = animSvg.querySelectorAll('.checker[data-point="' + movement.from_point + '"]');
|
|
311
|
+
|
|
312
|
+
if (checkers.length > 0) {{
|
|
313
|
+
const checker = checkers[0]; // Animate first (top) checker
|
|
314
|
+
|
|
315
|
+
// Create path for arc movement
|
|
316
|
+
const path = [
|
|
317
|
+
{{ x: movement.start[0], y: movement.start[1] }},
|
|
318
|
+
{{ x: movement.control[0], y: movement.control[1] }},
|
|
319
|
+
{{ x: movement.end[0], y: movement.end[1] }}
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
timeline.to(checker, {{
|
|
323
|
+
duration: duration,
|
|
324
|
+
motionPath: {{
|
|
325
|
+
path: path,
|
|
326
|
+
type: 'soft',
|
|
327
|
+
autoRotate: false
|
|
328
|
+
}},
|
|
329
|
+
ease: 'power2.inOut'
|
|
330
|
+
}}, index * stagger);
|
|
331
|
+
}}
|
|
332
|
+
}});
|
|
333
|
+
}}
|
|
334
|
+
}} else {{
|
|
335
|
+
// GSAP not available, fall back to fade
|
|
336
|
+
function animatePositionTransition() {{
|
|
337
|
+
const originalSvg = document.getElementById('{svg_id_original}');
|
|
338
|
+
const resultSvg = document.getElementById('{svg_id_result}');
|
|
339
|
+
|
|
340
|
+
if (!originalSvg || !resultSvg) return;
|
|
341
|
+
|
|
342
|
+
originalSvg.style.transition = 'opacity {duration}s';
|
|
343
|
+
resultSvg.style.transition = 'opacity {duration}s';
|
|
344
|
+
resultSvg.style.display = 'block';
|
|
345
|
+
resultSvg.style.opacity = '0';
|
|
346
|
+
|
|
347
|
+
setTimeout(() => {{
|
|
348
|
+
originalSvg.style.opacity = '0';
|
|
349
|
+
resultSvg.style.opacity = '1';
|
|
350
|
+
}}, 50);
|
|
351
|
+
|
|
352
|
+
setTimeout(() => {{
|
|
353
|
+
originalSvg.style.display = 'none';
|
|
354
|
+
}}, {duration * 1000 + 100});
|
|
355
|
+
}}
|
|
356
|
+
}}
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
def get_gsap_cdn_urls(self) -> List[str]:
|
|
360
|
+
"""
|
|
361
|
+
Get GSAP CDN URLs for inclusion in card HTML.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
List of CDN URLs
|
|
365
|
+
"""
|
|
366
|
+
return [
|
|
367
|
+
"https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js",
|
|
368
|
+
"https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js"
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
def generate_trigger_button_html(
|
|
372
|
+
self,
|
|
373
|
+
button_text: str = "▶ Animate Move",
|
|
374
|
+
button_class: str = "animate-btn"
|
|
375
|
+
) -> str:
|
|
376
|
+
"""
|
|
377
|
+
Generate HTML for animation trigger button.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
button_text: Text to display on button
|
|
381
|
+
button_class: CSS class for button
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
HTML string
|
|
385
|
+
"""
|
|
386
|
+
return f"""
|
|
387
|
+
<div class="animation-controls" style="text-align: center; margin: 10px 0;">
|
|
388
|
+
<button onclick="if(typeof animatePositionTransition === 'function') animatePositionTransition();"
|
|
389
|
+
class="{button_class}">{button_text}</button>
|
|
390
|
+
</div>
|
|
391
|
+
"""
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Helper for generating GSAP-based checker movement animations."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Tuple, Dict, Optional
|
|
5
|
+
from ankigammon.models import Position, Move, Player
|
|
6
|
+
from ankigammon.utils.move_parser import MoveParser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AnimationHelper:
|
|
10
|
+
"""Generates animation data and JavaScript for checker movements."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def parse_move_notation(notation: str, on_roll: Player) -> List[Tuple[int, int]]:
|
|
14
|
+
"""
|
|
15
|
+
Parse backgammon move notation into (from_point, to_point) pairs.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
notation: Move notation (e.g., "24/23 24/23" or "bar/24")
|
|
19
|
+
on_roll: Player making the move
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of (from_point, to_point) tuples (0-25, where 0=X bar, 25=O bar)
|
|
23
|
+
"""
|
|
24
|
+
moves = []
|
|
25
|
+
|
|
26
|
+
if not notation or notation == "Can't move":
|
|
27
|
+
return moves
|
|
28
|
+
|
|
29
|
+
parts = notation.strip().split()
|
|
30
|
+
|
|
31
|
+
for part in parts:
|
|
32
|
+
if '/' not in part:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
# Handle repetition notation like "6/4(4)"
|
|
36
|
+
repetition_count = 1
|
|
37
|
+
repetition_match = re.search(r'\((\d+)\)$', part)
|
|
38
|
+
if repetition_match:
|
|
39
|
+
repetition_count = int(repetition_match.group(1))
|
|
40
|
+
part = re.sub(r'\(\d+\)$', '', part)
|
|
41
|
+
|
|
42
|
+
# Handle compound notation like "6/5*/3"
|
|
43
|
+
segments = part.split('/')
|
|
44
|
+
segments = [seg.rstrip('*') for seg in segments]
|
|
45
|
+
if len(segments) > 2:
|
|
46
|
+
# Convert compound notation to individual moves
|
|
47
|
+
for i in range(len(segments) - 1):
|
|
48
|
+
from_str = segments[i]
|
|
49
|
+
to_str = segments[i + 1]
|
|
50
|
+
if from_str.lower() == 'bar':
|
|
51
|
+
from_point = 0 if on_roll == Player.X else 25
|
|
52
|
+
elif from_str.lower() == 'off':
|
|
53
|
+
continue
|
|
54
|
+
else:
|
|
55
|
+
try:
|
|
56
|
+
from_point = int(from_str)
|
|
57
|
+
except ValueError:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
if to_str.lower() == 'bar':
|
|
61
|
+
to_point = 25 if on_roll == Player.X else 0
|
|
62
|
+
elif to_str.lower() == 'off':
|
|
63
|
+
to_point = -1
|
|
64
|
+
else:
|
|
65
|
+
try:
|
|
66
|
+
to_point = int(to_str)
|
|
67
|
+
except ValueError:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
for _ in range(repetition_count):
|
|
71
|
+
moves.append((from_point, to_point))
|
|
72
|
+
else:
|
|
73
|
+
from_str = segments[0]
|
|
74
|
+
to_str = segments[1] if len(segments) > 1 else ''
|
|
75
|
+
|
|
76
|
+
if not to_str:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if from_str.lower() == 'bar':
|
|
80
|
+
from_point = 0 if on_roll == Player.X else 25
|
|
81
|
+
elif from_str.lower() == 'off':
|
|
82
|
+
continue
|
|
83
|
+
else:
|
|
84
|
+
try:
|
|
85
|
+
from_point = int(from_str)
|
|
86
|
+
except ValueError:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if to_str.lower() == 'bar':
|
|
90
|
+
to_point = 25 if on_roll == Player.X else 0
|
|
91
|
+
elif to_str.lower() == 'off':
|
|
92
|
+
to_point = -1
|
|
93
|
+
else:
|
|
94
|
+
try:
|
|
95
|
+
to_point = int(to_str)
|
|
96
|
+
except ValueError:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
for _ in range(repetition_count):
|
|
100
|
+
moves.append((from_point, to_point))
|
|
101
|
+
|
|
102
|
+
return moves
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def generate_animation_javascript(
|
|
106
|
+
move_notation: str,
|
|
107
|
+
on_roll: Player,
|
|
108
|
+
duration: float = 0.8,
|
|
109
|
+
stagger: float = 0.05
|
|
110
|
+
) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Generate JavaScript code for animating a backgammon move using GSAP.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
move_notation: Move notation string
|
|
116
|
+
on_roll: Player making the move
|
|
117
|
+
duration: Animation duration in seconds
|
|
118
|
+
stagger: Stagger time between checkers in seconds
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
JavaScript code string
|
|
122
|
+
"""
|
|
123
|
+
moves = AnimationHelper.parse_move_notation(move_notation, on_roll)
|
|
124
|
+
|
|
125
|
+
if not moves:
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
js_code = f"""
|
|
129
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
|
130
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js"></script>
|
|
131
|
+
<script>
|
|
132
|
+
if (typeof gsap !== 'undefined') {{
|
|
133
|
+
gsap.registerPlugin(MotionPathPlugin);
|
|
134
|
+
|
|
135
|
+
const moves = {moves};
|
|
136
|
+
const duration = {duration};
|
|
137
|
+
const stagger = {stagger};
|
|
138
|
+
|
|
139
|
+
function animateMove() {{
|
|
140
|
+
moves.forEach((move, index) => {{
|
|
141
|
+
const [fromPoint, toPoint] = move;
|
|
142
|
+
const checkers = document.querySelectorAll('.checker[data-point="' + fromPoint + '"]');
|
|
143
|
+
|
|
144
|
+
if (checkers.length > 0) {{
|
|
145
|
+
const checker = checkers[0];
|
|
146
|
+
|
|
147
|
+
gsap.to(checker, {{
|
|
148
|
+
duration: duration,
|
|
149
|
+
delay: index * stagger,
|
|
150
|
+
attr: {{
|
|
151
|
+
cx: '+=100',
|
|
152
|
+
cy: '+=0'
|
|
153
|
+
}},
|
|
154
|
+
ease: 'power2.inOut'
|
|
155
|
+
}});
|
|
156
|
+
}}
|
|
157
|
+
}});
|
|
158
|
+
}}
|
|
159
|
+
}}
|
|
160
|
+
</script>
|
|
161
|
+
"""
|
|
162
|
+
return js_code
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def get_gsap_cdn_scripts() -> str:
|
|
166
|
+
"""
|
|
167
|
+
Get GSAP CDN script tags.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
HTML script tags for GSAP and MotionPathPlugin
|
|
171
|
+
"""
|
|
172
|
+
return """
|
|
173
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
|
174
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js"></script>
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def generate_animation_button() -> str:
|
|
179
|
+
"""
|
|
180
|
+
Generate HTML for an animation trigger button.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
HTML button element
|
|
184
|
+
"""
|
|
185
|
+
return """
|
|
186
|
+
<div style="text-align: center; margin: 10px 0;">
|
|
187
|
+
<button onclick="animateCheckers()" class="toggle-btn">
|
|
188
|
+
▶ Animate Move
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
"""
|