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.

Files changed (56) hide show
  1. ankigammon/__init__.py +7 -0
  2. ankigammon/__main__.py +6 -0
  3. ankigammon/analysis/__init__.py +13 -0
  4. ankigammon/analysis/score_matrix.py +373 -0
  5. ankigammon/anki/__init__.py +6 -0
  6. ankigammon/anki/ankiconnect.py +224 -0
  7. ankigammon/anki/apkg_exporter.py +123 -0
  8. ankigammon/anki/card_generator.py +1307 -0
  9. ankigammon/anki/card_styles.py +1034 -0
  10. ankigammon/gui/__init__.py +8 -0
  11. ankigammon/gui/app.py +209 -0
  12. ankigammon/gui/dialogs/__init__.py +10 -0
  13. ankigammon/gui/dialogs/export_dialog.py +597 -0
  14. ankigammon/gui/dialogs/import_options_dialog.py +163 -0
  15. ankigammon/gui/dialogs/input_dialog.py +776 -0
  16. ankigammon/gui/dialogs/note_dialog.py +93 -0
  17. ankigammon/gui/dialogs/settings_dialog.py +384 -0
  18. ankigammon/gui/format_detector.py +292 -0
  19. ankigammon/gui/main_window.py +1071 -0
  20. ankigammon/gui/resources/icon.icns +0 -0
  21. ankigammon/gui/resources/icon.ico +0 -0
  22. ankigammon/gui/resources/icon.png +0 -0
  23. ankigammon/gui/resources/style.qss +394 -0
  24. ankigammon/gui/resources.py +26 -0
  25. ankigammon/gui/widgets/__init__.py +8 -0
  26. ankigammon/gui/widgets/position_list.py +193 -0
  27. ankigammon/gui/widgets/smart_input.py +268 -0
  28. ankigammon/models.py +322 -0
  29. ankigammon/parsers/__init__.py +7 -0
  30. ankigammon/parsers/gnubg_parser.py +454 -0
  31. ankigammon/parsers/xg_binary_parser.py +870 -0
  32. ankigammon/parsers/xg_text_parser.py +729 -0
  33. ankigammon/renderer/__init__.py +5 -0
  34. ankigammon/renderer/animation_controller.py +406 -0
  35. ankigammon/renderer/animation_helper.py +221 -0
  36. ankigammon/renderer/color_schemes.py +145 -0
  37. ankigammon/renderer/svg_board_renderer.py +824 -0
  38. ankigammon/settings.py +239 -0
  39. ankigammon/thirdparty/__init__.py +7 -0
  40. ankigammon/thirdparty/xgdatatools/__init__.py +17 -0
  41. ankigammon/thirdparty/xgdatatools/xgimport.py +160 -0
  42. ankigammon/thirdparty/xgdatatools/xgstruct.py +1032 -0
  43. ankigammon/thirdparty/xgdatatools/xgutils.py +118 -0
  44. ankigammon/thirdparty/xgdatatools/xgzarc.py +260 -0
  45. ankigammon/utils/__init__.py +13 -0
  46. ankigammon/utils/gnubg_analyzer.py +431 -0
  47. ankigammon/utils/gnuid.py +622 -0
  48. ankigammon/utils/move_parser.py +239 -0
  49. ankigammon/utils/ogid.py +335 -0
  50. ankigammon/utils/xgid.py +419 -0
  51. ankigammon-1.0.0.dist-info/METADATA +370 -0
  52. ankigammon-1.0.0.dist-info/RECORD +56 -0
  53. ankigammon-1.0.0.dist-info/WHEEL +5 -0
  54. ankigammon-1.0.0.dist-info/entry_points.txt +2 -0
  55. ankigammon-1.0.0.dist-info/licenses/LICENSE +21 -0
  56. ankigammon-1.0.0.dist-info/top_level.txt +1 -0
ankigammon/settings.py ADDED
@@ -0,0 +1,239 @@
1
+ """
2
+ Settings and configuration management for AnkiGammon.
3
+
4
+ Handles loading and saving user preferences such as color scheme selection.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+
13
+ class Settings:
14
+ """Manages application settings with persistence."""
15
+
16
+ DEFAULT_SETTINGS = {
17
+ "default_color_scheme": "classic",
18
+ "deck_name": "My AnkiGammon Deck",
19
+ "show_options": True,
20
+ "interactive_moves": True,
21
+ "export_method": "ankiconnect",
22
+ "gnubg_path": None,
23
+ "gnubg_analysis_ply": 3,
24
+ "generate_score_matrix": False,
25
+ "board_orientation": "counter-clockwise",
26
+ "last_apkg_directory": None,
27
+ "import_error_threshold": 0.080,
28
+ "import_include_player_x": True,
29
+ "import_include_player_o": True,
30
+ }
31
+
32
+ def __init__(self, config_path: Optional[Path] = None):
33
+ """
34
+ Initialize settings manager.
35
+
36
+ Args:
37
+ config_path: Path to config file. If None, uses default location.
38
+ """
39
+ if config_path is None:
40
+ config_dir = Path.home() / ".ankigammon"
41
+ config_dir.mkdir(parents=True, exist_ok=True)
42
+ config_path = config_dir / "config.json"
43
+
44
+ self.config_path = config_path
45
+ self._settings = self._load()
46
+
47
+ def _load(self) -> dict:
48
+ """Load settings from config file."""
49
+ if not self.config_path.exists():
50
+ return self.DEFAULT_SETTINGS.copy()
51
+
52
+ try:
53
+ with open(self.config_path, 'r', encoding='utf-8') as f:
54
+ loaded = json.load(f)
55
+ # Merge with defaults to handle new settings
56
+ settings = self.DEFAULT_SETTINGS.copy()
57
+ settings.update(loaded)
58
+ return settings
59
+ except (json.JSONDecodeError, IOError):
60
+ # If file is corrupted or unreadable, use defaults
61
+ return self.DEFAULT_SETTINGS.copy()
62
+
63
+ def _save(self) -> None:
64
+ """Save settings to config file."""
65
+ try:
66
+ with open(self.config_path, 'w', encoding='utf-8') as f:
67
+ json.dump(self._settings, f, indent=2)
68
+ except IOError:
69
+ # Silently fail if unable to save
70
+ pass
71
+
72
+ def get(self, key: str, default=None):
73
+ """Get a setting value."""
74
+ return self._settings.get(key, default)
75
+
76
+ def set(self, key: str, value) -> None:
77
+ """Set a setting value and save to disk."""
78
+ self._settings[key] = value
79
+ self._save()
80
+
81
+ @property
82
+ def color_scheme(self) -> str:
83
+ """Get the default color scheme."""
84
+ return self._settings.get("default_color_scheme", "classic")
85
+
86
+ @color_scheme.setter
87
+ def color_scheme(self, value: str) -> None:
88
+ """Set the default color scheme."""
89
+ self.set("default_color_scheme", value)
90
+
91
+ @property
92
+ def deck_name(self) -> str:
93
+ """Get the default deck name."""
94
+ return self._settings.get("deck_name", "My AnkiGammon Deck")
95
+
96
+ @deck_name.setter
97
+ def deck_name(self, value: str) -> None:
98
+ """Set the default deck name."""
99
+ self.set("deck_name", value)
100
+
101
+ @property
102
+ def show_options(self) -> bool:
103
+ """Get whether to show options on cards."""
104
+ return self._settings.get("show_options", True)
105
+
106
+ @show_options.setter
107
+ def show_options(self, value: bool) -> None:
108
+ """Set whether to show options on cards."""
109
+ self.set("show_options", value)
110
+
111
+ @property
112
+ def interactive_moves(self) -> bool:
113
+ """Get whether to enable interactive move visualization."""
114
+ return self._settings.get("interactive_moves", True)
115
+
116
+ @interactive_moves.setter
117
+ def interactive_moves(self, value: bool) -> None:
118
+ """Set whether to enable interactive move visualization."""
119
+ self.set("interactive_moves", value)
120
+
121
+ @property
122
+ def export_method(self) -> str:
123
+ """Get the default export method."""
124
+ return self._settings.get("export_method", "ankiconnect")
125
+
126
+ @export_method.setter
127
+ def export_method(self, value: str) -> None:
128
+ """Set the default export method."""
129
+ self.set("export_method", value)
130
+
131
+ @property
132
+ def gnubg_path(self) -> Optional[str]:
133
+ """Get the GnuBG executable path."""
134
+ return self._settings.get("gnubg_path", None)
135
+
136
+ @gnubg_path.setter
137
+ def gnubg_path(self, value: Optional[str]) -> None:
138
+ """Set the GnuBG executable path."""
139
+ self.set("gnubg_path", value)
140
+
141
+ @property
142
+ def gnubg_analysis_ply(self) -> int:
143
+ """Get the GnuBG analysis depth (ply)."""
144
+ return self._settings.get("gnubg_analysis_ply", 3)
145
+
146
+ @gnubg_analysis_ply.setter
147
+ def gnubg_analysis_ply(self, value: int) -> None:
148
+ """Set the GnuBG analysis depth (ply)."""
149
+ self.set("gnubg_analysis_ply", value)
150
+
151
+ @property
152
+ def generate_score_matrix(self) -> bool:
153
+ """Get whether to generate score matrix for cube decisions."""
154
+ return self._settings.get("generate_score_matrix", False)
155
+
156
+ @generate_score_matrix.setter
157
+ def generate_score_matrix(self, value: bool) -> None:
158
+ """Set whether to generate score matrix for cube decisions."""
159
+ self.set("generate_score_matrix", value)
160
+
161
+ @property
162
+ def board_orientation(self) -> str:
163
+ """Get the board orientation (clockwise or counter-clockwise)."""
164
+ return self._settings.get("board_orientation", "counter-clockwise")
165
+
166
+ @board_orientation.setter
167
+ def board_orientation(self, value: str) -> None:
168
+ """Set the board orientation (clockwise or counter-clockwise)."""
169
+ if value not in ["clockwise", "counter-clockwise"]:
170
+ raise ValueError("board_orientation must be 'clockwise' or 'counter-clockwise'")
171
+ self.set("board_orientation", value)
172
+
173
+ @property
174
+ def last_apkg_directory(self) -> Optional[str]:
175
+ """Get the last directory used for APKG export."""
176
+ return self._settings.get("last_apkg_directory", None)
177
+
178
+ @last_apkg_directory.setter
179
+ def last_apkg_directory(self, value: Optional[str]) -> None:
180
+ """Set the last directory used for APKG export."""
181
+ self.set("last_apkg_directory", value)
182
+
183
+ @property
184
+ def import_error_threshold(self) -> float:
185
+ """Get the error threshold for XG file imports."""
186
+ return self._settings.get("import_error_threshold", 0.080)
187
+
188
+ @import_error_threshold.setter
189
+ def import_error_threshold(self, value: float) -> None:
190
+ """Set the error threshold for XG file imports."""
191
+ self.set("import_error_threshold", value)
192
+
193
+ @property
194
+ def import_include_player_x(self) -> bool:
195
+ """Get whether to include Player X mistakes in imports."""
196
+ return self._settings.get("import_include_player_x", True)
197
+
198
+ @import_include_player_x.setter
199
+ def import_include_player_x(self, value: bool) -> None:
200
+ """Set whether to include Player X mistakes in imports."""
201
+ self.set("import_include_player_x", value)
202
+
203
+ @property
204
+ def import_include_player_o(self) -> bool:
205
+ """Get whether to include Player O mistakes in imports."""
206
+ return self._settings.get("import_include_player_o", True)
207
+
208
+ @import_include_player_o.setter
209
+ def import_include_player_o(self, value: bool) -> None:
210
+ """Set whether to include Player O mistakes in imports."""
211
+ self.set("import_include_player_o", value)
212
+
213
+ def is_gnubg_available(self) -> bool:
214
+ """
215
+ Check if GnuBG is configured and accessible.
216
+
217
+ Returns:
218
+ True if gnubg_path is set and the file exists and is executable.
219
+ """
220
+ path = self.gnubg_path
221
+ if path is None:
222
+ return False
223
+ try:
224
+ path_obj = Path(path)
225
+ return path_obj.exists() and os.access(path, os.X_OK)
226
+ except (OSError, ValueError):
227
+ return False
228
+
229
+
230
+ # Global settings instance
231
+ _settings = None
232
+
233
+
234
+ def get_settings() -> Settings:
235
+ """Get the global settings instance."""
236
+ global _settings
237
+ if _settings is None:
238
+ _settings = Settings()
239
+ return _settings
@@ -0,0 +1,7 @@
1
+ """
2
+ Third-party code with different licenses.
3
+
4
+ This directory contains code from external projects that use licenses
5
+ other than the main project license (MIT). Each subdirectory has its
6
+ own LICENSE file.
7
+ """
@@ -0,0 +1,17 @@
1
+ """
2
+ xgdatatools - eXtreme Gammon binary file parser
3
+
4
+ Original author: Michael Petch <mpetch@gnubg.org>
5
+ License: LGPL-3.0 or later
6
+ Source: https://github.com/oysteijo/xgdatatools
7
+
8
+ This package contains the xgdatatools library for parsing eXtreme Gammon
9
+ binary (.xg) file formats.
10
+ """
11
+
12
+ from . import xgimport
13
+ from . import xgstruct
14
+ from . import xgutils
15
+ from . import xgzarc
16
+
17
+ __all__ = ['xgimport', 'xgstruct', 'xgutils', 'xgzarc']
@@ -0,0 +1,160 @@
1
+ #
2
+ # xgimport.py - XG import module
3
+ # Copyright (C) 2013,2014 Michael Petch <mpetch@gnubg.org>
4
+ # <mpetch@capp-sysware.com>
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+ #
20
+
21
+ from __future__ import with_statement as _with
22
+ import tempfile as _tempfile
23
+ import shutil as _shutil
24
+ import struct as _struct
25
+ import os as _os
26
+ from . import xgutils as _xgutils
27
+ from . import xgzarc as _xgzarc
28
+ from . import xgstruct as _xgstruct
29
+
30
+
31
+ class Import(object):
32
+
33
+ class Segment(object):
34
+ GDF_HDR, GDF_IMAGE, XG_GAMEHDR, XG_GAMEFILE, XG_ROLLOUTS, XG_COMMENT, \
35
+ ZLIBARC_IDX, XG_UNKNOWN = range(8)
36
+ EXTENSIONS = ['_gdh.bin', '.jpg', '_gamehdr.bin', '_gamefile.bin',
37
+ '_rollouts.bin', '_comments.bin', '_idx.bin', None]
38
+ GDF_HDR_EXT, GDF_IMAGE_EXT, XG_GAMEHDR_EXT, XG_GAMEFILE_EXT, \
39
+ XG_ROLLOUTS_EXT, XG_COMMENTS_EXT, \
40
+ XG_IDX_EXT, XG_UNKNOWN = EXTENSIONS
41
+ XG_FILEMAP = {'temp.xgi': XG_GAMEHDR, 'temp.xgr': XG_ROLLOUTS,
42
+ 'temp.xgc': XG_COMMENT, 'temp.xg': XG_GAMEFILE}
43
+
44
+ XG_GAMEHDR_LEN = 556
45
+
46
+ __TMP_PREFIX = 'tmpXGI'
47
+
48
+ def __init__(self, type=GDF_HDR, delete=True, prefix=__TMP_PREFIX):
49
+ self.filename = None
50
+ self.fd = None
51
+ self.file = None
52
+ self.type = type
53
+ self.__prefix = prefix
54
+ self.__autodelete = delete
55
+ self.ext = self.EXTENSIONS[type]
56
+
57
+ def __enter__(self):
58
+ self.createtempfile()
59
+ return self
60
+
61
+ def __exit__(self, type, value, traceback):
62
+ self.closetempfile()
63
+
64
+ def closetempfile(self):
65
+ try:
66
+ if self.file is not None:
67
+ self.file.close()
68
+ finally:
69
+ self.fd = None
70
+ self.file = None
71
+ if self.__autodelete and self.filename is not None and \
72
+ _os.path.exists(self.filename):
73
+ try:
74
+ _os.unlink(self.filename)
75
+ finally:
76
+ self.filename = None
77
+
78
+ def copyto(self, fileto):
79
+ _shutil.copy(self.filename, fileto)
80
+
81
+ def createtempfile(self, mode="w+b"):
82
+ self.fd, self.filename = _tempfile.mkstemp(prefix=self.__prefix)
83
+ self.file = _os.fdopen(self.fd, mode)
84
+ return self
85
+
86
+ def __init__(self, filename):
87
+ self.filename = filename
88
+
89
+ def getfilesegment(self):
90
+ with open(self.filename, "rb") as xginfile:
91
+ # Extract the uncompressed Game Data Header (GDH)
92
+ # Note: MS Windows Vista feature
93
+ gdfheader = \
94
+ _xgstruct.GameDataFormatHdrRecord().fromstream(xginfile)
95
+ if gdfheader is None:
96
+ raise Error("Not a game data format file", self.filename)
97
+
98
+ # Extract the Game Format Header to a temporary file
99
+ with Import.Segment(type=Import.Segment.GDF_HDR) as segment:
100
+ xginfile.seek(0)
101
+ block = xginfile.read(gdfheader.HeaderSize)
102
+ segment.file.write(block)
103
+ segment.file.flush()
104
+ yield segment
105
+
106
+ # Extract the uncompressed thumbnail JPEG from the GDF hdr
107
+ if (gdfheader.ThumbnailSize > 0):
108
+ with Import.Segment(type=Import.Segment.GDF_IMAGE) as segment:
109
+ xginfile.seek(gdfheader.ThumbnailOffset, _os.SEEK_CUR)
110
+ imgbuf = xginfile.read(gdfheader.ThumbnailSize)
111
+ segment.file.write(imgbuf)
112
+ segment.file.flush()
113
+ yield segment
114
+
115
+ # Retrieve an archive object from the stream
116
+ archiveobj = _xgzarc.ZlibArchive(xginfile)
117
+
118
+ # Process all the files in the archive
119
+ for filerec in archiveobj.arcregistry:
120
+
121
+ # Retrieve the archive file to a temporary file
122
+ segment_file, seg_filename = archiveobj.getarchivefile(filerec)
123
+
124
+ # Create a file segment object to passback to the caller
125
+ xg_filetype = Import.Segment.XG_FILEMAP[filerec.name]
126
+ xg_filesegment = Import.Segment(type=xg_filetype,
127
+ delete=False)
128
+ xg_filesegment.filename = seg_filename
129
+ xg_filesegment.fd = segment_file
130
+
131
+ # If we are looking at the game info file then check
132
+ # the magic number to ensure it is valid
133
+ if xg_filetype == Import.Segment.XG_GAMEFILE:
134
+ segment_file.seek(Import.Segment.XG_GAMEHDR_LEN)
135
+ magicStr = bytearray(segment_file.read(4)).decode('ascii')
136
+ if magicStr != 'DMLI':
137
+ raise Error("Not a valid XG gamefile", self.filename)
138
+
139
+ yield xg_filesegment
140
+
141
+ segment_file.close()
142
+ _os.unlink(seg_filename)
143
+
144
+ return
145
+
146
+
147
+ class Error(Exception):
148
+
149
+ def __init__(self, error, filename):
150
+ self.value = "XG Import Error processing '%s': %s" % \
151
+ (filename, str(error))
152
+ self.error = error
153
+ self.filename = filename
154
+
155
+ def __str__(self):
156
+ return repr(self.value)
157
+
158
+
159
+ if __name__ == '__main__':
160
+ pass