43c8b942a9d8658c92727f622734f4e9f5e6f1ea
[backgammon/xgdatatools.git] / xgstruct.py
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 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 General Public License for more details.
15 #
16 #   You should have received a copy of the GNU General Public License
17 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19 #
20 #   This code is based upon Delphi data structures provided by
21 #   Xavier Dufaure de Citres <contact@extremegammon.com> for purposes
22 #   of interacting with the ExtremeGammon XG file formats. Field
23 #   descriptions derived from xg_format.pas. The file formats are 
24 #   published at http://www.extremegammon.com/xgformat.aspx
25 #
26
27 import xgutils as _xgutils
28 import struct as _struct
29 import os as _os
30 import uuid as _uuid
31 import binascii as _binascii
32
33
34 class GameDataFormatHdrRecord(dict):
35     SIZEOFREC = 8232
36
37     def __init__(self, **kw):
38         defaults = {
39             'MagicNumber': 0,             # $484D4752, RM_MAGICNUMBER
40             'HeaderVersion': 0,           # version
41             'HeaderSize': 0,              # size of the header
42             'ThumbnailOffset': 0,         # location of the thumbnail (jpg)
43             'ThumbnailSize': 0,           # size in bye of the thumbnail
44             'GameGUID': None,             # game id (GUID)
45             'GameName': None,             # Unicode game name
46             'SaveName': None,             # Unicode save name
47             'LevelName': None,            # Unicode level name
48             'Comments': None              # Unicode comments
49             }                              
50         super(GameDataFormatHdrRecord, self).__init__(defaults, **kw)
51
52     def __setattr__(self, key, value):
53         self[key] = value
54
55     def __getattr__(self, key):
56         return self[key]
57
58     def fromstream(self, stream):
59         try:
60             unpacked_data = \
61                     _struct.unpack('<4BiiQiLHHBB6s1024H1024H1024H1024H', 
62                     stream.read(self.SIZEOFREC))
63         except:
64             return None
65
66         self.MagicNumber = bytearray(unpacked_data[0:4][::-1]).decode('ascii')
67         self.HeaderVersion = unpacked_data[4]
68         if self.MagicNumber != 'HMGR' or self.HeaderVersion != 1:
69             return None
70             
71         self.HeaderSize = unpacked_data[5]
72         self.ThumbnailOffset = unpacked_data[6]
73         self.ThumbnailSize = unpacked_data[7]
74     
75         # Convert Delphi 4 component GUID to the 6 components 
76         # of a Python GUID.
77         guidp1, guidp2, guidp3, guidp4, guidp5 = unpacked_data[8:13]
78         guidp6 = int(_binascii.b2a_hex(unpacked_data[13]), 16)
79         self.GameGUID = str(_uuid.UUID(fields=(guidp1, guidp2, guidp3,
80                             guidp4, guidp5, guidp6)))
81
82         self.GameName = _xgutils.utf16intarraytostr(unpacked_data[14:1038])
83         self.SaveName = _xgutils.utf16intarraytostr(unpacked_data[1038:2062])
84         self.LevelName = _xgutils.utf16intarraytostr(unpacked_data[2062:3086])
85         self.Comments = _xgutils.utf16intarraytostr(unpacked_data[3086:4110])
86         return self
87         
88
89 class TimeSettingRecord(dict):
90
91     SIZEOFREC = 32
92
93     def __init__(self, **kw):
94         defaults = {
95             'ClockType': 0,                 # 0=None,0=Fischer,0=Bronstein
96             'PerGame': False,               # time is for session reset after each game
97             'Time1': 0,                     # initial time in sec
98             'Time2': 0,                     # time added (fisher) or reverved (bronstrein) per move in sec
99             'Penalty': 0,                   # point penalty when running our of time (in point)
100             'TimeLeft1': 0,                 # current time left
101             'TimeLeft2': 0,                 # current time left
102             'PenaltyMoney': 0               # point penalty when running our of time (in point)
103             }                              
104         super(TimeSettingRecord, self).__init__(defaults, **kw)
105
106     def __setattr__(self, key, value):
107         self[key] = value
108
109     def __getattr__(self, key):
110         return self[key]
111
112     def fromstream(self, stream):
113         unpacked_data = _struct.unpack(
114             '<lBxxxllllll',
115             stream.read(self.SIZEOFREC))
116         self.ClockType = unpacked_data[0]
117         self.PerGame = bool(unpacked_data[1])
118         self.Time1 = unpacked_data[2]
119         self.Time2 = unpacked_data[3]
120         self.Penalty = unpacked_data[4]
121         self.TimeLeft1 = unpacked_data[5]
122         self.TimeLeft2 = unpacked_data[6]
123         self.PenaltyMoney = unpacked_data[7]
124         return self
125
126
127 class EvalLevelRecord(dict):
128
129     SIZEOFREC = 4
130
131     def __init__(self, **kw):
132         defaults = {
133             'Level': 0,                     # Level used see PLAYERLEVEL table
134             'isDouble': False               # The analyze assume double for the very next move
135             }
136         super(EvalLevelRecord, self).__init__(defaults, **kw)
137
138     def __setattr__(self, key, value):
139         self[key] = value
140
141     def __getattr__(self, key):
142        return self[key]
143
144     def fromstream(self, stream):
145         unpacked_data = _struct.unpack(
146             '<hBb',
147             stream.read(self.SIZEOFREC))
148         self.Level = unpacked_data[0]
149         self.isDouble = bool(unpacked_data[1])
150
151         return self
152
153
154 class EngineStructBestMoveRecord(dict):
155
156     SIZEOFREC = 2184
157
158     def __init__(self, **kw):
159         defaults = {
160             'Pos': None,                    # Current position
161             'Dice': None,                   # Dice
162             'Level': 0,                     # analyze level requested
163             'Score': None,                  # current score
164             'Cube': 0,                      # cube value 1,2,4, etcc.
165             'CubePos': 0,                   # 0: Center 1: Player owns cube -1 Opponent owns cube
166             'Crawford': 0,                  # 1 = Crawford   0 = No Crawford
167             'Jacoby': 0,                    # 1 = Jacoby   0 = No Jacoby
168             'NMoves': 0,                    # number of move (max 32)
169             'PosPlayed': None,              # position played
170             'Moves': None,                  # move list as From1,dice1, from2,dice2 etc.. -1 show termination of list
171             'EvalLevel': None,              # evaluation level of each move
172             'Eval': None,                   # eval value of each move
173             'Unused': 0,                    # if 1 does not count as a decision
174             'met': 0,                       # unused
175             'Choice0': 0,                   # 1-ply choice (index to PosPlayed)
176             'Choice3': 0                    # 3-ply choice (index to PosPlayed)
177             }
178         super(EngineStructBestMoveRecord, self).__init__(defaults, **kw)
179
180     def __setattr__(self, key, value):
181         self[key] = value
182
183     def __getattr__(self, key):
184         return self[key]
185
186     def fromstream(self, stream):
187         unpacked_data = _struct.unpack(
188                 '<26bxx2ll2llllll',
189                 stream.read(68))
190         self.Pos = unpacked_data[0:26]
191         self.Dice = unpacked_data[26:28]
192         self.Level = unpacked_data[28]
193         self.Score = unpacked_data[29:31]
194         self.Cube = unpacked_data[31]
195         self.Cubepos = unpacked_data[32]
196         self.Crawford = unpacked_data[33]
197         self.Jacoby = unpacked_data[34]
198         self.NMoves = unpacked_data[35]
199
200         self.PosPlayed = ()
201         for row in range(32):
202             unpacked_data = _struct.unpack('<26b', stream.read(26))
203             self.PosPlayed += (unpacked_data[0:26],)
204
205         self.Moves = ()
206         for row in range(32):
207             self.Moves += (_struct.unpack('<8b', stream.read(8))[0:8],)
208
209         self.EvalLevel = ()
210         for row in range(32):
211             self.EvalLevel += (EvalLevelRecord().fromstream(stream),)
212
213         self.Eval = ()
214         for row in range(32):
215             unpacked_data = _struct.unpack('<7f', stream.read(28))
216             self.Eval += (unpacked_data,)
217
218         unpacked_data = _struct.unpack('<bbbb', stream.read(4))
219         self.Unused = unpacked_data[0]
220         self.met = unpacked_data[1]
221         self.Choice0 = unpacked_data[2]
222         self.Choice3 = unpacked_data[3]
223
224         return self
225
226
227 class EngineStructDoubleAction(dict):
228
229     SIZEOFREC = 132
230
231     def __init__(self, **kw):
232         defaults = {
233             'Pos': None,                    # Current position
234             'Level': 0,                     # analyze level performed
235             'Score': None,                  # current score
236             'Cube': 0,                      # cube value 1,2,4, etcc.
237             'CubePos': 0,                   # 0: Center 1: Player owns cube -1 Opponent owns cube
238             'Jacoby': 0,                    # 1 = Jacoby   0 = No Jacoby
239             'Crawford': 0,                  # 1 = Crawford   0 = No Crawford
240             'met': 0,                       # unused
241             'FlagDouble': 0,                # 0: Dont double 1: Double
242             'isBeaver': 0,                  # is it a beaver if doubled
243             'Eval': None,                   # eval value for No double
244             'equB': 0.0,                    # equity No Double
245             'equDouble': 0.0,               # equity Double/take
246             'equDrop': 0.0,                 # equity double/drop (-1)
247             'LevelRequest': 0,              # analyze level requested
248             'DoubleChoice3': 0,             # 3-ply choice as double+take*2
249             'EvalDouble': None              # eval value for Double/Take
250             }
251         super(EngineStructDoubleAction, self).__init__(defaults, **kw)
252
253     def __setattr__(self, key, value):
254         self[key] = value
255
256     def __getattr__(self, key):
257        return self[key]
258
259     def fromstream(self, stream):
260         unpacked_data = _struct.unpack(
261                 '<26bxxl2llllhhhh7ffffhh7f',
262                 stream.read(132))
263         self.Pos = unpacked_data[0:26]
264         self.Level = unpacked_data[26]
265         self.Score = unpacked_data[27:29]
266         self.Cube = unpacked_data[29]
267         self.CubePos = unpacked_data[30]
268         self.Jacoby = unpacked_data[31]
269         self.Crawford = unpacked_data[32]
270         self.met = unpacked_data[33]
271         self.FlagDouble = unpacked_data[34]
272         self.isBeaver = unpacked_data[35]
273         self.Eval = unpacked_data[36:43]
274         self.equB = unpacked_data[43]
275         self.equDouble = unpacked_data[44]
276         self.equDrop = unpacked_data[45]
277         self.LevelRequest = unpacked_data[46]
278         self.DoubleChoice3 = unpacked_data[47]
279         self.EvalDouble = unpacked_data[48:55]
280
281         return self
282
283 class HeaderMatchEntry(dict):
284
285     SIZEOFREC = 2560
286
287     def __init__(self, version=0, **kw):
288         defaults = {
289             'Name': 'MatchInfo',
290             'EntryType': GameFileRecord.ENTRYTYPE_HEADERMATCH,
291             'SPlayer1': None,              # player name in ANSI string for XG1 compatbility see "Player1" and "Player2" below for unicode
292             'SPlayer2': None,
293             'MatchLength': 0,              # Match length, 99999 for unlimited
294             'Variation': 0,                # 0:backgammon, 1: Nack, 2: Hyper, 3: Longgammon
295             'Crawford': False,             # Crawford in use
296             'Jacoby': False,               # Jacoby in use
297             'Beaver': False,               # Beaver in use
298             'AutoDouble': False,           # Automatic double in use
299             'Elo1': 0.0,                   # player1 elo
300             'Elo2': 0.0,                   # player2 experience
301             'Exp1': 0,                     # player1 elo
302             'Exp2': 0,                     # player2 experience
303             'Date': 0,                     # game date
304             'SEvent': None,                # event name, in ANSI string for XG1 compatbility see "event" below for unicode
305             'GameId': 0,                   # game ID, if player are swap make gameid:=-GameID
306             'CompLevel1': -1,              # Player level: see table at the end (PLAYERLEVEL TABLE)
307             'CompLevel2': -1,
308             'CountForElo': False,          # outcome of the session will affect elo
309             'AddtoProfile1': False,        # outcome of the session will affect player 1 profile
310             'AddtoProfile2': False,        # outcome of the session will affect player 2 profile
311             'SLocation': None,             # location name, in ANSI string for XG1 compatbility see "location" below for unicode
312             'GameMode': 0,                 # game mode : see table at the end (GAMEMODE TABLE)
313             'Imported': False,             # game was imported from an site (MAT, CBG etc..)
314             'SRound': None,                # round name, in ANSI string for XG1 compatbility see "round" below for unicode
315             'Invert': 0,                   # If the board is swap then invert=-invert and MatchID=-MatchID
316             'Version': version,            # file version, currently SaveFileVersion
317             'Magic': 0x494C4D44,           # must be MagicNumber = $494C4D44;
318             'MoneyInitG': 0,               # initial game played from the profile against that opp in money
319             'MoneyInitScore': [0, 0],      # initial score from the profile against that opp in money
320             'Entered': False,              # entered in profile
321             'Counted': False,              # already accounted in the profile elo
322             'UnratedImp': False,           # game was unrated on the site it was imported from
323             'CommentHeaderMatch': -1,      # index of the match comment header in temp.xgc
324             'CommentFooterMatch': -1,      # index of the match comment footer in temp.xgc
325             'isMoneyMatch': False,         # was player for real money
326             'WinMoney': 0.0,               # amount of money for the winner
327             'LoseMoney': 0.0,              # amount of money for the looser
328             'Currency': 0,                 # currency code from Currency.ini
329             'FeeMoney': 0.0,               # amount of rake
330             'TableStake': 0,               # max amount that can be lost -- NOT IMPLEMENTED
331             'SiteId': -1,                  # site id from siteinfo.ini
332             'CubeLimit': 0,                # v8: maximum cube value
333             'AutoDoubleMax': 0,            # v8: maximum c# of time the autodouble can be used
334             'Transcribed': False,          # v24: game was transcribed
335             'Event': None,                 # v24: Event name (unicode)
336             'Player1': None,               # v24: Player1 name (unicode)
337             'Player2': None,               # v24: Player2 name (unicode)
338             'Location': None,              # v24: Location (unicode)
339             'Round': None,                 # v24: Round (unicode)
340             'TimeSetting': None,           # v25: Time setting for the game
341             'TotTimeDelayMove': 0,         # v26: # of checker play marked for delayed RO
342             'TotTimeDelayCube': 0,         # v26: # of checker play marked for delayed RO done
343             'TotTimeDelayMoveDone': 0,     # v26: # of checker Cube action marked for delayed RO
344             'TotTimeDelayCubeDone': 0,     # v26: # of checker Cube action marked for delayed RO Done
345             'Transcriber': None            # v30: Name of the Transcriber (unicode)
346             }
347         super(HeaderMatchEntry, self).__init__(defaults, **kw)
348
349     def __setattr__(self, key, value):
350         self[key] = value
351
352     def __getattr__(self, key):
353        return self[key]
354
355     def fromstream(self, stream):
356
357         unpacked_data = _struct.unpack(
358             '<9x41B41BxllBBBBddlld129BxxxlllBBB129BlB129BxxllLl2lBBB'
359             'xllBxxxfflfll', stream.read(612))
360         self.SPlayer1 = _xgutils.delphishortstrtostr(unpacked_data[0:41])
361         self.SPlayer2 = _xgutils.delphishortstrtostr(unpacked_data[41:82])
362         self.MatchLength = unpacked_data[82]
363         self.Variation = unpacked_data[83]
364         self.Crawford = bool(unpacked_data[84])
365         self.Jacoby = bool(unpacked_data[85])
366         self.Beaver = bool(unpacked_data[86])
367         self.AutoDouble = bool(unpacked_data[87])
368         self.Elo1 = unpacked_data[88]
369         self.Elo2 = unpacked_data[89]
370         self.Exp1 = unpacked_data[90]
371         self.Exp2 = unpacked_data[91]
372         self.Date = str(_xgutils.delphidatetimeconv(unpacked_data[92]))
373         self.SEvent = _xgutils.delphishortstrtostr(unpacked_data[93:222])
374         self.GameId = unpacked_data[222]
375         self.CompLevel1 = unpacked_data[223]
376         self.CompLevel2 = unpacked_data[224]
377         self.CountForElo = bool(unpacked_data[225])
378         self.AddtoProfile1 = bool(unpacked_data[226])
379         self.AddtoProfile2 = bool(unpacked_data[227])
380         self.SLocation = _xgutils.delphishortstrtostr(unpacked_data[228:357])
381         self.GameMode = unpacked_data[357]
382         self.Imported = bool(unpacked_data[358])
383         self.SRound = _xgutils.delphishortstrtostr(unpacked_data[359:487])
384         self.Invert = unpacked_data[488]
385         self.Version = unpacked_data[489]
386         self.Magic = unpacked_data[490]
387         self.MoneyInitG = unpacked_data[491]
388         self.MoneyInitScore = unpacked_data[492:494]
389         self.Entered = bool(unpacked_data[494])
390         self.Counted = bool(unpacked_data[495])
391         self.UnratedImp = bool(unpacked_data[496])
392         self.CommentHeaderMatch = unpacked_data[497]
393         self.CommentFooterMatch = unpacked_data[498]
394         self.isMoneyMatch = bool(unpacked_data[499])
395         self.WinMoney = unpacked_data[500]
396         self.LoseMoney = unpacked_data[501]
397         self.Currency = unpacked_data[502]
398         self.FeeMoney = unpacked_data[503]
399         self.TableStake = unpacked_data[504]
400         self.SiteId = unpacked_data[505]
401         if self.Version >= 8:
402             unpacked_data = _struct.unpack('<ll', stream.read(8))
403             self.CubeLimit = unpacked_data[0]
404             self.AutoDoubleMax = unpacked_data[1]
405         if self.Version >= 24:
406             unpacked_data = _struct.unpack('<Bx129H129H129H129H129H',
407                                            stream.read(1292))
408             self.Transcribed = bool(unpacked_data[0])
409             self.Event = _xgutils.utf16intarraytostr(unpacked_data[1:130])
410             self.Player1 = _xgutils.utf16intarraytostr(unpacked_data[130:259])
411             self.Player2 = _xgutils.utf16intarraytostr(unpacked_data[259:388])
412             self.Location = _xgutils.utf16intarraytostr(unpacked_data[388:517])
413             self.Round = _xgutils.utf16intarraytostr(unpacked_data[517:646])
414         if self.Version >= 25:
415             self.TimeSetting = TimeSettingRecord().fromstream(stream)
416         if self.Version >= 26:
417             unpacked_data = _struct.unpack('<llll', stream.read(16))
418             self.TotTimeDelayMove = unpacked_data[0]
419             self.TotTimeDelayCube = unpacked_data[1]
420             self.TotTimeDelayMoveDone = unpacked_data[2]
421             self.TotTimeDelayCubeDone = unpacked_data[3]
422         if self.Version >= 30:
423             unpacked_data = _struct.unpack('<129H', stream.read(258))
424             self.Transcriber = _xgutils.utf16intarraytostr(
425                 unpacked_data[0:129])
426
427         return self
428
429
430 class FooterGameEntry(dict):
431
432     SIZEOFREC = 2560
433
434     def __init__(self, **kw):
435         defaults = {
436             'Name': 'GameFooter',
437             'EntryType': GameFileRecord.ENTRYTYPE_FOOTERGAME,
438             'Score1g': 0,                   # Final score
439             'Score2g': 0,                   # Final score
440             'CrawfordApplyg': False,        # will crawford apply next game
441             'Winner': 0,                    # who win +1=player1, -1 player 2
442             'PointsWon': 0,                 # point scored
443             'Termination': 0,               # 0=Drop 1=single 2=gammon 3=Backgamon 
444                                             # (0,1,2)+100=Resign  (0,1,2)+1000 settle
445             'ErrResign': 0.0,               # error made by resigning (-1000 if not analyze)
446             'ErrTakeResign': 0.0,           # error made by accepting the resign (-1000 if not analyze)
447             'Eval': None,                   # evaluation of the final position
448             'EvalLevel': 0
449             }
450         super(FooterGameEntry, self).__init__(defaults, **kw)
451
452     def __setattr__(self, key, value):
453         self[key] = value
454
455     def __getattr__(self, key):
456        return self[key]
457
458     def fromstream(self, stream):
459         unpacked_data = _struct.unpack('<9xxxxllBxxxlllxxxxdd7dl',
460                 stream.read(116))
461         self.Score1g = unpacked_data[0]
462         self.Score2g = unpacked_data[1]
463         self.CrawfordApplyg = bool(unpacked_data[2])
464         self.Winner = unpacked_data[3]
465         self.PointsWon = unpacked_data[4]
466         self.Termination = unpacked_data[5]
467         self.ErrResign = unpacked_data[6]
468         self.ErrTakeResign = unpacked_data[7]
469         self.Eval = unpacked_data[8:15]
470         self.EvalLevel = unpacked_data[15]
471         return self
472
473
474 class MissingEntry(dict):
475
476     SIZEOFREC = 2560
477
478     def __init__(self, **kw):
479         defaults = {
480             'Name': 'Missing',
481             'EntryType': GameFileRecord.ENTRYTYPE_MISSING,
482             'MissingErrLuck': 0.0,
483             'MissingWinner': 0,
484             'MissingPoints': 0
485             }
486         super(MissingEntry, self).__init__(defaults, **kw)
487
488     def __setattr__(self, key, value):
489         self[key] = value
490
491     def __getattr__(self, key):
492        return self[key]
493
494     def fromstream(self, stream):
495         unpacked_data = _struct.unpack('<9xxxxxxxxdll', stream.read(32))
496         self.MissingErrLuck = unpacked_data[0]
497         self.MissingWinner = unpacked_data[1]
498         self.MissingPoints = unpacked_data[2]
499         return self
500
501
502 class FooterMatchEntry(dict):
503
504     SIZEOFREC = 2560
505
506     def __init__(self, **kw):
507         defaults = {
508             'Name': 'MatchFooter',
509             'EntryType': GameFileRecord.ENTRYTYPE_FOOTERMATCH,
510             'Score1m': 0,                   # Final score of the match
511             'Score2m': 0,                   # Final score of the match
512             'WinnerM': 0,                   # who win +1=player1, -1 player 2
513             'Elo1m': 0.0,                   # resulting elo, player1
514             'Elo2m': 0.0,                   # resulting elo, player2
515             'Exp1m': 0,                     # resulting exp, player1
516             'Exp2m': 0,                     # resulting exp, player2
517             'Datem': 0.0                    # Date time of the match end
518             }
519         super(FooterMatchEntry, self).__init__(defaults, **kw)
520
521     def __setattr__(self, key, value):
522         self[key] = value
523
524     def __getattr__(self, key):
525        return self[key]
526
527     def fromstream(self, stream):
528         unpacked_data = _struct.unpack('<9xxxxlllddlld', stream.read(56))
529         self.Score1m = unpacked_data[0]
530         self.Score2m = unpacked_data[1]
531         self.WinnerM = unpacked_data[2]
532         self.Elo1m = unpacked_data[3]
533         self.Elo2m = unpacked_data[4]
534         self.Exp1m = unpacked_data[5]
535         self.Exp2m = unpacked_data[6]
536         self.Datem = str(_xgutils.delphidatetimeconv(unpacked_data[7]))
537
538         return self
539
540
541 class HeaderGameEntry(dict):
542
543     SIZEOFREC = 2560
544
545     def __init__(self, **kw):
546         defaults = {
547             'Name': 'GameHeader',
548             'EntryType': GameFileRecord.ENTRYTYPE_HEADERGAME,
549             'Score1': 0,                    # initial score player1
550             'Score2': 0,                    # initial score player1
551             'CrawfordApply': False,         # iDoes Crawford apply on that game
552             'PosInit': (0,) * 26,           # initial position
553             'GameNumber': 0,                # Game number (start at 1)
554             'InProgress': False,            # Game is still in progress
555             'CommentHeaderGame': -1,        # index of the game comment header in temp.xgc
556             'CommentFooterGame': -1,        # index of the game comment footer in temp.xgc
557             'NumberOfAutoDoubles': 0        # v26: Number of Autodouble that happen in that game
558                                             # note that in the rest of the game the cube still start at 1.
559                                             # For display purpose or point calculation add the 2^NumberOfAutoDouble
560             }
561         super(HeaderGameEntry, self).__init__(defaults, **kw)
562
563     def __setattr__(self, key, value):
564         self[key] = value
565
566     def __getattr__(self, key):
567        return self[key]
568
569     def fromstream(self, stream):
570         unpacked_data = _struct.unpack('<9xxxxllB26bxlBxxxlll',
571                 stream.read(68))
572         self.Score1 = unpacked_data[0]
573         self.Score2 = unpacked_data[1]
574         self.CrawfordApply = bool(unpacked_data[2])
575         self.PosInit = unpacked_data[3:29]
576         self.GameNumber = unpacked_data[29]
577         self.InProgress = bool(unpacked_data[30])
578         self.CommentHeaderGame = unpacked_data[31]
579         self.CommentFooterGame = unpacked_data[32]
580         if self.Version >= 26:
581             self.NumberOfAutoDoubles = unpacked_data[33]
582
583         return self
584
585
586 class CubeEntry(dict):
587
588     SIZEOFREC = 2560
589
590     def __init__(self, **kw):
591         defaults = {
592             'Name': 'Cube',
593             'EntryType': GameFileRecord.ENTRYTYPE_CUBE,
594             'ActiveP': 0,                   # Active player (1 or 2)
595             'Double': 0,                    # player double (0= no, 1=yes)
596             'Take': 0,                      # opp take (0= no, 1=yes, 2=beaver )
597             'BeaverR': 0,                   # player accept beaver (0= no, 1=yes, 2=raccoon)
598             'RaccoonR': 0,                  # player accept raccoon (0= no, 1=yes)
599             'CubeB': 0,                     # Cube value 0=center, +1=2 own, +2=4 own ... -1=2 opp, -2=4 opp
600             'Position': None,               # initial position
601             'Doubled': None,                # Analyze result
602             'ErrCube': 0.0,                 # error made on doubling (-1000 if not analyze)
603             'DiceRolled': None,             # dice rolled
604             'ErrTake': 0.0,                 # error made on taking (-1000 if not analyze)
605             'RolloutIndexD': 0,             # index of the Rollout in temp.xgr
606             'CompChoiceD': 0,               # 3-ply choice as Double+2*take
607             'AnalyzeC': 0,                  # Level of the analyze
608             'ErrBeaver': 0.0,               # error made on beavering (-1000 if not analyze)
609             'ErrRaccoon': 0.0,              # error made on racconning (-1000 if not analyze)
610             'AnalyzeCR': 0,                 # requested Level of the analyze (sometime a XGR+ request will stop at 4-ply when obivous)
611             'isValid': 0,                   # invalid decision 0=Ok, 1=error, 2=invalid
612             'TutorCube': 0,                 # player initial double in tutor mode (0= no, 1=yes)
613             'TutorTake': 0,                 # player initial take in tutor mode (0= no, 1=yes)
614             'ErrTutorCube': 0.0,            # error initialy made on doubling (-1000 if not analyze)
615             'ErrTutorTake': 0.0,            # error initialy made on taking (-1000 if not analyze)
616             'FlaggedDouble': False,         # cube has been flagged
617             'CommentCube': -1,              # index of the cube comment in temp.xgc
618             'EditedCube': False,            # v24: Position was edited at this point
619             'TimeDelayCube': False,         # v26: position is marked for later RO
620             'TimeDelayCubeDone': False,     # v26: position later RO has been done
621             'NumberOfAutoDoubleCube': 0,    # v27: Number of Autodouble that happen in that game
622             'TimeBot': 0,                   # v28: time left for both players
623             'TimeTop': 0
624             }
625         super(CubeEntry, self).__init__(defaults, **kw)
626
627     def __setattr__(self, key, value):
628         self[key] = value
629
630     def __getattr__(self, key):
631        return self[key]
632
633     def fromstream(self, stream):
634         unpacked_data = _struct.unpack('<9xxxxllllll26bxx',
635                                        stream.read(64))
636         self.ActiveP = unpacked_data[0]
637         self.Double = unpacked_data[1]
638         self.Take = unpacked_data[2]
639         self.BeaverR = unpacked_data[3]
640         self.RaccoonR = unpacked_data[4]
641         self.CubeB = unpacked_data[5]
642         self.Position = unpacked_data[6:32]
643         self.Doubled = EngineStructDoubleAction().fromstream(stream)
644         unpacked_data = _struct.unpack('<xxxxd3Bxxxxxdlllxxxx' \
645                                        'ddllbbxxxxxxddBxxxlBBBxlll',
646                                        stream.read(116))
647         self.ErrCube = unpacked_data[0]
648         self.DiceRolled = _xgutils.delphishortstrtostr(unpacked_data[1:4])
649         self.ErrTake = unpacked_data[4]
650         self.RolloutIndexD = unpacked_data[5]
651         self.CompChoiceD = unpacked_data[6]
652         self.AnalyzeC = unpacked_data[7]
653         self.ErrBeaver = unpacked_data[8]
654         self.ErrRaccoon = unpacked_data[9]
655         self.AnalyzeCR = unpacked_data[10]
656         self.isValid = unpacked_data[11]
657         self.TutorCube = unpacked_data[12]
658         self.TutorTake = unpacked_data[13]
659         self.ErrTutorCube = unpacked_data[14]
660         self.ErrTutorTake = unpacked_data[15]
661         self.FlaggedDouble = bool(unpacked_data[16])
662         self.CommentCube = unpacked_data[17]
663         if self.Version >= 24:
664             self.EditedCube = bool(unpacked_data[18])
665         if self.Version >= 26:
666             self.TimeDelayCube = bool(unpacked_data[19])
667             self.TimeDelayCubeDone = bool(unpacked_data[20])
668         if self.Version >= 27:
669             self.NumberOfAutoDoubleCube = unpacked_data[21]
670         if self.Version >= 28:
671             self.TimeBot = unpacked_data[22]
672             self.TimeTop = unpacked_data[23]
673         return self
674
675
676 class MoveEntry(dict):
677
678     SIZEOFREC = 2560
679
680     def __init__(self, **kw):
681         defaults = {
682             'Name:': 'Move',
683             'EntryType': GameFileRecord.ENTRYTYPE_MOVE,
684             'PositionI': None,              # Initial position
685             'PositionEnd': None,            # Final Position
686             'ActiveP': 0,                   # active player (1,2)
687             'Moves': None,                  # list of move as From1,dice1, from2,dice2 etc.. -1 show termination of list
688             'Dice': None,                   # dice rolled
689             'CubeA': 0,                     # Cube value 0=center, +1=2 own, +2=4 own ... -1=2 opp, -2=4 opp
690             'ErrorM': 0,                    # Not used anymore (not sure)
691             'NMoveEval': 0,                 # Number of candidate (max 32)
692             'DataMoves': None,              # analyze
693             'Played': False,                # move was played
694             'ErrMove': 0.0,                 # error made (-1000 if not analyze)
695             'ErrLuck': 0.0,                 # luck of the roll
696             'CompChoice': 0,                # computer choice (index to DataMoves.moveplayed)
697             'InitEq': 0.0,                  # Equity before the roll (for luck purposes)
698             'RolloutIndexM': None,          # index of the Rollout in temp.xgr
699             'AnalyzeM': 0,                  # level of analyze of the move
700             'AnalyzeL': 0,                  # level of analyze for the luck
701             'InvalidM': 0,                  # invalid decision 0=Ok, 1=error, 2=invalid
702             'PositionTutor': None,          # Position resulting of the initial move
703             'Tutor': 0,                     # index of the move played dataMoves.moveplayed
704             'ErrTutorMove': 0.0,            # error initialy made (-1000 if not analyze)
705             'Flagged': False,               # move has been flagged
706             'CommentMove': -1,              # index of the move comment in temp.xgc
707             'EditedMove': False,            # v24: Position was edited at this point
708             'TimeDelayMove': 0,             # v26: Bit list: position is marked for later RO
709             'TimeDelayMoveDone': 0,         # v26: Bit list: position later RO has been done
710             'NumberOfAutoDoubleMove': 0     # v27: Number of Autodouble that happen in that game
711             }
712         super(MoveEntry, self).__init__(defaults, **kw)
713
714     def __setattr__(self, key, value):
715         self[key] = value
716
717     def __getattr__(self, key):
718        return self[key]
719
720     def fromstream(self, stream):
721         unpacked_data = _struct.unpack('<9x26b26bxxxl8l2lldl',
722                                        stream.read(124))
723         self.PositionI = unpacked_data[0:26]
724         self.PositionEnd = unpacked_data[26:52]
725         self.ActiveP = unpacked_data[52]
726         self.Moves = unpacked_data[53:61]
727         self.Dice = unpacked_data[61:63]
728         self.CubeA = unpacked_data[63]
729         self.ErrorM = unpacked_data[64] # Not used
730         self.NMoveEval = unpacked_data[65]
731         self.DataMoves = EngineStructBestMoveRecord().fromstream(stream)
732
733         unpacked_data = _struct.unpack('<Bxxxddlxxxxd32llll26bbxdBxxxl',
734                                        stream.read(220))
735         self.Played = bool(unpacked_data[0])
736         self.ErrMove = unpacked_data[1]
737         self.ErrLuck = unpacked_data[2]
738         self.CompChoice = unpacked_data[3]
739         self.InitEq = unpacked_data[4]
740         self.RolloutIndexM = unpacked_data[5:37]
741         self.AnalyzeM = unpacked_data[37]
742         self.AnalyzeL = unpacked_data[38]
743         self.InvalidM = unpacked_data[39]
744         self.PositionTutor = unpacked_data[40:66]
745         self.Tutor = unpacked_data[66]
746         self.ErrTutorMove = unpacked_data[67]
747         self.Flagged = bool(unpacked_data[68])
748         self.CommentMove = unpacked_data[69]
749         if self.Version >= 24:
750             unpacked_data = _struct.unpack('<B', stream.read(1))
751             self.EditedMove = bool(unpacked_data[0])
752         if self.Version >= 26:
753             unpacked_data = _struct.unpack('<xxxLL', stream.read(11))
754             self.TimeDelayMove = unpacked_data[0]
755             self.TimeDelayMoveDone = unpacked_data[1]
756         if self.Version >= 27:
757             unpacked_data = _struct.unpack('<l', stream.read(4))
758             self.NumberOfAutoDoubleMove = unpacked_data[0]
759
760         return self
761
762
763 class UnimplementedEntry(dict):
764
765     """ Class for record types we have yet to implement
766     """
767
768     SIZEOFREC = 2560
769
770     def __init__(self, **kw):
771         defaults = {
772             'Name': 'Unimplemented'
773             }
774         super(UnimplementedEntry, self).__init__(defaults, **kw)
775
776     def __setattr__(self, key, value):
777         self[key] = value
778
779     def __getattr__(self, key):
780        return self[key]
781
782     def fromstream(self, stream):
783         return self
784
785
786 class GameFileRecord(dict):
787
788     __SIZEOFSRHDR = 9
789     __REC_CLASSES = [HeaderMatchEntry, HeaderGameEntry,
790                      CubeEntry, MoveEntry,
791                      FooterGameEntry, FooterMatchEntry,
792                      UnimplementedEntry, MissingEntry]
793
794     ENTRYTYPE_HEADERMATCH, ENTRYTYPE_HEADERGAME, ENTRYTYPE_CUBE, \
795             ENTRYTYPE_MOVE, ENTRYTYPE_FOOTERGAME, ENTRYTYPE_FOOTERMATCH, \
796             ENTRYTYPE_MISSING, ENTRYTYPE_UNIMPLEMENTED = range(8)
797
798     def __init__(self, version=-1, **kw):
799         """ Create a game file record based upon the given file version
800         number. The file version is first found in a HeaderMatchEntry
801         object. The version needs to be propogated to all other game
802         file objects within the same archive.
803         """
804         defaults = {
805             'Name': 'GameFileRecord',
806             'EntryType': -1,
807             'Record': None,
808             'Version': version
809             }
810         super(GameFileRecord, self).__init__(defaults, **kw)
811
812     def __setattr__(self, key, value):
813         self[key] = value
814
815     def __getattr__(self, key):
816        return self[key]
817
818     def fromstream(self, stream):
819         # Read the header. First 8 bytes are unused. 9th byte is record type
820         # The record type determines what object to create and load.
821         # If we catch a struct.error we have hit the EOF.
822         startpos = stream.tell()
823         try:
824             unpacked_data = _struct.unpack('<8xB',
825                                            stream.read(self.__SIZEOFSRHDR))
826         except _struct.error:
827             return None
828         self.EntryType = unpacked_data[0]
829
830         # Back up to the beginning of the record after getting the record
831         # type and feed the entire stream back into the corresponding
832         # record object.
833         stream.seek(-self.__SIZEOFSRHDR, _os.SEEK_CUR)
834
835         # Using the appropriate class, read the data stream
836         self.Record = self.__REC_CLASSES[self.EntryType]()
837         self.Record.Version = self.Version
838         self.Record.fromstream(stream)
839         realrecsize = stream.tell() - startpos
840
841         # Each record is actually 2560 bytes long. We need to advance past
842         # the unused filler data to be at the start of the next record
843         stream.seek(self.Record.SIZEOFREC - realrecsize, _os.SEEK_CUR)
844
845         return self.Record
846
847
848 class RolloutContextEntry(dict):
849
850     SIZEOFREC = 2184
851
852     def __init__(self, **kw):
853         defaults = {
854             'Name': 'Rollout',
855             'EntryType': RolloutFileRecord.ROLLOUTCONTEXT,
856             'Truncated': False,             # is truncated
857             'ErrorLimited': False,          # stop when CI under "ErrorLimit"
858             'Truncate': 0,                  # truncation level
859             'MinRoll': 0,                   # minimum games to roll
860             'ErrorLimit': 0.0,              # CI to stop the RO
861             'MaxRoll': 0,                   # maximum games to roll
862             'Level1': 0,                    # checker play Level used before "LevelCut"
863             'Level2': 0,                    # checker play Level used on and after "LevelCut"
864             'LevelCut': 0,                  # Cutoff for level1 and level2
865             'Variance': False,              # use variance reduction
866             'Cubeless': False,              # is a cubeless ro
867             'Time': False,                  # is time limited
868             'Level1C': 0,                   # cube Level used before "LevelCut"
869             'Level2C': 0,                   # cube Level used on and after "LevelCut"
870             'TimeLimit': 0,                 # limit in time (min)
871             'TruncateBO': 0,                # what do do when reaching BO db: 0=nothing; 1=?
872             'RandomSeed': 0,                # caculated seed=RandomSeedI+hashpos
873             'RandomSeedI': 0,               # used entered seed
874             'RollBoth': False,              # roll both line (ND and D/T)
875             'SearchInterval': 0.0,          # Search interval used (1=normal, 1.5=large, 2=huge, 4=gigantic)
876             'met': 0,                       # unused
877             'FirstRoll': False,             # is it a first roll rollout
878             'DoDouble': False,              # roll both line (ND and D/T) in multiple rollout
879             'Extent': False,                # if the ro is extended
880             'Rolled': 0,                    # game rolled
881             'DoubleFirst': False,           # a double happens immediatly.
882             'Sum1': None,                   # sum of equities for all 36 1st roll
883             'SumSquare1': None,             # sum of square equities for all 36 1st roll
884             'Sum2': None,                   # D/T sum of equities for all 36 1st roll
885             'SumSquare2': None,             # D/T sum of square equities for all 36 1st roll
886             'Stdev1': None,                 # Standard deviation for all 36 1st roll
887             'Stdev2': None,                 # D/T Stand deviation for all 36 1st roll
888             'RolledD': None,                # number of game rolled for all 36 1st roll
889             'Error1': 0.0,                  # 95% CI
890             'Error2': 0.0,                  # D/T 95% CI
891             'Result1': None,                # evaluation of the position
892             'Result2': None,                # D/T evaluation of the position
893             'Mwc1': 0.0,                    # ND  mwc equivalent of result1[1,6]
894             'Mwc2': 0.0,                    # D/T mwc equivalent of result2[1,6]
895             'PrevLevel': 0,                 # store the previous analyze level (for deleting RO)
896             'PrevEval': None,               # store the previous analyze result (for deleting RO)
897             'PrevND': 0.0,                  # store the previous analyze equities (for deleting RO)
898             'PrevD': 0.0,                   
899             'Duration': 0.0,                # duration in seconds
900             'LevelTrunc': 0,                # level used at truncation
901             'Rolled2': 0,                   # D/T number of game rolled
902             'MultipleMin': 0,               # Multiple RO minimum # of game
903             'MultipleStopAll': False,       # Multiple RO stop all if one move reach MultipleStopAllValue
904             'MultipleStopOne': False,       # Multiple RO stop one move is reach under MultipleStopOneValue
905             'MultipleStopAllValue': 0.0,    # value to stop all RO (for instance 99.9%)
906             'MultipleStopOneValue': 0.0,    # value to stop one move(for instance 0.01%)
907             'AsTake': False,                # when running ND and D/T if AsTake is true, checker decision are made using the cube position in the D/T line
908             'Rotation': 0,                  # 0=36 dice, 1=21 dice (XG1), 2=30 dice (for 1st pos)
909             'UserInterrupted': False,       # RO was interrupted by user
910             'VerMaj': 0,                    # Major version use for the RO, currently (2.20): 2
911             'VerMin': 0                     # Minor version use for the RO, currently (2.10): 10 (no change in RO or engine between 2.10 and 2.20)
912             }
913         super(RolloutContextEntry, self).__init__(defaults, **kw)
914
915     def __setattr__(self, key, value):
916         self[key] = value
917
918     def __getattr__(self, key):
919        return self[key]
920
921     def fromstream(self, stream):
922         unpacked_data = _struct.unpack('<BBxxllxxxxdllllBBBxllLlllBxxx' \
923                                        'flBBBxlBxxxxxxx37d37d37d37d37d37d37l' \
924                                        'ff7f7fffl7fllllllBBxxffBxxxlBxHH',
925                                        stream.read(2174))
926
927         self.Truncated = bool(unpacked_data[0])
928         self.ErrorLimited = bool(unpacked_data[1])
929         self.Truncate = unpacked_data[2]
930         self.MinRoll = unpacked_data[3]
931         self.ErrorLimit = unpacked_data[4]
932         self.MaxRoll = unpacked_data[5]
933         self.Level1 = unpacked_data[6]
934         self.Level2 = unpacked_data[7]
935         self.LevelCut = unpacked_data[8]
936         self.Variance = bool(unpacked_data[9])
937         self.Cubeless = bool(unpacked_data[10])
938         self.Time = bool(unpacked_data[11])
939         self.Level1C = unpacked_data[12]
940         self.Level2C = unpacked_data[13]
941         self.TimeLimit = unpacked_data[14]
942         self.TruncateBO = unpacked_data[15]
943         self.RandomSeed = unpacked_data[16]
944         self.RandomSeedI = unpacked_data[17]
945         self.RollBoth = bool(unpacked_data[18])
946         self.SearchInterval = unpacked_data[19]
947         self.met = unpacked_data[20]
948         self.FirstRoll = bool(unpacked_data[21])
949         self.DoDouble = bool(unpacked_data[22])
950         self.Extent = bool(unpacked_data[23])
951         self.Rolled = unpacked_data[24]
952         self.DoubleFirst = bool(unpacked_data[25])
953         self.Sum1 = unpacked_data[26:63]
954         self.SumSquare1 = unpacked_data[63:100]
955         self.Sum2 = unpacked_data[100:137]
956         self.SumSquare2 = unpacked_data[137:174]
957         self.Stdev1 = unpacked_data[174:211]
958         self.Stdev2 = unpacked_data[211:248]
959         self.RolledD = unpacked_data[248:285]
960         self.Error1 = unpacked_data[285]
961         self.Error2 = unpacked_data[286]
962         self.Result1 = unpacked_data[287:294]
963         self.Result2 = unpacked_data[294:301]
964         self.Mwc1 = unpacked_data[301]
965         self.Mwc2 = unpacked_data[302]
966         self.PrevLevel = unpacked_data[303]
967         self.PrevEval = unpacked_data[304:311]
968         self.PrevND = unpacked_data[311]
969         self.PrevD = unpacked_data[312]
970         self.Duration = unpacked_data[313]
971         self.LevelTrunc = unpacked_data[314]
972         self.Rolled2 = unpacked_data[315]
973         self.MultipleMin = unpacked_data[316]
974         self.MultipleStopAll = bool(unpacked_data[317])
975         self.MultipleStopOne = bool(unpacked_data[318])
976         self.MultipleStopAllValue = unpacked_data[319]
977         self.MultipleStopOneValue = unpacked_data[320]
978         self.AsTake = bool(unpacked_data[321])
979         self.Rotation = unpacked_data[322]
980         self.UserInterrupted = bool(unpacked_data[323])
981         self.VerMaj = unpacked_data[324]
982         self.VerMin = unpacked_data[325]
983
984         return self
985
986
987 class RolloutFileRecord(dict):
988
989     ROLLOUTCONTEXT = 0
990
991     def __init__(self, version=-1, **kw):
992         """ Create a game file record based upon the given file version
993         number. The file version is first found in a HeaderMatchEntry
994         object. The version needs to be propogated to all other game
995         file objects within the same archive.
996         """
997         defaults = {
998             'Name': 'RolloutFileRecord',
999             'EntryType': 0,
1000             'Record': None,
1001             'Version': version
1002             }
1003         super(RolloutFileRecord, self).__init__(defaults, **kw)
1004
1005     def __setattr__(self, key, value):
1006         self[key] = value
1007
1008     def __getattr__(self, key):
1009        return self[key]
1010
1011     def fromstream(self, stream):
1012         # If we are at EOF then return
1013         if len(stream.read(1)) <= 0:
1014             return None
1015
1016         stream.seek(-1, _os.SEEK_CUR)
1017         startpos = stream.tell()
1018
1019         # Using the appropriate class, read the data stream
1020         self.Record = RolloutContextEntry()
1021         self.Record.Version = self.Version
1022         self.Record.fromstream(stream)
1023         realrecsize = stream.tell() - startpos
1024         # Each record is actually 2184 bytes long. We need to advance past
1025         # the unused filler data to be at the start of the next record
1026         stream.seek(self.Record.SIZEOFREC - realrecsize, _os.SEEK_CUR)
1027
1028         return self.Record
1029
1030
1031 if __name__ == '__main__':
1032     pass