github.com/frankkopp/FrankyGo@v1.0.3/internal/openingbook/openingbook.go (about)

     1  //
     2  // FrankyGo - UCI chess engine in GO for learning purposes
     3  //
     4  // MIT License
     5  //
     6  // Copyright (c) 2018-2020 Frank Kopp
     7  //
     8  // Permission is hereby granted, free of charge, to any person obtaining a copy
     9  // of this software and associated documentation files (the "Software"), to deal
    10  // in the Software without restriction, including without limitation the rights
    11  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    12  // copies of the Software, and to permit persons to whom the Software is
    13  // furnished to do so, subject to the following conditions:
    14  //
    15  // The above copyright notice and this permission notice shall be included in all
    16  // copies or substantial portions of the Software.
    17  //
    18  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    19  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    20  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    21  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    22  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    23  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    24  // SOFTWARE.
    25  //
    26  
    27  // Package openingbook
    28  // The OpeningBook reads game databases of different formats into an internal
    29  // data structure. It can then be queried for a book move on a certain position.
    30  //
    31  // Supported formats are currently:
    32  //
    33  // BookFormat::SIMPLE for files storing a game per line with from-square and
    34  // to-square notation
    35  //
    36  // BookFormat::SAN for files with lines of moves in SAN notation
    37  //
    38  // BookFormat::PGN for PGN formatted games<br/>
    39  package openingbook
    40  
    41  import (
    42  	"bufio"
    43  	"encoding/gob"
    44  	"errors"
    45  	"os"
    46  	"path/filepath"
    47  	"regexp"
    48  	"strings"
    49  	"sync"
    50  	"time"
    51  
    52  	"github.com/op/go-logging"
    53  	"golang.org/x/text/language"
    54  	"golang.org/x/text/message"
    55  
    56  	myLogging "github.com/frankkopp/FrankyGo/internal/logging"
    57  	"github.com/frankkopp/FrankyGo/internal/movegen"
    58  	"github.com/frankkopp/FrankyGo/internal/position"
    59  	"github.com/frankkopp/FrankyGo/internal/types"
    60  	"github.com/frankkopp/FrankyGo/internal/util"
    61  )
    62  
    63  var out = message.NewPrinter(language.German)
    64  
    65  // setting to use multiple goroutines or not - useful for debugging.
    66  const parallel = true
    67  
    68  // BookFormat represent the supported book formats defined as constants.
    69  type BookFormat uint8
    70  
    71  // Supported book formats.
    72  const (
    73  	Simple BookFormat = iota
    74  	San    BookFormat = iota
    75  	Pgn    BookFormat = iota
    76  )
    77  
    78  var (
    79  	FormatFromString = map[string]BookFormat{
    80  		"Simple": Simple,
    81  		"San":    San,
    82  		"Pgn":    Pgn,
    83  	}
    84  )
    85  
    86  // Successor  represents a tuple of a move
    87  // and a zobrist key of the position the
    88  // move leads to.
    89  type Successor struct {
    90  	Move      uint32
    91  	NextEntry uint64
    92  }
    93  
    94  // BookEntry represents a data structure for a move in the opening book
    95  // data structure. It describes exactly one position defined by a zobrist
    96  // key and has links to other entries representing moves and successor
    97  // positions.
    98  type BookEntry struct {
    99  	ZobristKey uint64
   100  	Counter    int
   101  	Moves      []Successor
   102  }
   103  
   104  // Book represents a structure for chess opening books which can
   105  // be read from different file formats into an internal data structure.
   106  //  Create new book instance with NewBook()
   107  type Book struct {
   108  	log         *logging.Logger
   109  	bookMap     map[uint64]BookEntry
   110  	rootEntry   uint64
   111  	initialized bool
   112  }
   113  
   114  // NewBook create as new opening book instance.
   115  func NewBook() *Book {
   116  	return &Book{
   117  		log: myLogging.GetLog(),
   118  	}
   119  }
   120  
   121  // mutex to support concurrent writing to the book data structure.
   122  var bookLock sync.Mutex
   123  
   124  // Initialize reads game data from the given file into the internal data structure.
   125  // A binary cache file will be created in the same folder (postfix .cache) to
   126  // speedup subsequent loading if the opening book. Initialization only occurs once
   127  // multiple calls will be ignored. To re-initialize call Reset() first.
   128  func (b *Book) Initialize(bookPath string, bookFile string, bookFormat BookFormat, useCache bool, recreateCache bool) error {
   129  	if b.initialized {
   130  		return nil
   131  	}
   132  
   133  	// make absolute path
   134  	resolveFile, _ := util.ResolveFile(filepath.Join(bookPath, bookFile))
   135  	bookFilePath := filepath.Clean(resolveFile)
   136  
   137  	b.log.Infof("Initializing Opening Book [%s]", bookFilePath)
   138  	err := b.initialize(bookFilePath, bookFormat, useCache, recreateCache)
   139  	b.log.Debug(util.GcWithStats())
   140  
   141  	return err
   142  }
   143  
   144  func (b *Book) initialize(bookFilePath string, bookFormat BookFormat, useCache bool, recreateCache bool) error {
   145  	startTotal := time.Now()
   146  
   147  	// check file path
   148  	if _, err := os.Stat(bookFilePath); err != nil {
   149  		b.log.Warningf("file \"%s\" not found\n", bookFilePath)
   150  		return err
   151  	}
   152  
   153  	b.log.Debugf("Memory statistics: %s", util.MemStat())
   154  
   155  	// if cache enabled check if we have a cache file and load from cache
   156  	if useCache && !recreateCache {
   157  		startReading := time.Now()
   158  		hasCache, err := b.loadFromCache(bookFilePath)
   159  		elapsedReading := time.Since(startReading)
   160  		if err != nil {
   161  			b.log.Warningf("Cache could not be loaded. Reading original data from \"%s\"", bookFilePath)
   162  		}
   163  		if hasCache {
   164  			b.log.Debugf("Finished reading cache from file in: %d ms\n", elapsedReading.Milliseconds())
   165  			b.log.Debugf("Book from cache file contains %d entries\n", len(b.bookMap))
   166  			return nil
   167  		} // else no cache file just load the data from original file
   168  	}
   169  
   170  	b.log.Debugf("Memory statistics: %s", util.MemStat())
   171  
   172  	// read book from file
   173  	b.log.Infof("Reading opening book file: %s\n", bookFilePath)
   174  	startReading := time.Now()
   175  	lines, err := b.readFile(bookFilePath)
   176  	if err != nil {
   177  		b.log.Errorf("File \"%s\" could not be read: %s\n", bookFilePath, err)
   178  		return err
   179  	}
   180  	elapsedReading := time.Since(startReading)
   181  	b.log.Infof("Finished reading %d lines from file in: %d ms\n", len(*lines), elapsedReading.Milliseconds())
   182  	b.log.Debugf("Memory statistics: %s", util.MemStat())
   183  
   184  	// add root position
   185  	startPosition := position.NewPosition()
   186  	b.bookMap = make(map[uint64]BookEntry)
   187  	b.rootEntry = uint64(startPosition.ZobristKey())
   188  	b.bookMap[uint64(startPosition.ZobristKey())] = BookEntry{ZobristKey: uint64(startPosition.ZobristKey()), Counter: 0, Moves: []Successor{}}
   189  	b.log.Debugf("Memory statistics: %s", util.MemStat())
   190  
   191  	// process lines
   192  	if parallel {
   193  		b.log.Infof("Processing %d lines in parallel with format: %v\n", len(*lines), bookFormat)
   194  	} else {
   195  		b.log.Infof("Processing %d lines sequential with format: %v\n", len(*lines), bookFormat)
   196  	}
   197  	startProcessing := time.Now()
   198  	err = b.process(lines, bookFormat)
   199  	if err != nil {
   200  		b.log.Errorf("Error while processing: %s\n", err)
   201  		return err
   202  	}
   203  	elapsedProcessing := time.Since(startProcessing)
   204  	b.log.Infof("Finished processing %d lines in: %d ms\n", len(*lines), elapsedProcessing.Milliseconds())
   205  	b.log.Debugf("Memory statistics: %s", util.MemStat())
   206  
   207  	// finished
   208  	elapsedTotal := time.Since(startTotal)
   209  
   210  	b.log.Infof("Book contains %d entries\n", len(b.bookMap))
   211  	b.log.Infof("Total initialization time : %d ms\n", elapsedTotal.Milliseconds())
   212  
   213  	// saving to cache
   214  	if useCache {
   215  		b.log.Infof("Saving to cache...")
   216  		startSave := time.Now()
   217  		cacheFile, nBytes, err := b.saveToCache(bookFilePath)
   218  		if err != nil {
   219  			b.log.Errorf("Error while saving to cache: %s\n", err)
   220  		}
   221  		elapsedSave := time.Since(startSave)
   222  		bytes := out.Sprintf("%d", nBytes/1_024)
   223  		b.log.Infof("Saved %s kB to cache %s in %d ms\n", bytes, cacheFile, elapsedSave.Milliseconds())
   224  	}
   225  	b.log.Debugf("Memory statistics: %s", util.MemStat())
   226  
   227  	b.initialized = true
   228  	return nil
   229  }
   230  
   231  // NumberOfEntries returns the number of entries in the opening book
   232  func (b *Book) NumberOfEntries() int {
   233  	return len(b.bookMap)
   234  }
   235  
   236  // GetEntry returns a copy of the entry with the corresponding key
   237  func (b *Book) GetEntry(key position.Key) (BookEntry, bool) {
   238  	entryPtr, ok := b.bookMap[uint64(key)]
   239  	if ok {
   240  		return entryPtr, true
   241  	} else {
   242  		return entryPtr, false
   243  	}
   244  }
   245  
   246  // Reset resets the opening book so it can/must be initialized again
   247  func (b *Book) Reset() {
   248  	b.bookMap = map[uint64]BookEntry{}
   249  	b.rootEntry = 0
   250  	b.initialized = false
   251  }
   252  
   253  // /////////////////////////////////////////////////
   254  // Private
   255  // /////////////////////////////////////////////////
   256  
   257  // reads a complete file into a slice of strings
   258  func (b *Book) readFile(bookPath string) (*[]string, error) {
   259  	f, err := os.Open(bookPath)
   260  	if err != nil {
   261  		b.log.Errorf("File \"%s\" could not be read; %s\n", bookPath, err)
   262  		return nil, err
   263  	}
   264  	defer func() {
   265  		if err = f.Close(); err != nil {
   266  			b.log.Errorf("File \"%s\" could not be closed: %s\n", bookPath, err)
   267  		}
   268  	}()
   269  	var lines []string
   270  	s := bufio.NewScanner(f)
   271  	for s.Scan() {
   272  		lines = append(lines, s.Text())
   273  	}
   274  	err = s.Err()
   275  	if err != nil {
   276  		b.log.Errorf("Error while reading file \"%s\": %s\n", bookPath, err)
   277  		return nil, err
   278  	}
   279  	return &lines, nil
   280  }
   281  
   282  // sends all lines to the correct processing depending on format
   283  func (b *Book) process(lines *[]string, format BookFormat) error {
   284  	switch format {
   285  	case Simple:
   286  		b.processSimple(lines)
   287  	case San:
   288  		b.processSan(lines)
   289  	case Pgn:
   290  		b.processPgn(lines)
   291  	}
   292  	return nil
   293  }
   294  
   295  // processes all lines of Simple format
   296  // uses goroutines in parallel if enabled
   297  func (b *Book) processSimple(lines *[]string) {
   298  	if parallel {
   299  		sliceLength := len(*lines)
   300  		var wg sync.WaitGroup
   301  		wg.Add(sliceLength)
   302  		for _, line := range *lines {
   303  			go func(line string) {
   304  				defer wg.Done()
   305  				b.processSimpleLine(line)
   306  			}(line)
   307  		}
   308  		wg.Wait()
   309  	} else {
   310  		for _, line := range *lines {
   311  			b.processSimpleLine(line)
   312  		}
   313  	}
   314  }
   315  
   316  // regular expressions for detecting moves
   317  var regexSimpleUciMove = regexp.MustCompile("([a-h][1-8][a-h][1-8])")
   318  
   319  // processes one line of simple format and adds each move to book
   320  func (b *Book) processSimpleLine(line string) {
   321  	line = strings.TrimSpace(line)
   322  
   323  	// find Uci moves
   324  	matches := regexSimpleUciMove.FindAllString(line, -1)
   325  
   326  	// skip lines without matches
   327  	if len(matches) == 0 {
   328  		return
   329  	}
   330  
   331  	// start with root position
   332  	pos := position.NewPosition()
   333  
   334  	// increase counter for root position
   335  	bookLock.Lock()
   336  	e, found := b.bookMap[b.rootEntry]
   337  	if found {
   338  		e.Counter++
   339  		b.bookMap[b.rootEntry] = e
   340  	} else {
   341  		panic("root entry of book map not found")
   342  	}
   343  	bookLock.Unlock()
   344  
   345  	// move gen to check moves
   346  	// movegen is not thread safe therefore we create a new instance for every line
   347  	var mg = movegen.NewMoveGen()
   348  
   349  	// add all matches to book
   350  	for _, moveString := range matches {
   351  		err := b.processSingleMove(moveString, mg, pos)
   352  		// stop processing further matches when we had an error as it
   353  		// would probably be fruitless as position will be wrong
   354  		if err != nil {
   355  			break
   356  		}
   357  	}
   358  }
   359  
   360  // processes all lines of Simple format
   361  // uses goroutines in parallel if enabled
   362  func (b *Book) processSan(lines *[]string) {
   363  	if parallel {
   364  		sliceLength := len(*lines)
   365  		var wg sync.WaitGroup
   366  		wg.Add(sliceLength)
   367  		for _, line := range *lines {
   368  			go func(line string) {
   369  				defer wg.Done()
   370  				b.processSanLine(line)
   371  			}(line)
   372  		}
   373  		wg.Wait()
   374  	} else {
   375  		for _, line := range *lines {
   376  			b.processSanLine(line)
   377  		}
   378  	}
   379  }
   380  
   381  var regexResult = regexp.MustCompile("((1-0)|(0-1)|(1/2-1/2)|(\\*))$")
   382  
   383  // Processes PGN formatted file. PGN file have additional
   384  // metadata and spreads its move section over several lines.
   385  // We ignore the metadata for now and only look at the
   386  // move section.
   387  func (b *Book) processPgn(lines *[]string) {
   388  	// bundle games in slices of lines by finding result patterns
   389  	var gamesSlices [][]string
   390  
   391  	// find slices for each game
   392  	startSlicing := time.Now()
   393  	start := 0
   394  	// end := len(*lines)
   395  	for i, l := range *lines {
   396  		l = strings.TrimSpace(l)
   397  		if regexResult.MatchString(l) {
   398  			end := i + 1
   399  			gamesSlices = append(gamesSlices, (*lines)[start:end])
   400  			start = end
   401  		}
   402  	}
   403  	elapsedReading := time.Since(startSlicing)
   404  	b.log.Infof("Finished finding %d games from file in: %d ms\n", len(gamesSlices), elapsedReading.Milliseconds())
   405  
   406  	// process each game
   407  	// uses goroutines in parallel if enabled
   408  	startProcessing := time.Now()
   409  	if parallel {
   410  		noOfSlices := len(gamesSlices)
   411  		var wg sync.WaitGroup
   412  		wg.Add(noOfSlices)
   413  		for _, gs := range gamesSlices {
   414  			go func(gs []string) {
   415  				defer wg.Done()
   416  				b.processPgnGame(gs)
   417  			}(gs)
   418  		}
   419  		wg.Wait()
   420  	} else {
   421  		for _, gs := range gamesSlices {
   422  			b.processPgnGame(gs)
   423  		}
   424  
   425  	}
   426  	elapsedProcessing := time.Since(startProcessing)
   427  	b.log.Infof("Finished processing %d games from file in: %d ms\n", len(gamesSlices), elapsedProcessing.Milliseconds())
   428  }
   429  
   430  var regexTrailingComments = regexp.MustCompile(";.*$")
   431  var regexTagPairs = regexp.MustCompile("\\[\\w+ +\".*?\"\\]")
   432  var regexNagAnnotation = regexp.MustCompile("(\\$\\d{1,3})") // no NAG annotation supported
   433  var regexBracketComments = regexp.MustCompile("{[^{}]*}")    // bracket comments
   434  var regexReservedSymbols = regexp.MustCompile("<[^<>]*>")    // reserved symbols < >
   435  var regexRavVariants = regexp.MustCompile("\\([^()]*\\)")    // RAV variant comments < >
   436  
   437  // processes one game comprising of several input lines
   438  func (b *Book) processPgnGame(gameSlice []string) {
   439  	// build a cleaned up string of the move part of the PGN
   440  	var moveLine strings.Builder
   441  
   442  	// cleanup lines and concatenate move lines
   443  	for _, l := range gameSlice {
   444  		l = strings.TrimSpace(l)
   445  		if strings.HasPrefix(l, "%") { // skip comment lines
   446  			continue
   447  		}
   448  		// remove unnecessary parts and lines / order is important - comments last
   449  		l = regexTagPairs.ReplaceAllString(l, "")
   450  		l = regexResult.ReplaceAllString(l, "")
   451  		l = regexTrailingComments.ReplaceAllString(l, "")
   452  		l = strings.TrimSpace(l)
   453  		// after cleanup skip now empty lines
   454  		if len(l) == 0 {
   455  			continue
   456  		}
   457  		// add the rest to the moveLine
   458  		moveLine.WriteString(" ")
   459  		moveLine.WriteString(l)
   460  	}
   461  	line := moveLine.String()
   462  
   463  	// clean up move section of PGN
   464  	line = regexNagAnnotation.ReplaceAllString(line, " ")
   465  	line = regexBracketComments.ReplaceAllString(line, " ")
   466  	line = regexReservedSymbols.ReplaceAllString(line, " ")
   467  	// RAV variation comments can be nested - therefore loop
   468  	for regexRavVariants.MatchString(line) {
   469  		line = regexRavVariants.ReplaceAllString(line, " ")
   470  	}
   471  
   472  	// process as SAN line
   473  	b.processSanLine(line)
   474  }
   475  
   476  // regular expressions for handling SAN/UCI input lines
   477  var regexSanLineStart = regexp.MustCompile("^\\d+\\. ?")
   478  var regexSanLineCleanUpNumbers = regexp.MustCompile("(\\d+\\.{1,3} ?)")
   479  var regexSanLineCleanUpResults = regexp.MustCompile("(1/2|1|0)-(1/2|1|0)")
   480  var regexWhiteSpace = regexp.MustCompile("\\s+")
   481  
   482  // processes one line of SAN format
   483  func (b *Book) processSanLine(line string) {
   484  	line = strings.TrimSpace(line)
   485  
   486  	// check if line starts valid
   487  	found := regexSanLineStart.MatchString(line)
   488  	if !found {
   489  		return
   490  	}
   491  
   492  	/*
   493  	 Iterate over all tokens, ignore move numbers and results
   494  	 Example:
   495  	 1. f4 d5 2. Nf3 Nf6 3. e3 g6 4. b3 Bg7 5. Bb2 O-O 6. Be2 c5 7. O-O Nc6 8. Ne5 Qc7 1/2-1/2
   496  	 1. f4 d5 2. Nf3 Nf6 3. e3 Bg4 4. Be2 e6 5. O-O Bd6 6. b3 O-O 7. Bb2 c5 1/2-1/2
   497  	*/
   498  
   499  	// remove unnecessary parts
   500  	line = regexSanLineCleanUpNumbers.ReplaceAllString(line, "")
   501  	line = regexSanLineCleanUpResults.ReplaceAllString(line, "")
   502  	line = strings.TrimSpace(line)
   503  
   504  	// split at every whitespace and iterate through items
   505  	moveStrings := regexWhiteSpace.Split(line, -1)
   506  	// skip lines without matches
   507  	if len(moveStrings) == 0 {
   508  		return
   509  	}
   510  
   511  	// start with root position
   512  	pos := position.NewPosition()
   513  
   514  	// increase counter for root position
   515  	bookLock.Lock()
   516  	e, found := b.bookMap[b.rootEntry]
   517  	if found {
   518  		e.Counter++
   519  		b.bookMap[b.rootEntry] = e
   520  	} else {
   521  		panic("root entry of book map not found")
   522  	}
   523  	bookLock.Unlock()
   524  
   525  	// move gen to check moves
   526  	// movegen is not thread safe therefore we create a new instance for every line
   527  	var mg = movegen.NewMoveGen()
   528  
   529  	for _, moveString := range moveStrings {
   530  		err := b.processSingleMove(moveString, mg, pos)
   531  		// stop processing further matches when we had an error as it
   532  		// would probably be fruitless as position will be wrong
   533  		if err != nil {
   534  			b.log.Warningf("Move not valid %s on %s", moveString, pos.StringFen())
   535  			break
   536  		}
   537  	}
   538  }
   539  
   540  var regexUciMove = regexp.MustCompile("([a-h][1-8][a-h][1-8])([NBRQnbrq])?")
   541  var regexSanMove = regexp.MustCompile("([NBRQK])?([a-h])?([1-8])?x?([a-h][1-8]|O-O-O|O-O)(=?([NBRQ]))?([!?+#]*)?")
   542  
   543  // Process a single move as a string in either UCI or SAN format.
   544  // Uses pattern matching to distinguish format
   545  func (b *Book) processSingleMove(s string, mgPtr *movegen.Movegen, posPtr *position.Position) error {
   546  	// find move in the current position or stop processing
   547  	var move = types.MoveNone
   548  	if regexUciMove.MatchString(s) {
   549  		move = mgPtr.GetMoveFromUci(posPtr, s)
   550  	} else if regexSanMove.MatchString(s) {
   551  		move = mgPtr.GetMoveFromSan(posPtr, s)
   552  	}
   553  	// if move is invalid return stop processing further matches
   554  	if !move.IsValid() {
   555  		return errors.New("Invalid move " + s)
   556  	}
   557  	// execute move on position and store the keys for the positions
   558  	curPosKey := uint64(posPtr.ZobristKey())
   559  	posPtr.DoMove(move)
   560  	nextPosKey := uint64(posPtr.ZobristKey())
   561  	// add the move
   562  	b.addToBook(curPosKey, nextPosKey, uint32(move))
   563  	// no error
   564  	return nil
   565  }
   566  
   567  // adds a move to the book
   568  // this function is thread save to be used in parallel
   569  func (b *Book) addToBook(curPosKey uint64, nextPosKey uint64, move uint32) {
   570  	// out.Printf("Add %s to position %s\n", move.StringUci(), p.StringFen())
   571  
   572  	// mutex to synchronize parallel access
   573  	bookLock.Lock()
   574  	defer bookLock.Unlock()
   575  
   576  	// find the current position's entry
   577  	currentPosEntry, found := b.bookMap[curPosKey]
   578  	if !found {
   579  		b.log.Error("Could not find current position in book.")
   580  		return
   581  	}
   582  
   583  	// create or update book entry
   584  	nextPosEntry, found := b.bookMap[nextPosKey]
   585  	if found { // entry already exists - update
   586  		nextPosEntry.Counter++
   587  		b.bookMap[nextPosKey] = nextPosEntry
   588  		return
   589  	} else { // new entry
   590  		b.bookMap[nextPosKey] = BookEntry{
   591  			ZobristKey: nextPosKey,
   592  			Counter:    1,
   593  			Moves:      nil}
   594  		nextPosEntry = b.bookMap[nextPosKey]
   595  		// add move and link to child position entry to current entry
   596  		currentPosEntry.Moves = append(currentPosEntry.Moves, Successor{move, nextPosEntry.ZobristKey})
   597  		b.bookMap[curPosKey] = currentPosEntry
   598  	}
   599  }
   600  
   601  func (b *Book) loadFromCache(bookPath string) (bool, error) {
   602  	// determine cache file name
   603  	cachePath := bookPath + ".cache"
   604  
   605  	// Read cache file
   606  	// Open a RO file
   607  	decodeFile, err := os.Open(cachePath)
   608  	if err != nil {
   609  		return false, err
   610  	}
   611  	defer decodeFile.Close()
   612  
   613  	// Create a decoder
   614  	decoder := gob.NewDecoder(decodeFile)
   615  
   616  	// Decode -- We need to pass a pointer otherwise accounts2 isn't modified
   617  	bookLock.Lock()
   618  	err = decoder.Decode(&b.bookMap)
   619  	if err != nil {
   620  		return false, err
   621  	}
   622  	bookLock.Unlock()
   623  
   624  	// set root entry key
   625  	p := position.NewPosition()
   626  	b.rootEntry = uint64(p.ZobristKey())
   627  
   628  	// no error
   629  	return true, nil
   630  }
   631  
   632  func (b *Book) saveToCache(bookPath string) (string, int64, error) {
   633  	// determine cache file name
   634  	cachePath := bookPath + ".cache"
   635  
   636  	// Create a file for IO
   637  	encodeFile, err := os.Create(cachePath)
   638  	if err != nil {
   639  		return cachePath, 0, err
   640  	}
   641  	// create binary encoder
   642  	enc := gob.NewEncoder(encodeFile)
   643  
   644  	// encode bookMap
   645  	bookLock.Lock()
   646  	if err = enc.Encode(b.bookMap); err != nil {
   647  		panic(err)
   648  	}
   649  	bookLock.Unlock()
   650  
   651  	// close and return
   652  	err = encodeFile.Close()
   653  	if err != nil {
   654  		return cachePath, 0, err
   655  	}
   656  
   657  	// no error
   658  	fileInfo, _ := os.Stat(cachePath)
   659  	return cachePath, fileInfo.Size(), nil
   660  }