-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathchecker.py
492 lines (413 loc) Β· 17.6 KB
/
checker.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
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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
import os
import random
import time
from itertools import product
from typing import Optional
## tada
class Checker:
"""class to simulate checkers game where human/computer play against computer"""
# normal unit hopes
normal_unit_hops = [(1, 1), (1, -1), (-1, 1), (-1, -1)]
# tiles used to print the board
tile = {
"b": "β«",
"B": "β¬",
"w": "βͺ",
"W": "β¬",
"r": "π₯",
"y": "π¨",
"h": "π©"
}
def __init__(self,
human: bool = False,
clear_screen: bool = True,
delay: float = 0.0) -> None:
"""Constructor
Args:
human (bool, optional): if True then human will from white"s side. Defaults to False.
clear_screen (bool, optional): if True then clear screen after each new move. Defaults to True.
delay (float, optional): delay in number of seconds for next board to appear. Defaults to 0.0."""
self.human = human
self.clear_screen = clear_screen
self.delay = delay
self.stats = {"w": {}, "b": {}, "draw": 0}
def initiate_pieces(self) -> None:
"""places the pieces on the board"""
self.piece = dict()
for c in range(8):
for r in range(3):
# placing the black pieces on the upper three rows of the board
self.piece[(r, c)] = "b"
# placing the white pieces on the lower three rows of the board
self.piece[(7 - r, c)] = "w"
for r in range(3, 5):
# placing the empty pieces on the mid two rows of the board
self.piece[(r, c)] = "o"
def print_screen(self, move_count: int = 0) -> None:
"""print the board on the terminal
Args:
move_count (int, optional): move number. Defaults to 0."""
for i,j in product(range(8), repeat=2):
color = "y" if (i + j) % 2 else "r" # changing the color
# if piece was in the last move then its "h" for tiling
p = "h" if (i, j) in self.last_move else self.piece[(i, j)]
if p == "o":
p = color # color for empty piece
print(self.tile[p], end= f" {i+1}\n" if j == 7 else " ")
print(" ", end="")
for i in range(8):
print(i+1, end= None if i == 7 else " ")
print("\nBlack: ", self.count["b"])
print("White: ", self.count["w"])
print("Move:", move_count, end="\n\n")
@staticmethod
def L1_norm(x: tuple[int, int], y: tuple[int, int]) -> int:
"""returns the distance between x and y in L1 norm space.
Args:
x (tuple[int, int]): first point.
y (tuple[int, int]): second point.
Returns:
int: distance between x and y in L1 norm space.
"""
return abs(x[0] - y[0]) + abs(x[1] - y[1])
def move(self, pos: Optional[tuple[int, int]], side: str) -> None:
"""moves the piece according to the sequence of position given to
Args:
pos (Optional[tuple[int, int]]): sequence of position to move piece on the board
side (str): piece is from which side"""
# changing the sides
opp_side = "b" * (side == "w") + "w" * (side == "b")
# function to calculate mid of two position
mid_rc = lambda x, y: ((x[0] + y[0]) >> 1, (x[1] + y[1]) >> 1)
# if the are only two hops and they are trivial means only
# hoping to their digonally adjacent neighbours
if len(pos) == 2 and Checker.L1_norm(pos[0], pos[1]) == 2:
# checking if horse has to be made
if pos[1][0] == 7 * (side == "b"):
self.piece[pos[1]] = self.piece[pos[0]].upper()
else:
self.piece[pos[1]] = self.piece[pos[0]]
self.piece[pos[0]] = "o"
else:
# if the hop is beating the opposite side pieces
for i in range(len(pos) - 1):
if ( # there is always an empty piece after the hop
self.piece[pos[i + 1]] == "o"
# the hop must be jumping two squares doiagonally
and Checker.L1_norm(pos[i + 1], pos[i]) == 4
# there must be a peice of opposite side between the jump and landing position
and self.piece[mid_rc(pos[i + 1], pos[i])].lower()
== opp_side):
self.count[opp_side] -= 1 # reducing the count
# setting the middle piece as empty piece
self.piece[mid_rc(pos[i + 1], pos[i])] = "o"
# checking if horse has to be made
if pos[i + 1][0] == 7 * (side == "b"):
self.piece[pos[i + 1]] = self.piece[pos[i]].upper()
else:
self.piece[pos[i + 1]] = self.piece[pos[i]]
self.piece[pos[i]] = "o" # setting middle as empty
else:
# breaking the chain of hopping because we have illegal hops from now
break
@staticmethod
def direction(dr: int, dc: int) -> str:
"""returns the direction in which the change is happening
Args:
dr (int): change in row
dc (int): change in column
Returns:
str: the direction string"""
return ("dl" * (dc < 0) + "dr" *
(dc > 0)) * (dr > 0) + ("ul" * (dc < 0) + "ur" *
(dc > 0)) * (dr < 0)
def hops(self,
r: int,
c: int,
p: str,
steps: Optional[tuple[int, int]],
allowed: Optional[str],
path: dict = dict(),
visited: set = set(),
first: bool = True) -> dict:
"""returns all the hops than can happen from a given position.
Args:
r (int): row of the piece
c (int): column of the piece
steps (Optional[tuple[int, int]]): valid hops from a given position.
allowed (Optional[str]): pieces allowed to be hoped over.
path (dict, optional): container storing the path. Defaults to dict().
visited (set, optional): visited position. Defaults to set().
first (bool, optional): True if the hop is first hop. Defaults to True.
Returns:
dict: container that contains all the paths"""
path["coordinate"] = (r, c) # adding the coordinates
for d in ["ur", "ul", "dr", "dl"]:
path[d] = path.get(d, None) # to avoid any error
for dr, dc in steps:
# if it is our first hop only then we can hop onto your diagonal neighbours
if first:
r_ = r + dr # r" = r + Ξr
c_ = c + dc # c" = c + Ξc
if 0 <= r_ < 8 and 0 <= c_ < 8 and self.piece[(r_, c_)] == "o":
path[self.direction(dr, dc)] = {
"coordinate": (r_, c_),
"ur": None,
"ul": None,
"dr": None,
"dl": None
}
r_ = r + (dr << 1) # r" = r + 2 * Ξr
c_ = c + (dc << 1) # c" = c + 2 * Ξc
if (0 <= r_ < 8 and 0 <= c_ < 8 and (r_, c_) not in visited
and self.piece[(r + dr, c + dc)] in allowed
and self.piece[(r_, c_)] == "o"):
path[self.direction(dr, dc)] = {
"coordinate": (r_, c_),
"ur": None,
"ul": None,
"dr": None,
"dl": None
}
visited.add((r_, c_)) # visiting the position
# exploring more hops
self.hops(r_, c_, p, steps, allowed,
path[self.direction(dr, dc)], visited, False)
return path
def deepest_path(self, path: dict) -> Optional[tuple]:
"""returns the deepest path in a tree, if there are more
than one then select any one randomly
Args:
path (dict): tree in a form of nested dictionaries
Returns:
Optional[tuple]: list of all nodes that are on the
deepest path in the tree"""
# if there is no way to go more
if path is None:
return list()
# exploring path in down right direction
down_right = self.deepest_path(path["dr"])
# exploring path in down left direction
down_left = self.deepest_path(path["dl"])
# exploring path in up right direction
up_right = self.deepest_path(path["ur"])
# exploring path in up left direction
up_left = self.deepest_path(path["ul"])
length = [len(down_right), len(down_left), len(up_right), len(up_left)]
deepest = max(length)
if length[0] == deepest:
down_right.append(path["coordinate"])
if length[1] == deepest:
down_left.append(path["coordinate"])
if length[2] == deepest:
up_right.append(path["coordinate"])
if length[3] == deepest:
up_left.append(path["coordinate"])
# randomly chossing a path from all path
index = [0, 1, 2, 3]
i = random.choice(index)
deepest = max(length)
while length[i] != deepest:
index.remove(i)
i = random.choice(index)
if i == 0:
return down_right
elif i == 1:
return down_left
elif i == 2:
return up_right
else:
return up_left
def paths(self, side: str) -> Optional[Optional[tuple[int, int]]]:
"""returns the deepest path of all pieces from side
Args:
side (str): the side to find the deepest path of its pieces
Returns:
Optional[Optional[tuple[int,int]]]: list of a deepest
path possible for each piece"""
path = list() # container for all deepest paths from all position
side_parity = dict() # setting parity false for new run
for r, c in product(range(8), repeat=2):
p = self.piece[(r, c)]
if p.lower() == side:
# allowed hops and the pieces to hop on
if p == "b":
steps = [(1, 1), (1, -1)]
# steps = self.quad_count(r, c, p, False, True)
allowed = ["w"]
elif p == "w":
steps = [(-1, 1), (-1, -1)]
allowed = ["b"]
else:
steps = self.normal_unit_hops
if p == "B":
allowed = ["w", "W"]
else:
allowed = ["b", "B"]
hop = self.hops(r, c, p, steps, allowed, dict(), set(), True)
# appending the deepest path in the path container
dp = self.deepest_path(hop)
if len(dp) > 1:
path.append(dp)
# updating the parity
side_parity[(r + c) % 2] = side_parity.get(
(r + c) % 2, False) or True
self.parity[side] = side_parity # setting the parity
return path
def human_move(self) -> Optional[tuple[int, int]]:
"""collects the move human wants to play
Returns:
Optional[tuple[int, int]]: list of moves from human"""
print("Enter your move dear human.")
cin = input() # taking inpurt from human
pos = (int(cin[0]), int(cin[2]))
move = [pos]
while True:
# if human gives q then stops the input and return the collected moves
cin = input()
if cin == "q":
break
pos = (int(cin[0]), int(cin[2]))
move.append(pos)
return move
def computer_move(self, side: str) -> Optional[tuple[int, int]]:
"""returns the move computer wants to play
Args:
side (str): side which computer playing
Returns:
Optional[tuple[int, int]]: list of the position to
move piece on the board"""
path = self.paths(side)
if len(path) == 0:
return None
moves = dict() # dict for all moves
jump = 0 # depth of the hop
for k in path:
if len(k) > jump:
jump = len(k)
moves[len(k)] = moves.get(len(k), list()) + [k]
if jump == 2: # if we are hoping over once then it must be a beating hop
beating_hop = list(
filter(lambda k: Checker.L1_norm(k[0], k[1]) == 4, moves[2]))
if len(beating_hop) > 0:
moves[2] = beating_hop
# randomly chossing one hop from the collection of all deepest hops
move = random.choice(moves[jump])[::-1]
self.last_move = move[:-1]
return move
def start(self, MAX: int = 600) -> None:
"""starts the game
Args:
MAX (int, optional): maximum number of moves the game can take. Defaults to 600.
"""
# parity is True right now
charge = True
# number of moves are zero
move_count = 0
# set that contain all the last move piece had
self.last_move = list()
# places the pieces on the board
self.initiate_pieces()
# count of the pieces
self.count = {"b": 24, "w": 24}
# randomly selecting side for first move
side = random.choice(["b", "w"])
# parity to avoid the color segeration
self.parity = {"w": {0: True, 1: True}, "b": {0: True, 1: True}}
self.print_screen()
while self.count["b"] > 0 and self.count[
"w"] > 0 and charge and move_count <= MAX:
if side == "w":
# human playing from the side
if self.human:
path = self.human_move()
else:
path = self.computer_move("w")
else:
# computer playing from the side
path = self.computer_move("b")
if path is None:
self.count[side] = 0
break
self.move(path, side)
# channging the side for next move
side = "b" * (side == "w") + "w" * (side == "b")
move_count += 1
# checking the parity
charge = (self.parity["w"].get(0, False) and self.parity["b"].get(
0, False)) or (self.parity["w"].get(1, False)
and self.parity["b"].get(1, False))
# printing the board and count
time.sleep(self.delay)
if self.clear_screen:
os.system("clear")
self.print_screen(move_count)
if not charge or move_count == MAX: # if game draws
# print("Game draws :( in ", end="")
self.stats["draw"] += 1
elif self.count["b"] == 0: # if white wins
move_count = str(move_count)
# print("White wins! in ", end="")
self.stats["w"][move_count] = self.stats["w"].get(move_count,
0) + 1
else: # if black wins
move_count = str(move_count)
# print("Black wins! in ", end="")
self.stats["b"][move_count] = self.stats["b"].get(move_count,
0) + 1
# print(move_count, "moves.")
def save(self, changed: bool = True) -> None:
"""save the stats of the game into a file"""
# reading the file
import json
with open("game_stats.json", "r") as file:
file = json.load(file)
# updating the average moves of white
if self.stats != {"w": {}, "b": {}, "draw": {}}:
for m, c in self.stats["w"].items():
file["w"][m] = file["w"].get(m, 0) + c
# updating the average moves of black
for m, c in self.stats["b"].items():
file["b"][m] = file["b"].get(m, 0) + c
# updating the draw count
file["draw"] += self.stats["draw"]
# dumping the stats in the file
if '601' in file['b']:
del file["b"]["601"]
if changed:
with open("game_stats.json", "w") as outfile:
json.dump(file, outfile)
import numpy as np
A = np.array(list(file["w"].keys()), dtype=int)
B = np.array(list(file["b"].keys()), dtype=int)
start = min(np.min(A), np.min(B))
stop = max(np.max(A), np.max(B))
xrange = range(start, stop + 1)
white_y_range = list()
black_y_range = list()
for x in xrange:
white_y_range.append(file["w"].get(str(x), 0))
black_y_range.append(file["b"].get(str(x), 0))
from matplotlib import pyplot as plt
fig, ax = plt.subplots(1, 2, sharex='col', sharey='row')
plt.tight_layout()
ax[0].scatter(xrange,
white_y_range,
color="blue",
label="white",
marker=".")
ax[1].scatter(xrange,
black_y_range,
color="red",
label="black",
marker=".")
ax[0].set_title("white")
ax[0].set_xlabel("moves")
ax[0].set_ylabel("count")
ax[1].set_title("black")
ax[1].set_xlabel("moves")
ax[1].set_ylabel("count")
plt.show()
game = Checker(human=False, clear_screen=True, delay=0.1)
game.start()
game.save()