Here they are - I don't remember where I had downloaded them from

On Fri, Aug 12, 2022 at 12:01 PM Øystein Schønning-Johansen <
[email protected]> wrote:

> Thank you so much, Turker!
>
> However, it would still be good to have the Petch-tools. :-)
>
> -Øystein
>
> fre. 12. aug. 2022 kl. 16:58 skrev Turker Eflanli <[email protected]
> >:
>
>> Øystein,
>>
>> Find attached the raw file that you can analyze in GNU
>>
>> On Fri, Aug 12, 2022 at 10:12 AM Øystein Schønning-Johansen <
>> [email protected]> wrote:
>>
>>> Hi!
>>>
>>> I was thinking of analysing the WC 2022 final and found the final
>>> available in .xg format.
>>>
>>> I think Michael Petch wrote some tools to convert xg file into sgf
>>> format. Are these tools available somewhere?
>>>
>>> It states at: https://www.extremegammon.com/XGformat.aspx
>>> that the python source for his tools are here:
>>> http://vcs.capp-sysware.com/gitweb/?p=backgammon/xgdatatools.git
>>>
>>> However, it looks like this site is down.
>>>
>>> BTW: How is Michael? Is he still subscribing to this list? I've not
>>> heard from him in a long time. He posts on his FB profile, but he isn't
>>> very responsive. :-)
>>>
>>> -Øystein
>>>
>>
#
#   xgimport.py - XG import module
#   Copyright (C) 2013,2014  Michael Petch <[email protected]>
#                                          <[email protected]>
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Lesser General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU Lesser General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#

from __future__ import with_statement as _with
import tempfile as _tempfile
import shutil as _shutil
import struct as _struct
import os as _os
import xgutils as _xgutils
import xgzarc as _xgzarc
import xgstruct as _xgstruct


class Import(object):

    class Segment(object):
        GDF_HDR, GDF_IMAGE, XG_GAMEHDR, XG_GAMEFILE, XG_ROLLOUTS, XG_COMMENT, \
            ZLIBARC_IDX, XG_UNKNOWN = range(8)
        EXTENSIONS = ['_gdh.bin', '.jpg', '_gamehdr.bin', '_gamefile.bin',
                      '_rollouts.bin', '_comments.bin', '_idx.bin', None]
        GDF_HDR_EXT, GDF_IMAGE_EXT, XG_GAMEHDR_EXT, XG_GAMEFILE_EXT, \
            XG_ROLLOUTS_EXT, XG_COMMENTS_EXT, \
            XG_IDX_EXT, XG_UNKNOWN = EXTENSIONS
        XG_FILEMAP = {'temp.xgi': XG_GAMEHDR, 'temp.xgr': XG_ROLLOUTS,
                      'temp.xgc': XG_COMMENT, 'temp.xg': XG_GAMEFILE}

        XG_GAMEHDR_LEN = 556

        __TMP_PREFIX = 'tmpXGI'

        def __init__(self, type=GDF_HDR, delete=True, prefix=__TMP_PREFIX):
            self.filename = None
            self.fd = None
            self.file = None
            self.type = type
            self.__prefix = prefix
            self.__autodelete = delete
            self.ext = self.EXTENSIONS[type]

        def __enter__(self):
            self.createtempfile()
            return self

        def __exit__(self, type, value, traceback):
            self.closetempfile()

        def closetempfile(self):
            try:
                if self.file is not None:
                    self.file.close()
            finally:
                self.fd = None
                self.file = None
                if self.__autodelete and self.filename is not None and \
                        _os.path.exists(self.filename):
                    try:
                        _os.unlink(self.filename)
                    finally:
                        self.filename = None

        def copyto(self, fileto):
            _shutil.copy(self.filename, fileto)

        def createtempfile(self, mode="w+b"):
            self.fd, self.filename = _tempfile.mkstemp(prefix=self.__prefix)
            self.file = _os.fdopen(self.fd, mode)
            return self

    def __init__(self, filename):
        self.filename = filename

    def getfilesegment(self):
        with open(self.filename, "rb") as xginfile:
            # Extract the uncompressed Game Data Header (GDH)
            # Note: MS Windows Vista feature
            gdfheader = \
                    _xgstruct.GameDataFormatHdrRecord().fromstream(xginfile)
            if gdfheader is None:
                raise Error("Not a game data format file", self.filename)
            
            # Extract the Game Format Header to a temporary file
            with Import.Segment(type=Import.Segment.GDF_HDR) as segment:
                xginfile.seek(0)
                block = xginfile.read(gdfheader.HeaderSize)
                segment.file.write(block)
                segment.file.flush()
                yield segment

            # Extract the uncompressed thumbnail JPEG from the GDF hdr
            if (gdfheader.ThumbnailSize > 0):
                with Import.Segment(type=Import.Segment.GDF_IMAGE) as segment:
                    xginfile.seek(gdfheader.ThumbnailOffset, _os.SEEK_CUR)
                    imgbuf = xginfile.read(gdfheader.ThumbnailSize)
                    segment.file.write(imgbuf)
                    segment.file.flush()
                    yield segment

            # Retrieve an archive object from the stream
            archiveobj = _xgzarc.ZlibArchive(xginfile)

            # Process all the files in the archive
            for filerec in archiveobj.arcregistry:

                # Retrieve the archive file to a temporary file
                segment_file, seg_filename = archiveobj.getarchivefile(filerec)

                # Create a file segment object to passback to the caller
                xg_filetype = Import.Segment.XG_FILEMAP[filerec.name]
                xg_filesegment = Import.Segment(type=xg_filetype,
                                                delete=False)
                xg_filesegment.filename = seg_filename
                xg_filesegment.fd = segment_file

                # If we are looking at the game info file then check 
                # the magic number to ensure it is valid
                if xg_filetype == Import.Segment.XG_GAMEFILE:
                    segment_file.seek(Import.Segment.XG_GAMEHDR_LEN)
                    magicStr = bytearray(segment_file.read(4)).decode('ascii')
                    if magicStr != 'DMLI':
                        raise Error("Not a valid XG gamefile", self.filename)

                yield xg_filesegment

                segment_file.close()
                _os.unlink(seg_filename)

        return


class Error(Exception):

    def __init__(self, error, filename):
        self.value = "XG Import Error processing '%s': %s" % \
                     (filename, str(error))
        self.error = error
        self.filename = filename

    def __str__(self):
        return repr(self.value)


if __name__ == '__main__':
    pass
#
#   xgutils.py - XG related utility functions
#   Copyright (C) 2013  Michael Petch <[email protected]>
#                                     <[email protected]>
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Lesser General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU Lesser General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#

import sys as _sys
import zlib as _zlib
import datetime as _datetime


def streamcrc32(stream, numbytes=None, startpos=None, blksize=32768):
    """Compute the CRC32 on a given stream. Restore the original
    position in the stream upon finishing. Process the stream in
    chunks defined by blksize
    """

    crc32 = 0
    curstreampos = stream.tell()

    if startpos is not None:
        stream.seek(startpos, 0)

    if numbytes is None:
        block = stream.read(blksize)
        while len(block) > 0:
            crc32 = _zlib.crc32(block, crc32)
            block = stream.read(blksize)
    else:
        bytesleft = numbytes
        while True:
            if bytesleft < blksize:
                blksize = bytesleft

            block = stream.read(blksize)
            crc32 = _zlib.crc32(block, crc32)
            bytesleft = bytesleft - blksize

            if bytesleft == 0:
                break

    stream.seek(curstreampos)
    return crc32 & 0xffffffff


def utf16intarraytostr3x(intarray):
    """Python 3.x - Convert an array of integers (UTF16) to a
    string. Input array is null terminated.
    """
    newstr = []
    for intval in intarray:
        if intval == 0:
            break
        newstr += [chr(intval).encode('utf-8')]

    return (b''.join([x for x in newstr]))


def utf16intarraytostr2x(intarray):
    """Python 2.x - Convert an array of integers (UTF16) to a
    string. Input array is null terminated.
    """
    newstr = []
    for intval in intarray:
        if intval == 0:
            break
        newstr += [unichr(intval).encode('utf-8')]

    return ''.join(newstr)

def delphidatetimeconv(delphi_datetime):
    """Convert a double float Delphi style timedate object to a Python
    datetime object. Delphi uses the number of days since
    Dec 30th, 1899 in the whole number component. The fractional
    component represents the fraction of a day (multiply by 86400
    to translate to seconds)
    """

    delta = _datetime.timedelta(
        days=int(delphi_datetime),
        seconds=int(86400 * (delphi_datetime % 1)))
    return _datetime.datetime(1899, 12, 30) + delta


def delphishortstrtostr(shortstring_abytes):
    """Convert Delphi Pascal style shortstring to a Python string.
    shortstring is a single byte (length of string) followed by
    length number of bytes. shortstrings are not null terminated.
    """

    return ''.join([chr(char) for char in
                    shortstring_abytes[1:(shortstring_abytes[0] + 1)]])


if __name__ == '__main__':
    pass
else:
    # Map the utf16intarraytostr function depending on whether
    # we are using Python 3.x or 2.x
    if _sys.version_info >= (3, 0):
        utf16intarraytostr = utf16intarraytostr3x
    else:
        utf16intarraytostr = utf16intarraytostr2x

#
#   extractxgdata.py - Simple XG data extraction tool
#   Copyright (C) 2013,2014  Michael Petch <[email protected]>
#                                          <[email protected]>
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Lesser General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU Lesser General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#

from __future__ import with_statement
import sys
import struct
import uuid
import os
import argparse
import xgimport
import xgzarc
import xgstruct
import pprint

def parseoptsegments(parser, segments):

    segmentlist = segments.split(',')
    for segment in segmentlist:
        if segment not in ['all', 'comments', 'gdhdr', 'thumb', 'gameinfo',
                           'gamefile', 'rollouts', 'idx']:
            parser.error("%s is not a recognized segment" % segment)
    return segmentlist


def directoryisvalid(parser, dir):

    if not os.path.isdir(dir):
        parser.error("directory path '%s' doesn't exist" % dir)
    return dir


if __name__ == '__main__':

    parser = argparse.ArgumentParser(
        description='XG data extraction utility',
        formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument("-d", metavar='DIR', dest="outdir",
                        help="Directory to write segments to "
                        "(Default is same directory as the import file)\n",
                        type=lambda dir:
                        directoryisvalid(parser, dir), default=None)
    parser.add_argument('files', metavar='FILE', type=str, nargs='+',
                        help='An XG file to import')
    args = parser.parse_args()

    for xgfilename in args.files:
        xgbasepath = os.path.dirname(xgfilename)
        xgbasefile = os.path.basename(xgfilename)
        xgext = os.path.splitext(xgfilename)
        if (args.outdir is not None):
            xgbasepath = args.outdir

        try:
            xgobj = xgimport.Import(xgfilename)
            print ('Processing file: %s' % xgfilename)
            fileversion = -1
            # To do: move this code to XGImport where it belongs
            for segment in xgobj.getfilesegment():
                segment.copyto(os.path.abspath(
                        os.path.join(xgbasepath,
                        xgbasefile[:-len(xgext[1])] + segment.ext)))

                if segment.type == xgimport.Import.Segment.XG_GAMEFILE:
                    segment.fd.seek(os.SEEK_SET, 0)
                    while True:
                        rec = xgstruct.GameFileRecord(
                                version=fileversion).fromstream(segment.fd)
                        if rec is None:
                            break
                        if isinstance(rec, xgstruct.HeaderMatchEntry):
                            fileversion = rec.Version
                        elif isinstance(rec, xgstruct.UnimplementedEntry):
                            continue
                        pprint.pprint (rec,width=160)
                elif segment.type == xgimport.Import.Segment.XG_ROLLOUTS:
                    segment.fd.seek(os.SEEK_SET, 0)
                    while True:
                        rec = xgstruct.RolloutFileRecord().fromstream(segment.fd)
                        if rec is None:
                            break
                        pprint.pprint (rec,width=160)

        except (xgimport.Error, xgzarc.Error) as e:
            print (e.value)
#
#   xgzarc.py - XG ZLib archive module
#   Copyright (C) 2013,2014  Michael Petch <[email protected]>
#                                          <[email protected]>
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Lesser General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU Lesser General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#
#   This library is an interpretation of ZLBArchive 1.52 data structures.
#   Please see: http://www.delphipages.com/comp/zlibarchive-2104.html
#

from __future__ import with_statement as _with
import tempfile as _tempfile
import struct as _struct
import zlib as _zlib
import os as _os
import xgutils as _xgutils


class Error(Exception):

    def __init__(self, error):
        self.value = "Zlib archive: %s" % str(error)
        self.error = error

    def __str__(self):
        return repr(self.value)


class ArchiveRecord(dict):

    SIZEOFREC = 36

    def __init__(self, **kw):
        defaults = {
            'crc': 0,
            'filecount': 0,
            'version': 0,
            'registrysize': 0,
            'archivesize': 0,
            'compressedregistry': False,
            'reserved': []
            }
        super(ArchiveRecord, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
        return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack('<llllll12B',
                                       stream.read(self.SIZEOFREC))
        self.crc = unpacked_data[0] & 0xffffffff
        self.filecount = unpacked_data[1]
        self.version = unpacked_data[2]
        self.registrysize = unpacked_data[3]
        self.archivesize = unpacked_data[4]
        self.compressedregistry = bool(unpacked_data[5])
        self.reserved = unpacked_data[6:]


class FileRecord(dict):

    SIZEOFREC = 532

    def __init__(self, **kw):
        defaults = {
            'name': None,
            'path': None,
            'osize': 0,
            'csize': 0,
            'start': 0,
            'crc': 0,
            'compressed': False,
            'stored': False,
            'compressionlevel': 0
            }
        super(FileRecord, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
        return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack('<256B256BllllBBxx',
                                       stream.read(self.SIZEOFREC))
        self.name = _xgutils.delphishortstrtostr(unpacked_data[0:256])
        self.path = _xgutils.delphishortstrtostr(unpacked_data[256:512])
        self.osize = unpacked_data[512]
        self.csize = unpacked_data[513]
        self.start = unpacked_data[514]
        self.crc = unpacked_data[515] & 0xffffffff
        self.compressed = bool(unpacked_data[516] == 0)
        self.compressionlevel = unpacked_data[517]

    def __str__(self):
        return str(self.todict())


class ZlibArchive(object):
    __MAXBUFSIZE = 32768
    __TMP_PREFIX = 'tmpXGI'

    def __init__(self, stream=None, filename=None):
        self.arcrec = ArchiveRecord()
        self.arcregistry = []
        self.startofarcdata = -1
        self.endofarcdata = -1

        self.filename = filename
        self.stream = stream
        if stream is None:
            self.stream = open(filename, 'rb')

        self.__getarchiveindex()

    def __extractsegment(self, iscompressed=True, numbytes=None):
        # Extract a stored segment
        filename = None
        stream = []

        try:
            tmpfd, filename = _tempfile.mkstemp(prefix=self.__TMP_PREFIX)
            with _os.fdopen(tmpfd, "wb") as tmpfile:

                if (iscompressed):
                    # Extract a compressed segment
                    decomp = _zlib.decompressobj()
                    buf = self.stream.read(self.__MAXBUFSIZE)
                    stream = decomp.decompress(buf)

                    if len(stream) <= 0:
                        raise IOError()

                    tmpfile.write(stream)

                    # Read until we have uncompressed a complete segment
                    while len(decomp.unused_data) == 0:
                        block = self.stream.read(self.__MAXBUFSIZE)
                        if len(block) > 0:
                            try:
                                stream = decomp.decompress(block)
                                tmpfile.write(stream)
                            except:
                                break
                        else:
                            # EOF reached
                            break

                else:
                    # Extract an uncompressed segment
                    # Uncompressed segment needs numbytes specified
                    if numbytes is None:
                        raise IOError()

                    blksize = self.__MAXBUFSIZE
                    bytesleft = numbytes
                    while True:
                        if bytesleft < blksize:
                            blksize = bytesleft

                        block = self.stream.read(blksize)
                        tmpfile.write(block)
                        bytesleft = bytesleft - blksize

                        if bytesleft == 0:
                            break

        except (_zlib.error, IOError) as e:
            _os.unlink(filename)
            return None

        return filename

    def __getarchiveindex(self):

        try:
            # Advance to the archive record at the end and retrieve it
            filerecords = []
            curstreampos = self.stream.tell()

            self.stream.seek(-ArchiveRecord.SIZEOFREC, _os.SEEK_END)
            self.endofarcdata = self.stream.tell()
            self.arcrec.fromstream(self.stream)

            # Position ourselves at the beginning of the archive file index
            self.stream.seek(-ArchiveRecord.SIZEOFREC -
                             self.arcrec.registrysize, _os.SEEK_END)
            self.startofarcdata = self.stream.tell() - self.arcrec.archivesize

            # Compute the CRC32 of all the archive data including file index
            streamcrc = _xgutils.streamcrc32(
                    self.stream,
                    startpos=self.startofarcdata,
                    numbytes=(self.endofarcdata - self.startofarcdata))
            if streamcrc != self.arcrec.crc:
                raise Error("Archive CRC check failed - file corrupt")

            # Decompress the index into a temporary file
            idx_filename = self.__extractsegment(
                    iscompressed=self.arcrec.compressedregistry)
            if idx_filename is None:
                raise Error("Error extracting archive index")

            # Retrieve all the files in the index
            with open(idx_filename, "rb") as idx_file:
                for recordnum in range(0, self.arcrec.filecount):
                    curidxpos = self.stream.tell()

                    # Retrieve next file index record
                    filerec = FileRecord()
                    filerec.fromstream(idx_file)
                    filerecords.append(filerec)

                    self.stream.seek(curidxpos, 0)

            _os.unlink(idx_filename)
        finally:
            self.stream.seek(curstreampos, 0)

        self.arcregistry = filerecords

    def getarchivefile(self, filerec):
        # Do processing on the temporary file
        self.stream.seek(filerec.start + self.startofarcdata)
        tmpfilename = self.__extractsegment(iscompressed=filerec.compressed,
                                            numbytes=filerec.csize)
        if tmpfilename is None:
            raise Error("Error extracting archived file")
        tmpfile = open(tmpfilename, "rb")

        # Compute the CRC32 on the uncompressed file
        streamcrc = _xgutils.streamcrc32(tmpfile)
        if streamcrc != filerec.crc:
            raise Error("File CRC check failed - file corrupt")

        return tmpfile, tmpfilename

    def setblocksize(self, blksize):
        self.__MAXBUFSIZE = blksize


if __name__ == '__main__':
    pass
#
#   xgstruct.py - classes to read XG file structures
#   Copyright (C) 2013,2014  Michael Petch <[email protected]>
#                                          <[email protected]>
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Lesser General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU Lesser General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#
#   This code is based upon Delphi data structures provided by
#   Xavier Dufaure de Citres <[email protected]> for purposes
#   of interacting with the ExtremeGammon XG file formats. Field
#   descriptions derived from xg_format.pas. The file formats are 
#   published at http://www.extremegammon.com/xgformat.aspx
#

import xgutils as _xgutils
import struct as _struct
import os as _os
import uuid as _uuid
import binascii as _binascii


class GameDataFormatHdrRecord(dict):
    SIZEOFREC = 8232

    def __init__(self, **kw):
        defaults = {
            'MagicNumber': 0,             # $484D4752, RM_MAGICNUMBER
            'HeaderVersion': 0,           # version
            'HeaderSize': 0,              # size of the header
            'ThumbnailOffset': 0,         # location of the thumbnail (jpg)
            'ThumbnailSize': 0,           # size in bye of the thumbnail
            'GameGUID': None,             # game id (GUID)
            'GameName': None,             # Unicode game name
            'SaveName': None,             # Unicode save name
            'LevelName': None,            # Unicode level name
            'Comments': None              # Unicode comments
            }                              
        super(GameDataFormatHdrRecord, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
        return self[key]

    def fromstream(self, stream):
        try:
            unpacked_data = \
                    _struct.unpack('<4BiiQiLHHBB6s1024H1024H1024H1024H', 
                    stream.read(self.SIZEOFREC))
        except:
            return None

        self.MagicNumber = bytearray(unpacked_data[0:4][::-1]).decode('ascii')
        self.HeaderVersion = unpacked_data[4]
        if self.MagicNumber != 'HMGR' or self.HeaderVersion != 1:
            return None
            
        self.HeaderSize = unpacked_data[5]
        self.ThumbnailOffset = unpacked_data[6]
        self.ThumbnailSize = unpacked_data[7]
    
        # Convert Delphi 4 component GUID to the 6 components 
        # of a Python GUID.
        guidp1, guidp2, guidp3, guidp4, guidp5 = unpacked_data[8:13]
        guidp6 = int(_binascii.b2a_hex(unpacked_data[13]), 16)
        self.GameGUID = str(_uuid.UUID(fields=(guidp1, guidp2, guidp3,
                            guidp4, guidp5, guidp6)))

        self.GameName = _xgutils.utf16intarraytostr(unpacked_data[14:1038])
        self.SaveName = _xgutils.utf16intarraytostr(unpacked_data[1038:2062])
        self.LevelName = _xgutils.utf16intarraytostr(unpacked_data[2062:3086])
        self.Comments = _xgutils.utf16intarraytostr(unpacked_data[3086:4110])
        return self
        

class TimeSettingRecord(dict):

    SIZEOFREC = 32

    def __init__(self, **kw):
        defaults = {
            'ClockType': 0,                 # 0=None,0=Fischer,0=Bronstein
            'PerGame': False,               # time is for session reset after each game
            'Time1': 0,                     # initial time in sec
            'Time2': 0,                     # time added (fisher) or reverved (bronstrein) per move in sec
            'Penalty': 0,                   # point penalty when running our of time (in point)
            'TimeLeft1': 0,                 # current time left
            'TimeLeft2': 0,                 # current time left
            'PenaltyMoney': 0               # point penalty when running our of time (in point)
            }                              
        super(TimeSettingRecord, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
        return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack(
            '<lBxxxllllll',
            stream.read(self.SIZEOFREC))
        self.ClockType = unpacked_data[0]
        self.PerGame = bool(unpacked_data[1])
        self.Time1 = unpacked_data[2]
        self.Time2 = unpacked_data[3]
        self.Penalty = unpacked_data[4]
        self.TimeLeft1 = unpacked_data[5]
        self.TimeLeft2 = unpacked_data[6]
        self.PenaltyMoney = unpacked_data[7]
        return self


class EvalLevelRecord(dict):

    SIZEOFREC = 4

    def __init__(self, **kw):
        defaults = {
            'Level': 0,                     # Level used see PLAYERLEVEL table
            'isDouble': False               # The analyze assume double for the very next move
            }
        super(EvalLevelRecord, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack(
            '<hBb',
            stream.read(self.SIZEOFREC))
        self.Level = unpacked_data[0]
        self.isDouble = bool(unpacked_data[1])

        return self


class EngineStructBestMoveRecord(dict):

    SIZEOFREC = 2184

    def __init__(self, **kw):
        defaults = {
            'Pos': None,                    # Current position
            'Dice': None,                   # Dice
            'Level': 0,                     # analyze level requested
            'Score': None,                  # current score
            'Cube': 0,                      # cube value 1,2,4, etcc.
            'CubePos': 0,                   # 0: Center 1: Player owns cube -1 Opponent owns cube
            'Crawford': 0,                  # 1 = Crawford   0 = No Crawford
            'Jacoby': 0,                    # 1 = Jacoby   0 = No Jacoby
            'NMoves': 0,                    # number of move (max 32)
            'PosPlayed': None,              # position played
            'Moves': None,                  # move list as From1,dice1, from2,dice2 etc.. -1 show termination of list
            'EvalLevel': None,              # evaluation level of each move
            'Eval': None,                   # eval value of each move
            'Unused': 0,                    # if 1 does not count as a decision
            'met': 0,                       # unused
            'Choice0': 0,                   # 1-ply choice (index to PosPlayed)
            'Choice3': 0                    # 3-ply choice (index to PosPlayed)
            }
        super(EngineStructBestMoveRecord, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
        return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack(
                '<26bxx2ll2llllll',
                stream.read(68))
        self.Pos = unpacked_data[0:26]
        self.Dice = unpacked_data[26:28]
        self.Level = unpacked_data[28]
        self.Score = unpacked_data[29:31]
        self.Cube = unpacked_data[31]
        self.Cubepos = unpacked_data[32]
        self.Crawford = unpacked_data[33]
        self.Jacoby = unpacked_data[34]
        self.NMoves = unpacked_data[35]

        self.PosPlayed = ()
        for row in range(32):
            unpacked_data = _struct.unpack('<26b', stream.read(26))
            self.PosPlayed += (unpacked_data[0:26],)

        self.Moves = ()
        for row in range(32):
            self.Moves += (_struct.unpack('<8b', stream.read(8))[0:8],)

        self.EvalLevel = ()
        for row in range(32):
            self.EvalLevel += (EvalLevelRecord().fromstream(stream),)

        self.Eval = ()
        for row in range(32):
            unpacked_data = _struct.unpack('<7f', stream.read(28))
            self.Eval += (unpacked_data,)

        unpacked_data = _struct.unpack('<bbbb', stream.read(4))
        self.Unused = unpacked_data[0]
        self.met = unpacked_data[1]
        self.Choice0 = unpacked_data[2]
        self.Choice3 = unpacked_data[3]

        return self


class EngineStructDoubleAction(dict):

    SIZEOFREC = 132

    def __init__(self, **kw):
        defaults = {
            'Pos': None,                    # Current position
            'Level': 0,                     # analyze level performed
            'Score': None,                  # current score
            'Cube': 0,                      # cube value 1,2,4, etcc.
            'CubePos': 0,                   # 0: Center 1: Player owns cube -1 Opponent owns cube
            'Jacoby': 0,                    # 1 = Jacoby   0 = No Jacoby
            'Crawford': 0,                  # 1 = Crawford   0 = No Crawford
            'met': 0,                       # unused
            'FlagDouble': 0,                # 0: Dont double 1: Double
            'isBeaver': 0,                  # is it a beaver if doubled
            'Eval': None,                   # eval value for No double
            'equB': 0.0,                    # equity No Double
            'equDouble': 0.0,               # equity Double/take
            'equDrop': 0.0,                 # equity double/drop (-1)
            'LevelRequest': 0,              # analyze level requested
            'DoubleChoice3': 0,             # 3-ply choice as double+take*2
            'EvalDouble': None              # eval value for Double/Take
            }
        super(EngineStructDoubleAction, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack(
                '<26bxxl2llllhhhh7ffffhh7f',
                stream.read(132))
        self.Pos = unpacked_data[0:26]
        self.Level = unpacked_data[26]
        self.Score = unpacked_data[27:29]
        self.Cube = unpacked_data[29]
        self.CubePos = unpacked_data[30]
        self.Jacoby = unpacked_data[31]
        self.Crawford = unpacked_data[32]
        self.met = unpacked_data[33]
        self.FlagDouble = unpacked_data[34]
        self.isBeaver = unpacked_data[35]
        self.Eval = unpacked_data[36:43]
        self.equB = unpacked_data[43]
        self.equDouble = unpacked_data[44]
        self.equDrop = unpacked_data[45]
        self.LevelRequest = unpacked_data[46]
        self.DoubleChoice3 = unpacked_data[47]
        self.EvalDouble = unpacked_data[48:55]

        return self

class HeaderMatchEntry(dict):

    SIZEOFREC = 2560

    def __init__(self, version=0, **kw):
        defaults = {
            'Name': 'MatchInfo',
            'EntryType': GameFileRecord.ENTRYTYPE_HEADERMATCH,
            'SPlayer1': None,              # player name in ANSI string for XG1 compatbility see "Player1" and "Player2" below for unicode
            'SPlayer2': None,
            'MatchLength': 0,              # Match length, 99999 for unlimited
            'Variation': 0,                # 0:backgammon, 1: Nack, 2: Hyper, 3: Longgammon
            'Crawford': False,             # Crawford in use
            'Jacoby': False,               # Jacoby in use
            'Beaver': False,               # Beaver in use
            'AutoDouble': False,           # Automatic double in use
            'Elo1': 0.0,                   # player1 elo
            'Elo2': 0.0,                   # player2 experience
            'Exp1': 0,                     # player1 elo
            'Exp2': 0,                     # player2 experience
            'Date': 0,                     # game date
            'SEvent': None,                # event name, in ANSI string for XG1 compatbility see "event" below for unicode
            'GameId': 0,                   # game ID, if player are swap make gameid:=-GameID
            'CompLevel1': -1,              # Player level: see table at the end (PLAYERLEVEL TABLE)
            'CompLevel2': -1,
            'CountForElo': False,          # outcome of the session will affect elo
            'AddtoProfile1': False,        # outcome of the session will affect player 1 profile
            'AddtoProfile2': False,        # outcome of the session will affect player 2 profile
            'SLocation': None,             # location name, in ANSI string for XG1 compatbility see "location" below for unicode
            'GameMode': 0,                 # game mode : see table at the end (GAMEMODE TABLE)
            'Imported': False,             # game was imported from an site (MAT, CBG etc..)
            'SRound': None,                # round name, in ANSI string for XG1 compatbility see "round" below for unicode
            'Invert': 0,                   # If the board is swap then invert=-invert and MatchID=-MatchID
            'Version': version,            # file version, currently SaveFileVersion
            'Magic': 0x494C4D44,           # must be MagicNumber = $494C4D44;
            'MoneyInitG': 0,               # initial game played from the profile against that opp in money
            'MoneyInitScore': [0, 0],      # initial score from the profile against that opp in money
            'Entered': False,              # entered in profile
            'Counted': False,              # already accounted in the profile elo
            'UnratedImp': False,           # game was unrated on the site it was imported from
            'CommentHeaderMatch': -1,      # index of the match comment header in temp.xgc
            'CommentFooterMatch': -1,      # index of the match comment footer in temp.xgc
            'isMoneyMatch': False,         # was player for real money
            'WinMoney': 0.0,               # amount of money for the winner
            'LoseMoney': 0.0,              # amount of money for the looser
            'Currency': 0,                 # currency code from Currency.ini
            'FeeMoney': 0.0,               # amount of rake
            'TableStake': 0,               # max amount that can be lost -- NOT IMPLEMENTED
            'SiteId': -1,                  # site id from siteinfo.ini
            'CubeLimit': 0,                # v8: maximum cube value
            'AutoDoubleMax': 0,            # v8: maximum c# of time the autodouble can be used
            'Transcribed': False,          # v24: game was transcribed
            'Event': None,                 # v24: Event name (unicode)
            'Player1': None,               # v24: Player1 name (unicode)
            'Player2': None,               # v24: Player2 name (unicode)
            'Location': None,              # v24: Location (unicode)
            'Round': None,                 # v24: Round (unicode)
            'TimeSetting': None,           # v25: Time setting for the game
            'TotTimeDelayMove': 0,         # v26: # of checker play marked for delayed RO
            'TotTimeDelayCube': 0,         # v26: # of checker play marked for delayed RO done
            'TotTimeDelayMoveDone': 0,     # v26: # of checker Cube action marked for delayed RO
            'TotTimeDelayCubeDone': 0,     # v26: # of checker Cube action marked for delayed RO Done
            'Transcriber': None            # v30: Name of the Transcriber (unicode)
            }
        super(HeaderMatchEntry, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):

        unpacked_data = _struct.unpack(
            '<9x41B41BxllBBBBddlld129BxxxlllBBB129BlB129BxxllLl2lBBB'
            'xllBxxxfflfll', stream.read(612))
        self.SPlayer1 = _xgutils.delphishortstrtostr(unpacked_data[0:41])
        self.SPlayer2 = _xgutils.delphishortstrtostr(unpacked_data[41:82])
        self.MatchLength = unpacked_data[82]
        self.Variation = unpacked_data[83]
        self.Crawford = bool(unpacked_data[84])
        self.Jacoby = bool(unpacked_data[85])
        self.Beaver = bool(unpacked_data[86])
        self.AutoDouble = bool(unpacked_data[87])
        self.Elo1 = unpacked_data[88]
        self.Elo2 = unpacked_data[89]
        self.Exp1 = unpacked_data[90]
        self.Exp2 = unpacked_data[91]
        self.Date = str(_xgutils.delphidatetimeconv(unpacked_data[92]))
        self.SEvent = _xgutils.delphishortstrtostr(unpacked_data[93:222])
        self.GameId = unpacked_data[222]
        self.CompLevel1 = unpacked_data[223]
        self.CompLevel2 = unpacked_data[224]
        self.CountForElo = bool(unpacked_data[225])
        self.AddtoProfile1 = bool(unpacked_data[226])
        self.AddtoProfile2 = bool(unpacked_data[227])
        self.SLocation = _xgutils.delphishortstrtostr(unpacked_data[228:357])
        self.GameMode = unpacked_data[357]
        self.Imported = bool(unpacked_data[358])
        self.SRound = _xgutils.delphishortstrtostr(unpacked_data[359:487])
        self.Invert = unpacked_data[488]
        self.Version = unpacked_data[489]
        self.Magic = unpacked_data[490]
        self.MoneyInitG = unpacked_data[491]
        self.MoneyInitScore = unpacked_data[492:494]
        self.Entered = bool(unpacked_data[494])
        self.Counted = bool(unpacked_data[495])
        self.UnratedImp = bool(unpacked_data[496])
        self.CommentHeaderMatch = unpacked_data[497]
        self.CommentFooterMatch = unpacked_data[498]
        self.isMoneyMatch = bool(unpacked_data[499])
        self.WinMoney = unpacked_data[500]
        self.LoseMoney = unpacked_data[501]
        self.Currency = unpacked_data[502]
        self.FeeMoney = unpacked_data[503]
        self.TableStake = unpacked_data[504]
        self.SiteId = unpacked_data[505]
        if self.Version >= 8:
            unpacked_data = _struct.unpack('<ll', stream.read(8))
            self.CubeLimit = unpacked_data[0]
            self.AutoDoubleMax = unpacked_data[1]
        if self.Version >= 24:
            unpacked_data = _struct.unpack('<Bx129H129H129H129H129H',
                                           stream.read(1292))
            self.Transcribed = bool(unpacked_data[0])
            self.Event = _xgutils.utf16intarraytostr(unpacked_data[1:130])
            self.Player1 = _xgutils.utf16intarraytostr(unpacked_data[130:259])
            self.Player2 = _xgutils.utf16intarraytostr(unpacked_data[259:388])
            self.Location = _xgutils.utf16intarraytostr(unpacked_data[388:517])
            self.Round = _xgutils.utf16intarraytostr(unpacked_data[517:646])
        if self.Version >= 25:
            self.TimeSetting = TimeSettingRecord().fromstream(stream)
        if self.Version >= 26:
            unpacked_data = _struct.unpack('<llll', stream.read(16))
            self.TotTimeDelayMove = unpacked_data[0]
            self.TotTimeDelayCube = unpacked_data[1]
            self.TotTimeDelayMoveDone = unpacked_data[2]
            self.TotTimeDelayCubeDone = unpacked_data[3]
        if self.Version >= 30:
            unpacked_data = _struct.unpack('<129H', stream.read(258))
            self.Transcriber = _xgutils.utf16intarraytostr(
                unpacked_data[0:129])

        return self


class FooterGameEntry(dict):

    SIZEOFREC = 2560

    def __init__(self, **kw):
        defaults = {
            'Name': 'GameFooter',
            'EntryType': GameFileRecord.ENTRYTYPE_FOOTERGAME,
            'Score1g': 0,                   # Final score
            'Score2g': 0,                   # Final score
            'CrawfordApplyg': False,        # will crawford apply next game
            'Winner': 0,                    # who win +1=player1, -1 player 2
            'PointsWon': 0,                 # point scored
            'Termination': 0,               # 0=Drop 1=single 2=gammon 3=Backgamon 
                                            # (0,1,2)+100=Resign  (0,1,2)+1000 settle
            'ErrResign': 0.0,               # error made by resigning (-1000 if not analyze)
            'ErrTakeResign': 0.0,           # error made by accepting the resign (-1000 if not analyze)
            'Eval': None,                   # evaluation of the final position
            'EvalLevel': 0
            }
        super(FooterGameEntry, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack('<9xxxxllBxxxlllxxxxdd7dl',
                stream.read(116))
        self.Score1g = unpacked_data[0]
        self.Score2g = unpacked_data[1]
        self.CrawfordApplyg = bool(unpacked_data[2])
        self.Winner = unpacked_data[3]
        self.PointsWon = unpacked_data[4]
        self.Termination = unpacked_data[5]
        self.ErrResign = unpacked_data[6]
        self.ErrTakeResign = unpacked_data[7]
        self.Eval = unpacked_data[8:15]
        self.EvalLevel = unpacked_data[15]
        return self


class MissingEntry(dict):

    SIZEOFREC = 2560

    def __init__(self, **kw):
        defaults = {
            'Name': 'Missing',
            'EntryType': GameFileRecord.ENTRYTYPE_MISSING,
            'MissingErrLuck': 0.0,
            'MissingWinner': 0,
            'MissingPoints': 0
            }
        super(MissingEntry, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack('<9xxxxxxxxdll', stream.read(32))
        self.MissingErrLuck = unpacked_data[0]
        self.MissingWinner = unpacked_data[1]
        self.MissingPoints = unpacked_data[2]
        return self


class FooterMatchEntry(dict):

    SIZEOFREC = 2560

    def __init__(self, **kw):
        defaults = {
            'Name': 'MatchFooter',
            'EntryType': GameFileRecord.ENTRYTYPE_FOOTERMATCH,
            'Score1m': 0,                   # Final score of the match
            'Score2m': 0,                   # Final score of the match
            'WinnerM': 0,                   # who win +1=player1, -1 player 2
            'Elo1m': 0.0,                   # resulting elo, player1
            'Elo2m': 0.0,                   # resulting elo, player2
            'Exp1m': 0,                     # resulting exp, player1
            'Exp2m': 0,                     # resulting exp, player2
            'Datem': 0.0                    # Date time of the match end
            }
        super(FooterMatchEntry, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack('<9xxxxlllddlld', stream.read(56))
        self.Score1m = unpacked_data[0]
        self.Score2m = unpacked_data[1]
        self.WinnerM = unpacked_data[2]
        self.Elo1m = unpacked_data[3]
        self.Elo2m = unpacked_data[4]
        self.Exp1m = unpacked_data[5]
        self.Exp2m = unpacked_data[6]
        self.Datem = str(_xgutils.delphidatetimeconv(unpacked_data[7]))

        return self


class HeaderGameEntry(dict):

    SIZEOFREC = 2560

    def __init__(self, **kw):
        defaults = {
            'Name': 'GameHeader',
            'EntryType': GameFileRecord.ENTRYTYPE_HEADERGAME,
            'Score1': 0,                    # initial score player1
            'Score2': 0,                    # initial score player1
            'CrawfordApply': False,         # iDoes Crawford apply on that game
            'PosInit': (0,) * 26,           # initial position
            'GameNumber': 0,                # Game number (start at 1)
            'InProgress': False,            # Game is still in progress
            'CommentHeaderGame': -1,        # index of the game comment header in temp.xgc
            'CommentFooterGame': -1,        # index of the game comment footer in temp.xgc
            'NumberOfAutoDoubles': 0        # v26: Number of Autodouble that happen in that game
                                            # note that in the rest of the game the cube still start at 1.
                                            # For display purpose or point calculation add the 2^NumberOfAutoDouble
            }
        super(HeaderGameEntry, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack('<9xxxxllB26bxlBxxxlll',
                stream.read(68))
        self.Score1 = unpacked_data[0]
        self.Score2 = unpacked_data[1]
        self.CrawfordApply = bool(unpacked_data[2])
        self.PosInit = unpacked_data[3:29]
        self.GameNumber = unpacked_data[29]
        self.InProgress = bool(unpacked_data[30])
        self.CommentHeaderGame = unpacked_data[31]
        self.CommentFooterGame = unpacked_data[32]
        if self.Version >= 26:
            self.NumberOfAutoDoubles = unpacked_data[33]

        return self


class CubeEntry(dict):

    SIZEOFREC = 2560

    def __init__(self, **kw):
        defaults = {
            'Name': 'Cube',
            'EntryType': GameFileRecord.ENTRYTYPE_CUBE,
            'ActiveP': 0,                   # Active player (1 or 2)
            'Double': 0,                    # player double (0= no, 1=yes)
            'Take': 0,                      # opp take (0= no, 1=yes, 2=beaver )
            'BeaverR': 0,                   # player accept beaver (0= no, 1=yes, 2=raccoon)
            'RaccoonR': 0,                  # player accept raccoon (0= no, 1=yes)
            'CubeB': 0,                     # Cube value 0=center, +1=2 own, +2=4 own ... -1=2 opp, -2=4 opp
            'Position': None,               # initial position
            'Doubled': None,                # Analyze result
            'ErrCube': 0.0,                 # error made on doubling (-1000 if not analyze)
            'DiceRolled': None,             # dice rolled
            'ErrTake': 0.0,                 # error made on taking (-1000 if not analyze)
            'RolloutIndexD': 0,             # index of the Rollout in temp.xgr
            'CompChoiceD': 0,               # 3-ply choice as Double+2*take
            'AnalyzeC': 0,                  # Level of the analyze
            'ErrBeaver': 0.0,               # error made on beavering (-1000 if not analyze)
            'ErrRaccoon': 0.0,              # error made on racconning (-1000 if not analyze)
            'AnalyzeCR': 0,                 # requested Level of the analyze (sometime a XGR+ request will stop at 4-ply when obivous)
            'isValid': 0,                   # invalid decision 0=Ok, 1=error, 2=invalid
            'TutorCube': 0,                 # player initial double in tutor mode (0= no, 1=yes)
            'TutorTake': 0,                 # player initial take in tutor mode (0= no, 1=yes)
            'ErrTutorCube': 0.0,            # error initialy made on doubling (-1000 if not analyze)
            'ErrTutorTake': 0.0,            # error initialy made on taking (-1000 if not analyze)
            'FlaggedDouble': False,         # cube has been flagged
            'CommentCube': -1,              # index of the cube comment in temp.xgc
            'EditedCube': False,            # v24: Position was edited at this point
            'TimeDelayCube': False,         # v26: position is marked for later RO
            'TimeDelayCubeDone': False,     # v26: position later RO has been done
            'NumberOfAutoDoubleCube': 0,    # v27: Number of Autodouble that happen in that game
            'TimeBot': 0,                   # v28: time left for both players
            'TimeTop': 0
            }
        super(CubeEntry, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack('<9xxxxllllll26bxx',
                                       stream.read(64))
        self.ActiveP = unpacked_data[0]
        self.Double = unpacked_data[1]
        self.Take = unpacked_data[2]
        self.BeaverR = unpacked_data[3]
        self.RaccoonR = unpacked_data[4]
        self.CubeB = unpacked_data[5]
        self.Position = unpacked_data[6:32]
        self.Doubled = EngineStructDoubleAction().fromstream(stream)
        unpacked_data = _struct.unpack('<xxxxd3Bxxxxxdlllxxxx' \
                                       'ddllbbxxxxxxddBxxxlBBBxlll',
                                       stream.read(116))
        self.ErrCube = unpacked_data[0]
        self.DiceRolled = _xgutils.delphishortstrtostr(unpacked_data[1:4])
        self.ErrTake = unpacked_data[4]
        self.RolloutIndexD = unpacked_data[5]
        self.CompChoiceD = unpacked_data[6]
        self.AnalyzeC = unpacked_data[7]
        self.ErrBeaver = unpacked_data[8]
        self.ErrRaccoon = unpacked_data[9]
        self.AnalyzeCR = unpacked_data[10]
        self.isValid = unpacked_data[11]
        self.TutorCube = unpacked_data[12]
        self.TutorTake = unpacked_data[13]
        self.ErrTutorCube = unpacked_data[14]
        self.ErrTutorTake = unpacked_data[15]
        self.FlaggedDouble = bool(unpacked_data[16])
        self.CommentCube = unpacked_data[17]
        if self.Version >= 24:
            self.EditedCube = bool(unpacked_data[18])
        if self.Version >= 26:
            self.TimeDelayCube = bool(unpacked_data[19])
            self.TimeDelayCubeDone = bool(unpacked_data[20])
        if self.Version >= 27:
            self.NumberOfAutoDoubleCube = unpacked_data[21]
        if self.Version >= 28:
            self.TimeBot = unpacked_data[22]
            self.TimeTop = unpacked_data[23]
        return self


class MoveEntry(dict):

    SIZEOFREC = 2560

    def __init__(self, **kw):
        defaults = {
            'Name:': 'Move',
            'EntryType': GameFileRecord.ENTRYTYPE_MOVE,
            'PositionI': None,              # Initial position
            'PositionEnd': None,            # Final Position
            'ActiveP': 0,                   # active player (1,2)
            'Moves': None,                  # list of move as From1,dice1, from2,dice2 etc.. -1 show termination of list
            'Dice': None,                   # dice rolled
            'CubeA': 0,                     # Cube value 0=center, +1=2 own, +2=4 own ... -1=2 opp, -2=4 opp
            'ErrorM': 0,                    # Not used anymore (not sure)
            'NMoveEval': 0,                 # Number of candidate (max 32)
            'DataMoves': None,              # analyze
            'Played': False,                # move was played
            'ErrMove': 0.0,                 # error made (-1000 if not analyze)
            'ErrLuck': 0.0,                 # luck of the roll
            'CompChoice': 0,                # computer choice (index to DataMoves.moveplayed)
            'InitEq': 0.0,                  # Equity before the roll (for luck purposes)
            'RolloutIndexM': None,          # index of the Rollout in temp.xgr
            'AnalyzeM': 0,                  # level of analyze of the move
            'AnalyzeL': 0,                  # level of analyze for the luck
            'InvalidM': 0,                  # invalid decision 0=Ok, 1=error, 2=invalid
            'PositionTutor': None,          # Position resulting of the initial move
            'Tutor': 0,                     # index of the move played dataMoves.moveplayed
            'ErrTutorMove': 0.0,            # error initialy made (-1000 if not analyze)
            'Flagged': False,               # move has been flagged
            'CommentMove': -1,              # index of the move comment in temp.xgc
            'EditedMove': False,            # v24: Position was edited at this point
            'TimeDelayMove': 0,             # v26: Bit list: position is marked for later RO
            'TimeDelayMoveDone': 0,         # v26: Bit list: position later RO has been done
            'NumberOfAutoDoubleMove': 0     # v27: Number of Autodouble that happen in that game
            }
        super(MoveEntry, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack('<9x26b26bxxxl8l2lldl',
                                       stream.read(124))
        self.PositionI = unpacked_data[0:26]
        self.PositionEnd = unpacked_data[26:52]
        self.ActiveP = unpacked_data[52]
        self.Moves = unpacked_data[53:61]
        self.Dice = unpacked_data[61:63]
        self.CubeA = unpacked_data[63]
        self.ErrorM = unpacked_data[64] # Not used
        self.NMoveEval = unpacked_data[65]
        self.DataMoves = EngineStructBestMoveRecord().fromstream(stream)

        unpacked_data = _struct.unpack('<Bxxxddlxxxxd32llll26bbxdBxxxl',
                                       stream.read(220))
        self.Played = bool(unpacked_data[0])
        self.ErrMove = unpacked_data[1]
        self.ErrLuck = unpacked_data[2]
        self.CompChoice = unpacked_data[3]
        self.InitEq = unpacked_data[4]
        self.RolloutIndexM = unpacked_data[5:37]
        self.AnalyzeM = unpacked_data[37]
        self.AnalyzeL = unpacked_data[38]
        self.InvalidM = unpacked_data[39]
        self.PositionTutor = unpacked_data[40:66]
        self.Tutor = unpacked_data[66]
        self.ErrTutorMove = unpacked_data[67]
        self.Flagged = bool(unpacked_data[68])
        self.CommentMove = unpacked_data[69]
        if self.Version >= 24:
            unpacked_data = _struct.unpack('<B', stream.read(1))
            self.EditedMove = bool(unpacked_data[0])
        if self.Version >= 26:
            unpacked_data = _struct.unpack('<xxxLL', stream.read(11))
            self.TimeDelayMove = unpacked_data[0]
            self.TimeDelayMoveDone = unpacked_data[1]
        if self.Version >= 27:
            unpacked_data = _struct.unpack('<l', stream.read(4))
            self.NumberOfAutoDoubleMove = unpacked_data[0]

        return self


class UnimplementedEntry(dict):

    """ Class for record types we have yet to implement
    """

    SIZEOFREC = 2560

    def __init__(self, **kw):
        defaults = {
            'Name': 'Unimplemented'
            }
        super(UnimplementedEntry, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        return self


class GameFileRecord(dict):

    __SIZEOFSRHDR = 9
    __REC_CLASSES = [HeaderMatchEntry, HeaderGameEntry,
                     CubeEntry, MoveEntry,
                     FooterGameEntry, FooterMatchEntry,
                     UnimplementedEntry, MissingEntry]

    ENTRYTYPE_HEADERMATCH, ENTRYTYPE_HEADERGAME, ENTRYTYPE_CUBE, \
            ENTRYTYPE_MOVE, ENTRYTYPE_FOOTERGAME, ENTRYTYPE_FOOTERMATCH, \
            ENTRYTYPE_MISSING, ENTRYTYPE_UNIMPLEMENTED = range(8)

    def __init__(self, version=-1, **kw):
        """ Create a game file record based upon the given file version
        number. The file version is first found in a HeaderMatchEntry
        object. The version needs to be propogated to all other game
        file objects within the same archive.
        """
        defaults = {
            'Name': 'GameFileRecord',
            'EntryType': -1,
            'Record': None,
            'Version': version
            }
        super(GameFileRecord, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        # Read the header. First 8 bytes are unused. 9th byte is record type
        # The record type determines what object to create and load.
        # If we catch a struct.error we have hit the EOF.
        startpos = stream.tell()
        try:
            unpacked_data = _struct.unpack('<8xB',
                                           stream.read(self.__SIZEOFSRHDR))
        except _struct.error:
            return None
        self.EntryType = unpacked_data[0]

        # Back up to the beginning of the record after getting the record
        # type and feed the entire stream back into the corresponding
        # record object.
        stream.seek(-self.__SIZEOFSRHDR, _os.SEEK_CUR)

        # Using the appropriate class, read the data stream
        self.Record = self.__REC_CLASSES[self.EntryType]()
        self.Record.Version = self.Version
        self.Record.fromstream(stream)
        realrecsize = stream.tell() - startpos

        # Each record is actually 2560 bytes long. We need to advance past
        # the unused filler data to be at the start of the next record
        stream.seek(self.Record.SIZEOFREC - realrecsize, _os.SEEK_CUR)

        return self.Record


class RolloutContextEntry(dict):

    SIZEOFREC = 2184

    def __init__(self, **kw):
        defaults = {
            'Name': 'Rollout',
            'EntryType': RolloutFileRecord.ROLLOUTCONTEXT,
            'Truncated': False,             # is truncated
            'ErrorLimited': False,          # stop when CI under "ErrorLimit"
            'Truncate': 0,                  # truncation level
            'MinRoll': 0,                   # minimum games to roll
            'ErrorLimit': 0.0,              # CI to stop the RO
            'MaxRoll': 0,                   # maximum games to roll
            'Level1': 0,                    # checker play Level used before "LevelCut"
            'Level2': 0,                    # checker play Level used on and after "LevelCut"
            'LevelCut': 0,                  # Cutoff for level1 and level2
            'Variance': False,              # use variance reduction
            'Cubeless': False,              # is a cubeless ro
            'Time': False,                  # is time limited
            'Level1C': 0,                   # cube Level used before "LevelCut"
            'Level2C': 0,                   # cube Level used on and after "LevelCut"
            'TimeLimit': 0,                 # limit in time (min)
            'TruncateBO': 0,                # what do do when reaching BO db: 0=nothing; 1=?
            'RandomSeed': 0,                # caculated seed=RandomSeedI+hashpos
            'RandomSeedI': 0,               # used entered seed
            'RollBoth': False,              # roll both line (ND and D/T)
            'SearchInterval': 0.0,          # Search interval used (1=normal, 1.5=large, 2=huge, 4=gigantic)
            'met': 0,                       # unused
            'FirstRoll': False,             # is it a first roll rollout
            'DoDouble': False,              # roll both line (ND and D/T) in multiple rollout
            'Extent': False,                # if the ro is extended
            'Rolled': 0,                    # game rolled
            'DoubleFirst': False,           # a double happens immediatly.
            'Sum1': None,                   # sum of equities for all 36 1st roll
            'SumSquare1': None,             # sum of square equities for all 36 1st roll
            'Sum2': None,                   # D/T sum of equities for all 36 1st roll
            'SumSquare2': None,             # D/T sum of square equities for all 36 1st roll
            'Stdev1': None,                 # Standard deviation for all 36 1st roll
            'Stdev2': None,                 # D/T Stand deviation for all 36 1st roll
            'RolledD': None,                # number of game rolled for all 36 1st roll
            'Error1': 0.0,                  # 95% CI
            'Error2': 0.0,                  # D/T 95% CI
            'Result1': None,                # evaluation of the position
            'Result2': None,                # D/T evaluation of the position
            'Mwc1': 0.0,                    # ND  mwc equivalent of result1[1,6]
            'Mwc2': 0.0,                    # D/T mwc equivalent of result2[1,6]
            'PrevLevel': 0,                 # store the previous analyze level (for deleting RO)
            'PrevEval': None,               # store the previous analyze result (for deleting RO)
            'PrevND': 0,                  # store the previous analyze equities (for deleting RO)
            'PrevD': 0,                   
            'Duration': 0,                # duration in seconds
            'LevelTrunc': 0,                # level used at truncation
            'Rolled2': 0,                   # D/T number of game rolled
            'MultipleMin': 0,               # Multiple RO minimum # of game
            'MultipleStopAll': False,       # Multiple RO stop all if one move reach MultipleStopAllValue
            'MultipleStopOne': False,       # Multiple RO stop one move is reach under MultipleStopOneValue
            'MultipleStopAllValue': 0.0,    # value to stop all RO (for instance 99.9%)
            'MultipleStopOneValue': 0.0,    # value to stop one move(for instance 0.01%)
            '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
            'Rotation': 0,                  # 0=36 dice, 1=21 dice (XG1), 2=30 dice (for 1st pos)
            'UserInterrupted': False,       # RO was interrupted by user
            'VerMaj': 0,                    # Major version use for the RO, currently (2.20): 2
            'VerMin': 0                     # Minor version use for the RO, currently (2.10): 10 (no change in RO or engine between 2.10 and 2.20)
            }
        super(RolloutContextEntry, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        unpacked_data = _struct.unpack('<BBxxllxxxxdllllBBBxllLlllBxxx' \
                                       'flBBBxlBxxxxxxx37d37d37d37d37d37d37l' \
                                       'ff7f7fffl7fllllllBBxxffBxxxlBxHH',
                                       stream.read(2174))

        self.Truncated = bool(unpacked_data[0])
        self.ErrorLimited = bool(unpacked_data[1])
        self.Truncate = unpacked_data[2]
        self.MinRoll = unpacked_data[3]
        self.ErrorLimit = unpacked_data[4]
        self.MaxRoll = unpacked_data[5]
        self.Level1 = unpacked_data[6]
        self.Level2 = unpacked_data[7]
        self.LevelCut = unpacked_data[8]
        self.Variance = bool(unpacked_data[9])
        self.Cubeless = bool(unpacked_data[10])
        self.Time = bool(unpacked_data[11])
        self.Level1C = unpacked_data[12]
        self.Level2C = unpacked_data[13]
        self.TimeLimit = unpacked_data[14]
        self.TruncateBO = unpacked_data[15]
        self.RandomSeed = unpacked_data[16]
        self.RandomSeedI = unpacked_data[17]
        self.RollBoth = bool(unpacked_data[18])
        self.SearchInterval = unpacked_data[19]
        self.met = unpacked_data[20]
        self.FirstRoll = bool(unpacked_data[21])
        self.DoDouble = bool(unpacked_data[22])
        self.Extent = bool(unpacked_data[23])
        self.Rolled = unpacked_data[24]
        self.DoubleFirst = bool(unpacked_data[25])
        self.Sum1 = unpacked_data[26:63]
        self.SumSquare1 = unpacked_data[63:100]
        self.Sum2 = unpacked_data[100:137]
        self.SumSquare2 = unpacked_data[137:174]
        self.Stdev1 = unpacked_data[174:211]
        self.Stdev2 = unpacked_data[211:248]
        self.RolledD = unpacked_data[248:285]
        self.Error1 = unpacked_data[285]
        self.Error2 = unpacked_data[286]
        self.Result1 = unpacked_data[287:294]
        self.Result2 = unpacked_data[294:301]
        self.Mwc1 = unpacked_data[301]
        self.Mwc2 = unpacked_data[302]
        self.PrevLevel = unpacked_data[303]
        self.PrevEval = unpacked_data[304:311]
        self.PrevND = unpacked_data[311]
        self.PrevD = unpacked_data[312]
        self.Duration = unpacked_data[313]
        self.LevelTrunc = unpacked_data[314]
        self.Rolled2 = unpacked_data[315]
        self.MultipleMin = unpacked_data[316]
        self.MultipleStopAll = bool(unpacked_data[317])
        self.MultipleStopOne = bool(unpacked_data[318])
        self.MultipleStopAllValue = unpacked_data[319]
        self.MultipleStopOneValue = unpacked_data[320]
        self.AsTake = bool(unpacked_data[321])
        self.Rotation = unpacked_data[322]
        self.UserInterrupted = bool(unpacked_data[323])
        self.VerMaj = unpacked_data[324]
        self.VerMin = unpacked_data[325]

        return self


class RolloutFileRecord(dict):

    ROLLOUTCONTEXT = 0

    def __init__(self, version=-1, **kw):
        """ Create a game file record based upon the given file version
        number. The file version is first found in a HeaderMatchEntry
        object. The version needs to be propogated to all other game
        file objects within the same archive.
        """
        defaults = {
            'Name': 'RolloutFileRecord',
            'EntryType': 0,
            'Record': None,
            'Version': version
            }
        super(RolloutFileRecord, self).__init__(defaults, **kw)

    def __setattr__(self, key, value):
        self[key] = value

    def __getattr__(self, key):
       return self[key]

    def fromstream(self, stream):
        # If we are at EOF then return
        if len(stream.read(1)) <= 0:
            return None

        stream.seek(-1, _os.SEEK_CUR)
        startpos = stream.tell()

        # Using the appropriate class, read the data stream
        self.Record = RolloutContextEntry()
        self.Record.Version = self.Version
        self.Record.fromstream(stream)
        realrecsize = stream.tell() - startpos
        # Each record is actually 2184 bytes long. We need to advance past
        # the unused filler data to be at the start of the next record
        stream.seek(self.Record.SIZEOFREC - realrecsize, _os.SEEK_CUR)

        return self.Record


if __name__ == '__main__':
    pass

Reply via email to