-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGameBoard.py
311 lines (260 loc) · 12.5 KB
/
GameBoard.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
import chess
import chess.svg
from ChessAI import *
from PyQt6.QtWidgets import QWidget, QLabel, QMessageBox
from PyQt6.QtSvgWidgets import QSvgWidget
from PyQt6.QtGui import QShortcut, QKeySequence, QClipboard
from PyQt6.QtCore import Qt, QTimer
class GameBoard(QWidget):
def __init__(self):
"""
Initialize chess game board and UI components and AI
"""
super().__init__() # call initializer of base class QWidget to setup window correctly
self.setWindowTitle("Chess")
self.setGeometry(0, 0, 1000, 1000)
self.setupBoard()
self.setupUI()
# Variables for Tracking Game State
self.pieceToMove = [None, None] # track piece selected for moving
self.starting_color = self.startingSide() # starting color
self.board = self.startingFEN() # starting FEN
self.lastMove = None # last move made
self.possibleMoves = [] # possible moves for selected piece
self.redoStack = [] # stack to hold moves for redo
# Initialize AI settings
self.aiColor = chess.BLACK if not self.starting_color else chess.WHITE
self.ai = ChessAI(3, self.aiColor)
self.isAIEnabled = True # turn off for human v human
self.checkAndMakeAIMove() # check and make AI move
self.drawBoard() # chess board visualization
def setupBoard(self):
"""
Set up the chess board display using SVG widget
"""
# Chess board display setup
self.widgetSvg = QSvgWidget(parent = self)
self.widgetSvg.setGeometry(10, 10, 700, 700)
# Calculating size and margins for chessboard display
self.boardSize = min(self.widgetSvg.width(), self.widgetSvg.height())
self.coordinates = True # show coordinates around chess board
self.margin = 0.05 * self.boardSize if self.coordinates else 0
self.squareSize = (self.boardSize - 2 * self.margin) / 8.0 # size of each board square
def setupUI(self):
"""
Set up the UI components including labels and shortcuts
"""
# UI for game status
self.turnLabel = QLabel("Turn: ", parent=self) # display which player turn it is
self.turnLabel.setGeometry(720, 10, 200, 30)
self.FENLabel = QLabel("FEN:\n\nrnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", parent=self) # display which player turn it is
self.FENLabel.setGeometry(100, 750, 500, 50)
self.FENLabel.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
self.shortcuts() # keyboard shortcuts
def startingSide(self):
"""
Selection of starting color
@return: Boolean True if user chooses black, False if white/default
"""
starting_color = input('Select (W)hite or (B)lack: ')
return True if starting_color and starting_color[0].lower() == "b" else False
def startingFEN(self):
"""
Selection of starting FEN or default FEN
@return: chess.Board instance initialized to specified FEN
"""
FEN = input('Press <Enter> to Begin or Input FEN: ')
# Validation (FEN)
try:
return chess.Board(FEN)
except:
return chess.Board()
def drawBoard(self):
"""
Draw chess board w/ current game state and check for game end
"""
self.chessboardSvg = chess.svg.board(self.board,
lastmove=self.lastMove,
squares = self.possibleMoves,
flipped = self.starting_color
).encode("UTF-8")
self.widgetSvg.load(self.chessboardSvg)
self.updateTurnLabel()
self.updateFENLabel()
self.checkGameEnd()
def updateTurnLabel(self):
"""
Update the label indicating whose turn it is
"""
turn_text = "Turn: " + ("Black" if self.board.turn == chess.BLACK else "White")
self.turnLabel.setText(turn_text)
def updateFENLabel(self):
"""
Update FEN label for current position
"""
FEN_text = "FEN:\n\n" + self.board.fen()
self.FENLabel.setText(FEN_text)
def checkGameEnd(self):
"""
Check for game-ending conditions and display a message if the game has ended.
"""
if self.board.is_checkmate():
winner = "Black" if self.board.turn == chess.WHITE else "White"
self.turnLabel.setText("Game Over ... " + f"Checkmate!\n{winner} wins.")
elif self.board.is_stalemate():
self.turnLabel.setText("Game Over ... " + "Stalemate!\nThe game is a draw.")
elif self.board.is_insufficient_material():
self.turnLabel.setText("Game Over!" + "\nDraw due to insufficient material.")
elif self.board.is_seventyfive_moves():
self.turnLabel.setText("Game Over!" + "\nDraw due to 75-move rule.")
elif self.board.is_fivefold_repetition():
self.turnLabel.setText("Game Over!" + "\nDraw due to fivefold repetition.")
elif self.board.is_variant_draw():
self.turnLabel.setText("Game Over!" + "\nDraw due to variant-specific reason.")
def pawnPromotion(self):
"""
Handles pawn promotion by prompting the user to select a piece for promotion.
@return: chosen piece type for pawn promotion
"""
dialog = QMessageBox(self)
dialog.setWindowTitle("Pawn Promotion")
dialog.setText("Choose piece for promotion: ")
# Custom buttons for each promotion choice
queenButton = dialog.addButton("Queen", QMessageBox.ButtonRole.YesRole)
rookButton = dialog.addButton("Rook", QMessageBox.ButtonRole.YesRole)
bishopButton = dialog.addButton("Bishop", QMessageBox.ButtonRole.YesRole)
knightButton = dialog.addButton("Knight", QMessageBox.ButtonRole.YesRole)
dialog.exec()
# Return the chosen piece
if dialog.clickedButton() == queenButton:
return chess.QUEEN
elif dialog.clickedButton() == rookButton:
return chess.ROOK
elif dialog.clickedButton() == bishopButton:
return chess.BISHOP
elif dialog.clickedButton() == knightButton:
return chess.KNIGHT
else:
return chess.QUEEN # Default to queen
def shortcuts(self):
"""
Keyboard shortcuts for window operations
<Ctrl + W> close window
<Ctrl + M> minimize window
<Ctrl + Z> undo last move
"""
# Close window
self.shortcut_close = QShortcut(QKeySequence('Ctrl+W'), self)
self.shortcut_close.activated.connect(self.close)
# Minimize window
self.shortcut_minimize = QShortcut(QKeySequence('Ctrl+M'), self)
self.shortcut_minimize.activated.connect(self.showMinimized)
# Undo Last Move
self.shortcut_undo = QShortcut(QKeySequence('Ctrl+Z'), self)
self.shortcut_undo.activated.connect(self.undoMove)
# Redo Last Move
self.shortcut_redo = QShortcut(QKeySequence('Ctrl+Y'), self)
self.shortcut_redo.activated.connect(self.redoMove)
def mousePressEvent(self, e):
"""
Handle mouse press events for selecting and moving chess pieces.
@param: event containing details of mouse press
"""
# Get positions of mouse click
pos = e.position()
x, y = pos.x(), pos.y()
# Check if left mouse button was pressed in board bounds
if e.button() == Qt.MouseButton.LeftButton and x <= self.boardSize and y <= self.boardSize:
# Adjust file and rank calculation based on whether the board is flipped
if self.starting_color: # If starting color is True (black), then the board is flipped
file = 7 - int((x - self.margin) / self.squareSize)
rank = int((y - self.margin) / self.squareSize)
else:
file = int((x - self.margin) / self.squareSize)
rank = 7 - int((y - self.margin) / self.squareSize)
# EDGE CASE: If file is not in valid 0-7 range, return
if file < 0 or file > 7 or rank < 0 or rank > 7:
return
# Convert the file and rank to a square in the chess library's notation
square = chess.square(file, rank)
coordinates = "{}{}".format(chr(file + 97), str(rank + 1))
# Check if piece is already selected for moving
if self.pieceToMove[1] is not None:
# Check if current click is in a different square than initial position
if self.pieceToMove[1] != coordinates:
# Create move in UCI format
move = chess.Move.from_uci(f"{self.pieceToMove[1]}{coordinates}")
#Check for pawn promotion
if (self.board.piece_at(move.from_square).piece_type == chess.PAWN and (move.to_square >= chess.A8 or move.to_square <= chess.H1)):
promotion_choice = self.pawnPromotion() # Prompt for pawn promotion
move = chess.Move(move.from_square, move.to_square, promotion=promotion_choice)
# If move is valid...
if move in self.board.legal_moves:
self.board.push(move) # make move on board
self.lastMove = move # update last move state
self.possibleMoves = [] # clear possible moves
self.redoStack = [] # clear redo stack b/c new moves invalidate it
else:
# If move isn't legal, highlight possible moves for piece
self.possibleMoves = [move.to_square for move in self.board.legal_moves if move.from_square == square]
else:
# Deselect piece if clicked square is same as selected piece square
self.possibleMoves = []
self.pieceToMove = [None, None]
else:
# If no piece selected and clicked square has a piece, select that piece and show possible moves
piece = self.board.piece_at(square)
if piece:
self.pieceToMove = [piece, f"{chr(file + 97)}{str(rank + 1)}"]
self.possibleMoves = [move.to_square for move in self.board.legal_moves if move.from_square == square]
else:
# If clicked square doesn't have piece, clear possiblities
self.possibleMoves = []
# Redraw board to reflect changes
self.drawBoard()
# AI Move
self.checkAndMakeAIMove()
def undoMove(self):
"""
Undo last move and update game state
"""
if len(self.board.move_stack) > 0:
# Add last move to redo stack before undoing it
self.redoStack.append(self.board.pop())
# If there was a last move, update it to the new last move if any
self.lastMove = self.board.peek() if len(self.board.move_stack) > 0 else None
# Clear possible moves as the selection is now potentially invalid
self.possibleMoves = []
# Reset piece to move
self.pieceToMove = [None, None]
# Redraw the board with the updated state
self.drawBoard()
def redoMove(self):
"""Redo last move and update game state"""
if len(self.redoStack) > 0:
# If there is move in redoStack, pop it and add to board
move = self.redoStack.pop()
self.board.push(move)
# Update state
self.lastMove = move
# Clear possible moves as the selection is now potentially invalid
self.possibleMoves = []
# Reset piece to move
self.pieceToMove = [None, None]
# Redraw the board with the updated state
self.drawBoard()
def checkAndMakeAIMove(self):
"""
Checks if it's the AI's turn to move and initiates the AI move-making process.
"""
if self.isAIEnabled and self.board.turn == self.aiColor:
QTimer.singleShot(100, self.makeAIMove) # Delay AI move by 100 milliseconds (allows for user input to go through first)
def makeAIMove(self):
"""
Executes the best move determined by the AI, updating the game state and UI.
"""
if self.isAIEnabled and self.board.turn == self.aiColor:
best_move = self.ai.select_best_move(self.board)
if best_move:
self.board.push(best_move)
self.drawBoard()