-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathWizard_chess.py
More file actions
368 lines (291 loc) · 11.9 KB
/
Wizard_chess.py
File metadata and controls
368 lines (291 loc) · 11.9 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
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
import threading
import chess
import chess.uci
from abc import abstractmethod
import RPi.GPIO as GPIO
from motor_class import Motor
class Player:
@abstractmethod
def get_next_move(self, board):
pass
class HumanPlayer(Player):
def get_next_move(self, board):
possible_moves = list(map(lambda move: move.uci(), board.generate_legal_moves()))
print('Possible moves you can make: {}'.format(possible_moves))
while True:
move_uci_str = input('{}, please enter where to move. '.format(get_player_for_current_turn(board)))
try:
move = chess.Move.from_uci(move_uci_str.rstrip().lstrip())
except ValueError:
print('"{}" not formatted correctly'.format(move_uci_str))
continue
moved_piece = board.piece_at(move.from_square)
if move in board.legal_moves:
break
elif moved_piece is None:
print('No piece is there!')
elif moved_piece.color != board.turn:
print("That's someone else's piece!")
elif board.piece_at(move.to_square) is not None and board.piece_at(move.to_square).color == board.turn:
print('You cannot take your own piece!')
elif board.is_into_check(move):
print('You cannot move into check!')
else:
print('A {} cannot make that move!'.format(chess.PIECE_NAMES[moved_piece.piece_type]))
return move
class ComputerPlayer(Player):
def __init__(self, uci_engine_path, skill_level, think_time_ms=100):
"""
Initialize the computer
:param uci_engine_path: the path to the uci engine executable
:param skill_level: How good of a player it is. For stockfish this can be anything in he range of [0, 20],
where 20 is the best, and 0 is the worst
:param think_time_ms: how long the engine should spend thinking each turn in milliseconds. default 100ms
"""
self.engine = chess.uci.popen_engine(uci_engine_path)
self.engine.uci()
# see http://support.stockfishchess.org/kb/advanced-topics/engine-parameters for stockfish parameters.
# note: if you don't use stockfish these will be different
opts = {'Skill Level': skill_level}
self.engine.setoption(opts)
self.think_time_ms = think_time_ms
def get_next_move(self, board):
self.engine.ucinewgame()
self.engine.position(board)
best_move, ponder_move = self.engine.go(movetime=self.think_time_ms)
self.engine.stop()
return best_move
class Magnet:
"""
Simple wrapper around GPIO calls, just to make it more clear what's g oin
"""
@staticmethod
def initialize():
"""
Sets up the magnet
:return: none
"""
GPIO.setup(7, GPIO.OUT)
@staticmethod
def grab():
"""
Grab a piece with the magnet
:return: none
"""
GPIO.output(7, 0)
@staticmethod
def release():
"""
Release the piece from the magnet
:return: none
"""
GPIO.output(7, 1)
@staticmethod
def cleanup():
"""
Cleans up the magnet
:return: none
"""
GPIO.cleanup()
class MotorMover:
"""
Handles moving the motor to a specific position on the board
"""
def __init__(self, motor_x, motor_y, unit_dist=0.65):
"""
Initialize the motor mover
:param motor_x: motor for the x axis
:param motor_y: motor for the y axis
:param unit_dist: distance that traveling 1 unit takes
"""
self.x = 0
self.y = 0
self.motors = [motor_x, motor_y]
self.unit_dist = unit_dist
for motor in self.motors:
motor.init()
def _compute_dist_and_rot_dir(self, new_value, old_value):
"""
Calculates the distance and rotation that the motor has to travel for going to a new location
:param new_value: the new location
:param old_value: the old location
:return: (distance, rotation_direction), where rotation_direction is either Motor.CLOCKWISE or Motor.ANTICLOCKWISE
"""
distance = self.unit_dist * abs(old_value - new_value)
rotation_direction = Motor.CLOCKWISE if new_value > old_value else Motor.ANTICLOCKWISE
return distance, rotation_direction
def _start_moving_motor(self, motor_ind, distance, rotation_direction):
"""
:param motor_ind: The index into self.motors that the motor is at
:param distance: the distance to move
:param rotation_direction: the direction to turn the motor
:return: the thread that was started
"""
motor_thread = threading.Thread(group=None, target=self.motors[motor_ind].turn,
args=(distance * Motor.Revolution, rotation_direction),
name="motor" + str(motor_ind))
motor_thread.start()
return motor_thread
def move_to_x(self, x):
"""
Move only along the x axis
:param x: the new x value
:return: none
"""
distance, rotation = self._compute_dist_and_rot_dir(x, self.x)
motor_x_thread = self._start_moving_motor(0, distance, rotation)
motor_x_thread.join()
self.x = x
def move_to_y(self, y):
"""
Move only along the y axis
:param y: the new y value
:return: none
"""
distance, rotation = self._compute_dist_and_rot_dir(y, self.y)
motor_y_thread = self._start_moving_motor(1, distance, rotation)
motor_y_thread.join()
self.y = y
def move_to(self, x, y):
"""
Move to a new location along both axes
:param x: the new x value
:param y: the new y value
:return: none
"""
x_distance, x_rotation = self._compute_dist_and_rot_dir(x, self.x)
y_distance, y_rotation = self._compute_dist_and_rot_dir(y, self.y)
motor_x_thread = self._start_moving_motor(0, x_distance, x_rotation)
motor_y_thread = self._start_moving_motor(1, y_distance, y_rotation)
motor_x_thread.join()
motor_y_thread.join()
self.x = x
self.y = y
def __str__(self):
return 'MotorMover({}, {})'.format(self.x, self.y)
def get_player_for_current_turn(board):
return chess.COLOR_NAMES[board.turn].capitalize()
def execute_move(board, move, mover):
rank_from = chess.square_rank(move.from_square)
file_from = chess.square_file(move.from_square)
rank_to = chess.square_rank(move.to_square)
file_to = chess.square_file(move.to_square)
# take any capture piece
if board.piece_at(move.to_square) is not None:
take_piece(move.to_square, mover)
elif board.is_en_passant(move):
take_piece(chess.square(rank_from, file_to), mover)
# move the rook if this is a castling move
if board.is_castling(move):
if board.is_queenside_castling(move):
mover.move_to(0, rank_from)
Magnet.grab()
mover.move_to(file_to + 1, rank_from)
Magnet.release()
elif board.is_kingside_castling(move):
mover.move_to(7, rank_from)
Magnet.grab()
mover.move_to(file_to - 1, rank_from)
Magnet.release()
# if this is a promotion move, move the piece off the board and wait for user to replace it
if move.promotion is not None:
take_piece(move.from_square, mover)
input('Please replace the {} at {} with a {}, and hit enter once done.'.format(
board.piece_at(move.from_square), chess.SQUARE_NAMES[move.from_square], chess.PIECE_NAMES[move.promotion]))
# move the actual piece
mover.move_to(file_from, rank_from)
Magnet.grab()
mover.move_to(file_to, rank_to)
Magnet.release()
board.push(move)
def take_piece(square, mover):
"""
Moves the captured piece off of the board
:param square: the square the piece is on
:param mover: the MotorMover
:return: none
"""
rank = chess.square_rank(square)
file = chess.square_file(square)
mover.move_to(file, rank)
Magnet.grab()
mover.move_to_x(8)
Magnet.release()
def display_board(board):
# note: this was modified from chess.BaseBoard.__str__ method. so print(board) works just fine, but doesn't actually
# print the ranks & files, which this modification does.
builder = []
files = [' ', ' ']
files.extend(chess.FILE_NAMES)
builder.append(' '.join(files))
builder.append('')
for i in reversed(range(8)):
rank_entries = [chess.RANK_NAMES[i], ' ']
for j in range(8):
piece = board.piece_at(chess.square(j, i))
rank_entries.append('.' if piece is None else piece.symbol())
rank_entries.append(' ')
rank_entries.append(chess.RANK_NAMES[i])
builder.append(' '.join(rank_entries))
builder.append('')
builder.append(' '.join(files))
print('\n'.join(builder))
def display_taken_pieces(pieces_taken):
print("Pieces Taken")
print("White | Black")
print('--------------')
white_taken = len(pieces_taken[chess.WHITE])
black_taken = len(pieces_taken[chess.BLACK])
for i in range(max(white_taken, black_taken)):
white_str = str(pieces_taken[chess.WHITE][i]) if i < white_taken else '-'
black_str = str(pieces_taken[chess.BLACK][i]) if i < black_taken else '-'
print('{:<6} | {:<6}'.format(white_str, black_str))
def display_moves_made(moves_made):
print(" White Black ")
print("-" * 25)
for i in range(len(moves_made)):
move, piece, captured_piece = moves_made[i]
print('{} {}\t\t'.format(piece, move), end=('\n' if i % 2 == 1 else ''))
print('')
def setup_players():
play_computer = input('Play against a computer (y/n)? ')
if play_computer:
player_color_str = input('What color do you want to play as (w/b)? ')
player_color = chess.WHITE if player_color_str == 'w' else chess.BLACK
# modify the skill level here if you want to make the computer better or worse.
# 0 is the worst, 20 is the best
players = {player_color: HumanPlayer(), not player_color: ComputerPlayer('./stockfish_8_x64', skill_level=10)}
else:
players = {chess.WHITE: HumanPlayer(), chess.BLACK: HumanPlayer()}
return players
def main():
players = setup_players()
mover = MotorMover(Motor(17, 18, 27, 22), Motor(4, 25, 24, 23))
# initialize the magnet and make sure its not grabbing anything
Magnet.initialize()
Magnet.release()
# initialize the board
board = chess.Board()
moves_made = []
pieces_taken = {chess.WHITE: [], chess.BLACK: []}
display_board(board)
while not board.is_checkmate():
move = players[board.turn].get_next_move(board)
moved_piece = board.piece_at(move.from_square)
captured_piece = board.piece_at(move.to_square)
moves_made.append((move, moved_piece, captured_piece))
if board.piece_at(move.to_square) is not None:
pieces_taken[board.turn].append(captured_piece)
execute_move(board, move, mover)
display_board(board)
# display_taken_pieces(pieces_taken)
# display_moves_made(moves_made)
if board.is_check():
print('Check')
print('Checkmate')
print('{} wins!'.format(chess.COLOR_NAMES[board.turn]))
print("End of Game")
mover.move_to(0, 0)
Magnet.cleanup()
if __name__ == '__main__':
main()