xgparse 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.
- xgparse/__init__.py +0 -0
- xgparse/extractxgdata.py +101 -0
- xgparse/xgimport.py +214 -0
- xgparse/xgstruct.py +964 -0
- xgparse/xgutils.py +95 -0
- xgparse/xgzarc.py +256 -0
- xgparse-0.1.0.dist-info/METADATA +521 -0
- xgparse-0.1.0.dist-info/RECORD +9 -0
- xgparse-0.1.0.dist-info/WHEEL +4 -0
xgparse/xgstruct.py
ADDED
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
#
|
|
2
|
+
# xgstruct.py - classes to read XG file structures
|
|
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
|
+
# This code is based upon Delphi data structures provided by
|
|
20
|
+
# Xavier Dufaure de Citres <contact@extremegammon.com> for purposes
|
|
21
|
+
# of interacting with the ExtremeGammon XG file formats. Field
|
|
22
|
+
# descriptions derived from xg_format.pas. The file formats are
|
|
23
|
+
# published at http://www.extremegammon.com/xgformat.aspx
|
|
24
|
+
#
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import binascii
|
|
29
|
+
import os
|
|
30
|
+
import struct
|
|
31
|
+
import uuid
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from enum import IntEnum
|
|
34
|
+
from typing import BinaryIO
|
|
35
|
+
|
|
36
|
+
import xgutils
|
|
37
|
+
|
|
38
|
+
# Convenience alias used throughout: a 26-element signed-byte board position.
|
|
39
|
+
Position = tuple[int, ...]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Helper: read an exact number of bytes or raise EOFError
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _read(stream: BinaryIO, size: int) -> bytes:
|
|
48
|
+
data = stream.read(size)
|
|
49
|
+
if len(data) < size:
|
|
50
|
+
raise EOFError(f"Expected {size} bytes, got {len(data)}")
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ===========================================================================
|
|
55
|
+
# Outer container — the RichGame / GDF header
|
|
56
|
+
# ===========================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class GameDataFormatHdrRecord:
|
|
61
|
+
"""The 8 232-byte Rich Game Format header that wraps every .xg file.
|
|
62
|
+
|
|
63
|
+
Fields correspond to ``TRichGameHeader`` in ``xg_format.pas``.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
SIZE = 8232
|
|
67
|
+
|
|
68
|
+
magic_number: str = "" # Must be "HMGR"
|
|
69
|
+
header_version: int = 0 # Must be 1
|
|
70
|
+
header_size: int = 0 # Byte length of this header
|
|
71
|
+
thumbnail_offset: int = 0 # Offset to thumbnail JPEG (from end of header)
|
|
72
|
+
thumbnail_size: int = 0 # Byte length of thumbnail JPEG; 0 = absent
|
|
73
|
+
game_guid: str = "" # Game GUID
|
|
74
|
+
game_name: str = "" # Unicode game name
|
|
75
|
+
save_name: str = "" # Unicode save name
|
|
76
|
+
level_name: str = "" # Unicode level name
|
|
77
|
+
comments: str = "" # Unicode comments
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_stream(cls, stream: BinaryIO) -> "GameDataFormatHdrRecord | None":
|
|
81
|
+
"""Return a populated instance, or ``None`` if the magic/version is wrong."""
|
|
82
|
+
try:
|
|
83
|
+
data = struct.unpack(
|
|
84
|
+
"<4BiiQiLHHBB6s1024H1024H1024H1024H",
|
|
85
|
+
stream.read(cls.SIZE),
|
|
86
|
+
)
|
|
87
|
+
except struct.error:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
magic = bytearray(data[0:4][::-1]).decode("ascii")
|
|
91
|
+
if magic != "HMGR" or data[4] != 1:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
guid_p1, guid_p2, guid_p3, guid_p4, guid_p5 = data[8:13]
|
|
95
|
+
guid_p6 = int(binascii.b2a_hex(data[13]), 16)
|
|
96
|
+
game_guid = str(
|
|
97
|
+
uuid.UUID(fields=(guid_p1, guid_p2, guid_p3, guid_p4, guid_p5, guid_p6))
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return cls(
|
|
101
|
+
magic_number=magic,
|
|
102
|
+
header_version=data[4],
|
|
103
|
+
header_size=data[5],
|
|
104
|
+
thumbnail_offset=data[6],
|
|
105
|
+
thumbnail_size=data[7],
|
|
106
|
+
game_guid=game_guid,
|
|
107
|
+
game_name=xgutils.utf16intarraytostr(data[14:1038]),
|
|
108
|
+
save_name=xgutils.utf16intarraytostr(data[1038:2062]),
|
|
109
|
+
level_name=xgutils.utf16intarraytostr(data[2062:3086]),
|
|
110
|
+
comments=xgutils.utf16intarraytostr(data[3086:4110]),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ===========================================================================
|
|
115
|
+
# Sub-records embedded inside game records
|
|
116
|
+
# ===========================================================================
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class TimeSettingRecord:
|
|
121
|
+
"""Clock settings stored inside a :class:`HeaderMatchEntry` (v25+)."""
|
|
122
|
+
|
|
123
|
+
SIZE = 32
|
|
124
|
+
|
|
125
|
+
clock_type: int = 0 # 0=None, 1=Fischer, 2=Bronstein
|
|
126
|
+
per_game: bool = False # Reset clock after each game
|
|
127
|
+
time1: int = 0 # Initial time in seconds
|
|
128
|
+
time2: int = 0 # Time added (Fischer) / reserved (Bronstein) per move
|
|
129
|
+
penalty: int = 0 # Point penalty when time expires
|
|
130
|
+
time_left1: int = 0 # Current time left, player 1
|
|
131
|
+
time_left2: int = 0 # Current time left, player 2
|
|
132
|
+
penalty_money: int = 0 # Monetary penalty when time expires
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_stream(cls, stream: BinaryIO) -> "TimeSettingRecord":
|
|
136
|
+
d = struct.unpack("<lBxxxllllll", _read(stream, cls.SIZE))
|
|
137
|
+
return cls(
|
|
138
|
+
clock_type=d[0],
|
|
139
|
+
per_game=bool(d[1]),
|
|
140
|
+
time1=d[2],
|
|
141
|
+
time2=d[3],
|
|
142
|
+
penalty=d[4],
|
|
143
|
+
time_left1=d[5],
|
|
144
|
+
time_left2=d[6],
|
|
145
|
+
penalty_money=d[7],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class EvalLevelRecord:
|
|
151
|
+
"""Analysis level attached to each candidate move."""
|
|
152
|
+
|
|
153
|
+
SIZE = 4
|
|
154
|
+
|
|
155
|
+
level: int = 0 # See PLAYERLEVEL table in xg_format.pas
|
|
156
|
+
is_double: bool = False # Analysis assumes double on the next move
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_stream(cls, stream: BinaryIO) -> "EvalLevelRecord":
|
|
160
|
+
d = struct.unpack("<hBb", _read(stream, cls.SIZE))
|
|
161
|
+
return cls(level=d[0], is_double=bool(d[1]))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class EngineStructBestMoveRecord:
|
|
166
|
+
"""Full checker-play analysis for one position (``EngineStructBestMove``)."""
|
|
167
|
+
|
|
168
|
+
SIZE = 2184
|
|
169
|
+
|
|
170
|
+
pos: Position = field(default_factory=tuple) # Current position (26 bytes)
|
|
171
|
+
dice: tuple[int, int] = (0, 0) # Dice values
|
|
172
|
+
level: int = 0 # Analysis level requested
|
|
173
|
+
score: tuple[int, int] = (0, 0) # Match score
|
|
174
|
+
cube: int = 0 # Cube value
|
|
175
|
+
cube_pos: int = 0 # 0=centre, +1=owns, -1=opponent
|
|
176
|
+
crawford: int = 0
|
|
177
|
+
jacoby: int = 0
|
|
178
|
+
n_moves: int = 0 # Number of candidates (max 32)
|
|
179
|
+
pos_played: tuple[Position, ...] = field(default_factory=tuple)
|
|
180
|
+
moves: tuple[tuple[int, ...], ...] = field(default_factory=tuple)
|
|
181
|
+
eval_level: tuple[EvalLevelRecord, ...] = field(default_factory=tuple)
|
|
182
|
+
eval: tuple[tuple[float, ...], ...] = field(default_factory=tuple)
|
|
183
|
+
unused: int = 0
|
|
184
|
+
met: int = 0
|
|
185
|
+
choice0: int = 0 # 1-ply computer choice (index into pos_played)
|
|
186
|
+
choice3: int = 0 # 3-ply computer choice
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def from_stream(cls, stream: BinaryIO) -> "EngineStructBestMoveRecord":
|
|
190
|
+
d = struct.unpack("<26bxx2ll2llllll", _read(stream, 68))
|
|
191
|
+
pos_played = tuple(
|
|
192
|
+
struct.unpack("<26b", _read(stream, 26))[0:26] for _ in range(32)
|
|
193
|
+
)
|
|
194
|
+
moves = tuple(struct.unpack("<8b", _read(stream, 8))[0:8] for _ in range(32))
|
|
195
|
+
eval_level = tuple(EvalLevelRecord.from_stream(stream) for _ in range(32))
|
|
196
|
+
evals = tuple(struct.unpack("<7f", _read(stream, 28)) for _ in range(32))
|
|
197
|
+
tail = struct.unpack("<bbbb", _read(stream, 4))
|
|
198
|
+
|
|
199
|
+
return cls(
|
|
200
|
+
pos=d[0:26],
|
|
201
|
+
dice=(d[26], d[27]),
|
|
202
|
+
level=d[28],
|
|
203
|
+
score=(d[29], d[30]),
|
|
204
|
+
cube=d[31],
|
|
205
|
+
cube_pos=d[32],
|
|
206
|
+
crawford=d[33],
|
|
207
|
+
jacoby=d[34],
|
|
208
|
+
n_moves=d[35],
|
|
209
|
+
pos_played=pos_played,
|
|
210
|
+
moves=moves,
|
|
211
|
+
eval_level=eval_level,
|
|
212
|
+
eval=evals,
|
|
213
|
+
unused=tail[0],
|
|
214
|
+
met=tail[1],
|
|
215
|
+
choice0=tail[2],
|
|
216
|
+
choice3=tail[3],
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@dataclass
|
|
221
|
+
class EngineStructDoubleAction:
|
|
222
|
+
"""Cube-decision analysis for one position (``EngineStructDoubleAction``)."""
|
|
223
|
+
|
|
224
|
+
SIZE = 132
|
|
225
|
+
|
|
226
|
+
pos: Position = field(default_factory=tuple)
|
|
227
|
+
level: int = 0
|
|
228
|
+
score: tuple[int, int] = (0, 0)
|
|
229
|
+
cube: int = 0
|
|
230
|
+
cube_pos: int = 0
|
|
231
|
+
jacoby: int = 0
|
|
232
|
+
crawford: int = 0
|
|
233
|
+
met: int = 0
|
|
234
|
+
flag_double: int = 0 # 0=don't double, 1=double
|
|
235
|
+
is_beaver: int = 0
|
|
236
|
+
eval_nd: tuple[float, ...] = field(default_factory=tuple) # No-double equities
|
|
237
|
+
equ_b: float = 0.0 # Equity: No Double
|
|
238
|
+
equ_double: float = 0.0 # Equity: Double/Take
|
|
239
|
+
equ_drop: float = 0.0 # Equity: Double/Drop (should be -1)
|
|
240
|
+
level_request: int = 0
|
|
241
|
+
double_choice3: int = 0 # 3-ply choice (Double + 2*Take)
|
|
242
|
+
eval_double: tuple[float, ...] = field(
|
|
243
|
+
default_factory=tuple
|
|
244
|
+
) # Double/Take equities
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def from_stream(cls, stream: BinaryIO) -> "EngineStructDoubleAction":
|
|
248
|
+
d = struct.unpack("<26bxxl2llllhhhh7ffffhh7f", _read(stream, 132))
|
|
249
|
+
return cls(
|
|
250
|
+
pos=d[0:26],
|
|
251
|
+
level=d[26],
|
|
252
|
+
score=(d[27], d[28]),
|
|
253
|
+
cube=d[29],
|
|
254
|
+
cube_pos=d[30],
|
|
255
|
+
jacoby=d[31],
|
|
256
|
+
crawford=d[32],
|
|
257
|
+
met=d[33],
|
|
258
|
+
flag_double=d[34],
|
|
259
|
+
is_beaver=d[35],
|
|
260
|
+
eval_nd=d[36:43],
|
|
261
|
+
equ_b=d[43],
|
|
262
|
+
equ_double=d[44],
|
|
263
|
+
equ_drop=d[45],
|
|
264
|
+
level_request=d[46],
|
|
265
|
+
double_choice3=d[47],
|
|
266
|
+
eval_double=d[48:55],
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ===========================================================================
|
|
271
|
+
# Game-file record types (EntryType values in TSaveRec)
|
|
272
|
+
# ===========================================================================
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class EntryType(IntEnum):
|
|
276
|
+
HEADER_MATCH = 0
|
|
277
|
+
HEADER_GAME = 1
|
|
278
|
+
CUBE = 2
|
|
279
|
+
MOVE = 3
|
|
280
|
+
FOOTER_GAME = 4
|
|
281
|
+
FOOTER_MATCH = 5
|
|
282
|
+
COMMENT = 6 # unused by XG, treated as UnimplementedEntry
|
|
283
|
+
MISSING = 7
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# All TSaveRec records are padded to exactly this size on disk.
|
|
287
|
+
_SAVE_REC_SIZE = 2560
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass
|
|
291
|
+
class HeaderMatchEntry:
|
|
292
|
+
"""Match metadata — the first record in every ``temp.xg`` file."""
|
|
293
|
+
|
|
294
|
+
entry_type: EntryType = EntryType.HEADER_MATCH
|
|
295
|
+
version: int = 0 # File format version; forward to all other records
|
|
296
|
+
|
|
297
|
+
# ANSI (XG1 compatibility) player/event/location/round names
|
|
298
|
+
s_player1: str = ""
|
|
299
|
+
s_player2: str = ""
|
|
300
|
+
s_event: str = ""
|
|
301
|
+
s_location: str = ""
|
|
302
|
+
s_round: str = ""
|
|
303
|
+
|
|
304
|
+
match_length: int = 0 # 99999 = unlimited (money game)
|
|
305
|
+
variation: int = 0 # 0=backgammon 1=Nack 2=Hyper 3=Longgammon
|
|
306
|
+
crawford: bool = False
|
|
307
|
+
jacoby: bool = False
|
|
308
|
+
beaver: bool = False
|
|
309
|
+
auto_double: bool = False
|
|
310
|
+
elo1: float = 0.0
|
|
311
|
+
elo2: float = 0.0
|
|
312
|
+
exp1: int = 0
|
|
313
|
+
exp2: int = 0
|
|
314
|
+
date: str = ""
|
|
315
|
+
game_id: int = 0
|
|
316
|
+
comp_level1: int = -1 # See PLAYERLEVEL table
|
|
317
|
+
comp_level2: int = -1
|
|
318
|
+
count_for_elo: bool = False
|
|
319
|
+
add_to_profile1: bool = False
|
|
320
|
+
add_to_profile2: bool = False
|
|
321
|
+
game_mode: int = 0 # See GAMEMODE table
|
|
322
|
+
imported: bool = False
|
|
323
|
+
invert: int = 0
|
|
324
|
+
magic: int = 0x494C4D44
|
|
325
|
+
money_init_g: int = 0
|
|
326
|
+
money_init_score: tuple[int, int] = (0, 0)
|
|
327
|
+
entered: bool = False
|
|
328
|
+
counted: bool = False
|
|
329
|
+
unrated_imp: bool = False
|
|
330
|
+
comment_header_match: int = -1
|
|
331
|
+
comment_footer_match: int = -1
|
|
332
|
+
is_money_match: bool = False
|
|
333
|
+
win_money: float = 0.0
|
|
334
|
+
lose_money: float = 0.0
|
|
335
|
+
currency: int = 0
|
|
336
|
+
fee_money: float = 0.0
|
|
337
|
+
table_stake: float = 0.0
|
|
338
|
+
site_id: int = -1
|
|
339
|
+
# v8+
|
|
340
|
+
cube_limit: int = 0
|
|
341
|
+
auto_double_max: int = 0
|
|
342
|
+
# v24+
|
|
343
|
+
transcribed: bool = False
|
|
344
|
+
event: str = ""
|
|
345
|
+
player1: str = ""
|
|
346
|
+
player2: str = ""
|
|
347
|
+
location: str = ""
|
|
348
|
+
round: str = ""
|
|
349
|
+
# v25+
|
|
350
|
+
time_setting: TimeSettingRecord | None = None
|
|
351
|
+
# v26+
|
|
352
|
+
tot_time_delay_move: int = 0
|
|
353
|
+
tot_time_delay_cube: int = 0
|
|
354
|
+
tot_time_delay_move_done: int = 0
|
|
355
|
+
tot_time_delay_cube_done: int = 0
|
|
356
|
+
# v30+
|
|
357
|
+
transcriber: str = ""
|
|
358
|
+
|
|
359
|
+
@classmethod
|
|
360
|
+
def from_stream(cls, stream: BinaryIO, version: int = 0) -> "HeaderMatchEntry":
|
|
361
|
+
d = struct.unpack(
|
|
362
|
+
"<9x41B41BxllBBBBddlld129BxxxlllBBB129BlB129BxxllLl2lBBBxllBxxxfflfll",
|
|
363
|
+
_read(stream, 612),
|
|
364
|
+
)
|
|
365
|
+
obj = cls(version=version)
|
|
366
|
+
obj.s_player1 = xgutils.delphishortstrtostr(d[0:41])
|
|
367
|
+
obj.s_player2 = xgutils.delphishortstrtostr(d[41:82])
|
|
368
|
+
obj.match_length = d[82]
|
|
369
|
+
obj.variation = d[83]
|
|
370
|
+
obj.crawford = bool(d[84])
|
|
371
|
+
obj.jacoby = bool(d[85])
|
|
372
|
+
obj.beaver = bool(d[86])
|
|
373
|
+
obj.auto_double = bool(d[87])
|
|
374
|
+
obj.elo1 = d[88]
|
|
375
|
+
obj.elo2 = d[89]
|
|
376
|
+
obj.exp1 = d[90]
|
|
377
|
+
obj.exp2 = d[91]
|
|
378
|
+
obj.date = str(xgutils.delphidatetimeconv(d[92]))
|
|
379
|
+
obj.s_event = xgutils.delphishortstrtostr(d[93:222])
|
|
380
|
+
obj.game_id = d[222]
|
|
381
|
+
obj.comp_level1 = d[223]
|
|
382
|
+
obj.comp_level2 = d[224]
|
|
383
|
+
obj.count_for_elo = bool(d[225])
|
|
384
|
+
obj.add_to_profile1 = bool(d[226])
|
|
385
|
+
obj.add_to_profile2 = bool(d[227])
|
|
386
|
+
obj.s_location = xgutils.delphishortstrtostr(d[228:357])
|
|
387
|
+
obj.game_mode = d[357]
|
|
388
|
+
obj.imported = bool(d[358])
|
|
389
|
+
obj.s_round = xgutils.delphishortstrtostr(d[359:487])
|
|
390
|
+
obj.invert = d[488]
|
|
391
|
+
obj.version = d[489]
|
|
392
|
+
obj.magic = d[490]
|
|
393
|
+
obj.money_init_g = d[491]
|
|
394
|
+
obj.money_init_score = (d[492], d[493])
|
|
395
|
+
obj.entered = bool(d[494])
|
|
396
|
+
obj.counted = bool(d[495])
|
|
397
|
+
obj.unrated_imp = bool(d[496])
|
|
398
|
+
obj.comment_header_match = d[497]
|
|
399
|
+
obj.comment_footer_match = d[498]
|
|
400
|
+
obj.is_money_match = bool(d[499])
|
|
401
|
+
obj.win_money = d[500]
|
|
402
|
+
obj.lose_money = d[501]
|
|
403
|
+
obj.currency = d[502]
|
|
404
|
+
obj.fee_money = d[503]
|
|
405
|
+
obj.table_stake = d[504]
|
|
406
|
+
obj.site_id = d[505]
|
|
407
|
+
|
|
408
|
+
if obj.version >= 8:
|
|
409
|
+
d2 = struct.unpack("<ll", _read(stream, 8))
|
|
410
|
+
obj.cube_limit = d2[0]
|
|
411
|
+
obj.auto_double_max = d2[1]
|
|
412
|
+
|
|
413
|
+
if obj.version >= 24:
|
|
414
|
+
d2 = struct.unpack("<Bx129H129H129H129H129H", _read(stream, 1292))
|
|
415
|
+
obj.transcribed = bool(d2[0])
|
|
416
|
+
obj.event = xgutils.utf16intarraytostr(d2[1:130])
|
|
417
|
+
obj.player1 = xgutils.utf16intarraytostr(d2[130:259])
|
|
418
|
+
obj.player2 = xgutils.utf16intarraytostr(d2[259:388])
|
|
419
|
+
obj.location = xgutils.utf16intarraytostr(d2[388:517])
|
|
420
|
+
obj.round = xgutils.utf16intarraytostr(d2[517:646])
|
|
421
|
+
|
|
422
|
+
if obj.version >= 25:
|
|
423
|
+
obj.time_setting = TimeSettingRecord.from_stream(stream)
|
|
424
|
+
|
|
425
|
+
if obj.version >= 26:
|
|
426
|
+
d2 = struct.unpack("<llll", _read(stream, 16))
|
|
427
|
+
obj.tot_time_delay_move = d2[0]
|
|
428
|
+
obj.tot_time_delay_cube = d2[1]
|
|
429
|
+
obj.tot_time_delay_move_done = d2[2]
|
|
430
|
+
obj.tot_time_delay_cube_done = d2[3]
|
|
431
|
+
|
|
432
|
+
if obj.version >= 30:
|
|
433
|
+
d2 = struct.unpack("<129H", _read(stream, 258))
|
|
434
|
+
obj.transcriber = xgutils.utf16intarraytostr(d2[0:129])
|
|
435
|
+
|
|
436
|
+
return obj
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@dataclass
|
|
440
|
+
class HeaderGameEntry:
|
|
441
|
+
"""Per-game header — one instance per game inside ``temp.xg``."""
|
|
442
|
+
|
|
443
|
+
entry_type: EntryType = EntryType.HEADER_GAME
|
|
444
|
+
version: int = 0
|
|
445
|
+
|
|
446
|
+
score1: int = 0 # Player 1 score at game start
|
|
447
|
+
score2: int = 0 # Player 2 score at game start
|
|
448
|
+
crawford_apply: bool = False # Crawford rule applies to this game
|
|
449
|
+
pos_init: Position = field(default_factory=lambda: (0,) * 26)
|
|
450
|
+
game_number: int = 0 # 1-based
|
|
451
|
+
in_progress: bool = False
|
|
452
|
+
comment_header_game: int = -1
|
|
453
|
+
comment_footer_game: int = -1
|
|
454
|
+
# v26+
|
|
455
|
+
number_of_auto_doubles: int = 0
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def from_stream(cls, stream: BinaryIO, version: int = 0) -> "HeaderGameEntry":
|
|
459
|
+
d = struct.unpack("<9xxxxllB26bxlBxxxlll", _read(stream, 68))
|
|
460
|
+
obj = cls(version=version)
|
|
461
|
+
obj.score1 = d[0]
|
|
462
|
+
obj.score2 = d[1]
|
|
463
|
+
obj.crawford_apply = bool(d[2])
|
|
464
|
+
obj.pos_init = d[3:29]
|
|
465
|
+
obj.game_number = d[29]
|
|
466
|
+
obj.in_progress = bool(d[30])
|
|
467
|
+
obj.comment_header_game = d[31]
|
|
468
|
+
obj.comment_footer_game = d[32]
|
|
469
|
+
if version >= 26:
|
|
470
|
+
obj.number_of_auto_doubles = d[33]
|
|
471
|
+
return obj
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@dataclass
|
|
475
|
+
class CubeEntry:
|
|
476
|
+
"""A cube-decision record (double / take / beaver / raccoon)."""
|
|
477
|
+
|
|
478
|
+
entry_type: EntryType = EntryType.CUBE
|
|
479
|
+
version: int = 0
|
|
480
|
+
|
|
481
|
+
active_p: int = 0 # Active player (1 or 2)
|
|
482
|
+
double: int = 0 # 0=no double, 1=doubled
|
|
483
|
+
take: int = 0 # 0=no, 1=take, 2=beaver
|
|
484
|
+
beaver_r: int = 0 # 0=no, 1=accept, 2=raccoon
|
|
485
|
+
raccoon_r: int = 0
|
|
486
|
+
cube_b: int = 0 # Cube level: 0=centre, +n=2^n own, -n=2^n opponent
|
|
487
|
+
position: Position = field(default_factory=tuple)
|
|
488
|
+
doubled: EngineStructDoubleAction | None = None
|
|
489
|
+
err_cube: float = 0.0 # Error on doubling (-1000 = not analysed)
|
|
490
|
+
dice_rolled: str = ""
|
|
491
|
+
err_take: float = 0.0
|
|
492
|
+
rollout_index_d: int = 0
|
|
493
|
+
comp_choice_d: int = 0
|
|
494
|
+
analyze_c: int = 0
|
|
495
|
+
err_beaver: float = 0.0
|
|
496
|
+
err_raccoon: float = 0.0
|
|
497
|
+
analyze_cr: int = 0
|
|
498
|
+
is_valid: int = 0
|
|
499
|
+
tutor_cube: int = 0
|
|
500
|
+
tutor_take: int = 0
|
|
501
|
+
err_tutor_cube: float = 0.0
|
|
502
|
+
err_tutor_take: float = 0.0
|
|
503
|
+
flagged_double: bool = False
|
|
504
|
+
comment_cube: int = -1
|
|
505
|
+
# v24+
|
|
506
|
+
edited_cube: bool = False
|
|
507
|
+
# v26+
|
|
508
|
+
time_delay_cube: bool = False
|
|
509
|
+
time_delay_cube_done: bool = False
|
|
510
|
+
# v27+
|
|
511
|
+
number_of_auto_double_cube: int = 0
|
|
512
|
+
# v28+
|
|
513
|
+
time_bot: int = 0
|
|
514
|
+
time_top: int = 0
|
|
515
|
+
|
|
516
|
+
@classmethod
|
|
517
|
+
def from_stream(cls, stream: BinaryIO, version: int = 0) -> "CubeEntry":
|
|
518
|
+
d = struct.unpack("<9xxxxllllll26bxx", _read(stream, 64))
|
|
519
|
+
obj = cls(version=version)
|
|
520
|
+
obj.active_p = d[0]
|
|
521
|
+
obj.double = d[1]
|
|
522
|
+
obj.take = d[2]
|
|
523
|
+
obj.beaver_r = d[3]
|
|
524
|
+
obj.raccoon_r = d[4]
|
|
525
|
+
obj.cube_b = d[5]
|
|
526
|
+
obj.position = d[6:32]
|
|
527
|
+
obj.doubled = EngineStructDoubleAction.from_stream(stream)
|
|
528
|
+
|
|
529
|
+
d2 = struct.unpack(
|
|
530
|
+
"<xxxxd3BxxxxxdlllxxxxddllbbxxxxxxddBxxxlBBBxlll",
|
|
531
|
+
_read(stream, 116),
|
|
532
|
+
)
|
|
533
|
+
obj.err_cube = d2[0]
|
|
534
|
+
obj.dice_rolled = xgutils.delphishortstrtostr(d2[1:4])
|
|
535
|
+
obj.err_take = d2[4]
|
|
536
|
+
obj.rollout_index_d = d2[5]
|
|
537
|
+
obj.comp_choice_d = d2[6]
|
|
538
|
+
obj.analyze_c = d2[7]
|
|
539
|
+
obj.err_beaver = d2[8]
|
|
540
|
+
obj.err_raccoon = d2[9]
|
|
541
|
+
obj.analyze_cr = d2[10]
|
|
542
|
+
obj.is_valid = d2[11]
|
|
543
|
+
obj.tutor_cube = d2[12]
|
|
544
|
+
obj.tutor_take = d2[13]
|
|
545
|
+
obj.err_tutor_cube = d2[14]
|
|
546
|
+
obj.err_tutor_take = d2[15]
|
|
547
|
+
obj.flagged_double = bool(d2[16])
|
|
548
|
+
obj.comment_cube = d2[17]
|
|
549
|
+
|
|
550
|
+
if version >= 24:
|
|
551
|
+
obj.edited_cube = bool(d2[18])
|
|
552
|
+
if version >= 26:
|
|
553
|
+
obj.time_delay_cube = bool(d2[19])
|
|
554
|
+
obj.time_delay_cube_done = bool(d2[20])
|
|
555
|
+
if version >= 27:
|
|
556
|
+
obj.number_of_auto_double_cube = d2[21]
|
|
557
|
+
if version >= 28:
|
|
558
|
+
obj.time_bot = d2[22]
|
|
559
|
+
obj.time_top = d2[23]
|
|
560
|
+
|
|
561
|
+
return obj
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
@dataclass
|
|
565
|
+
class MoveEntry:
|
|
566
|
+
"""A checker-play record."""
|
|
567
|
+
|
|
568
|
+
entry_type: EntryType = EntryType.MOVE
|
|
569
|
+
version: int = 0
|
|
570
|
+
|
|
571
|
+
position_i: Position = field(default_factory=tuple) # Before the move
|
|
572
|
+
position_end: Position = field(default_factory=tuple) # After the move
|
|
573
|
+
active_p: int = 0
|
|
574
|
+
moves: tuple[int, ...] = field(default_factory=tuple) # From1,die1,… –1=terminator
|
|
575
|
+
dice: tuple[int, int] = (0, 0)
|
|
576
|
+
cube_a: int = 0
|
|
577
|
+
error_m: float = 0.0 # Unused
|
|
578
|
+
n_move_eval: int = 0
|
|
579
|
+
data_moves: EngineStructBestMoveRecord | None = None
|
|
580
|
+
played: bool = False
|
|
581
|
+
err_move: float = 0.0 # Checker-play error (-1000 = not analysed)
|
|
582
|
+
err_luck: float = 0.0
|
|
583
|
+
comp_choice: int = 0
|
|
584
|
+
init_eq: float = 0.0
|
|
585
|
+
rollout_index_m: tuple[int, ...] = field(default_factory=tuple)
|
|
586
|
+
analyze_m: int = 0
|
|
587
|
+
analyze_l: int = 0
|
|
588
|
+
invalid_m: int = 0
|
|
589
|
+
position_tutor: Position = field(default_factory=tuple)
|
|
590
|
+
tutor: int = 0
|
|
591
|
+
err_tutor_move: float = 0.0
|
|
592
|
+
flagged: bool = False
|
|
593
|
+
comment_move: int = -1
|
|
594
|
+
# v24+
|
|
595
|
+
edited_move: bool = False
|
|
596
|
+
# v26+
|
|
597
|
+
time_delay_move: int = 0
|
|
598
|
+
time_delay_move_done: int = 0
|
|
599
|
+
# v27+
|
|
600
|
+
number_of_auto_double_move: int = 0
|
|
601
|
+
|
|
602
|
+
@classmethod
|
|
603
|
+
def from_stream(cls, stream: BinaryIO, version: int = 0) -> "MoveEntry":
|
|
604
|
+
d = struct.unpack("<9x26b26bxxxl8l2lldl", _read(stream, 124))
|
|
605
|
+
obj = cls(version=version)
|
|
606
|
+
obj.position_i = d[0:26]
|
|
607
|
+
obj.position_end = d[26:52]
|
|
608
|
+
obj.active_p = d[52]
|
|
609
|
+
obj.moves = d[53:61]
|
|
610
|
+
obj.dice = (d[61], d[62])
|
|
611
|
+
obj.cube_a = d[63]
|
|
612
|
+
obj.error_m = d[64]
|
|
613
|
+
obj.n_move_eval = d[65]
|
|
614
|
+
obj.data_moves = EngineStructBestMoveRecord.from_stream(stream)
|
|
615
|
+
|
|
616
|
+
d2 = struct.unpack("<Bxxxddlxxxxd32llll26bbxdBxxxl", _read(stream, 220))
|
|
617
|
+
obj.played = bool(d2[0])
|
|
618
|
+
obj.err_move = d2[1]
|
|
619
|
+
obj.err_luck = d2[2]
|
|
620
|
+
obj.comp_choice = d2[3]
|
|
621
|
+
obj.init_eq = d2[4]
|
|
622
|
+
obj.rollout_index_m = d2[5:37]
|
|
623
|
+
obj.analyze_m = d2[37]
|
|
624
|
+
obj.analyze_l = d2[38]
|
|
625
|
+
obj.invalid_m = d2[39]
|
|
626
|
+
obj.position_tutor = d2[40:66]
|
|
627
|
+
obj.tutor = d2[66]
|
|
628
|
+
obj.err_tutor_move = d2[67]
|
|
629
|
+
obj.flagged = bool(d2[68])
|
|
630
|
+
obj.comment_move = d2[69]
|
|
631
|
+
|
|
632
|
+
if version >= 24:
|
|
633
|
+
obj.edited_move = bool(struct.unpack("<B", _read(stream, 1))[0])
|
|
634
|
+
if version >= 26:
|
|
635
|
+
d3 = struct.unpack("<xxxLL", _read(stream, 11))
|
|
636
|
+
obj.time_delay_move = d3[0]
|
|
637
|
+
obj.time_delay_move_done = d3[1]
|
|
638
|
+
if version >= 27:
|
|
639
|
+
obj.number_of_auto_double_move = struct.unpack("<l", _read(stream, 4))[0]
|
|
640
|
+
|
|
641
|
+
return obj
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
@dataclass
|
|
645
|
+
class FooterGameEntry:
|
|
646
|
+
"""End-of-game summary record."""
|
|
647
|
+
|
|
648
|
+
entry_type: EntryType = EntryType.FOOTER_GAME
|
|
649
|
+
version: int = 0
|
|
650
|
+
|
|
651
|
+
score1g: int = 0
|
|
652
|
+
score2g: int = 0
|
|
653
|
+
crawford_applyg: bool = False
|
|
654
|
+
winner: int = 0 # +1=player1, -1=player2
|
|
655
|
+
points_won: int = 0
|
|
656
|
+
termination: int = (
|
|
657
|
+
0 # 0=drop 1=single 2=gammon 3=backgammon +100=resign +1000=settle
|
|
658
|
+
)
|
|
659
|
+
err_resign: float = 0.0
|
|
660
|
+
err_take_resign: float = 0.0
|
|
661
|
+
eval: tuple[float, ...] = field(default_factory=tuple)
|
|
662
|
+
eval_level: int = 0
|
|
663
|
+
|
|
664
|
+
@classmethod
|
|
665
|
+
def from_stream(cls, stream: BinaryIO, version: int = 0) -> "FooterGameEntry":
|
|
666
|
+
d = struct.unpack("<9xxxxllBxxxlllxxxxdd7dl", _read(stream, 116))
|
|
667
|
+
return cls(
|
|
668
|
+
version=version,
|
|
669
|
+
score1g=d[0],
|
|
670
|
+
score2g=d[1],
|
|
671
|
+
crawford_applyg=bool(d[2]),
|
|
672
|
+
winner=d[3],
|
|
673
|
+
points_won=d[4],
|
|
674
|
+
termination=d[5],
|
|
675
|
+
err_resign=d[6],
|
|
676
|
+
err_take_resign=d[7],
|
|
677
|
+
eval=d[8:15],
|
|
678
|
+
eval_level=d[15],
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@dataclass
|
|
683
|
+
class FooterMatchEntry:
|
|
684
|
+
"""End-of-match summary record."""
|
|
685
|
+
|
|
686
|
+
entry_type: EntryType = EntryType.FOOTER_MATCH
|
|
687
|
+
version: int = 0
|
|
688
|
+
|
|
689
|
+
score1m: int = 0
|
|
690
|
+
score2m: int = 0
|
|
691
|
+
winner_m: int = 0
|
|
692
|
+
elo1m: float = 0.0
|
|
693
|
+
elo2m: float = 0.0
|
|
694
|
+
exp1m: int = 0
|
|
695
|
+
exp2m: int = 0
|
|
696
|
+
datem: str = ""
|
|
697
|
+
|
|
698
|
+
@classmethod
|
|
699
|
+
def from_stream(cls, stream: BinaryIO, version: int = 0) -> "FooterMatchEntry":
|
|
700
|
+
d = struct.unpack("<9xxxxlllddlld", _read(stream, 56))
|
|
701
|
+
return cls(
|
|
702
|
+
version=version,
|
|
703
|
+
score1m=d[0],
|
|
704
|
+
score2m=d[1],
|
|
705
|
+
winner_m=d[2],
|
|
706
|
+
elo1m=d[3],
|
|
707
|
+
elo2m=d[4],
|
|
708
|
+
exp1m=d[5],
|
|
709
|
+
exp2m=d[6],
|
|
710
|
+
datem=str(xgutils.delphidatetimeconv(d[7])),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
@dataclass
|
|
715
|
+
class MissingEntry:
|
|
716
|
+
"""Placeholder for a missing / unknown position."""
|
|
717
|
+
|
|
718
|
+
entry_type: EntryType = EntryType.MISSING
|
|
719
|
+
version: int = 0
|
|
720
|
+
|
|
721
|
+
missing_err_luck: float = 0.0
|
|
722
|
+
missing_winner: int = 0
|
|
723
|
+
missing_points: int = 0
|
|
724
|
+
|
|
725
|
+
@classmethod
|
|
726
|
+
def from_stream(cls, stream: BinaryIO, version: int = 0) -> "MissingEntry":
|
|
727
|
+
d = struct.unpack("<9xxxxxxxxdll", _read(stream, 32))
|
|
728
|
+
return cls(
|
|
729
|
+
version=version,
|
|
730
|
+
missing_err_luck=d[0],
|
|
731
|
+
missing_winner=d[1],
|
|
732
|
+
missing_points=d[2],
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
@dataclass
|
|
737
|
+
class UnimplementedEntry:
|
|
738
|
+
"""Catch-all for record types not yet parsed."""
|
|
739
|
+
|
|
740
|
+
entry_type: EntryType = EntryType.COMMENT
|
|
741
|
+
version: int = 0
|
|
742
|
+
|
|
743
|
+
@classmethod
|
|
744
|
+
def from_stream(cls, stream: BinaryIO, version: int = 0) -> "UnimplementedEntry":
|
|
745
|
+
return cls(version=version)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
# Union type for all possible game-file record payloads.
|
|
749
|
+
GameRecord = (
|
|
750
|
+
HeaderMatchEntry
|
|
751
|
+
| HeaderGameEntry
|
|
752
|
+
| CubeEntry
|
|
753
|
+
| MoveEntry
|
|
754
|
+
| FooterGameEntry
|
|
755
|
+
| FooterMatchEntry
|
|
756
|
+
| MissingEntry
|
|
757
|
+
| UnimplementedEntry
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
# Maps the raw EntryType byte to the appropriate dataclass.
|
|
761
|
+
_RECORD_CLASSES: dict[EntryType, type[GameRecord]] = {
|
|
762
|
+
EntryType.HEADER_MATCH: HeaderMatchEntry,
|
|
763
|
+
EntryType.HEADER_GAME: HeaderGameEntry,
|
|
764
|
+
EntryType.CUBE: CubeEntry,
|
|
765
|
+
EntryType.MOVE: MoveEntry,
|
|
766
|
+
EntryType.FOOTER_GAME: FooterGameEntry,
|
|
767
|
+
EntryType.FOOTER_MATCH: FooterMatchEntry,
|
|
768
|
+
EntryType.COMMENT: UnimplementedEntry,
|
|
769
|
+
EntryType.MISSING: MissingEntry,
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
_SAVE_REC_HDR_SIZE = 9 # 8 unused bytes + 1 EntryType byte
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def read_game_record(stream: BinaryIO, version: int = -1) -> GameRecord | None:
|
|
776
|
+
"""Read one ``TSaveRec`` from *stream* and return the parsed record.
|
|
777
|
+
|
|
778
|
+
Returns ``None`` at end-of-file. The *version* must be the value read
|
|
779
|
+
from the preceding :class:`HeaderMatchEntry`; pass ``-1`` before the
|
|
780
|
+
first header is encountered.
|
|
781
|
+
|
|
782
|
+
The function always advances the stream to the start of the next record
|
|
783
|
+
(records are padded to exactly 2 560 bytes on disk).
|
|
784
|
+
"""
|
|
785
|
+
start_pos = stream.tell()
|
|
786
|
+
raw = stream.read(_SAVE_REC_HDR_SIZE)
|
|
787
|
+
if len(raw) < _SAVE_REC_HDR_SIZE:
|
|
788
|
+
return None
|
|
789
|
+
|
|
790
|
+
entry_type = EntryType(struct.unpack("<8xB", raw)[0])
|
|
791
|
+
stream.seek(-_SAVE_REC_HDR_SIZE, os.SEEK_CUR)
|
|
792
|
+
|
|
793
|
+
cls = _RECORD_CLASSES[entry_type]
|
|
794
|
+
record: GameRecord = cls.from_stream(stream, version=version)
|
|
795
|
+
|
|
796
|
+
# Pad to the fixed record size.
|
|
797
|
+
consumed = stream.tell() - start_pos
|
|
798
|
+
stream.seek(_SAVE_REC_SIZE - consumed, os.SEEK_CUR)
|
|
799
|
+
|
|
800
|
+
return record
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
# ===========================================================================
|
|
804
|
+
# Rollout file (temp.xgr)
|
|
805
|
+
# ===========================================================================
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
@dataclass
|
|
809
|
+
class RolloutContextEntry:
|
|
810
|
+
"""One ``TRolloutContext`` record from ``temp.xgr``."""
|
|
811
|
+
|
|
812
|
+
SIZE = 2184
|
|
813
|
+
|
|
814
|
+
version: int = 0
|
|
815
|
+
|
|
816
|
+
# Inputs
|
|
817
|
+
truncated: bool = False
|
|
818
|
+
error_limited: bool = False
|
|
819
|
+
truncate: int = 0
|
|
820
|
+
min_roll: int = 0
|
|
821
|
+
error_limit: float = 0.0
|
|
822
|
+
max_roll: int = 0
|
|
823
|
+
level1: int = 0
|
|
824
|
+
level2: int = 0
|
|
825
|
+
level_cut: int = 0
|
|
826
|
+
variance: bool = False
|
|
827
|
+
cubeless: bool = False
|
|
828
|
+
time: bool = False
|
|
829
|
+
level1c: int = 0
|
|
830
|
+
level2c: int = 0
|
|
831
|
+
time_limit: int = 0
|
|
832
|
+
truncate_bo: int = 0
|
|
833
|
+
random_seed: int = 0
|
|
834
|
+
random_seed_i: int = 0
|
|
835
|
+
roll_both: bool = False
|
|
836
|
+
search_interval: float = 0.0
|
|
837
|
+
met: int = 0
|
|
838
|
+
first_roll: bool = False
|
|
839
|
+
do_double: bool = False
|
|
840
|
+
extent: bool = False
|
|
841
|
+
|
|
842
|
+
# Outputs
|
|
843
|
+
rolled: int = 0
|
|
844
|
+
double_first: bool = False
|
|
845
|
+
sum1: tuple[float, ...] = field(default_factory=tuple)
|
|
846
|
+
sum_square1: tuple[float, ...] = field(default_factory=tuple)
|
|
847
|
+
sum2: tuple[float, ...] = field(default_factory=tuple)
|
|
848
|
+
sum_square2: tuple[float, ...] = field(default_factory=tuple)
|
|
849
|
+
stdev1: tuple[float, ...] = field(default_factory=tuple)
|
|
850
|
+
stdev2: tuple[float, ...] = field(default_factory=tuple)
|
|
851
|
+
rolled_d: tuple[int, ...] = field(default_factory=tuple)
|
|
852
|
+
error1: float = 0.0
|
|
853
|
+
error2: float = 0.0
|
|
854
|
+
result1: tuple[float, ...] = field(default_factory=tuple)
|
|
855
|
+
result2: tuple[float, ...] = field(default_factory=tuple)
|
|
856
|
+
mwc1: float = 0.0
|
|
857
|
+
mwc2: float = 0.0
|
|
858
|
+
prev_level: int = 0
|
|
859
|
+
prev_eval: tuple[float, ...] = field(default_factory=tuple)
|
|
860
|
+
prev_nd: float = 0.0
|
|
861
|
+
prev_d: float = 0.0
|
|
862
|
+
duration: float = 0.0
|
|
863
|
+
level_trunc: int = 0
|
|
864
|
+
rolled2: int = 0
|
|
865
|
+
multiple_min: int = 0
|
|
866
|
+
multiple_stop_all: bool = False
|
|
867
|
+
multiple_stop_one: bool = False
|
|
868
|
+
multiple_stop_all_value: float = 0.0
|
|
869
|
+
multiple_stop_one_value: float = 0.0
|
|
870
|
+
as_take: bool = False
|
|
871
|
+
rotation: int = 0
|
|
872
|
+
user_interrupted: bool = False
|
|
873
|
+
ver_maj: int = 0
|
|
874
|
+
ver_min: int = 0
|
|
875
|
+
|
|
876
|
+
@classmethod
|
|
877
|
+
def from_stream(cls, stream: BinaryIO, version: int = 0) -> "RolloutContextEntry":
|
|
878
|
+
d = struct.unpack(
|
|
879
|
+
"<BBxxllxxxxdllllBBBxllLlllBxxx"
|
|
880
|
+
"flBBBxlBxxxxxxx37d37d37d37d37d37d37l"
|
|
881
|
+
"ff7f7fffl7fllllllBBxxffBxxxlBxHH",
|
|
882
|
+
_read(stream, 2174),
|
|
883
|
+
)
|
|
884
|
+
return cls(
|
|
885
|
+
version=version,
|
|
886
|
+
truncated=bool(d[0]),
|
|
887
|
+
error_limited=bool(d[1]),
|
|
888
|
+
truncate=d[2],
|
|
889
|
+
min_roll=d[3],
|
|
890
|
+
error_limit=d[4],
|
|
891
|
+
max_roll=d[5],
|
|
892
|
+
level1=d[6],
|
|
893
|
+
level2=d[7],
|
|
894
|
+
level_cut=d[8],
|
|
895
|
+
variance=bool(d[9]),
|
|
896
|
+
cubeless=bool(d[10]),
|
|
897
|
+
time=bool(d[11]),
|
|
898
|
+
level1c=d[12],
|
|
899
|
+
level2c=d[13],
|
|
900
|
+
time_limit=d[14],
|
|
901
|
+
truncate_bo=d[15],
|
|
902
|
+
random_seed=d[16],
|
|
903
|
+
random_seed_i=d[17],
|
|
904
|
+
roll_both=bool(d[18]),
|
|
905
|
+
search_interval=d[19],
|
|
906
|
+
met=d[20],
|
|
907
|
+
first_roll=bool(d[21]),
|
|
908
|
+
do_double=bool(d[22]),
|
|
909
|
+
extent=bool(d[23]),
|
|
910
|
+
rolled=d[24],
|
|
911
|
+
double_first=bool(d[25]),
|
|
912
|
+
sum1=d[26:63],
|
|
913
|
+
sum_square1=d[63:100],
|
|
914
|
+
sum2=d[100:137],
|
|
915
|
+
sum_square2=d[137:174],
|
|
916
|
+
stdev1=d[174:211],
|
|
917
|
+
stdev2=d[211:248],
|
|
918
|
+
rolled_d=d[248:285],
|
|
919
|
+
error1=d[285],
|
|
920
|
+
error2=d[286],
|
|
921
|
+
result1=d[287:294],
|
|
922
|
+
result2=d[294:301],
|
|
923
|
+
mwc1=d[301],
|
|
924
|
+
mwc2=d[302],
|
|
925
|
+
prev_level=d[303],
|
|
926
|
+
prev_eval=d[304:311],
|
|
927
|
+
prev_nd=d[311],
|
|
928
|
+
prev_d=d[312],
|
|
929
|
+
duration=d[313],
|
|
930
|
+
level_trunc=d[314],
|
|
931
|
+
rolled2=d[315],
|
|
932
|
+
multiple_min=d[316],
|
|
933
|
+
multiple_stop_all=bool(d[317]),
|
|
934
|
+
multiple_stop_one=bool(d[318]),
|
|
935
|
+
multiple_stop_all_value=d[319],
|
|
936
|
+
multiple_stop_one_value=d[320],
|
|
937
|
+
as_take=bool(d[321]),
|
|
938
|
+
rotation=d[322],
|
|
939
|
+
user_interrupted=bool(d[323]),
|
|
940
|
+
ver_maj=d[324],
|
|
941
|
+
ver_min=d[325],
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
_ROLLOUT_REC_SIZE = 2184
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def read_rollout_record(
|
|
949
|
+
stream: BinaryIO, version: int = 0
|
|
950
|
+
) -> RolloutContextEntry | None:
|
|
951
|
+
"""Read one rollout record from *stream*.
|
|
952
|
+
|
|
953
|
+
Returns ``None`` at end-of-file. Records are padded to exactly 2 184 bytes.
|
|
954
|
+
"""
|
|
955
|
+
start_pos = stream.tell()
|
|
956
|
+
probe = stream.read(1)
|
|
957
|
+
if not probe:
|
|
958
|
+
return None
|
|
959
|
+
stream.seek(-1, os.SEEK_CUR)
|
|
960
|
+
|
|
961
|
+
record = RolloutContextEntry.from_stream(stream, version=version)
|
|
962
|
+
consumed = stream.tell() - start_pos
|
|
963
|
+
stream.seek(_ROLLOUT_REC_SIZE - consumed, os.SEEK_CUR)
|
|
964
|
+
return record
|