Skip to content

Instantly share code, notes, and snippets.

@HarshilPatel007
Last active March 27, 2025 07:24
Show Gist options
  • Save HarshilPatel007/28b17962ff234078ab472d1534e4347a to your computer and use it in GitHub Desktop.
Save HarshilPatel007/28b17962ff234078ab472d1534e4347a to your computer and use it in GitHub Desktop.
ChessQT
import asyncio
import random
import sys
from datetime import datetime
import chess
import chess.engine
import chess.pgn
from PySide6.QtCore import QLineF, QPointF, QRectF, Qt
from PySide6.QtGui import (QAction, QBrush, QColor, QPainter, QPen, QPixmap,
QPolygonF, QTransform)
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtSvgWidgets import QGraphicsSvgItem
from PySide6.QtWidgets import *
SQUARE_SIZE = 70
THEME_COLORS = {
"dark_square": "#769656",
"light_square": "#eeeed2",
"highlight_square": "#d7e81c",
"highlight_legal_moves": "#3b3b3b",
"marked_square_ctrl": "#4287f5",
"marked_square_alt": "#eb4034",
"marked_square_shift": "#f5a442",
"arrow_ctrl": "#4287f5",
"arrow_alt": "#eb4034",
"arrow_shift": "#f5a442",
}
class StartDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("LibreSquares - Choose Options")
self.setFixedWidth(350)
self.fischer_random_checkbox = QCheckBox("Use Fischer Random aka chess960")
self.white_radiobutton = QRadioButton("Play as White")
self.black_radiobutton = QRadioButton("Play as Black")
self.white_radiobutton.setChecked(True)
self.white_engine_checkbox = QCheckBox("Play Engine As White")
self.black_engine_checkbox = QCheckBox("Play Engine As Black")
self.button_box = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel
)
layout = QVBoxLayout()
layout.addWidget(self.fischer_random_checkbox)
layout.addWidget(self.white_radiobutton)
layout.addWidget(self.black_radiobutton)
layout.addWidget(self.white_engine_checkbox)
layout.addWidget(self.black_engine_checkbox)
layout.addWidget(self.button_box)
self.setLayout(layout)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
def get_options(self):
return {
"fischer_random": self.fischer_random_checkbox.isChecked(),
"play_as": "white" if self.white_radiobutton.isChecked() else "black",
"play_engine_as_white": self.white_engine_checkbox.isChecked(),
"play_engine_as_black": self.black_engine_checkbox.isChecked(),
}
class ApplicationWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("LibreSquares")
# Open dialog box and get user options
options_dialog = StartDialog()
if options_dialog.exec() == QDialog.Accepted:
options = options_dialog.get_options()
else:
sys.exit()
self.fischer_random = options["fischer_random"]
self.play_as = options["play_as"]
self.play_engine_as_white = options["play_engine_as_white"]
self.play_engine_as_black = options["play_engine_as_black"]
# Initialize chessboard
self.board = chess.Board(chess960=self.fischer_random)
if self.fischer_random:
self.board.set_chess960_pos(random.randint(1, 959))
# Create chessboard GUI
self.chessboard = Chessboard(self.board, True)
self.chessboard.setFixedSize(SQUARE_SIZE * 8.5, SQUARE_SIZE * 8.5)
if self.play_as == "black":
self.chessboard.flip_chessboard()
self.chessboard.engine_play_as_white = self.play_engine_as_white
self.chessboard.engine_play_as_black = self.play_engine_as_black
# Create a table widget for displaying moves
self.move_table = QTableWidget()
self.move_table.setColumnCount(2)
self.move_table.setHorizontalHeaderLabels(["White", "Black"])
self.move_table.setColumnWidth(0, 125)
self.move_table.setColumnWidth(1, 125)
self.move_table.setFixedWidth(300)
self.move_table.setFixedHeight(300)
self.move_table.setEditTriggers(QTableWidget.NoEditTriggers)
self.move_table.setSelectionMode(QAbstractItemView.NoSelection)
self.move_table.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)
self.move_table.itemClicked.connect(self.chessboard.move_navigator.go_to_move)
self.chessboard.move_table = self.move_table
self.pgn_loader = PGNLoader(self.chessboard)
fen_input = QLineEdit()
fen_input.setPlaceholderText("Enter a fen string")
fen_input.setText(self.chessboard.get_chessboard_fen())
fen_input.setFixedWidth(250)
load_fen_button = QPushButton("Load Fen")
load_fen_button.setFixedWidth(150)
load_fen_button.clicked.connect(
lambda: self.chessboard.update_chessboard(fen_input.text())
)
fen_widget = QWidget()
fen_layout = QHBoxLayout(fen_widget)
fen_layout.addWidget(fen_input)
fen_layout.addWidget(load_fen_button)
captured_pieces_widget = QWidget()
captured_pieces_widget_layout = QHBoxLayout(captured_pieces_widget)
captured_pieces_widget_layout.addWidget(
self.chessboard.captured_pieces.white_pieces_view
)
captured_pieces_widget_layout.addWidget(
self.chessboard.captured_pieces.black_pieces_view
)
# Set up the layout
main_widget = QWidget()
layout = QGridLayout(main_widget)
layout.setContentsMargins(0, 0, 0, 0)
# Add widgets to the layout
layout.addWidget(self.pgn_loader, 0, 0)
layout.addWidget(self.chessboard, 0, 1)
layout.addWidget(self.move_table, 0, 2)
layout.addWidget(fen_widget, 1, 0)
layout.addWidget(captured_pieces_widget, 1, 1)
self.setCentralWidget(main_widget)
# Create a toolbar
toolbar = QToolBar()
self.addToolBar(toolbar)
previous_move_action = QAction("Previous Move", self)
previous_move_action.triggered.connect(
self.chessboard.move_navigator.previous_move
)
toolbar.addAction(previous_move_action)
next_move_action = QAction("Next Move", self)
next_move_action.triggered.connect(self.chessboard.move_navigator.next_move)
toolbar.addAction(next_move_action)
undo_last_move_action = QAction("Undo Last Move", self)
undo_last_move_action.triggered.connect(
self.chessboard.move_navigator.undo_last_move
)
toolbar.addAction(undo_last_move_action)
flip_board_action = QAction("Flip Board", self)
flip_board_action.triggered.connect(self.chessboard.flip_chessboard)
toolbar.addAction(flip_board_action)
reset_game_action = QAction("Reset Game", self)
reset_game_action.triggered.connect(self.chessboard.reset_game)
toolbar.addAction(reset_game_action)
self.set_initial_engine_move()
def set_initial_engine_move(self):
if self.play_engine_as_white:
self.chessboard.make_engine_move()
class Chessboard(QGraphicsView):
def __init__(self, board, show_labels=True):
super().__init__()
self.board = board
self.show_labels = show_labels
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
# Enable antialiasing for smooth rendering
self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
self.move_manager = MoveManager(self)
self.move_navigator = MoveNavigator(self)
self.captured_pieces = CapturedPieces()
self.is_navigating_moves = False # Flag to indicate if user is navigating moves
self.is_board_flipped = False # Flag to indicate if the board is flipped
self.is_piece_moved = False # Flag to indicate if piece has been moved
self.marked_squares = {}
self.arrows = []
self.is_drawing_arrow = False # Flag to indicate if user is drawing arrow
self.start_pos = QPointF(0, 0)
self.end_pos = QPointF(0, 0)
self.arrow_color = None
self.engine = None
self.engine_play_as_white = False
self.engine_play_as_black = False
self.chess_pieces = ChessPieces(
self, self.scene, self.is_board_flipped, "cardinal"
)
self.chess_pieces.load_chess_piece_images()
self.draw_chessboard()
async def engine_move(self):
engine_options = {
"Skill Level": 10,
"UCI_Elo": 1399,
}
if not self.engine_play_as_white or not self.engine_play_as_black:
transport, self.engine = await chess.engine.popen_uci("engine/stockfish")
result = await self.engine.play(
self.board, chess.engine.Limit(time=0.1, depth=1), options=engine_options
)
self.move_navigator.get_san_move(result.move)
self.board.push(result.move)
self.is_piece_moved = True
await self.engine.quit()
self.engine = None
def make_engine_move(self):
if (self.engine_play_as_white and self.board.turn == chess.WHITE) or (
self.engine_play_as_black and self.board.turn == chess.BLACK
):
asyncio.run(self.engine_move())
self.after_move_ops()
def after_move_ops(self):
"""
do some operations after move has been made
"""
if self.is_piece_moved:
self.move_navigator.update_moves()
self.move_navigator.update_move_table()
self.move_manager.selected_square = None
self.delete_arrows()
self.delete_marked_squares()
self.delete_highlighted_squares()
self.highlight_source_and_destination_squares()
self.chess_pieces.delete_pieces()
self.chess_pieces.draw_pieces()
self.delete_highlighted_legal_moves()
def get_square_coordinates(self, square):
"""
col, row, x, y = self.get_square_coordinates(square)
"""
if self.is_board_flipped:
col = 7 - chess.square_file(square)
row = chess.square_rank(square)
else:
col = chess.square_file(square)
row = 7 - chess.square_rank(square)
return col, row, col * SQUARE_SIZE, row * SQUARE_SIZE
def draw_squares(self):
"""
draws squares forming a chessboard
"""
for square in chess.SQUARES:
col, row, x, y = self.get_square_coordinates(square)
rect = self.scene.addRect(x, y, SQUARE_SIZE, SQUARE_SIZE)
rect_color = (
THEME_COLORS["light_square"]
if (row + col) % 2 == 0
else THEME_COLORS["dark_square"]
)
rect.setPen(Qt.NoPen)
rect.setBrush(QColor(rect_color))
def draw_labels(self):
"""
draws rank and file label. a-h,1-8
"""
for square in chess.SQUARES:
col, row, x, y = self.get_square_coordinates(square)
# Determine label position
row_label_x = x + SQUARE_SIZE / 8 - 10
row_label_y = y + SQUARE_SIZE / 8 - 10
col_label_x = x + SQUARE_SIZE - SQUARE_SIZE / 15 - 10
col_label_y = y + SQUARE_SIZE - SQUARE_SIZE / 8 - 15
label_color = (
THEME_COLORS["light_square"]
if (row + col) % 2 != 0
else THEME_COLORS["dark_square"]
)
if self.show_labels:
# Add label for the first set of columns (a-h)
if row == 7:
if self.is_board_flipped:
label = self.scene.addText(f'{chr(ord("h")-col)}')
else:
label = self.scene.addText(f'{chr(ord("a")+col)}')
label.setDefaultTextColor(QColor(label_color))
label.setPos(col_label_x, col_label_y)
# Add label for the first set of rows (1-8)
if col == 0:
if self.is_board_flipped:
label = self.scene.addText(f"{row+1}")
else:
label = self.scene.addText(f"{8-row}")
label.setDefaultTextColor(QColor(label_color))
label.setPos(row_label_x, row_label_y)
def draw_chessboard(self):
self.scene.clear()
self.draw_squares()
self.draw_labels()
self.highlight_source_and_destination_squares()
self.chess_pieces.draw_pieces()
def flip_chessboard(self):
self.is_board_flipped = not self.is_board_flipped
self.chess_pieces.flipped = self.is_board_flipped
self.draw_chessboard()
def update_chessboard(self, fen):
self.board.set_fen(fen)
self.draw_chessboard()
def reset_game(self):
self.board.reset()
self.board.clear_stack()
self.move_navigator.moves_made.clear()
self.move_navigator.moves_made_san.clear()
self.move_table.setRowCount(0)
self.draw_chessboard()
def delete_highlighted_squares(self):
items = self.scene.items()
for item in items:
if isinstance(item, QGraphicsRectItem):
brush_color = item.brush().color()
if brush_color == QColor(THEME_COLORS["highlight_square"]):
self.scene.removeItem(item)
def delete_highlighted_legal_moves(self):
items = self.scene.items()
for item in items:
if isinstance(item, QGraphicsEllipseItem):
brush_color = item.brush().color()
if brush_color == QColor(THEME_COLORS["highlight_legal_moves"]):
self.scene.removeItem(item)
def get_square_number_at_pos(self, event):
pos = event.position().toPoint()
mapped_pos = self.mapToScene(pos)
col = int(mapped_pos.x() / SQUARE_SIZE)
row = int(mapped_pos.y() / SQUARE_SIZE)
if self.is_board_flipped:
return chess.square(7 - col, row)
return chess.square(col, 7 - row)
def draw_arrow(self):
arrow = Arrow(self.start_pos, self.end_pos, self.arrow_color)
self.scene.addItem(arrow)
def delete_arrows(self):
for arrow in self.arrows:
self.scene.removeItem(arrow)
self.arrows = []
self.scene.update()
def highlight_legal_moves(self, square):
"""
highlights the legal moves of a selected piece
"""
legal_moves = self.move_manager.get_legal_moves(square)
for target_square in set(move.to_square for move in legal_moves):
col, row, x, y = self.get_square_coordinates(target_square)
# Add a circle in the center of the square
circle = self.scene.addEllipse(
x + SQUARE_SIZE / 4,
y + SQUARE_SIZE / 4,
SQUARE_SIZE / 2,
SQUARE_SIZE / 2,
)
circle.setPen(Qt.NoPen)
circle.setBrush(QColor(THEME_COLORS["highlight_legal_moves"]))
circle.setOpacity(0.45)
def create_highlighted_square(self, square, color):
"""
creates the rect at the source and destination squares
"""
rect = QGraphicsRectItem()
rect.setRect(
square[0] * SQUARE_SIZE,
square[1] * SQUARE_SIZE,
SQUARE_SIZE,
SQUARE_SIZE,
)
rect.setPen(Qt.NoPen)
rect.setBrush(QColor(color))
rect.setOpacity(0.45)
return self.scene.addItem(rect)
def create_marked_square(self, square, color):
"""
creates the rect at the given pos.
"""
if square in self.marked_squares:
return
col, row, x, y = self.get_square_coordinates(square)
# Check if there is already a rectangle at the given position
existing_rects = self.scene.items(QRectF(x, y, SQUARE_SIZE, SQUARE_SIZE))
for rect in existing_rects:
if isinstance(rect, QGraphicsRectItem):
brush_color = rect.brush().color()
if brush_color == QColor(color):
return
rect = self.scene.addRect(x, y, SQUARE_SIZE, SQUARE_SIZE)
rect.setPen(Qt.NoPen)
rect.setBrush(QColor(color))
rect.setOpacity(0.90)
self.marked_squares[square] = rect
def delete_marked_square(self, square):
if square in self.marked_squares:
rect = self.marked_squares[square]
self.scene.removeItem(rect)
del self.marked_squares[square]
def delete_marked_squares(self):
for square, rect in self.marked_squares.items():
self.scene.removeItem(rect)
self.marked_squares.clear()
def highlight_source_and_destination_squares(self):
if self.board.move_stack:
last_move = self.board.move_stack[-1]
source_square = self.move_manager.get_source_square_from_move(last_move)
destination_square = self.move_manager.get_destination_square_from_move(
last_move
)
self.create_highlighted_square(
source_square, THEME_COLORS["highlight_square"]
)
self.create_highlighted_square(
destination_square, THEME_COLORS["highlight_square"]
)
def mousePressEvent(self, event):
if event.buttons() == Qt.LeftButton and not self.is_navigating_moves:
square_number = self.get_square_number_at_pos(event)
piece = self.board.piece_at(square_number)
if self.move_manager.selected_square is None:
self.move_manager.select_square(square_number)
else:
if square_number == self.move_manager.selected_square:
self.move_manager.selected_square = None
self.delete_highlighted_legal_moves()
return
self.move_manager.move_piece(square_number)
self.after_move_ops()
self.make_engine_move()
if event.buttons() == Qt.LeftButton:
modifiers = event.modifiers()
if modifiers == Qt.ControlModifier:
self.arrow_color = QColor(THEME_COLORS["arrow_ctrl"])
elif modifiers == Qt.AltModifier:
self.arrow_color = QColor(THEME_COLORS["arrow_alt"])
elif modifiers == Qt.ShiftModifier:
self.arrow_color = QColor(THEME_COLORS["arrow_shift"])
else:
return
if not self.is_drawing_arrow:
self.is_drawing_arrow = True
self.move_manager.selected_square = None
self.delete_highlighted_legal_moves()
square_number = self.get_square_number_at_pos(event)
col, row, x, y = self.get_square_coordinates(square_number)
self.start_pos = QPointF(x + SQUARE_SIZE / 2, y + SQUARE_SIZE / 2)
self.end_pos = QPointF(x + SQUARE_SIZE / 2, y + SQUARE_SIZE / 2)
if event.button() == Qt.RightButton:
square_number = self.get_square_number_at_pos(event)
self.delete_marked_square(square_number)
modifiers = event.modifiers()
if modifiers == Qt.ControlModifier:
self.create_marked_square(
square_number, THEME_COLORS["marked_square_ctrl"]
)
elif modifiers == Qt.AltModifier:
self.create_marked_square(
square_number, THEME_COLORS["marked_square_alt"]
)
elif modifiers == Qt.ShiftModifier:
self.create_marked_square(
square_number, THEME_COLORS["marked_square_shift"]
)
else:
return
self.chess_pieces.delete_pieces()
self.chess_pieces.draw_pieces()
def mouseMoveEvent(self, event):
if self.is_drawing_arrow:
square_number = self.get_square_number_at_pos(event)
col, row, x, y = self.get_square_coordinates(square_number)
self.end_pos = QPointF(x + SQUARE_SIZE / 2, y + SQUARE_SIZE / 2)
self.scene.update()
def mouseReleaseEvent(self, event):
if self.is_drawing_arrow:
self.is_drawing_arrow = False
self.draw_arrow()
self.scene.update()
def next_move(self):
self.move_navigator.next_move()
def previous_move(self):
self.move_navigator.previous_move()
def game_result(self, board):
if board.is_checkmate():
GameResult.show_checkmate()
elif board.is_stalemate():
GameResult.show_stalemate()
def get_chessboard_fen(self):
return self.board.fen()
def update_captured_pieces(self, move):
capture_piece = self.board.is_capture(move)
if capture_piece:
captured_piece = self.board.piece_at(move.to_square)
if captured_piece:
self.captured_pieces.add_piece(captured_piece)
if self.board.is_en_passant(move): # Handle en passant captures
target_square = self.board.ep_square
captured_piece_square = chess.square(
chess.square_file(target_square), chess.square_rank(move.from_square)
)
captured_piece = self.board.piece_at(captured_piece_square)
if captured_piece:
self.captured_pieces.add_piece(captured_piece)
class CapturedPieces(QWidget):
def __init__(self):
super().__init__()
layout = QHBoxLayout(self)
self.white_pieces_view = QGraphicsView()
white_pieces_scene = QGraphicsScene(self.white_pieces_view)
self.white_pieces_view.setScene(white_pieces_scene)
self.white_pieces_view.setFixedWidth(200)
self.white_pieces_view.setFixedHeight(60)
layout.addWidget(self.white_pieces_view)
self.black_pieces_view = QGraphicsView()
black_pieces_scene = QGraphicsScene(self.black_pieces_view)
self.black_pieces_view.setScene(black_pieces_scene)
self.black_pieces_view.setFixedWidth(200)
self.black_pieces_view.setFixedHeight(60)
layout.addWidget(self.black_pieces_view)
def add_piece(self, captured_piece):
piece_color = "w" if captured_piece.color == chess.WHITE else "b"
piece_name = captured_piece.symbol().upper()
image_path = f"assets/img/{piece_color}{piece_name}.svg"
renderer = QSvgRenderer(image_path)
pixmap = QPixmap(SQUARE_SIZE - 10, SQUARE_SIZE - 10)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
pixmap = pixmap.scaledToHeight(SQUARE_SIZE / 2, Qt.SmoothTransformation)
piece_item = QGraphicsPixmapItem(pixmap)
if piece_color == "w":
self.white_pieces_view.scene().addItem(piece_item)
piece_item.setPos(
len(self.white_pieces_view.scene().items()) * SQUARE_SIZE / 2, 0
)
else:
self.black_pieces_view.scene().addItem(piece_item)
piece_item.setPos(
len(self.black_pieces_view.scene().items()) * SQUARE_SIZE / 2, 0
)
class MoveNavigator:
def __init__(self, chessboard):
self.moves_made = []
self.moves_made_san = []
self.chessboard = chessboard
self.current_move_index = 0
def update_moves(self):
self.moves_made.clear()
self.moves_made.extend(self.chessboard.board.move_stack)
self.current_move_index = len(self.moves_made) - 1
def undo_last_move(self):
self.moves_made.pop()
self.moves_made_san.pop()
self.chessboard.board.pop()
self.update_moves()
# Delete the last row from the move table
if len(self.moves_made) % 2 == 0:
self.chessboard.move_table.removeRow(len(self.moves_made) // 2 - 1)
self.update_move_table()
self.chessboard.delete_highlighted_squares()
self.chessboard.highlight_source_and_destination_squares()
self.chessboard.chess_pieces.delete_pieces()
self.chessboard.chess_pieces.draw_pieces()
def next_move(self):
if self.current_move_index < len(self.moves_made) - 1:
self.chessboard.is_navigating_moves = True
self.current_move_index += 1
move = self.moves_made[self.current_move_index]
self.chessboard.board.push(move)
self.chessboard.delete_highlighted_squares()
self.chessboard.highlight_source_and_destination_squares()
self.chessboard.chess_pieces.delete_pieces()
self.chessboard.chess_pieces.draw_pieces()
# Check if the user has reached the last move
if self.current_move_index == len(self.moves_made) - 1:
self.chessboard.is_navigating_moves = False
def previous_move(self):
if self.current_move_index >= 0:
self.chessboard.is_navigating_moves = True
self.chessboard.board.pop()
self.current_move_index -= 1
self.chessboard.delete_highlighted_squares()
self.chessboard.highlight_source_and_destination_squares()
self.chessboard.chess_pieces.delete_pieces()
self.chessboard.chess_pieces.draw_pieces()
def get_san_move(self, move):
return self.moves_made_san.append(self.chessboard.board.san(move))
def update_move_table(self):
self.chessboard.move_table.clearContents()
num_moves = len(self.moves_made)
num_rows = num_moves // 2 if num_moves % 2 == 0 else num_moves // 2 + 1
while self.chessboard.move_table.rowCount() < num_rows:
self.chessboard.move_table.insertRow(self.chessboard.move_table.rowCount())
for i, move in enumerate(self.moves_made_san):
row = i // 2
col = i % 2
if (
row >= self.chessboard.move_table.rowCount()
or col >= self.chessboard.move_table.columnCount()
):
continue
item = self.chessboard.move_table.item(row, col)
if item is None:
item = QTableWidgetItem(move)
self.chessboard.move_table.setItem(row, col, item)
else:
item.setText(move)
def go_to_move(self, item):
row = item.row()
col = item.column()
index = row * 2 + col
if 0 <= index < len(self.moves_made):
while index < self.current_move_index:
self.previous_move()
while index > self.current_move_index:
self.next_move()
class MoveManager:
def __init__(self, chessboard):
self.chessboard = chessboard
self.selected_square = None
def select_square(self, square):
if square is not None:
piece = self.chessboard.board.piece_at(square)
if piece is not None and piece.color == self.chessboard.board.turn:
self.selected_square = square
self.chessboard.highlight_legal_moves(square)
def get_destination_square_from_move(self, move):
if self.chessboard.is_board_flipped:
return (
7 - chess.square_file(move.to_square),
chess.square_rank(move.to_square),
)
return chess.square_file(move.to_square), 7 - chess.square_rank(move.to_square)
def get_source_square_from_move(self, move):
if self.chessboard.is_board_flipped:
return (
7 - chess.square_file(move.from_square),
chess.square_rank(move.from_square),
)
return chess.square_file(move.from_square), 7 - chess.square_rank(
move.from_square
)
def get_legal_moves(self, square):
moves = []
for move in self.chessboard.board.legal_moves:
if move.from_square == square:
moves.append(move)
return moves
def move_piece(self, target_square):
if self.selected_square is not None:
for move in self.chessboard.board.legal_moves:
if (
move.from_square == self.selected_square
and move.to_square == target_square
):
if self.chessboard.board.piece_type_at(
self.selected_square
) == chess.PAWN and (
(
self.chessboard.board.turn == chess.WHITE
and chess.square_rank(target_square) == 7
)
or (
self.chessboard.board.turn == chess.BLACK
and chess.square_rank(target_square) == 0
)
):
self.handle_pawn_promotion(move)
self.chessboard.move_navigator.get_san_move(move)
self.chessboard.get_chessboard_fen()
self.chessboard.update_captured_pieces(move)
self.chessboard.board.push(move)
self.chessboard.game_result(self.chessboard.board)
self.chessboard.highlight_legal_moves(target_square)
self.chessboard.is_piece_moved = True
break
def handle_pawn_promotion(self, move):
piece_options = ["Queen", "Rook", "Knight", "Bishop"]
dialog = QDialog(self.chessboard)
dialog.setModal(True)
dialog.setWindowTitle("Promote Pawn")
dialog.setFixedWidth(300)
layout = QVBoxLayout(dialog)
dialog.setLayout(layout)
# Create a button for each piece option
for piece in piece_options:
button = QPushButton(piece)
button.clicked.connect(
lambda move=move, piece=piece: self.promote_pawn(dialog, move, piece)
)
layout.addWidget(button)
dialog.exec()
def promote_pawn(self, dialog, move, piece):
piece_map = {
"Queen": chess.QUEEN,
"Rook": chess.ROOK,
"Knight": chess.KNIGHT,
"Bishop": chess.BISHOP,
}
move.promotion = piece_map[piece]
dialog.accept() # Close the dialog after promoting the pawn
class ChessPieces:
def __init__(self, chessboard, scene, flipped, piece_set="img"):
self.chessboard = chessboard
self.scene = scene
self.flipped = flipped
self.piece_images = {}
self.piece_set = piece_set
def load_chess_piece_images(self):
piece_names = ["P", "N", "B", "R", "Q", "K"]
for piece_name in piece_names:
piece_image_paths = {
"w": f"assets/{self.piece_set}/w{piece_name}.svg",
"b": f"assets/{self.piece_set}/b{piece_name}.svg",
}
for piece_color, image_path in piece_image_paths.items():
renderer = QSvgRenderer(image_path)
pixmap = QPixmap(SQUARE_SIZE - 10, SQUARE_SIZE - 10)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
self.piece_images[(piece_color, piece_name)] = pixmap
def draw_pieces(self):
piece_list = self.chessboard.board.fen().split()[0]
square = 0
for char in piece_list:
if char.isdigit():
square += int(char)
elif char != "/":
piece_name = char.upper()
piece_color = "w" if char.isupper() else "b"
if self.flipped:
x = (7 - square % 8) * SQUARE_SIZE + 5
y = (7 - square // 8) * SQUARE_SIZE + 5
else:
x = (square % 8) * SQUARE_SIZE + 5
y = (square // 8) * SQUARE_SIZE + 5
piece_item = QGraphicsPixmapItem(
self.piece_images[(piece_color, piece_name)]
)
piece_item.setPos(x, y)
self.scene.addItem(piece_item)
square += 1
def delete_pieces(self):
items = self.scene.items()
for item in items:
if isinstance(item, QGraphicsPixmapItem):
self.scene.removeItem(item)
class Arrow(QGraphicsItem):
def __init__(self, start_pos, end_pos, arrow_color):
super().__init__()
self.start_pos = start_pos
self.end_pos = end_pos
self.arrow_color = arrow_color
def boundingRect(self):
return QRectF(self.start_pos, self.end_pos).normalized()
def paint(self, painter, option, widget):
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
pen = QPen(QColor(self.arrow_color), 3.5)
pen.setCapStyle(Qt.RoundCap)
brush = QBrush(QColor(self.arrow_color))
painter.setPen(pen)
painter.setBrush(brush)
line = QLineF(self.start_pos, self.end_pos)
painter.drawLine(line)
arrow_head_length = 10
# Check if line length is non-zero before performing division
if line.length() != 0:
arrow_line_length = line.length() - arrow_head_length
angle = line.angle()
arrow_point = line.p2()
arrow_line1 = QLineF()
arrow_line1.setP1(QPointF(line.pointAt(arrow_line_length / line.length())))
arrow_line1.setAngle(angle + 180 - 45)
arrow_line1.setLength(arrow_head_length)
arrow_line2 = QLineF()
arrow_line2.setP1(QPointF(line.pointAt(arrow_line_length / line.length())))
arrow_line2.setAngle(angle + 180 + 45)
arrow_line2.setLength(arrow_head_length)
arrow_head = QPolygonF()
arrow_head.append(arrow_line1.p2())
arrow_head.append(arrow_point)
arrow_head.append(arrow_line2.p2())
painter.drawPolygon(arrow_head)
class PGNLoader(QWidget):
def __init__(self, chessboard):
super().__init__()
self.chessboard = chessboard
self.games_list = QListWidget()
self.games_list.setFixedWidth(300)
self.games_list.itemClicked.connect(self.load_game)
load_pgn_button = QPushButton("Import PGN")
load_pgn_button.setFixedWidth(300)
load_pgn_button.clicked.connect(self.open_pgn_file)
layout = QVBoxLayout()
layout.addWidget(self.games_list)
layout.addWidget(load_pgn_button)
self.setLayout(layout)
def open_pgn_file(self):
file_dialog = QFileDialog()
file_path, _ = file_dialog.getOpenFileName(
self, "Select PGN File", "", "PGN Files (*.pgn)"
)
if file_path:
self.list_pgn_games(file_path)
def list_pgn_games(self, file_path):
self.games_list.clear()
try:
with open(file_path, "r", encoding="utf-8") as file:
pgn_game = chess.pgn.read_game(file)
while pgn_game:
item = QListWidgetItem(
f"{pgn_game.headers['White']} vs {pgn_game.headers['Black']} - ({pgn_game.headers['Result']})"
)
item.setData(Qt.UserRole, pgn_game)
self.games_list.addItem(item)
pgn_game = chess.pgn.read_game(file)
self.chessboard.move_table.setRowCount(0)
except FileNotFoundError:
print("File not found.")
def load_game(self, item):
pgn_game = item.data(Qt.UserRole)
self.chessboard.reset_game()
for move in pgn_game.mainline_moves():
self.chessboard.move_navigator.get_san_move(move)
self.chessboard.board.push(move)
self.chessboard.move_navigator.update_moves()
self.chessboard.move_navigator.update_move_table()
class GameResult:
@staticmethod
def show_checkmate():
msg_box = QMessageBox()
msg_box.setWindowTitle("Game Result")
msg_box.setText("Checkmate!")
msg_box.exec()
@staticmethod
def show_stalemate():
msg_box = QMessageBox()
msg_box.setWindowTitle("Game Result")
msg_box.setText("Stalemate!")
msg_box.exec()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ApplicationWindow()
window.showMaximized()
sys.exit(app.exec())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment