try:
from PyQt6 import QtWidgets, QtGui, QtCore
from PyQt6.QtGui import QAction
from PyQt6 import uic
import PyQt6.QtSvgWidgets
import PyQt6.QtCharts
except:
try:
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtWidgets import QAction
from PyQt5 import uic
import PyQt5.QtSvg
import PyQt5.QtChart
except:
raise ModuleNotFoundError('Neither the required PyQt6 nor PyQt5 modules completely installed')
import sys
# We must create the QtWidgets.QApplication here to avoid Sphinx issues
qApp = QtWidgets.QApplication(sys.argv)
'''
Main Window of the Chess GUI
================================
|MainWindow|
The main Window consists of 4 parts:
* On the left, the *game board* with the *material* and *next turn* labels. The *next turn* may 4 colors ``white``, ``black``, ``red`` (mate), ``yellow`` (draw)
* On the right, the TAB widget with the
* *Game* tree widget displaying the body of the actual game
* *Headers* list widget displaying the header of the actual game
* *Database* list widget displaying all loaded games
* *Score* plot displaying engine and material scores
* *Log* edit displaying communication with the engine in use
* On the bottom, the status bar with the labels
* *info/error* displaying information or errors
* *engine* displaying the engine in use
* *hint/score* displaying the hint and score computed by the engine
* *ECO code* displaying the eco code of the current opening, the hint shows the full opening string
* *square* displaying the square under the cursor within the board
Main Window Menu
================================
On the top, the menu with
* *File* menu handling files in the Portable Game Notation (`PGN`_) and pickled PGN (*PPNG*) formats
* *Encoding* sub-menu to set encoding for opening/saveing PNG-format
* *Open DB ...* action to open a PPGN- or PGN-file and replace the current database
* *Recent* sub-menu with the recent PPGN- or PGN-files
* *Append to DB ...* action to open a PPGN- or PGN-file and append to the current database
* *Save DB ...* action to save the whole database as a PPGN- or PGN-file
* *Save Game ...* action to save the actual game as a PGN-file
* *Exit* action to terminate the GUI
* *Edit* menu with obvious functionality with the exception of
* *Copy Game*, i.e. the actual game PGN is copied to the clipboard
* *Copy FEN*, i.e. the actual position is copied to the clipboard
* *Promote/Demote Variant* promote/demote the variant selected in the Game TAB
* *Promote Variant to Main* promote the variant selected in the Game TAB to mainline
* *Delete Variant* delete the variant selected in the Game TAB
* *Undo Current Action* deletes the end move of a sequence (main line or variant)
* *Database* menu with obvious functionality with the exception of
* *Remove Games* removes selected games in the *Database* TAB
* *Move Games>Up/Down* moves a selected game up and down in the *Database*
* *Paste Game*, i.e. the actual game PGN from the clipboard and added to the *Database*
* *Paste FEN*, i.e. the actual position is pasted from the clipboard and added to the *Database*
* *Game* menu with obvious functionality with the exception of
* *Select Header Elements ...* opens a dialog to add/remove header elements according to the `PGN`_ standard
* *Show Move Options* shows by left-button selecting a square the weighted options
* *Warn of Danger* shows squares attacked by the opponent
* *Engine* menu for handling Universal Chess Interface (`UCI`_) engines with
* *Select Engine* sub-menu to select the engine for hints/scores and annotations
* *Search Depth* sub-menu to set the search depth of the selected engine
* *Score Current Move* annotates the move leading to actual position
* *Annotate All* annotates the actual game or variant
* *# Annotations* defines the number of variants to be suggested in case of a blunder
* *Blunder Limit* defines the limit to add a variant
* *Annotate Variants* defines the number of half moves (PLY) to be shown in variants
* *Show Scores* toggle actions enables the *score* part of the *hint/score* label of the status bar
* *Show Hints* toggle actions enables the *hint* part of the *hint/score* label of the status bar
* *Configure ...* opens a dialog to add/remove/configure engines
* *Debug* logs the communication with engine in the *Log* TAB
Keyboard and Mouse Contol
================================
To allow for a user-friendly usage, the game can be controlled by keyboard and/or mouse:
.. csv-table:: Game Control by Keyboard/Mouse/Mouse Wheel
:header: "Key/Wheel", "Board", "Game-TAB", "Description"
:widths: 30, 10, 10, 50
:kbd:`up` | :kbd:`scroll-up` , :kbd:`✔`, :kbd:`✔`, goto last move
:kbd:`down` | :kbd:`scroll-down`, :kbd:`✔`, :kbd:`✔`, goto next move
:kbd:`left` , , :kbd:`✔`, step into variant
:kbd:`right` , , :kbd:`✔`, step out of a variant
:kbd:`home` , , :kbd:`✔`, goto initial move
:kbd:`end` , , :kbd:`✔`, goto end-of-game
:kbd:`mouse-left-press` , :kbd:`✔`, , begin move
:kbd:`mouse-left-release` , :kbd:`✔`, , end move
:kbd:`Control-P` , , :kbd:`✔`, promote variant
:kbd:`Control-D` , , :kbd:`✔`, demote variant
:kbd:`Control-R` , , :kbd:`✔`, remove variant
:kbd:`Control-M` , , :kbd:`✔`, promote variant to mainline
:kbd:`Control-W` , , :kbd:`✔`, toggle warn of danger
In addition, several *standard* key strokes are supported:
.. csv-table:: Other Key-Codes
:header: "Key", "Description"
:widths: 30, 50
:kbd:`Control-Z` , undo last move
:kbd:`Control-F` , flip board
:kbd:`Control-C` , copy game (PGN) to clipboard
:kbd:`Control-V` , paste game (PGN) from clipboard
:kbd:`Control-Shift-C` , copy position (FEN) to clipboard
:kbd:`Control-Shift-V` , paste position (FEN) from clipboard
.. |MainWindow| image:: mainWindow.png
:width: 800
:alt: Main Window
.. _PGN: https://github.com/fsmosca/PGN-Standard
.. _UCI: http://wbec-ridderkerk.nl/html/UCIProtocol.html
'''
from typing import Optional, Callable, Dict, List, Tuple, Union, Any
import configparser
import os, os.path
import copy
import platform
import pickle
import io
import re
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import chess, chess.pgn
import MzChess
from MzChess import read_game
import AboutDialog
[docs]class ChessMainWindow(QtWidgets.QMainWindow):
logSignal = QtCore.pyqtSignal(str)
notifySignal = QtCore.pyqtSignal(str)
notifyGameSelectedSignal = QtCore.pyqtSignal(int)
notifyGameListHeaderChangedSignal = QtCore.pyqtSignal(list)
notifyGameListChangedSignal = QtCore.pyqtSignal()
notifyGameHeadersChangedSignal = QtCore.pyqtSignal(chess.pgn.Headers)
notifyGameNodeSelectedSignal = QtCore.pyqtSignal(chess.pgn.GameNode)
notifyGameNodeChangedSignal = QtCore.pyqtSignal(chess.pgn.GameNode, tuple)
notifyGameChangedSignal = QtCore.pyqtSignal(chess.pgn.Game)
notifyNewGameNodeSignal = QtCore.pyqtSignal(chess.pgn.GameNode)
hintDict = { 'None' : '0', 'White' : '1', 'Black' : '2', 'All' : '3' }
hintList = [ 'None', 'White', 'Black', 'All' ]
fileDialogOptions = QtWidgets.QFileDialog.Option.DontUseNativeDialog
fileDirectory = os.path.dirname(os.path.abspath(__file__))
intRe = re.compile(r"^[+-]?[1-9][0-9]*$")
boolRe = re.compile(r"^(True|False)$")
def __init__(self, parent = None) -> None:
super(ChessMainWindow, self).__init__(parent)
MzChess.installLeipFont()
uic.loadUi(os.path.join(self.fileDirectory, 'chessMainWindow.ui'), self)
# self.setupUi(self)
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(os.path.join(self.fileDirectory,'schach.png')), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.setWindowIcon(icon)
self.pgm = MzChess.__name__
self.version = MzChess.__version__
self.dateString = MzChess.__date__
# self.helpIndex = QtCore.QUrl.fromLocalFile(os.path.join(os.path.dirname(self.fileDirectory), 'doc_build', 'html', 'index.html'))
self.helpIndex = QtCore.QUrl('https://reinhardm-dev.github.io/MzChess')
self.ecoDB = MzChess.ECODatabase()
self.ecoFen2IdDict = self.ecoDB.fen2Id()
self.toolBar = QtWidgets.QToolBar()
self._setIcon(self.actionSaveDB, QtWidgets.QStyle.StandardPixmap.SP_DialogSaveButton)
self.toolBar.addSeparator()
self._setIcon(self.actionAddGame, QtWidgets.QStyle.StandardPixmap.SP_FileIcon)
self._setIcon(self.actionFlipBoard, QtWidgets.QStyle.StandardPixmap.SP_BrowserReload)
self.toolBar.addSeparator()
self._setIcon(self.actionNextMove, QtWidgets.QStyle.StandardPixmap.SP_ArrowDown)
self._setIcon(self.actionPreviousMove, QtWidgets.QStyle.StandardPixmap.SP_ArrowUp)
self._setIcon(self.actionNextVariant, QtWidgets.QStyle.StandardPixmap.SP_ArrowForward)
self._setIcon(self.actionPreviousVariant, QtWidgets.QStyle.StandardPixmap.SP_ArrowBack)
self._setIcon(self.actionPromoteVariant, QtWidgets.QStyle.StandardPixmap.SP_MediaSeekBackward)
self._setIcon(self.actionDemoteVariant, QtWidgets.QStyle.StandardPixmap.SP_MediaSeekForward)
self._setIcon(self.actionPromoteVariant2Main, QtWidgets.QStyle.StandardPixmap.SP_MediaSkipBackward)
self._setIcon(self.actionDeleteVariant, QtWidgets.QStyle.StandardPixmap.SP_DialogCloseButton)
self.toolBar.addSeparator()
self._setIcon(self.actionUndo, QtWidgets.QStyle.StandardPixmap.SP_BrowserStop)
self.addToolBar(self.toolBar)
self.gameOptions = {
'showOptions' : (self.actionShowOptions, False),
'warnOfDanger' : (self.actionWarnOfDanger, False),
'encoding' : (self.menuEncoding, 'UTF-8')
}
self.encodingDict = { 'UTF-8' : 'utf-8-sig', 'ISO 8859/1' : 'iso-8859-1', 'ASCII' : 'ascii'}
self.engineOptions = {
'selectedEngine' : (self.menuSelectEngine, None),
'searchDepth' : (self.menuSearchDepth, 15),
'numberOfAnnotations' : (self.menuNumberOfAnnotations, 1),
'annotateVariants' : (self.menuAnnotateVariants, None),
'showScores' : (self.actionShowScores, False),
}
# 'blunderLimit' : (self.menuBlunderLimit, -float('inf')),
self.gameListHeaders = ['Date', 'White', 'Black', 'Result']
self.engineDict = dict()
self.recentPGN = dict()
self.eventList = list()
self.siteList = list()
self.playerList = list()
self.optionalHeaderItems = list()
self.hintEngine = None
self.mateScore = 100
self.debugEngine = False
if platform.system() == 'Windows':
self.settingsDir = os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'MzChess')
else:
self.settingsDir = os.path.join(os.path.expanduser('~'), '.config', 'MzChess')
if not os.path.isdir(self.settingsDir):
os.mkdir(self.settingsDir, 0o755)
self.settingsFile = os.path.join(self.settingsDir, 'settings.ini')
self.recoverFile = os.path.join(self.settingsDir, 'recover.ppgn')
self.settings = configparser.ConfigParser(delimiters=['='], allow_no_value=True)
self.settings.optionxform = str
self.recentPGN = dict()
self.engineDict = dict()
self.eventList = list()
self.siteList = list()
self.playerList = list()
self.optionalHeaderItems = list()
if os.path.isfile(self.settingsFile):
self.settings.read(self.settingsFile, encoding = 'utf-8')
self.engineDict = MzChess.loadEngineSettings(self.settings)
if 'Recent' in self.settings.sections():
for recentDB, enc in dict(self.settings['Recent']).items():
if os.path.exists(recentDB):
self.recentPGN[recentDB] = enc
if 'GameListHeaders' in self.settings.sections():
self.gameListHeaders = self.settings.options('GameListHeaders')
if 'Events' in self.settings.sections():
self.eventList = self.settings.options('Events')
if 'Sites' in self.settings.sections():
self.siteList = self.settings.options('Sites')
if 'Players' in self.settings.sections():
self.playerList = self.settings.options('Players')
if 'OptionalHeaderItems' in self.settings.sections():
self.optionalHeaderItems = list(self.settings['OptionalHeaderItems'])
if self.settings['Menu/Engine']['selectedEngine'] not in self.engineDict:
self.settings['Menu/Engine']['selectedEngine'] = None
self.settings['Menu/Engine']['showHints'] = '0'
self.settings['Menu/Engine']['showScores'] = False
self.menuRecentDB.clear()
if self.recentPGN is None:
self.recentPGN = dict()
else:
actDir = os.path.expanduser('~')
for rItem in self.recentPGN.keys():
if rItem is not None and os.path.isfile(rItem):
if actDir == os.path.expanduser('~'):
actDir = os.path.dirname(rItem)
self.menuRecentDB.addAction(rItem)
os.chdir(actDir)
self.aboutDialog = AboutDialog.AboutDialog()
self.aboutDialog.setup(
pgm = self.pgm,
version = self.version,
dateString = self.dateString)
sbText = self.statusBar().font()
sbText.setPointSize(12)
self.moveLabel = QtWidgets.QLabel()
self.moveLabel.setFont(sbText)
self.moveLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.moveLabel.setToolTip("Total Moves/Half-moves since the last capture or pawn move")
self.statusBar().addWidget(self.moveLabel, 20)
self.infoLabel = QtWidgets.QLabel()
self.infoLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
self.infoLabel.setFont(sbText)
self.statusBar().addWidget(self.infoLabel, 130)
self.engineLabel = QtWidgets.QLabel()
self.engineLabel.setFont(sbText)
self.engineLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.engineLabel.setToolTip("Engine in use")
self.statusBar().addWidget(self.engineLabel, 50)
self.hintLabel = QtWidgets.QLabel()
self.hintLabel.setFont(sbText)
self.hintLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.hintLabel.setToolTip("Best move/Score[Pawns]")
self.statusBar().addWidget(self.hintLabel, 30)
self.ecoLabel = QtWidgets.QLabel()
self.ecoLabel.setFont(sbText)
self.ecoLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.statusBar().addWidget(self.ecoLabel, 10)
self.squareLabel = QtWidgets.QLabel()
self.squareLabel.setFont(sbText)
self.squareLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.squareLabel.setToolTip("Position")
self.statusBar().addWidget(self.squareLabel, 10)
self.itemSelector = MzChess.ItemSelector('Header Elements (without 7-tag roster)...', pointSize = 10)
self.loadOptionDict('Menu/Game', self.gameOptions)
self.resetSelectEngine()
self.loadOptionDict('Menu/Engine', self.engineOptions)
blunderLimit = float(self.settings['Menu/Engine'].get('blunderLimit', '-inf'))
for action in self.menuBlunderLimit.actions():
tList = action.text().split(' ')
action.setChecked((len(tList) == 1 and blunderLimit == -float('inf')) \
or (len(tList) == 2 and blunderLimit == -float(tList[0])))
hintValue = int(self.settings['Menu/Engine'].get('showHints', 0))
for actAction in self.menuShowHints.actions():
if actAction.text() == self.hintList[hintValue]:
actAction.setChecked(True)
self.gameListTableView.setup(notifyDoubleClickSignal = self.notifyGameSelectedSignal,
notifyHeaderChangedSignal = self.notifyGameListHeaderChangedSignal,
notifyListChangedSignal = self.notifyGameListChangedSignal)
self.gameListTableView.gameHeaderKeys = self.gameListHeaders
self.notifyGameSelectedSignal.connect(self.gameSelected)
self.notifyGameListHeaderChangedSignal.connect(self.gameListHeaderChanged)
self.notifyGameListChangedSignal.connect(self.gameListChanged)
self.pgnFile = None
self.gameListFile = ''
self.gameList = list()
self.gameListChanged = False
self.gameFile = ''
self.game = chess.pgn.Game()
self.gameNode = self.game
self.gameID = None
self.undoListList = list()
self.redoListList = list()
self._addGame(game = None, isNew = True)
self.setChessWindowTitle()
self.gameSelected(0)
self.gameHeaderTableView.setup(
notifyGameHeadersChangedSignal = self.notifyGameHeadersChangedSignal,
eventList = self.eventList, siteList = self.siteList, playerList = self.playerList)
self.notifyGameHeadersChangedSignal.connect(self.gameHeadersChanged)
self.boardGraphicsView.setup(
notifyNewGameNodeSignal = self.notifyNewGameNodeSignal, notifyGameNodeSelectedSignal =self.notifyGameNodeSelectedSignal,
materialLabel = self.materialLabel, squareLabel = self.squareLabel,
turnFrame= self.turnFrame, hintLabel = self.hintLabel,
flipped = False)
self.gameTreeViewWidget.setup(self.notifyGameNodeSelectedSignal, self.notifyGameNodeChangedSignal)
self.scorePlotGraphicsView.setup(self.notifyGameNodeSelectedSignal)
self.notifyGameNodeSelectedSignal.connect(self.gameNodeSelected)
self.notifyGameNodeChangedSignal.connect(self.gameNodeChanged)
self.notifyNewGameNodeSignal.connect(self.newGameNode)
self.notifyGameChangedSignal.connect(self.gameChanged)
self.notifySignal.connect(self.notify)
self.logSignal.connect(self.toLog)
self.show_HintsScores()
def setup(self) -> None:
sizes = self.splitter.sizes()
self.splitter.setSizes([self.boardGraphicsView.height(), sizes[0] + sizes[1] - self.boardGraphicsView.height()])
self.boardGraphicsView.setDrawOptions(self.actionShowOptions.isChecked())
self.boardGraphicsView.setGameNode(self.game)
self.gameTreeViewWidget.setGame(self.game)
self.gameListTableView.resetDB()
self.gameHeaderTableView.resetGame()
def loadOptionDict(self, name : str, n2oDict : Dict[str, Union[QAction, Tuple[QtWidgets.QMenu, Any]]]) -> Dict[str, Union[int, float]]:
if name not in self.settings:
self.settings[name] = dict()
optionsDict = dict()
for opt, o2v in n2oDict.items():
obj, defValue = o2v
optionsDict[opt] = defValue
optValue = self.settings[name].get(opt, defValue)
if isinstance(obj, QAction):
optionsDict[opt] = (optValue == 'True')
obj.setChecked(optionsDict[opt])
elif isinstance(obj, QtWidgets.QMenu):
for action in obj.actions():
tListString = action.text()
if len(tListString) == 0:
continue
tList = tListString.split(' ')
if self.intRe.match(tList[0]) is not None:
actionValue = int(tList[0])
elif tList[0] == 'All':
actionValue = 2^32 - 1
elif tList[0] == 'None':
actionValue = None
else:
try:
actionValue = float(tList[0])
except:
actionValue = tListString
if str(actionValue) == optValue or '{}.0'.format(actionValue) == optValue:
action.setChecked(True)
optionsDict[opt] = actionValue
else:
action.setChecked(False)
for tag, value in optionsDict.items():
self.settings[name][tag] = str(value)
def resetSelectEngine(self) -> None:
self.menuSelectEngine.clear()
action = self.menuSelectEngine.addAction('None')
action.setCheckable(True)
if 'Menu/Engine' in self.settings.sections():
if self.settings['Menu/Engine']['selectedEngine'] is None or self.settings['Menu/Engine']['selectedEngine'] not in self.engineDict:
action.setChecked(True)
self.menuSelectEngine.addSeparator()
for eItem in self.engineDict:
action = self.menuSelectEngine.addAction(eItem)
action.setCheckable(True)
if self.settings['Menu/Engine']['selectedEngine'] == eItem:
action.setChecked(True)
def updateSettingsList(self, section : str, valueList : List[Union[str, Tuple[str, str]]] = list(), firstValue : Union[str, Tuple[str, str], None] = None) -> None:
if section not in self.settings.sections():
self.settings.add_section(section)
newDict = dict()
if len(valueList) > 0:
newDict = dict(self.settings[section])
if isinstance(firstValue, tuple):
key = firstValue[0]
value = firstValue[1]
else:
key = firstValue
value = None
keys = list()
if key is not None:
newDict[key] = value
self.settings.remove_option(section, key)
keys = [key]
if len(valueList) == 0:
keys += self.settings.options(section)
self.settings.remove_section(section)
self.settings.add_section(section)
for key in keys:
if key in newDict:
value = newDict[key]
self.settings[section][key] = value
for kv in valueList:
if isinstance(kv, tuple):
self.settings[section][kv[0]] = kv[1]
else:
self.settings[section][kv] = None
def saveSettings(self) -> None:
with open(self.settingsFile, 'w', encoding = 'utf-8') as f:
self.settings.write(f)
# --------------------------------------------------------------------------------------------------
@QtCore.pyqtSlot(str)
def notifyError(self, str : str) -> None:
msgBox = QtWidgets.QMessageBox()
msgBox.setIcon(QtWidgets.QMessageBox.Icon.Critical)
msgBox.setText(str)
msgBox.setWindowTitle("Error ...")
msgBox.exec()
def setInfoLabel(self, setupGameID = False):
if setupGameID:
if self.game in self.gameList:
self.gameID = self.gameList.index(self.game)
else:
if self.gameID > 0:
self.gameID = self.gameID - 1
self.game = self.gameList[self.game]
self.infoLabel.setText('game #{} of {} ({} moves): {} = {}'.format(self.gameID, len(self.gameList),
self.gameList[self.gameID].end().board().fullmove_number, self.gameListHeaders[0], self.game.headers[self.gameListHeaders[0]]))
self.infoLabel.update()
QtWidgets.QApplication.processEvents()
def setMoveLabel(self, board):
self.moveLabel.setText('{}/{}'.format(board.fullmove_number, board.halfmove_clock))
self.moveLabel.update()
QtWidgets.QApplication.processEvents()
def notify(self, str):
self.statusBar().showMessage(str, 3000)
@QtCore.pyqtSlot(str)
def toLog(self, msg):
self.uciTextEdit.moveCursor (QtGui.QTextCursor.MoveOperation.End)
self.uciTextEdit.insertPlainText (msg)
self.uciTextEdit.moveCursor(QtGui.QTextCursor.MoveOperation.End)
def _setIcon(self, action, stdIcon, toToolbar = True):
action.setIcon(self.style().standardIcon(stdIcon))
if toToolbar:
self.toolBar.addAction(action)
def _showEcoCode(self, gameNode, fromBeginning = False):
if fromBeginning:
actNode = gameNode.game().next()
self.ecoCode = '---'
self.ecoDescription = ''
else:
actNode = gameNode
id = -1
while actNode is not None:
fen = ' '.join(actNode.board().fen().split(' ')[:-2])
if fen in self.ecoFen2IdDict:
id = self.ecoFen2IdDict[fen]
actNode = actNode.next()
if id >= 0:
self.ecoCode = self.ecoDB[id][0]
self.ecoDescription = self.ecoDB[id][1]
gameNode.game().headers['Opening'] = self.ecoDescription
gameNode.game().headers['ECO'] = self.ecoCode
self.ecoLabel.setText(self.ecoCode)
self.ecoLabel.setToolTip(self.ecoDescription)
def _allowNewGameList(self):
if self.gameListChanged:
rc = QtWidgets.QMessageBox.warning(self,
'Game database not saved ...', 'Do you like to close anyway ?',
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
QtWidgets.QMessageBox.StandardButton.No)
if rc == QtWidgets.QMessageBox.StandardButton.No:
return False
self.gameListChanged = False
self.undoListList = list()
self.redoListList = list()
self.gameList = list()
self.pgnFile = None
self._addGame(game = None, isNew = True)
return True
def _addGame(self, game = None, isNew = False):
if game is not None:
self.game = game
else:
self.game = chess.pgn.Game()
self.game.headers = self._setGameHeader(self.optionalHeaderItems)
self.gameNode = self.game
self.gameID = len(self.gameList)
self.gameList.append(self.game)
self.undoListList.append(list())
self.redoListList.append(list())
self.gameListTableView.setGameList(self.gameList)
self._showEcoCode(self.game, fromBeginning = True)
self.gameSelected(self.gameID)
if not isNew:
self.setChessWindowTitle()
def _setGameHeader(self, optionalHeaderItems : List[str]) -> chess.pgn.Headers:
selectedItems = list(chess.pgn.Headers()) + optionalHeaderItems
newHeaders = chess.pgn.Headers()
for el in selectedItems:
if el in self.game.headers:
newHeaders[el] = self.game.headers[el]
else:
newHeaders[el] = self.gameHeaderTableView.stdKey2ValueTypeDict[el][0]
self.optionalHeaderItems = optionalHeaderItems
return newHeaders
# ---------------------------------------------------------------------------
def openPGN(self, pgnFile : str, encoding : str = None):
if not self._allowNewGameList():
return
self.notify('Opening {} ...'.format(os.path.basename(pgnFile)))
self.gameListFile = os.path.split(pgnFile)[1]
_, ext = os.path.splitext(pgnFile)
if ext == '.ppgn':
try:
with open(pgnFile, mode = 'rb') as f:
self.gameList = pickle.load(f)
encoding = None
except:
self.notifyError('Cannot open PPGN file {}'.format(pgnFile))
return
elif ext == '.pgn':
try:
pgn = open(pgnFile, mode = 'r', encoding = encoding)
except:
self.notifyError('Cannot open PGN file {}'.format(pgnFile))
return
n = 1
self.gameList = list()
while True:
actGame = read_game(pgn)
if actGame is None:
break
if len(actGame.errors) == 0:
self.gameList.append(actGame)
else:
self.notifyError('Failed to load game #{}'.format(n))
return
n += 1
else:
self.notifyError('Cannot handle file with extension "{}"'.format(ext))
return
if len(self.gameList) < 1:
self.notifyError('No game loaded')
return
with open(self.recoverFile, mode = 'wb') as f:
pickle.dump(self.gameList, f)
self.gameListTableView.setGameList(self.gameList)
self.gameSelected(0)
self.settings['Recent'] = {pgnFile : encoding}
newRecentPGN = {pgnFile : encoding}
for file, enc in self.recentPGN.items():
if file != pgnFile:
newRecentPGN[file] = enc
self.settings['Recent'][file] = enc
self.recentPGN = newRecentPGN
self.saveSettings()
self.menuRecentDB.clear()
for rItem in self.recentPGN:
if rItem is not None and os.path.isfile(rItem):
self.menuRecentDB.addAction(rItem)
if pgnFile != self.recoverFile:
os.chdir(os.path.dirname(pgnFile))
self.pgnFile = pgnFile
self.setChessWindowTitle()
@QtCore.pyqtSlot(QAction)
def on_menuEncoding_triggered(self, action):
for actAction in self.menuEncoding.actions():
actAction.setChecked(False)
action.setChecked(True)
self.settings['Menu/Game']['encoding'] = action.text()
self.saveSettings()
@QtCore.pyqtSlot()
def on_actionOpenDB_triggered(self):
pgnFile, _ = QtWidgets.QFileDialog.getOpenFileName(self,"Load Game Database ...", "",
"Pickle Portable Game Notation Files (*.ppgn);;Portable Game Notation Files (*.pgn);;All Files (*)", options = self.fileDialogOptions)
if pgnFile is not None and len(pgnFile) > 0:
self.openPGN(pgnFile, self.encodingDict[self.settings['Menu/Game']['encoding']])
@QtCore.pyqtSlot()
def on_actionRecoverDB_triggered(self):
if not os.path.exists(self.recoverFile):
self.notifyError('Recovery database not existing')
return
self.openPGN(self.recoverFile)
@QtCore.pyqtSlot()
def on_actionGameUp_triggered(self):
if self.gameListTableView.on_menuMoveGame_triggered(self.actionGameUp):
self.setInfoLabel(True)
self.setChessWindowTitle()
@QtCore.pyqtSlot()
def on_actionCloseDB_triggered(self):
if not self._allowNewGameList():
return
self.notify('Closing database ...')
self.setChessWindowTitle()
[docs] @QtCore.pyqtSlot(QtGui.QCloseEvent)
def closeEvent(self, ev):
if not self._allowNewGameList():
ev.ignore()
else:
if self.hintEngine is not None:
self.hintEngine.kill(True)
ev.accept()
@QtCore.pyqtSlot()
def on_actionExit_triggered(self):
if self._allowNewGameList():
self.close()
def saveDB(self, forceAppend : bool = False, saveCurrent : bool = False):
if len(self.gameList) == 0:
self.notifyError('No Game Database available')
return
if forceAppend:
mode = 'a'
else:
mode = 'w'
if saveCurrent and self.pgnFile is not None:
pgnFile = self.pgnFile
else:
pgnFile, _ = QtWidgets.QFileDialog.getSaveFileName(self,
"Save Game Database ...", self.gameListFile,
"Portable Game Notation Files (*.ppgn *.pgn)", options = self.fileDialogOptions)
if pgnFile is None or len(pgnFile) == 0:
return
_, ext = os.path.splitext(pgnFile)
if len(ext) == 0:
ext = '.pgn'
self.notify('Saving database to PGN file {}.pgn ...'.format(pgnFile))
else:
self.notify('Saving database to file {} ...'.format(pgnFile))
if ext == '.ppgn':
if mode == 'a':
self.notifyError('Cannot append to PPGN file {}'.format(pgnFile))
return
try:
with open(pgnFile, mode = 'wb') as f:
pickle.dump(self.gameList, f)
except:
self.notifyError('Cannot open PPGN file {}'.format(pgnFile))
return
encoding = None
elif ext == '.pgn':
try:
encoding = self.settings['Menu/Game']['encoding']
pgnString = str()
for game in self.gameList:
exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
pgnString += game.accept(exporter)
if game != self.gameList[-1]:
pgnString += '\n\n'
with open(pgnFile, mode = mode, encoding = self.encodingDict[encoding]) as f:
f.write(pgnString)
except:
self.notifyError('Cannot save PGN file {}'.format(pgnFile))
return
else:
self.notifyError('PGN file {} has improper extension (.pgn or .pgn expected)'.format(pgnFile))
return
self.updateSettingsList('Recent', self.recentPGN.items(), firstValue = (pgnFile, encoding))
self.saveSettings()
with open(self.recoverFile, mode = 'wb') as f:
pickle.dump(self.gameList, f)
self.gameListFile = os.path.split(pgnFile)[1]
os.chdir(os.path.dirname(pgnFile))
self.pgnFile = pgnFile
self.undoListList = list()
self.redoListList = list()
self.setChessWindowTitle()
@QtCore.pyqtSlot()
def on_actionAppend2DB_triggered(self):
self.notify('Appending database ...')
self.saveDB(forceAppend = True, saveCurrent = False)
@QtCore.pyqtSlot()
def on_actionSaveDB_triggered(self):
self.saveDB(forceAppend = False, saveCurrent = True)
@QtCore.pyqtSlot()
def on_actionSaveDBAs_triggered(self):
self.saveDB(forceAppend = False, saveCurrent = False)
@QtCore.pyqtSlot()
def on_actionAddGame_triggered(self):
self.notify('Adding game #{} ...'.format(self.gameID))
self._addGame()
@QtCore.pyqtSlot()
def on_actionRemoveGames_triggered(self):
if self.gameListTableView.on_actionRemoveGames_triggered():
if self.game in self.gameList:
self.setInfoLabel(True)
else:
self.gameSelected(0)
self.setChessWindowTitle()
@QtCore.pyqtSlot()
def on_actionGameDown_triggered(self):
if self.gameListTableView.on_menuMoveGame_triggered(self.actionGameDown):
self.setInfoLabel(True)
self.setChessWindowTitle()
@QtCore.pyqtSlot(QAction)
def on_menuRecentDB_triggered(self, action):
fileName = action.text()
self.openPGN(fileName, self.recentPGN[fileName])
@QtCore.pyqtSlot(QAction)
def on_menuEndGame_triggered(self, action):
endGame = self.game.end()
if endGame.board().is_game_over():
self.notifyError('You cannot change the game result any more')
return
self.game.headers['Result'] = action.text()
self.boardGraphicsView.setGameNode(self.gameNode)
self.gameChanged(self.game)
@QtCore.pyqtSlot()
def on_actionSaveGame_triggered(self):
pgnFile, _ = QtWidgets.QFileDialog.getSaveFileName(self,
"Save Game ...", self.gameFile,"Portable Game Notation Files (*.pgn);;All Files (*)", options = self.fileDialogOptions)
if pgnFile is not None and len(pgnFile) > 0:
try:
encoding = self.settings['Menu/Game']['encoding']
f = open(pgnFile, mode = 'w', encoding = self.encodingDict[encoding])
except:
self.notifyError('Cannot open PGN file {}'.format(pgnFile))
return
self.notify('Saving game #{} ...'.format(self.gameID))
exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
pgnString = self.game.accept(exporter)
f.write(pgnString)
f.close()
self.updateSettingsList('Recent', self.recentPGN.items(), firstValue = (pgnFile, encoding))
self.saveSettings()
self.gameFile = os.path.split(pgnFile)[1]
os.chdir(os.path.dirname(pgnFile))
# ---------------------------------------------------------------------------
[docs] @QtCore.pyqtSlot(QtGui.QWheelEvent)
def wheelEvent(self, ev):
numDegrees = ev.angleDelta()
if numDegrees.y() < 0:
self.on_actionNextMove_triggered()
elif numDegrees.y() > 0:
self.on_actionPreviousMove_triggered()
ev.accept()
@QtCore.pyqtSlot()
def on_actionNextMove_triggered(self):
self.gameNode = self.boardGraphicsView.nextMove()
self.gameTreeViewWidget.selectNodeItem(self.gameNode)
self.scorePlotGraphicsView.selectNodeItem(self.gameNode)
self.setMoveLabel(self.gameNode.board())
@QtCore.pyqtSlot()
def on_actionPreviousMove_triggered(self):
self.gameNode = self.boardGraphicsView.previousMove()
self.gameTreeViewWidget.selectNodeItem(self.gameNode)
self.scorePlotGraphicsView.selectNodeItem(self.gameNode)
self.setMoveLabel(self.gameNode.board())
@QtCore.pyqtSlot()
def on_actionNextVariant_triggered(self):
self.gameTreeViewWidget.selectSubnodeItem(self.gameNode, next = True)
@QtCore.pyqtSlot()
def on_actionPreviousVariant_triggered(self):
self.gameTreeViewWidget.selectSubnodeItem(self.gameNode, next = False)
@staticmethod
def variantHead(gameNode):
if gameNode is None:
return (None, None)
parentNode = gameNode.parent
while parentNode is not None and len(parentNode.variations) < 2:
gameNode = parentNode
parentNode = gameNode.parent
return (parentNode, gameNode)
def promoteVariant(self, toMain):
headParentNode, headNode = self.variantHead(self.gameNode)
oldVariations = copy.copy(headParentNode.variations)
oldID = oldVariations.index(headNode)
if headParentNode is None or oldID == 0:
self.notifyError('You cannot promote main line nodes')
return
if toMain or oldID < 2:
self.gameTreeViewWidget.moveVariant2Main(headParentNode, oldID)
headParentNode.promote_to_main(headNode)
else:
self.gameTreeViewWidget.moveVariant(headParentNode, oldID, True)
headParentNode.promote(headNode)
self.undoListList[self.gameID].append(('variations', [(headParentNode, oldVariations)]))
self.gameNodeSelected(self.gameNode)
if self.gameNode.is_mainline():
self.scorePlotGraphicsView.setGame(self.game)
self.scorePlotGraphicsView.selectNodeItem(self.gameNode)
self.setChessWindowTitle()
@QtCore.pyqtSlot()
def on_actionPromoteVariant_triggered(self):
self.promoteVariant(False)
@QtCore.pyqtSlot()
def on_actionPromoteVariant2Main_triggered(self):
self.promoteVariant(True)
@QtCore.pyqtSlot()
def on_actionDemoteVariant_triggered(self):
headParentNode, headNode = self.variantHead(self.gameNode)
oldVariations = copy.copy(headParentNode.variations)
oldID = oldVariations.index(headNode)
isMainline = headNode.is_mainline()
if headParentNode is None \
or oldID == len(headParentNode.variations) - 1:
self.notifyError('You cannot demote last line nodes')
return
if oldID > 0:
self.gameTreeViewWidget.moveVariant(headParentNode, oldID, False)
else:
self.gameTreeViewWidget.moveVariant2Main(headParentNode, 1)
headParentNode.demote(headNode)
self.undoListList[self.gameID].append(('variations', [(headParentNode, oldVariations)]))
if False:
self.gameTreeViewWidget.setGame(self.game)
self.gameNodeSelected(self.gameNode)
if isMainline:
self.scorePlotGraphicsView.setGame(self.game)
self.scorePlotGraphicsView.selectNodeItem(self.gameNode)
self.setChessWindowTitle()
@QtCore.pyqtSlot()
def on_actionDeleteVariant_triggered(self):
headParentNode, headNode = self.variantHead(self.gameNode)
if headParentNode is None:
self.notifyError('Use "Database/Remove Games" to remove a complete game')
return
oldVariations = copy.copy(headParentNode.variations)
oldID = oldVariations.index(headNode)
isMainline = headNode.is_mainline()
if oldID > 0:
self.gameTreeViewWidget.moveVariant(headParentNode, oldID, None)
else:
self.gameTreeViewWidget.moveVariant2Main(headParentNode, None)
headParentNode.remove_variation(headNode)
self.undoListList[self.gameID].append(('variations', [(headParentNode, oldVariations)]))
if False:
self.gameTreeViewWidget.setGame(self.game)
if isMainline:
self.scorePlotGraphicsView.setGame(self.game)
self.gameNodeSelected(headParentNode)
self.setChessWindowTitle()
@QtCore.pyqtSlot()
def on_actionDeleteAllVariants_triggered(self):
while not self.gameNode.is_mainline():
self.gameNode = self.gameNode.parent
gameNodeValueDict = dict()
for gameNode in self.game.mainline():
if len(gameNode.variations) > 1:
gameNodeValueDict[gameNode] = copy.copy(gameNode.variations)
del gameNode.variations[:1]
if len(gameNodeValueDict) > 0:
gameNodeValueList = [gameNodeValueDict.pop(self.gameNode, str())]
gameNodeValueList += gameNodeValueDict.items()
self.undoListList[self.gameID].append(('nags', gameNodeValueList))
self.gameNodeSelected(self.gameNode)
self.setChessWindowTitle()
@staticmethod
def _deleteGameNodesLenGtZero(gameNode, attr, clearedAttr, gameNodeValueDict = dict()):
attrValue = getattr(gameNode, attr)
if len(attrValue) > 0:
gameNodeValueDict[gameNode] = attrValue
setattr(gameNode, attr, clearedAttr)
for subNode in gameNode.variations:
ChessMainWindow._deleteGameNodesLenGtZero(subNode, attr, clearedAttr, gameNodeValueDict)
return gameNodeValueDict
@QtCore.pyqtSlot()
def on_actionDeleteAllNAGs_triggered(self):
gameNodeValueDict = self._deleteGameNodesLenGtZero(self.game, 'nags', set())
if len(gameNodeValueDict) != 0:
gameNodeValueList = [(self.game, gameNodeValueDict.pop(self.game, set()))]
gameNodeValueList += gameNodeValueDict.items()
self.undoListList[self.gameID].append(('nags', gameNodeValueList))
self.gameTreeViewWidget.setGame(self.game)
self.setChessWindowTitle()
@QtCore.pyqtSlot()
def on_actionDeleteAllComments_triggered(self):
gameNodeValueDict = self._deleteGameNodesLenGtZero(self.game, 'comment', str())
if len(gameNodeValueDict) != 0:
gameNodeValueList = [(self.game, gameNodeValueDict.pop(self.game, str()))]
gameNodeValueList += gameNodeValueDict.items()
self.undoListList[self.gameID].append(('comment', gameNodeValueList))
self.gameTreeViewWidget.setGame(self.game)
self.setChessWindowTitle()
@QtCore.pyqtSlot()
def on_actionUndoCurrentMove_triggered(self):
if self.gameNode is None or self.gameNode.parent is None:
return
if not self.gameNode.is_end():
self.notifyError('Select the end of the mainline or a variant')
return
gameNode = self.gameNode
if len(gameNode.parent.variations) > 1:
self.notifyError('Cannot undo move, node owns variants')
return
self.gameNode = self.gameNode.parent
self.boardGraphicsView.setGameNode(self.gameNode)
self.gameTreeViewWidget.removeGameNode(gameNode)
if gameNode.is_mainline():
self.scorePlotGraphicsView.removeLastNode()
self.gameNode.remove_variation(gameNode)
self.setChessWindowTitle()
@QtCore.pyqtSlot()
def on_actionUndo_triggered(self):
if self.gameID is None or len(self.undoListList[self.gameID]) == 0:
return
item = self.undoListList[self.gameID].pop(-1)
if isinstance(item, chess.pgn.GameNode):
self.redoListList[self.gameID].append(item)
gameNode = item
parent = gameNode.parent
self.gameTreeViewWidget.removeGameNode(gameNode)
if gameNode.is_mainline():
self.scorePlotGraphicsView.removeLastNode()
parent.remove_variation(gameNode)
self.gameNodeSelected(parent)
elif isinstance(item, tuple):
attr, gameNodeValueList = item
gameNode = gameNodeValueList[0][0]
if attr == 'headers':
self.redoListList[self.gameID].append((attr, (gameNode, getattr(self.game, attr))))
setattr(self.game, attr, gameNodeValueList[0][1])
else:
redoGameNodeValueList = list()
if attr == 'variations':
for headParentNode, savedVariations in gameNodeValueList:
redoGameNodeValueList.append((headParentNode, getattr(headParentNode, attr)))
variantDeleted = len(headParentNode.variations) < len(savedVariations)
mainlineChanged = headParentNode.variations[0] != savedVariations[0]
promotedToMain = mainlineChanged and not variantDeleted
if not (promotedToMain or variantDeleted):
for firstId, gameNode in enumerate(savedVariations):
if headParentNode.variations[firstId] != savedVariations[firstId]:
self.gameTreeViewWidget.moveVariant(headParentNode, firstId, False)
headParentNode.variations = savedVariations
break
else:
gameNode.variations = savedVariations
self.gameSelected(self.gameID)
elif attr == 'game':
self.redoListList[self.gameID].append((attr, [(self.gameNode, pickle.dumps(self.game))]))
self.game = pickle.dumps(gameNodeValueList[0][1])
else:
for node, oldAttrValue in gameNodeValueList:
redoGameNodeValueList.append((node, getattr(node, attr)))
setattr(node, attr, oldAttrValue)
self.gameSelected(self.gameID)
self.redoListList[self.gameID].append((attr, redoGameNodeValueList))
self.gameNodeSelected(gameNode)
else:
raise ValueError('UIE: Undo item type {} not expected'.format(type(item)))
if self.gameNode == self.game:
self.scorePlotGraphicsView.setGame(self.game)
self.gameTreeViewWidget.setGame(self.game)
self.setChessWindowTitle()
@QtCore.pyqtSlot()
def on_actionRedo_triggered(self):
if self.gameID is None or len(self.redoListList[self.gameID]) == 0:
return
item = self.redoListList[self.gameID].pop(-1)
if isinstance(item, chess.pgn.GameNode):
item.parent.variations.append(item)
self.newGameNode(item)
self.gameNodeSelected(item)
elif isinstance(item, tuple):
attr, gameNodeValueList = item
gameNode = gameNodeValueList[0][0]
if attr == 'headers':
self.undoListList[self.gameID].append((attr, (gameNode, getattr(self.game, attr))))
setattr(self.game, attr, gameNodeValueList[0][1])
else:
redoGameNodeValueList = list()
if attr == 'variations':
for headParentNode, savedVariations in gameNodeValueList:
redoGameNodeValueList.append((headParentNode, getattr(headParentNode, attr)))
variantDeleted = len(headParentNode.variations) < len(savedVariations)
mainlineChanged = headParentNode.variations[0] != savedVariations[0]
promotedToMain = mainlineChanged and not variantDeleted
if not (promotedToMain or variantDeleted):
for firstId, gameNode in enumerate(savedVariations):
if headParentNode.variations[firstId] != savedVariations[firstId]:
self.gameTreeViewWidget.moveVariant(headParentNode, firstId, False)
headParentNode.variations = savedVariations
break
else:
headParentNode.variations = savedVariations
self.gameTreeViewWidget.setGame(self.game)
else:
for node, oldAttrValue in gameNodeValueList:
redoGameNodeValueList.append((node, getattr(node, attr)))
setattr(node, attr, oldAttrValue)
self.undoListList[self.gameID].append((attr, redoGameNodeValueList))
self.gameNodeSelected(gameNode)
else:
raise ValueError('UIE: Undo item type {} not expected'.format(type(item)))
if self.gameNode == self.game:
self.scorePlotGraphicsView.setGame(self.game)
self.gameTreeViewWidget.setGame(self.game)
self.setChessWindowTitle()
def setChessWindowTitle(self):
self.gameListChanged = False
for undoList in self.undoListList:
self.gameListChanged = self.gameListChanged or len(undoList) > 0
if self.gameListChanged:
break
title = 'MzChess - '
if self.pgnFile is not None:
title += os.path.basename(self.pgnFile)
else:
title += '<UNKNOWN>'
if self.gameListChanged:
title += ' *'
self.setWindowTitle(title)
# ---------------------------------------------------------------------------
@QtCore.pyqtSlot(QAction)
def on_menuSelectEngine_triggered(self, action):
oldValue = self.settings['Menu/Engine']['selectedEngine']
for actAction in self.menuSelectEngine.actions():
actAction.setChecked(False)
action.setChecked(True)
self.settings['Menu/Engine']['selectedEngine'] = action.text()
if oldValue is None:
self.show_HintsScores()
self.saveSettings()
@QtCore.pyqtSlot(QAction)
def on_menuSearchDepth_triggered(self, action):
for actAction in self.menuSearchDepth.actions():
actAction.setChecked(False)
action.setChecked(True)
tList = action.text().split(' ')
self.settings['Menu/Engine']['searchDepth'] = str(int(tList[0]))
self.saveSettings()
@QtCore.pyqtSlot(QAction)
def on_menuNumberOfAnnotations_triggered(self, action):
for actAction in self.menuBlunderLimit.actions():
actAction.setChecked(False)
action.setChecked(True)
naValue = action.text()
self.settings['Menu/Engine']['numberOfAnnotations'] = naValue
self.saveSettings()
@QtCore.pyqtSlot(QAction)
def on_menuBlunderLimit_triggered(self, action):
for actAction in self.menuBlunderLimit.actions():
actAction.setChecked(False)
action.setChecked(True)
blValue = action.text()
if blValue == 'None':
blunderLimit = -float('inf')
else:
blunderLimit = -float(blValue.split(' ')[0])
self.settings['Menu/Engine']['blunderLimit'] = str(blunderLimit)
self.saveSettings()
@QtCore.pyqtSlot(QAction)
def on_menuAnnotateVariants_triggered(self, action):
for actAction in self.menuAnnotateVariants.actions():
actAction.setChecked(False)
action.setChecked(True)
avValue = action.text().split(' ')[0]
if avValue == 'None':
self.settings['Menu/Engine']['annotateVariants'] = str(None)
elif avValue == 'All':
self.settings['Menu/Engine']['annotateVariants'] = str(2^32 - 1)
else:
self.settings['Menu/Engine']['annotateVariants'] = str(int(avValue))
self.saveSettings()
@QtCore.pyqtSlot()
def on_actionAnnotateCurrentMove_triggered(self):
if self.settings['Menu/Engine']['selectedEngine'] is None:
self.notifyError('No engine selected')
return
if self.debugEngine:
logFunction = self.logSignal.emit
else:
logFunction = None
engine = MzChess.ChessEngine(
self.engineDict[self.settings['Menu/Engine']['selectedEngine']],
limit = chess.engine.Limit(depth = self.settings['Menu/Engine']['searchDepth']),
log = logFunction)
aEngine = MzChess.AnnotateEngine(notifyFunction = self.notifySignal.emit)
annotateVariants = self.settings['Menu/Engine']['annotateVariants']
if annotateVariants is not None and annotateVariants.isdigit():
annotateVariants = int(annotateVariants)
else:
annotateVariants = 0
self.notify('Scoring move {} of game #{} ...'.format(self.gameNode.move.uci(), self.gameID))
aEngine.setup(engine, hintPLYs = annotateVariants, multiPV = int(self.settings['Menu/Engine']['numberOfAnnotations']))
if aEngine.run(self.gameNode, numberOfPlys = 1):
annotator = MzChess.Annotator(self.settings['Menu/Engine']['selectedEngine'], notifyFunction = self.notifySignal.emit)
annotator.setBlunder(-float('inf'), addVariant = False)
oldAttrValue = self.gameNode.comment
annotator.apply(game = self.gameNode, scoreListList = aEngine.scoreListList, pvListList = None)
self.undoListList[self.gameID].append(('comment', [(self.gameNode, oldAttrValue)]))
if self.gameNode == self.game:
self.scorePlotGraphicsView.setGame(self.game)
self.gameTreeViewWidget.setGame(self.game)
# self.gameHeaderTableView.setGame(self.game)
self.gameNodeSelected(self.gameNode)
self.setChessWindowTitle()
else:
self.notifyError('Annotation failed')
return
@QtCore.pyqtSlot()
def on_actionAnnotateAll_triggered(self):
if self.settings['Menu/Engine']['selectedEngine'] is None:
self.notifyError('No engine selected')
return
if self.debugEngine:
logFunction = self.logSignal.emit
else:
logFunction = None
engine = MzChess.ChessEngine(
self.engineDict[self.settings['Menu/Engine']['selectedEngine']],
limit = chess.engine.Limit(depth = self.settings['Menu/Engine']['searchDepth']),
log = logFunction)
aEngine = MzChess.AnnotateEngine(notifyFunction = self.notifySignal.emit)
annotateVariants = self.settings['Menu/Engine']['annotateVariants']
if annotateVariants is not None and annotateVariants.isdigit():
annotateVariants = int(annotateVariants.split(' ')[0])
else:
annotateVariants = 0
gameNode = self.gameNode
while gameNode.parent:
if gameNode.parent.variations[0] != gameNode:
break
gameNode = gameNode.parent
if gameNode == self.game:
self.notify('Annotating game #{} ...'.format(self.gameID))
else:
self.notify('Annotating variant {} of #{} ...'.format(gameNode, self.gameID))
aEngine.setup(engine, hintPLYs = annotateVariants, multiPV = int(self.settings['Menu/Engine']['numberOfAnnotations']))
if aEngine.run(gameNode, numberOfPlys = None):
annotator = MzChess.Annotator(self.settings['Menu/Engine']['selectedEngine'])
addVariant = self.settings['Menu/Engine']['blunderLimit'] != '-inf'
annotator.setBlunder(float(self.settings['Menu/Engine']['blunderLimit']), addVariant = addVariant)
undoGameNodeValueList = list()
for actGameNode in gameNode.mainline():
if actGameNode.comment != '':
undoGameNodeValueList.append((actGameNode, gameNode.comment))
hintsAdded = annotator.apply(game = gameNode, scoreListList = aEngine.scoreListList, pvListList = aEngine.pvListList)
if hintsAdded:
self.undoListList[self.gameID].append(('game', [(self.gameNode, pickle.dumps(self.game))]))
else:
self.undoListList[self.gameID].append(('comment', [(self.gameNode, undoGameNodeValueList)]))
if gameNode == self.game:
self.scorePlotGraphicsView.setGame(self.game)
self.gameTreeViewWidget.setGame(self.game)
# self.gameHeaderTableView.setGame(self.game)
self.gameNodeSelected(self.gameNode)
self.setChessWindowTitle()
else:
self.notifyError('Annotation failed')
return
@QtCore.pyqtSlot(bool)
def on_actionShowOptions_toggled(self, checked):
self.boardGraphicsView.setDrawOptions(checked)
self.settings['Menu/Game']['showOptions'] = str(checked)
self.saveSettings()
@QtCore.pyqtSlot(bool)
def on_actionWarnOfDanger_toggled(self, checked):
self.boardGraphicsView.setWarnOfDanger(checked)
self.settings['Menu/Game']['warnOfDanger'] = str(checked)
self.saveSettings()
@QtCore.pyqtSlot(bool)
def on_actionFlipBoard_toggled(self, checked):
self.boardGraphicsView.setFlipped(checked)
@QtCore.pyqtSlot()
def on_actionSelectHeaderElements_triggered(self):
headerElements = self.gameHeaderTableView.headerElements(withoutSevenTagRoster = True)
self.itemSelector.setContent(headerElements, self.optionalHeaderItems)
if not self.itemSelector.exec():
return None
self.game.headers = self._setGameHeader(self.itemSelector.selectedItems() )
self.gameHeaderTableView.setGame(self.game)
self.updateSettingsList('OptionalHeaderItems', self.optionalHeaderItems)
self.saveSettings()
def show_HintsScores(self):
if self.settings['Menu/Engine']['selectedEngine'] is None:
self.notifyError('No engine selected')
return
scoresChecked = self.actionShowScores.isChecked()
hintsChecked = int(self.settings['Menu/Engine'].get('showHints', 0))
self.settings['Menu/Engine']['showScores'] = str(scoresChecked)
self.saveSettings()
if hintsChecked > 0 or scoresChecked:
if self.settings['Menu/Engine']['searchDepth'] is None:
self.notifyError('"Engine/Search Depth" undefined.')
return
if self.debugEngine:
logFunction = self.logSignal.emit
else:
logFunction = None
self.hintEngine = MzChess.ChessEngine(
self.engineDict[self.settings['Menu/Engine']['selectedEngine']],
limit = chess.engine.Limit(depth = self.settings['Menu/Engine']['searchDepth']),
log = logFunction)
self.boardGraphicsView.setHint(enableHint = hintsChecked, enableScore = scoresChecked, engine = self.hintEngine)
self.engineLabel.setText(self.settings['Menu/Engine']['selectedEngine'])
else:
self.hintEngine = None
self.boardGraphicsView.setHint(enableHint = 0, enableScore = False, engine = None)
self.engineLabel.setText('---')
@QtCore.pyqtSlot(QAction)
def on_menuShowHints_triggered(self, action):
for actAction in self.menuShowHints.actions():
actAction.setChecked(False)
action.setChecked(True)
naValue = self.hintDict[action.text()]
self.settings['Menu/Engine']['showHints'] = naValue
self.show_HintsScores()
@QtCore.pyqtSlot(bool)
def on_actionShowScores_toggled(self, checked):
self.show_HintsScores()
@QtCore.pyqtSlot()
def on_actionConfigureEngine_triggered(self):
configForm = MzChess.ConfigureEngine()
newEngineDict = configForm.run(engineDict = self.engineDict, log = self.debugEngine)
if newEngineDict is not None:
self.engineDict = newEngineDict
self.resetSelectEngine()
MzChess.saveEngineSettings(self.settings, self.engineDict)
self.saveSettings()
@QtCore.pyqtSlot(bool)
def on_actionDebugEngine_toggled(self, checked):
self.debugEngine = checked
if self.hintEngine is not None:
if self.debugEngine:
logFunction = self.logSignal.emit
else:
logFunction = None
self.hintEngine.setLog(logFunction)
@QtCore.pyqtSlot()
def on_actionAbout_triggered(self):
self.aboutDialog.exec()
@QtCore.pyqtSlot()
def on_actionHelp_triggered(self):
QtGui.QDesktopServices.openUrl(self.helpIndex)
# ---------------------------------------------------------------------------
@QtCore.pyqtSlot()
def on_actionCopyFEN_triggered(self):
self.notify('Copying current position of game #{} to clipboard ...'.format(self.gameID))
fen = self.gameNode.board().fen()
QtWidgets.QApplication.clipboard().setText(fen)
@QtCore.pyqtSlot()
def on_actionPasteFEN_triggered(self):
fen = QtWidgets.QApplication.clipboard().text()
try:
MzChess.checkFEN(fen)
except ValueError as err:
self.notifyError('Improper FEN {}:\n{}'.format(fen, str(err)))
return
self.notify('Pasting position from clipboard to game #{} ...'.format(len(self.gameList)))
board = chess.Board(fen)
game = chess.pgn.Game()
game.headers = self.game.headers
game.headers["Result"] = "*"
game.setup(board)
game.headers.pop("SetUp", None) # remove non-standard header element used by chess package
self._addGame(game)
@QtCore.pyqtSlot()
def on_actionFENBuilder_triggered(self):
self.fenBuilder = QtCore.QProcess()
self.fenBuilder.start(sys.executable, [os.path.join(self.fileDirectory,'qbuildfen.py')], QtCore.QIODevice.OpenModeFlag.NotOpen)
@QtCore.pyqtSlot()
def on_actionCopyGame_triggered(self):
self.notify('Copying game #{} to clipboard ...'.format(self.gameID))
exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
pgnString = self.game.accept(exporter)
QtWidgets.QApplication.clipboard().setText(pgnString)
@QtCore.pyqtSlot()
def on_actionPasteGame_triggered(self):
pgnString = QtWidgets.QApplication.clipboard().text()
pgn = io.StringIO(pgnString)
game = read_game(pgn)
if len(game.errors) != 0:
self.notifyError('Improper PGN')
return
self.notify('Pasting game from clipboard to game #{} ...'.format(self.gameID + 1))
self._addGame(game)
# ---------------------------------------------------------------------------
@QtCore.pyqtSlot(int)
def gameSelected(self, gameID):
if gameID != self.gameID or True:
self.gameID = gameID
self.game = self.gameList[gameID]
self.gameNode = self.game
self.notify('Loading game #{} ...'.format(self.gameID))
try:
self._showEcoCode(self.game, fromBeginning = True)
self.boardGraphicsView.setGameNode(self.game)
self.gameTreeViewWidget.setGame(self.game)
self.gameHeaderTableView.setGame(self.game)
self.scorePlotGraphicsView.setGame(self.game)
self.setInfoLabel()
self.setMoveLabel(self.gameNode.board())
except:
self.notifyError('UIE: Improper game ="{}"'.format(self.game))
return
@QtCore.pyqtSlot(list)
def gameListHeaderChanged(self, gameListHeader):
self.gameListHeader = gameListHeader
self.updateSettingsList('GameListHeaders', self.gameListHeader)
self.saveSettings()
@QtCore.pyqtSlot()
def gameListChanged(self):
self.setInfoLabel(True)
self.setChessWindowTitle()
@QtCore.pyqtSlot(chess.pgn.Headers)
def gameHeadersChanged(self, headers):
self.undoListList[self.gameID].append(('headers', [(self.gameNode, self.game.headers)]))
if 'Comment' in headers:
self.game.comment = headers['Comment']
del headers['Comment']
else:
self.game.comment = ''
if '?' not in headers["White"] and headers["White"] not in self.playerList:
self.updateSettingsList('Players', self.playerList, firstValue = headers["White"])
if '?' not in headers["Black"] and headers["Black"] not in self.playerList:
self.updateSettingsList('Players', self.playerList, firstValue = headers["Black"])
if '?' not in headers["Event"] and headers["Event"] not in self.eventList:
self.updateSettingsList('Events', self.eventList, firstValue = headers["Event"])
if '?' not in headers["Site"] and headers["Site"] not in self.siteList:
self.updateSettingsList('Sites', self.siteList, firstValue = headers["Site"])
self.saveSettings()
self.game.headers = headers
self.setChessWindowTitle()
@QtCore.pyqtSlot(chess.pgn.GameNode)
def newGameNode(self, gameNode):
self.gameNode = gameNode
self.undoListList[self.gameID].append(self.gameNode)
self._showEcoCode(self.game, fromBeginning = False)
if gameNode.is_mainline():
self.scorePlotGraphicsView.addGameNodes(gameNode)
self.scorePlotGraphicsView.selectNodeItem(gameNode)
if 'PlyCount' in self.game.headers:
self.game.headers['PlyCount'] = str(gameNode.board().ply())
self.gameHeaderTableView.setPlyCount(self.game.headers['PlyCount'])
board = self.gameNode.board()
if self.gameNode.is_end() and board.is_game_over():
if board.is_checkmate():
if board.turn == chess.WHITE:
self.game.headers['Result'] = '0-1'
else:
self.game.headers['Result'] = '1-0'
else:
self.game.headers['Result'] = '1/2-1/2'
if gameNode.is_main_variation():
self.gameTreeViewWidget.addGameNodes(gameNode)
else:
self.gameTreeViewWidget.addVariant(gameNode)
self.gameChanged(self.game)
self.setMoveLabel(self.gameNode.board())
@QtCore.pyqtSlot(chess.pgn.GameNode)
def gameNodeSelected(self, gameNode):
self.gameNode = gameNode
self.boardGraphicsView.setGameNode(self.gameNode)
self.gameTreeViewWidget.selectNodeItem(self.gameNode)
if self.gameNode.is_mainline():
self.scorePlotGraphicsView.selectNodeItem(self.gameNode)
self.setMoveLabel(self.gameNode.board())
@QtCore.pyqtSlot(chess.pgn.Game)
def gameChanged(self, game):
self.gameHeaderTableView.setGameResult(game.headers['Result'])
self.gameTreeViewWidget.setGameResult(game.headers['Result'])
self.game = game
self.setChessWindowTitle()
@QtCore.pyqtSlot(chess.pgn.GameNode, tuple)
def gameNodeChanged(self, gameNode, attrTuple):
attr, oldAttrValue = attrTuple
self.undoListList[self.gameID].append((attr, [(gameNode, oldAttrValue)]))
self.setChessWindowTitle()
[docs]class MzClassApplication(QtWidgets.QApplication):
def __init__(self, argv : List[str], notifyFct : Callable[[str], None] = print) -> None:
super(MzClassApplication, self).__init__(argv)
self.notifyFct = notifyFct
[docs] def notify(self, rec, ev):
rc = super(MzClassApplication, self).notify(rec, ev)
self.notifyFct('{} -> Type(Event)= {}, handled = {}'.format(rec, ev.type(), rc))
return rc
# ---------------------------------------------------------------------------
import os, os.path
def runMzChess(notifyFct : Optional[Callable[[str], None]] = None):
global qApp
os.chdir(os.path.expanduser('~'))
if notifyFct is not None:
qApp = MzClassApplication(sys.argv)
chessMainWindow = ChessMainWindow()
chessMainWindow.show()
chessMainWindow.setup()
qApp.exec()
def _runMzChess():
print('Hello, world')
if __name__ == "__main__":
runMzChess(None)