chessboardwidget
chessapp.view.chessboardwidget.PieceMovement
dataclass
represents a move on a chessboard of a piece from a source square to a destination square
Source code in chessapp\view\chessboardwidget.py
uci_format()
returns the move in UCI format
Returns:
Name | Type | Description |
---|---|---|
str |
str
|
the move in UCI format |
chessapp.view.chessboardwidget.ChessBoardWidget
Bases: QWidget
a widget that displays a chessboard and allows to play moves. It also displays an eval bar if enabled.
Source code in chessapp\view\chessboardwidget.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
|
__init__()
constructor for the ChessBoardWidget class. It initializes the chessboard and the eval bar.
Source code in chessapp\view\chessboardwidget.py
display(board, node=None, previous_node=None, last_move=None, show_last_move_icon=True, last_move_is_opponent_move=False, play_sound=False)
displays the chessboard in a way that is compatible with the given board.
TODO: refactor the playing of sound. The sound should not be depend on when the move is displayed but when the move is played.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
board |
Board
|
the board to display |
required |
node |
Node
|
used to display the eval bar (e.g. evaluation value) |
None
|
previous_node |
Node
|
used to calculate the last move cp loss to display corresponding last move icon (e.g. best move, blunder, etc.) |
None
|
last_move |
Move
|
the last move that was played (used together with previous_node to calculate the last move cp loss) |
None
|
show_last_move_icon |
bool
|
True if the last move icon should be displayed. |
True
|
last_move_is_opponent_move |
bool
|
true if the last move played was the opponent's move. |
False
|
play_sound |
bool
|
true if a sound should be played when the last move is displayed. |
False
|
Source code in chessapp\view\chessboardwidget.py
flip_board()
get_board_length()
the length of the board is the minimum of the width and the height of the widget (the evalbar width is also respected). the length then is used as a width and height of the bounding box of the chessboard.
Returns:
Name | Type | Description |
---|---|---|
int |
int
|
the length of the board |
Source code in chessapp\view\chessboardwidget.py
is_inside_bounding_box(event)
checks if the event is inside the bounding box of the chessboard
Parameters:
Name | Type | Description | Default |
---|---|---|---|
event |
QMouseEvent
|
the mouse event |
required |
Returns:
Name | Type | Description |
---|---|---|
bool |
bool
|
true if the event is inside the bounding box of the chessboard |
Source code in chessapp\view\chessboardwidget.py
keyReleaseEvent(key_event)
handles the key release event. If the left arrow key or the backspace key is pressed, the back actions are executed.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
key_event |
QKeyEvent
|
the key event |
required |
Source code in chessapp\view\chessboardwidget.py
mouseMoveEvent(event)
handles the mouse move event. the position of the mouse is saved to handle the piece movement before the mouse is released (e.g. drag and drop)
Parameters:
Name | Type | Description | Default |
---|---|---|---|
event |
QMouseEvent
|
the mouse event |
required |
Source code in chessapp\view\chessboardwidget.py
mousePressEvent(event)
handles the mouse press event. If the left mouse button is pressed, the piece is selected if it is inside the bounding box of the chessboard.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
event |
QMouseEvent
|
the mouse event |
required |
Source code in chessapp\view\chessboardwidget.py
mouseReleaseEvent(event)
handles the mouse release event. If the left mouse button is released, the piece is moved if it is inside the bounding box of the chessboard and the destination square is not the current square of the piece. If the squares are the same then the peice is "clicked" and selected. This is used to display legal moves of the piece. If the piece is actually moved then the piece movement observers are called.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
event |
QMouseEvent
|
the mouse event |
required |
Source code in chessapp\view\chessboardwidget.py
paintEvent(event)
paints the widget and is called when the widget needs to be updated
Parameters:
Name | Type | Description | Default |
---|---|---|---|
event |
QPaintEvent
|
the paint event |
required |
Source code in chessapp\view\chessboardwidget.py
perspective(perspective)
sets the perspective of the board
TODO: is this even needed?
Parameters:
Name | Type | Description | Default |
---|---|---|---|
perspective |
str
|
"w" for white, "b" for black |
required |
Source code in chessapp\view\chessboardwidget.py
reset()
resets the chessboard to its initial state
sizeHint()
returns the size hint for the widget that is used to calculate the size of the widget
Returns:
Name | Type | Description |
---|---|---|
QSize |
QSize
|
the size hint for the widget |
view_black()
Source
import chess
from PyQt5.QtGui import QKeyEvent, QMouseEvent, QPainter, QPaintEvent
from PyQt5.QtCore import QRect, Qt, QSize
from PyQt5.QtWidgets import QWidget
from chessapp.view.chessboard import ChessBoard
from chess import Board
from chessapp.view.evalbar import EvalBar
from chessapp.model.node import Node
import chessapp.model.move
from chessapp.model.sourcetype import SourceType
from chessapp.sound.chessboardsound import ChessboardSound
import traceback
from dataclasses import dataclass
s_size_scale = 100
s_width_hint = 8 * s_size_scale
s_height_hint = s_width_hint
s_eval_bar_min_width = 10
s_min_depth_best_move = 20
@dataclass
class PieceMovement():
""" represents a move on a chessboard of a piece from a source square to a destination square
"""
source_square: str
destination_square: str
def uci_format(self) -> str:
""" returns the move in UCI format
Returns:
str: the move in UCI format
"""
return self.source_square + self.destination_square
class ChessBoardWidget(QWidget):
""" a widget that displays a chessboard and allows to play moves. It also displays an eval bar if enabled.
"""
def __init__(self):
""" constructor for the ChessBoardWidget class. It initializes the chessboard and the eval bar.
"""
super().__init__()
self.board = ChessBoard()
self.last_press_x = 0
self.last_press_y = 0
self.piece_movement_observer = []
self.back_actions = []
self.eval_bar = EvalBar()
def sizeHint(self) -> QSize:
""" returns the size hint for the widget that is used to calculate the size of the widget
Returns:
QSize: the size hint for the widget
"""
return QSize(s_width_hint, s_height_hint)
def get_board_length(self) -> int:
""" the length of the board is the minimum of the width and the height of the widget (the evalbar width is also respected).
the length then is used as a width and height of the bounding box of the chessboard.
Returns:
int: the length of the board
"""
return 8 * (min(self.width() - self.eval_bar.width, self.height()) // 8)
def paintEvent(self, event: QPaintEvent):
""" paints the widget and is called when the widget needs to be updated
Args:
event (QPaintEvent): the paint event
"""
qp = QPainter(self)
qp.fillRect(0, 0, self.width(), self.height(), Qt.GlobalColor.black)
# make sure there is enough width for an eval bar
if self.eval_bar.is_visible and self.width() > 2 * s_eval_bar_min_width:
self.eval_bar.width = max(
s_eval_bar_min_width, int(self.width() * 0.03))
self.eval_bar.drawOn(
qp, QRect(0, 0, self.eval_bar.width, self.get_board_length()))
else:
self.eval_bar.width = 0
self.board.drawOn(qp, QRect(self.eval_bar.width, 0,
self.get_board_length(), self.get_board_length()))
def display(self, board: Board, node: Node = None, previous_node: Node = None, last_move: chessapp.model.move.Move = None, show_last_move_icon: bool = True, last_move_is_opponent_move: bool = False, play_sound: bool = False):
""" displays the chessboard in a way that is compatible with the given board.
TODO: refactor the playing of sound. The sound should not be depend on when the move is displayed but when the move is played.
Args:
board (Board): the board to display
node (Node, optional): used to display the eval bar (e.g. evaluation value)
previous_node (Node, optional): used to calculate the last move cp loss to display corresponding last move icon (e.g. best move, blunder, etc.)
last_move (chessapp.model.move.Move, optional): the last move that was played (used together with previous_node to calculate the last move cp loss)
show_last_move_icon (bool, optional): True if the last move icon should be displayed.
last_move_is_opponent_move (bool, optional): true if the last move played was the opponent's move.
play_sound (bool, optional): true if a sound should be played when the last move is displayed.
"""
self.board.ascii_board = str(board)
self.board.legal_moves = []
for move in board.legal_moves:
self.board.legal_moves.append(move)
self.eval_bar.node = node
self.board.last_move_source = None
self.board.last_move_destination = None
self.board.last_move_is_best_known = False
self.board.last_move_is_book = False
self.board.best_move = None
self.board.show_last_move_icon = show_last_move_icon
if node:
move = node.get_best_move(min_depth=s_min_depth_best_move)
if move:
self.board.best_move = board.uci(board.parse_san(move.san))
self.board.best_move_cp_loss = node.get_cp_loss(move)
if previous_node and last_move:
previous_board = Board(previous_node.state)
try:
self.board.last_move_cp_loss = previous_node.get_cp_loss(
last_move)
# https://www.reddit.com/r/ComputerChess/comments/qisai2/conversion_from_algebraic_notation_to_uci_notation/
uci_text = previous_board.push_san(last_move.san).uci()
self.board.last_move_destination = uci_text[2:]
self.board.last_move_source = uci_text[0:2]
except:
print("error while trying to set SquareIcon associated with last move")
print(traceback.format_exc())
equivalent_move = previous_node.get_equivalent_move(last_move)
if equivalent_move:
last_move = equivalent_move
self.board.last_move_is_book = last_move.source == SourceType.BOOK
# can be None if no move is known
previous_node_best_move = previous_node.get_best_move()
self.board.last_move_is_best_known = previous_node_best_move and previous_node_best_move.is_equivalent_to(
last_move)
if node:
self.board.node_depth = node.eval_depth
self.update()
# play move sound
if play_sound:
if last_move and "#" in last_move.san:
ChessboardSound.GAME_END.play()
elif last_move and "+" in last_move.san:
ChessboardSound.MOVE_CHECK.play()
elif last_move and "-" in last_move.san:
ChessboardSound.MOVE_CASTLE.play()
elif last_move and "x" in last_move.san:
ChessboardSound.CAPTURE_PIECE.play()
elif last_move_is_opponent_move:
ChessboardSound.MOVE_OPPONENT.play()
else:
ChessboardSound.MOVE_SELF.play()
def flip_board(self):
""" flips the board and the eval bar
"""
self.board.flip()
self.eval_bar.flip()
self.update()
def view_white(self):
""" sets the perspective to white
TODO: is this even needed?
"""
self.perspective("w")
def view_black(self):
""" sets the perspective to black
TODO: is this even needed?
"""
self.perspective("b")
def perspective(self, perspective: str):
""" sets the perspective of the board
TODO: is this even needed?
Args:
perspective (str): "w" for white, "b" for black
"""
self.board.flip_board = perspective != "w"
self.eval_bar.is_flipped = perspective != "w"
self.update()
def is_inside_bounding_box(self, event: QMouseEvent) -> bool:
""" checks if the event is inside the bounding box of the chessboard
Args:
event (QMouseEvent): the mouse event
Returns:
bool: true if the event is inside the bounding box of the chessboard
"""
return self.eval_bar.width < event.x() and event.x() < self.get_board_length(
) + self.eval_bar.width and event.y() < self.get_board_length() and event.y() >= 0
def keyReleaseEvent(self, key_event: QKeyEvent) -> None:
""" handles the key release event. If the left arrow key or the backspace key is pressed, the back actions are executed.
Args:
key_event (QKeyEvent): the key event
"""
if key_event.key() == Qt.Key.Key_Left or key_event.key() == Qt.Key.Key_Backspace:
for back_action in self.back_actions:
back_action()
def mousePressEvent(self, event: QMouseEvent) -> None:
""" handles the mouse press event. If the left mouse button is pressed, the piece is selected if it is inside the bounding box of the chessboard.
Args:
event (QMouseEvent): the mouse event
"""
self.board.mouse_x = event.x()
self.board.mouse_y = event.y()
if self.is_inside_bounding_box(event) and event.button() == Qt.MouseButton.LeftButton:
self.board.select_piece(event.x() - self.eval_bar.width, event.y(),
self.get_board_length(), self.get_board_length())
self.board.enable_piece_to_cursor = True
self.update()
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
""" handles the mouse release event. If the left mouse button is released, the piece is moved if it is inside
the bounding box of the chessboard and the destination square is not the current square of the piece. If the
squares are the same then the peice is "clicked" and selected. This is used to display legal moves of the piece.
If the piece is actually moved then the piece movement observers are called.
Args:
event (QMouseEvent): the mouse event
"""
self.board.mouse_x = event.x()
self.board.mouse_y = event.y()
self.board.enable_piece_to_cursor = False
self.board.active_piece = None
if self.is_inside_bounding_box(event) and event.button() == Qt.MouseButton.LeftButton:
piece_movement = PieceMovement(self.board.active_piece_origin, self.board.coords_to_square(
event.x() - self.eval_bar.width, event.y(), self.get_board_length(), self.get_board_length()))
if piece_movement.source_square != piece_movement.destination_square:
if chess.Move.from_uci(piece_movement.uci_format()) in self.board.legal_moves:
for observer in self.piece_movement_observer:
observer(PieceMovement(self.board.active_piece_origin, self.board.coords_to_square(
event.x() - self.eval_bar.width, event.y(), self.get_board_length(), self.get_board_length())))
else:
ChessboardSound.MOVE_ILLEGAL.play()
self.update()
def mouseMoveEvent(self, event: QMouseEvent) -> None:
""" handles the mouse move event. the position of the mouse is saved to handle the piece movement before the mouse
is released (e.g. drag and drop)
Args:
event (QMouseEvent): the mouse event
"""
self.board.mouse_x = event.x()
self.board.mouse_y = event.y()
self.update()
def reset(self):
""" resets the chessboard to its initial state
"""
self.last_press_x = 0
self.last_press_y = 0
self.board.last_move_destination = None
self.board.last_move_source = None