-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchess_utilities.py
More file actions
345 lines (284 loc) · 12.1 KB
/
chess_utilities.py
File metadata and controls
345 lines (284 loc) · 12.1 KB
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
"""Import and export board states to and from a truncated version of
Forsyth-Edwards Notation (FEN).
Primarily written to convert board positions into board.Board objects for
debugging.
Functions
---------
print_fen_to_terminal()
import_fen_to_board()
export_board_to_fen()
pickle_and_add_board_to_db()
load_board_from_db()
create_board_database()
"""
import copy
import pickle
import sqlite3
from time import gmtime, strftime
import board
import pieces
# TODO: expand this for saving game state using a 'Save' button?
def print_fen_to_terminal(fen_string):
"""Print a FEN string into the terminal as an ASCII chess board.
Similar to board.Board.__repr__.
"""
fen_ls = fen_string.split(' ')
fen_ls[0] = fen_ls[0].split('/')
assert len(fen_ls[0]) == 8
print(fen_ls)
return_ls = []
for row in fen_ls[0]:
row_out = ['|']
for char in row:
if char.isalpha():
row_out.append(char)
row_out.append('|')
elif char.isdigit():
row_out.append(' |' * int(char))
return_ls.append(''.join(row_out))
for row in return_ls:
print(row)
def import_fen_to_board(fen: str, autopromote=False):
"""Convert FEN string to board.Board object.
Does not consider en passant square, half-move count, or move count.
"""
chessboard = board.Board()
fen = fen.strip().split(' ')
if len(fen) > 3:
raise ValueError('FEN en passant square and move counts not '
'supported.')
castling_options = None
if len(fen) == 3:
castling_options = fen.pop()
turn_to_move = fen.pop()
if turn_to_move == 'w':
last_move_color = 'black'
elif turn_to_move == 'b':
last_move_color = 'white'
else:
raise ValueError('Invalid symbol for piece color.')
chessboard.last_move_piece = pieces.Pawn('placeholder',
last_move_color,
100)
fen = fen[0].split('/')
letter_to_piece = {'p': pieces.Pawn, 'n': pieces.Knight,
'b': pieces.Bishop, 'r': pieces.Rook,
'q': pieces.Queen, 'k': pieces.King}
# Add pieces and empty squares to chessboard.squares.
squares_ind_counter = 0
fen = reversed(fen)
for row in fen:
for char in row:
if char.isalpha():
# Make a piece in chessboard.squares.
if char.isupper():
piece_color = 'white'
elif char.islower():
piece_color = 'black'
piece = letter_to_piece[char.lower()](char,
piece_color,
squares_ind_counter)
chessboard.squares[squares_ind_counter] = piece
if piece.color == 'white':
if isinstance(piece, pieces.King):
chessboard.white_king = piece
else:
chessboard.white_pieces.append(piece)
elif piece.color == 'black':
if isinstance(piece, pieces.King):
chessboard.black_king = piece
else:
chessboard.black_pieces.append(piece)
squares_ind_counter += 1
elif char.isdigit():
# Set one or more squares equal to ' '.
for _ in range(int(char)):
chessboard.squares[squares_ind_counter] = ' '
squares_ind_counter += 1
# Kings must be last piece in piece lists for check to work.
if chessboard.white_king:
chessboard.white_pieces.append(chessboard.white_king)
if chessboard.black_king:
chessboard.black_pieces.append(chessboard.black_king)
for piece in chessboard.white_pieces + chessboard.black_pieces:
if isinstance(piece, pieces.Pawn):
if piece.color == 'white':
if piece.square not in pieces.ranks_files.rank_2:
piece.has_moved = True
elif piece.square not in pieces.ranks_files.rank_7:
piece.has_moved = True
if autopromote:
piece.autopromote = True
elif isinstance(piece, pieces.Rook):
if piece.color == 'white':
if piece.square not in [0, 7]:
piece.has_moved = True
elif piece.square not in [56, 63]:
piece.has_moved = True
elif isinstance(piece, pieces.King):
if piece.color == 'white':
if piece.square != 4:
piece.has_moved = True
elif piece.square != 60:
piece.has_moved = True
if castling_options:
if 'K' not in castling_options and 'Q' not in castling_options:
chessboard.white_king.has_moved = True
if 'k' not in castling_options and 'q' not in castling_options:
chessboard.black_king.has_moved = True
return chessboard
def export_board_to_fen(chessboard):
"""Convert board.Board.squares list to a truncated FEN string (without
castling, en passant, and move count indicators).
"""
# Deepcopy seems necessary here because the original board is not
# equal to and is not the same board object after calling this
# function, based on the tests.
squares = copy.deepcopy(chessboard.squares)
fen = []
for slice_start in range(0, 57, 8):
slice_end = slice_start + 8
fen.append(squares[slice_start:slice_end])
fen = list(reversed(fen))
# Add piece positions
for ind, row in enumerate(fen):
partial_fen = []
adjacent_empty_squares_count = 0
for square in row:
if square == ' ':
adjacent_empty_squares_count += 1
else:
# Reset adjacent_empty_squares_count and add piece's name[0].
if adjacent_empty_squares_count > 0:
partial_fen.append(str(adjacent_empty_squares_count))
adjacent_empty_squares_count = 0
partial_fen.append(square.name[0])
if adjacent_empty_squares_count > 0:
partial_fen.append(str(adjacent_empty_squares_count))
fen[ind] = ''.join(partial_fen)
fen = '/'.join(fen)
# Add who's turn it is to move.
white_or_black_to_move = ''
last_move_piece = chessboard.last_move_piece
if last_move_piece is None:
white_or_black_to_move = 'w'
elif last_move_piece.color == 'black':
white_or_black_to_move = 'w'
elif last_move_piece.color == 'white':
white_or_black_to_move = 'b'
else:
raise Exception(f'Invalid Board.last_move_piece: {last_move_piece}')
fen = ' '.join([fen, white_or_black_to_move])
# with open('FEN_exports.txt', 'a') as export:
# current_time = strftime('%Y-%m-%d %H:%M', gmtime())
# export.write(f'{current_time}\n')
# export.write(f'{fen}\n\n')
return fen
def pickle_and_add_board_to_db(chessboard, e_type, e_val):
"""Serialize and store board.Board object in an sqlite database.
Called after an error has occurred so the board object can be debugged
easily.
Note: Traceback object can't be stored in the database.
("InterfaceError... probably unsupported type." It would be nice to
include but it is not necessary. Consider manually adding traceback
as a string after error.
"""
current_time = strftime('%Y-%m-%d %H:%M', gmtime())
pickled_board = pickle.dumps(chessboard)
con = sqlite3.connect('chessboards.sqlite')
cur = con.cursor()
fields = (current_time, e_type, e_val, pickled_board)
cur.execute('''INSERT INTO chessboards
('date', 'error_type', 'error_value', 'board_obj')
VALUES (?, ?, ?, ?)''', fields)
con.commit()
con.close()
def load_board_from_db(row_id=None):
"""As a default, pulls the most recent board.Board object and
associated information from the database. The row_id parameter can
select a specific row from the table if needed.
"""
# Should this instead return cur.fetchall() for more obvious bugs
# where multiple rows are returned? Or is that an unrealistic outcome?
con = sqlite3.connect('chessboards.sqlite')
cur = con.cursor()
# There could be a row with an id of 0. Need to avoid "if 0:" scenario.
if row_id is not None:
cur.execute('SELECT * FROM chessboards WHERE id = ?', (row_id,))
return cur.fetchone()
else:
cur.execute('''SELECT * FROM chessboards
WHERE id = (SELECT MAX(id) FROM chessboards)''')
return cur.fetchone()
con.close()
def create_board_database():
"""Create a SQLite database in the current directory, if a file named
"chessboards.sqlite" does not already exist.
"""
con = sqlite3.connect('chessboards.sqlite')
cur = con.cursor()
cur.execute('''CREATE TABLE IF NOT EXISTS chessboards
(id INTEGER PRIMARY KEY AUTOINCREMENT,
date text,
error_type text,
error_value text,
board_obj)
''')
con.commit()
con.close()
if __name__ == '__main__':
import unittest
class TestChessUtilities(unittest.TestCase):
"""chess_utilities.py tests"""
def test_export_board_to_fen(self):
"""Board object is exportable to Forsyth-Edwards Notation (FEN)."""
chessboard = board.Board()
chessboard.initialize_pieces()
# original_board = copy.deepcopy(chessboard)
exported_fen = export_board_to_fen(chessboard)
self.assertEqual(exported_fen,
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w')
# Test below fails so the export_board_to_fen function uses
# copy.deepcopy.
# self.assertEquals(original_board, chessboard)
def test_import_fen_to_board(self):
"""Board imports without error. Requires visual check of the
printed chessboard for a more detailed check.
"""
check_test = 'rnbB2kr/1p1p3p/8/2pP2Q1/p3P3/P7/1PP2PPP/RN2KBNR b'
chessboard = import_fen_to_board(check_test)
# print(chessboard)
self.assertIsInstance(chessboard.squares[0], pieces.Rook)
self.assertEqual(chessboard.squares[0].color, 'white')
def test_pickle_and_add_board_to_db(self):
"""Serialize the Board object and add it to the SQLite database."""
chessboard = board.Board()
chessboard.initialize_pieces()
chessboard.squares[8].update_moves(chessboard)
chessboard.squares[8].move_piece(chessboard, 24)
con = sqlite3.connect('chessboards.sqlite')
cur = con.cursor()
cur.execute('SELECT COUNT(id) FROM chessboards')
old_rows_in_db_count = cur.fetchall()[0][0]
pickle_and_add_board_to_db(chessboard, self.e_type,
self.e_val)
cur.execute('SELECT COUNT(id) FROM chessboards')
new_rows_in_db_count = cur.fetchall()[0][0]
self.assertEqual(new_rows_in_db_count, old_rows_in_db_count + 1)
def test_load_board_from_db(self):
"""Load the serialized Board object from the database to a
useable object.
"""
_, _, error_type, error_value, chessboard = load_board_from_db()
chessboard = pickle.loads(chessboard)
self.assertEqual(error_type, 'Exception')
self.assertEqual(error_value, 'Raise Exception for tests.')
self.assertIsInstance(chessboard.squares[24], pieces.Pawn)
# Clean up database after tests.
con = sqlite3.connect('chessboards.sqlite')
cur = con.cursor()
cur.execute('''DELETE FROM chessboards
WHERE id = (SELECT MAX(id) FROM chessboards)''')
con.commit()
con.close()
unittest.main()