-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathOskaBoard.py
623 lines (483 loc) · 21.3 KB
/
OskaBoard.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
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
from copy import deepcopy
# Set of classes for throwing errors in board intake functions.
class Error(Exception):
pass
class InvalidRowLength(Error):
pass
class InvalidInput(Error):
pass
class InvalidPiece(Error):
pass
class TooManyPiece(Error):
pass
##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
# Object for an Oska board. Contains all relevent board manipulation and move generation functions.
class OskaBoard:
# Board Constructor. Takes in a board as list of strings and converts it into a board object.
#
# Inputs: list of strings, player colour character
#
# Return: None
def __init__(self, inList, playerTurn):
# Assign immediate variables for object
self.totalRows = len(inList)
self.maxRowLength = len(inList[0])
pastCenter = 1
if playerTurn == 'W' or playerTurn == 'w':
self.playerTurn = 'w'
elif playerTurn == 'B' or playerTurn == 'b':
self.playerTurn = 'b'
self.board = []
# Attempt to convert list of strings into a board object.
# If it fails or the list of strings is not a valid board, throw an error.
try:
# Ensure that board length is 2n - 3 where n is length of first row
if self.totalRows == 2 * self.maxRowLength - 3:
# Intake strings one at a time, each being a single row. If length is not expected length, throw error.
rowNum = 0
for row in inList:
if rowNum <= (self.totalRows - 1) / 2:
if len(inList[rowNum]) == self.maxRowLength - rowNum:
self.board += [row]
else:
raise InvalidRowLength('First Half', rowNum, inList[rowNum])
else:
if len(inList[rowNum]) == 2 + pastCenter:
pastCenter += 1
self.board += [row]
else:
raise InvalidRowLength('Second Half', rowNum, inList[rowNum])
rowNum += 1
else:
raise InvalidInput(inList)
#If board was valid structure, check for all pieces
self.findPieces()
except InvalidRowLength as inst:
print('Invalid row length: ', inst.args)
except InvalidInput as inst:
print('Invalid input: ', inst.args)
# Function to find pieces and gather them into member variable lists.
# If too many pieces for a colour are found or an invalid character is found, throws error.
#
# Inputs: None
#
# Return: None
def findPieces(self):
try:
self.wPieces = []
self.bPieces = []
wCount = 0
bCount = 0
# Check each character in each row. If character is 'w' or 'b', add it's coordinates to its corresponding list.
# If character is '-', ignore it. If character is none of the above, throw and error.
for row in range(len(self.board)):
for col in range(len(self.board[row])):
char = self.board[row][col]
if char == 'W' or char == 'w':
self.wPieces += [(row, col)]
wCount += 1
elif char == 'B' or char == 'b':
self.bPieces += [(row, col)]
bCount += 1
elif char != '-':
raise InvalidPiece(char, (row, col))
# If more pieces of a single colour were found than there were spaces in the first row, throw an error.
if wCount > self.maxRowLength:
raise TooManyPiece('W', self.wPieces)
elif bCount > self.maxRowLength:
raise TooManyPiece('B', self.bPieces)
except InvalidPiece as inst:
print('Invalid Piece: ', inst.args)
except TooManyPiece as inst:
print('Too many', inst.args[0], 'pieces')
# Straightforward board printing function. For each row, print it. Then print player's turn.
#
# Inputs: None
#
# Return: None
def printBoard(self):
for i in range(len(self.board)):
print('Row #', i, ':', self.board[i])
print('Player turn: ', self.playerTurn)
# Function for replace characters in a board's character array.
#
# Copies old string, but replaces old character with new character. Reassigns old row with newly created row string.
#
# Inputs: row and column replacement is occuring at, character that is replacing
#
# Return: None
def replacechar(self, row, col, char):
newStr = ''
for i in range(len(self.board[row])):
if i == col:
newStr += char
else:
newStr += self.board[row][i]
self.board[row] = newStr
# Prep a selected piece to move in a chosen direction. Used for manual play either against another human or against AI.
# Not error handled, assumes valid inputs. Preps similar to prep_move_data, then sends data to make_move to actually process move.
#
# Inputs: piece row, piece column, desired direction (l/r)
#
# Return: make_move function, which returns a new OskaBoard.
def movepiece(self, currRow, currCol, direction):
nextCol = nextRow = jumpRow = jump = oppTurn = index= None
if self.playerTurn == 'w':
oppTurn = 'b'
else:
oppTurn = 'w'
if self.playerTurn == 'w':
index = self.wPieces.index((currRow, currCol))
nextRow = currRow + 1
jumpRow = nextRow + 1
if currRow < (self.totalRows - 1) / 2:
if direction == 'l' or direction == 'L':
nextCol = currCol - 1
if jumpRow <= (self.totalRows - 1) / 2:
jump = -1
else:
jump = 0
else:
nextCol = currCol
if jumpRow <= (self.totalRows - 1) / 2:
jump = 0
else:
jump = 1
else:
if direction == 'l' or direction == 'L':
nextCol = currCol
if jumpRow <= (self.totalRows - 1) / 2:
jump = -1
else:
jump = 0
else:
nextCol = currCol + 1
if jumpRow <= (self.totalRows - 1) / 2:
jump = 0
else:
jump = 1
else:
index = self.bPieces.index(((currRow, currCol)))
nextRow = currRow - 1
jumpRow = nextRow - 1
if currRow > (self.totalRows - 1) / 2:
if direction == 'l' or direction == 'L':
nextCol = currCol - 1
if jumpRow >= (self.totalRows - 1) / 2:
jump = -1
else:
jump = 0
else:
nextCol = currCol
if jumpRow >= (self.totalRows - 1) / 2:
jump = 0
else:
jump = 1
else:
if direction == 'l' or direction == 'L':
nextCol = currCol
if jumpRow >= (self.totalRows - 1) / 2:
jump = -1
else:
jump = 0
else:
nextCol = currCol + 1
if jumpRow >= (self.totalRows - 1) / 2:
jump = 0
else:
jump = 1
#Call make_move function, passing necessary modifiers
return self.make_move(index, oppTurn, currRow, nextRow, jumpRow, currCol, ((nextCol, jump),))
# Generate children of board
#
# Inputs: None
#
# Return: List of all boards that can be reached in one move
def generatechildren(self):
# Declare list to hold new boards
newBoards = []
# For whichever player's turn it is, attempt to move each piece in their piece list
if self.playerTurn == 'w':
for piece in self.wPieces:
index = self.wPieces.index(piece)
newBoards += self.prep_move_data(piece, index, self.playerTurn, self.make_move)
else:
for piece in self.bPieces:
index = self.bPieces.index(piece)
newBoards += self.prep_move_data(piece, index, self.playerTurn, self.make_move)
# Return any boards generated
if newBoards != []:
return newBoards
else:
thisBoard = deepcopy(self)
if self.playerTurn == 'w':
thisBoard.playerTurn = 'b'
else:
thisBoard.playerTurn = 'w'
return [thisBoard]
# Preps relevant data for use by cb function. In essence, finds the positions ahead of a given piece and passes them to callback.
#
# Inputs: piece object (tuple of row, col) to be moved, index of piece to be moved, player turn character, callback function
#
# Return: calls cb passing move data
def prep_move_data(self, piece, index, playerTurn, cb):
#Gather data from piece object into named variables
currRow = piece[0]
currCol = piece[1]
#Declare pointers which will need to pull values out of future conditional statements
oppTurn = nextRow = jumpRow = None
#Prep default values for modifiers
leftCol = currCol
rightCol = currCol + 1
leftJump = 0
rightJump = 1
#Set a variable to hold the name of the opponent
if playerTurn == 'w':
oppTurn = 'b'
else:
oppTurn = 'w'
#Based on whose turn it is, prepare any of the relevant modifiers that have not been prepared.
if playerTurn == 'w':
nextRow = currRow + 1
jumpRow = nextRow + 1
if currRow < (self.totalRows - 1) / 2:
leftCol = currCol - 1
rightCol = currCol
if jumpRow <= (self.totalRows - 1) / 2:
leftJump = -1
rightJump = 0
else:
nextRow = currRow - 1
jumpRow = nextRow - 1
if currRow > (self.totalRows - 1) / 2:
leftCol = currCol - 1
rightCol = currCol
if jumpRow >= (self.totalRows - 1) / 2:
leftJump = -1
rightJump = 0
return cb(index, oppTurn, currRow, nextRow, jumpRow, currCol, ((leftCol, leftJump), (rightCol, rightJump)))
# Carries out the possible moves for a given piece, based on prep it is passed from prep_move_data
#
# Inputs: piece index, opponent color, row of piece, row piece could move to, row piece could jump to, current column of piece,
# possible columps piece could move to as nested tuples <<((leftCol, leftJump), (rightCol, rightJump))>>
#
# Return: List of boards that can be reached by moving a specific piece
def make_move(self, index, oppTurn, currRow, nextRow, jumpRow, currCol, nextCols):
# Declare empty list to hold boards
newBoards = []
# Attempt a move in each direction, left and right
for nextCol in nextCols:
# Ensure that both currRow and nextRow are within bounds of the board
if currRow < self.totalRows and currRow >= 0 and nextRow < self.totalRows and nextRow >= 0 and nextCol[0] >= 0 and nextCol[0] < len(self.board[nextRow]):
# If observed space is empty, move piece there
if self.board[nextRow][nextCol[0]] == '-':
board = deepcopy(self)
# Call move function, passing where the piece is coming from and where it is moving to
board.generate_move(index, (currRow, currCol), (nextRow, nextCol[0]))
board.playerTurn = oppTurn
newBoards += [board]
# If observed space is not empty, but is occupied by an opponents piece, attempt to jump it.
elif self.board[nextRow][nextCol[0]] == oppTurn:
# Check to see if jump target space is empty. If it is, make jump.
jumpCol = nextCol[0] + nextCol[1]
if jumpRow < self.totalRows and jumpRow >= 0 and jumpCol >= 0 and jumpCol < len(self.board[jumpRow]) and self.board[jumpRow][jumpCol] == '-':
board = deepcopy(self)
# Call jumping function, passing where the piece is coming from, which piece it is jumping, and where it is landing
board.generate_jump(index, (currRow, currCol), (nextRow, nextCol[0]), (jumpRow, jumpCol))
board.playerTurn = oppTurn
newBoards += [board]
# Return any generated boards
return newBoards
# Performs a move that is not a jump
#
# Inputs: index of piece moving, space piece is moving from <<fromSpace = (currRow, currCol)>>,
# space piece is moving to <<toSpace = (nextRow, nextCol)>>
#
# Return: None
def generate_move(self, index, fromSpace, toSpace):
# Reassign moving piece to new location in piece list
if self.playerTurn == 'w':
self.wPieces[index] = (toSpace[0], toSpace[1])
else:
self.bPieces[index] = (toSpace[0], toSpace[1])
# Update characters on the character list to reflect move
self.replacechar(fromSpace[0], fromSpace[1], '-')
self.replacechar(toSpace[0], toSpace[1], self.playerTurn)
# Perform a jump
#
# Inputs: player name, index of piece jumping, space piece is jumping from <<fromSpace = (currRow, currCol)>>,
# space piece is jumping over <<nextSpace = (nextRow, nextCol)>>, space piece is jumping to <<jumpSpace = (jumpRow, jumpCol)>>
#
# Return: None
def generate_jump(self, index, fromSpace, nextSpace, jumpSpace):
# Reassign moving piece to new location in piece list
# Remove jumped piece from piece list
if self.playerTurn == 'w':
self.bPieces.remove((nextSpace[0], nextSpace[1]))
self.wPieces[index] = (jumpSpace[0], jumpSpace[1])
else:
self.wPieces.remove((nextSpace[0], nextSpace[1]))
self.bPieces[index] = (jumpSpace[0], jumpSpace[1])
# Update characters on the character list to reflect move
self.replacechar(nextSpace[0], nextSpace[1], '-')
self.replacechar(fromSpace[0], fromSpace[1], '-')
self.replacechar(jumpSpace[0], jumpSpace[1], self.playerTurn)
# Calculates minimax value of a board.
#
# Inputs: None
#
# Return: Minimax value
def evaluate_board(self):
if self.wWin():
return 1000
elif self.bWin():
return -1000
wNum = len(self.wPieces)
bNum = len(self.bPieces)
val = (wNum - bNum)
#print(val)
wDist = 0
for wpiece in self.wPieces:
wDist += self.totalRows - wpiece[0] - 1
bDist = 0
for bpiece in self.bPieces:
bDist += bpiece[0]
if wDist < bDist:
val += 5
elif wDist > bDist:
val += -5
for piece in self.wPieces:
index = self.wPieces.index(piece)
moves = self.prep_move_data(piece, index, 'w', self.can_move)
jumps = self.prep_move_data(piece, index, 'w', self.can_jump)
if moves > 0:
val += 1
val += jumps
if piece[0] > (self.totalRows + 1) / 2:
val += 1
for piece in self.bPieces:
index = self.bPieces.index(piece)
moves = self.prep_move_data(piece, index, 'b', self.can_move)
jumps = self.prep_move_data(piece, index, 'b', self.can_jump)
if moves > 0:
val += -1
val += -jumps
if piece[0] < (self.totalRows + 1) / 2:
val += -1
wInGoal = False
bInGoal = False
wGoalCount = 0
bGoalCount = 0
for char in self.board[0]:
if char == 'b':
bInGoal = True
bGoalCount += 1
for char in self.board[self.totalRows - 1]:
if char == 'w':
wInGoal = True
wGoalCount += 1
if wInGoal == True:
val += 2 * self.maxRowLength
val += (len(self.wPieces) - wGoalCount) * (-2)
if bInGoal == True:
val += -2 * self.maxRowLength
val += (len(self.bPieces) - bGoalCount) * 2
return val
# Check to see how many available non-jump moves there are for a given piece. Utilizes prep from prep_move_data.
#
# Inputs: Row ahead of piece, columns ahead of piece.
#
# Return: Number of available non-jump moves
def can_move(self, index, oppTurn, currRow, nextRow, jumpRow, currCol, nextCols):
#self, nextRow, nextCols):
openMoves = 0
for colData in nextCols:
col = colData[0]
if nextRow < self.totalRows and nextRow >= 0 and col >= 0 and col < len(self.board[nextRow]) and self.board[nextRow][col] == '-':
openMoves += 1
return openMoves
# Check to see how many available jump moves there are for a given piece. Utilizes prep from prep_move_data.
#
# Inputs: Opponent character, Row ahead of piece, row piece would jump to, current column, (columns ahead of piece, columns piece would jump to).
#
# Return: Number of available jumps
def can_jump(self, index, oppTurn, currRow, nextRow, jumpRow, currCol, nextCols):
#self, oppTurn, nextRow, jumpRow, currCol, nextCols):
availableJumps = 0
for col in nextCols:
nextCol = col[0]
jumpCol = nextCol + col[1]
#print('nextRow:',nextRow)
#print('jumpRow:',jumpRow)
#print('nextCol:',nextCol)
#print('jumpCol:',jumpCol)
if nextRow < self.totalRows and jumpRow < self.totalRows and nextRow >= 0 and jumpRow >= 0 and nextCol >= 0 and jumpCol >= 0 and nextCol < len(self.board[nextRow]) and jumpCol < len(self.board[jumpRow]) and self.board[nextRow][nextCol] == oppTurn and self.board[jumpRow][jumpCol] == '-':
availableJumps += 1
return availableJumps
# Checks to see if white has a win. If so, returns True.
#
# Inputs: None
#
# Return: True if white has won
def wWin(self):
if len(self.bPieces) == 0:
return True
count = 0
for char in self.board[self.totalRows - 1]:
if char == 'w':
count += 1
if count != 0 and count == len(self.wPieces):
return True
# Checks to see if black has a win. If so, returns True.
#
# Inputs: None
#
# Return: True if black has won
def bWin(self):
if len(self.wPieces) == 0:
return True
count = 0
for char in self.board[0]:
if char == 'b':
count += 1
if count != 0 and count == len(self.bPieces):
return True
# Checks to see if either player has a win. If so, returns True.
#
# Inputs: None
#
# Return: True if game is over
def gameover(self):
if len(self.wPieces) == 0:
return True
if len(self.bPieces) == 0:
return True
if self.playerTurn == 'w' and self.bWin() == True:
return True
elif self.playerTurn == 'b' and self.wWin() == True:
return True
# Functions for future implementations#
#def is_safe(self, index, piece):
# isSafe = True
# nextPieces = self.prep_move_data(index, piece, self.playerTurn, self.find_pieces_ahead)
# leftPiece = nextPieces[0]
# rightPiece = nextPieces[1]
# if leftPiece != None:
# if self.can_jump(index, oppTurn, nextRow, currRow, currRow + currRow - nextRow, leftPiece[1], piece[1]):
# isSafe = False
#
# if rightPiece != None:
# if self.can_jump(index, oppTurn, nextRow, currRow, currRow + currRow - nextRow, rightPiece[1], piece[1]):
# isSafe = False
# return isSafe
#def find_pieces_ahead(self, index, oppTurn, currRow, nextRow, jumpRow, currCol, nextCols):
# leftCol = nextCols[0][0]
# rightCol = nextCols[1][0]
# leftPiece = rightPiece = None
# if nextRow >= 0 and nextRow < self.totalRows:
# if leftCol >= 0 and leftCol < len(self.board[nextRow]) and self.board[nextRow][leftCol] == oppTurn:
# leftPiece = (nextRow, leftCol)
#
# if rightCol >= 0 and rightCol < len(self.board[nextRow]) and self.board[nextRow][rightCol] == oppTurn:
# rightPiece = (nextRow, rightCol)
# return (leftPiece, rightPiece)