'''A Wrapper for Encyclopaedia of Chess Openings (ECO) (see `fiekas.eco`_)
It makes use of `polyglot`_ books to arrange the games.
.. _fiekas.eco: https://github.com/niklasf/eco
.. _polyglot: https://sourceforge.net/projects/codekiddy-chess/files/Books/Polyglot%20books/
'''
from typing import Optional, Union, Dict, List, Any
import os, os.path
import glob
import warnings
from enum import IntEnum, unique
import chess, chess.pgn, chess.polyglot
[docs]@unique
class TSVType(IntEnum):
'Columns of TSV-files'
ECO = 0
OPENING = 1
FEN = 2
MOVES = 3
[docs]class ECODatabase(list):
'''A Wrapper class for Encyclopaedia of Chess Openings (ECO)
:param ecoDirectory: directory of tsv - files
:param tsvPattern: a glob pattern describing the files to be loaded. ``None`` -> nothing is loaded
'''
keys = ['eco', 'name', 'fen', 'moves']
def __init__(self,
ecoDirectory : os.PathLike = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'eco'),
tsvPattern : Optional[str] = '*.tsv') -> None:
super(ECODatabase, self).__init__()
self.ecoDirectory = os.path.abspath(ecoDirectory)
if tsvPattern is None or len(tsvPattern) == 0:
return
for tsvFile in glob.glob(os.path.join(self.ecoDirectory, tsvPattern)):
self.loadTSVFile(tsvFile)
# print('{} lines read from file {}'.format(len(self), os.path.basename(tsvFile)))
[docs] def loadTSVFile(self, tsvFile : os.PathLike) -> None:
'''Adds a *tsvFile* to the database
:param tsvFile: tsv - file to be added
'''
if not os.path.isabs(tsvFile):
tsvFile = os.path.join(self.ecoDirectory, tsvFile)
if not os.path.isfile(tsvFile):
raise IOError('ECODatabase/loadTSVFile: ECO file {} not found'.format(tsvFile))
with open(tsvFile, mode='r') as f:
isFirst = True
while True:
row = f.readline().strip(' \n').split('\t')
if isFirst:
if row != self.keys:
raise IOError('ECODatabase/loadTSVFile: {} is not a valid ECO file'.format(tsvFile))
isFirst = False
continue
if len(row) != 4:
f.close()
return
moveList = list()
for uciCode in row[3].split(' '):
moveList.append(chess.Move.from_uci(uciCode))
self.append((row[0], row[1], row[2], moveList))
def column2IdList(self, key : TSVType) -> Dict[str, List[int]]:
resultDict = dict()
for id, tpl in enumerate(self):
_key = tpl[key]
if _key not in resultDict:
resultDict[_key] = list()
resultDict[_key].append(id)
return resultDict
def column2Id(self, column : int) -> Dict[str, int]:
# standard databases => only fen is unique
assert column >= 0 and column < 3
resultDict = dict()
for id, tpl in enumerate(self):
key = tpl[column]
if key in resultDict:
warnings.warn('ECODatabase/column2Id: Duplicate key {}'.format(key), RuntimeWarning)
resultDict[key] = id
return resultDict
def fen2Id(self) -> Dict[str, int]:
return self.column2Id(2)
def column2Count(self, column) -> Dict[str, int]:
assert column >= 0 and column < 3
resultDict = dict()
for tpl in self:
key = tpl[column]
if key not in resultDict:
resultDict[key] = 0
resultDict[key] += 1
return resultDict
@staticmethod
def commonLength(listOLists : List[List[Any]]) -> int:
n = 0
while True:
itemSet = set()
for valueList in listOLists:
if len(valueList) < n + 1:
return n
itemSet.add(valueList[n])
if len(itemSet) > 1:
return n
n += 1
return 0
[docs] def statistics(self, key : TSVType = TSVType.ECO) -> Dict[str, Dict[str, Union[int, List[int]]]]:
'''Delivers statistical data. ``key`` one of the 4 columns of a TSV-file, i.e.
* *ECO* : eco code
* *OPENING* : opening name
* *FEN* : Forsyth-Edwards-Notation of the starting position
* *MOVES* : move list
The values of the returned dictionary are
* *idList* : list of entries in the TSV table
* *nItems* : len(idlist)
* *nCommon* : number of common moves
:param key: column of a TSV-file (see above)
:returns: return dictionary
'''
statisticsDict = dict()
for columnValue, idList in self.column2IdList(key).items():
newDict = dict()
newDict['idList'] = idList
newDict['nItems'] = len(idList)
listOfMoveLists = list()
for id in idList:
listOfMoveLists.append(self[id][3])
newDict['nCommon'] = self.commonLength(listOfMoveLists)
statisticsDict[columnValue] = newDict
return statisticsDict
@staticmethod
def sortGameNode(gameNode : chess.pgn.GameNode):
if len(gameNode.variations) == 0:
return 1
if len(gameNode.variations) == 1:
return ECODatabase.sortGameNode(gameNode.variations[0]) + 1
nItems2idList = list()
for n in range(len(gameNode.variations)):
nItems2idList.append((ECODatabase.sortGameNode(gameNode.variations[n]), n))
nItems2idList = sorted(nItems2idList, reverse = True, key = lambda el : el[0])
newVariations = list()
for nItems, id in nItems2idList:
newVariations.append(gameNode.variations[id])
gameNode.variations = newVariations
return nItems2idList[0][0] + 1
def _createVariation(self, gameNode : chess.pgn.GameNode, nPly : int, idList : List[int], polyglotBook : Optional[chess.polyglot.MemoryMappedReader] = None) -> None:
move2idDict = dict()
for id in idList:
actMoveList = self[id][3]
if len(actMoveList) == nPly:
gameNode.comment = '{} : {}'.format(self[id][0], self[id][1])
else:
actMove = actMoveList[nPly]
if actMove not in move2idDict:
move2idDict[actMove] = list()
move2idDict[actMove].append(id)
if polyglotBook is not None:
w2mDict = dict()
for entry in polyglotBook.find_all(gameNode.board()):
if entry.move in move2idDict.keys():
w2mDict[entry.weight] = entry.move
moveList = list()
for weight in sorted(w2mDict.keys(), reverse = True):
moveList.append(w2mDict[weight])
for move in move2idDict.keys():
if move not in moveList:
moveList.append(move)
else:
moveList = move2idDict.keys()
for move in moveList:
newIdList = move2idDict[move]
newGameNode = gameNode.add_variation(move)
self._createVariation(newGameNode, nPly+1, newIdList)
[docs] def createGame(self, listOfFirstMoves : List[chess.Move], polyglotBook : Optional[chess.polyglot.MemoryMappedReader] = None) -> chess.pgn.Game:
'''Creates a opening game starting with ``listOfFirstMoves``. The opening variants are implemented as variants
:param listOfFirstMoves: list of starting moves
:param polyglotBook: if present, the moves are sorted with respect to frequency of use
:returns: return dictionary
'''
title = 'Openings starting with '
gameNode = chess.pgn.Game()
for move in listOfFirstMoves:
title += ' {}'.format(chess.square_name(move.to_square))
gameNode = gameNode.add_variation(move)
game = gameNode.game()
game.comment = title
game.headers['Site'] = 'ECO Tables'
game.headers['White'] = 'Niklas Fiekas/White'
game.headers['Black'] = 'Niklas Fiekas/Black'
nFirst = len(listOfFirstMoves)
idList = list()
for id, tpl in enumerate(self):
actMoves = tpl[3]
if len(actMoves) >= nFirst and actMoves[:nFirst] == listOfFirstMoves:
idList.append(id)
self._createVariation(gameNode, nFirst, idList, polyglotBook)
return game
if __name__ == "__main__":
def createOpeningGame(opening, moveList):
global fileDirectory, eco, polyglotBook
game = eco.createGame(moveList, polyglotBook = polyglotBook)
newPgnName = os.path.join(fileDirectory, 'training','openings', '{}.pgn'.format(opening))
ECODatabase.sortGameNode(game)
# print(game)
exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
f = open(newPgnName, "w", encoding="utf-8")
pgnString = game.accept(exporter)
f.write(pgnString)
f.close()
print('{} bytes written to {}'.format(len(pgnString), newPgnName))
fileDirectory = os.path.dirname(os.path.abspath(__file__))
bookFile = os.path.join(fileDirectory,'books','elo2400.bin')
polyglotBook = chess.polyglot.open_reader(bookFile)
eco = ECODatabase()
fen2IdDict = eco.fen2Id()
statistics = eco.statistics()
for ecoCode in sorted(statistics):
iDict = statistics[ecoCode]
print('{:3} : {:3}, {} items'.format(ecoCode, iDict['nCommon'], iDict['nItems'] ))
createOpeningGame('kingsPawn', [chess.Move.from_uci('e2e4'), chess.Move.from_uci('e7e5')])
createOpeningGame('sicilianDefense', [chess.Move.from_uci('e2e4'), chess.Move.from_uci('c7c5')])
createOpeningGame('frenchDefense', [chess.Move.from_uci('e2e4'), chess.Move.from_uci('e7e6')])
createOpeningGame('caroKannDefense', [chess.Move.from_uci('e2e4'), chess.Move.from_uci('c7c6')])
createOpeningGame('pircDefense', [chess.Move.from_uci('e2e4'), chess.Move.from_uci('d7d6')])
createOpeningGame('queensGambit', [chess.Move.from_uci('d2d4'), chess.Move.from_uci('d7d5')])
createOpeningGame('indianDefense', [chess.Move.from_uci('d2d4'), chess.Move.from_uci('g8f6')])
createOpeningGame('englishOpening', [chess.Move.from_uci('c2c4')])
createOpeningGame('retiOpening', [chess.Move.from_uci('g7f6')])