Skip to content

Commit 769fb74

Browse files
AdityaDaflapurkarnorvig
authored andcommitted
Add play_game function and fix backgammon game play issues (#904)
* Resolve recursion issue in Backgammon class * Handle empty action list in player functions * Add play_game method for backgammon * Refactor functions * Update argmax function call
1 parent 5dee9c1 commit 769fb74

File tree

1 file changed

+72
-58
lines changed

1 file changed

+72
-58
lines changed

games.py

Lines changed: 72 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ def min_value(state):
4141

4242
# ______________________________________________________________________________
4343

44+
dice_rolls = list(itertools.combinations_with_replacement([1, 2, 3, 4, 5, 6], 2))
45+
direction = {'W' : -1, 'B' : 1}
46+
4447
def expectiminimax(state, game):
4548
"""Return the best move for a player after dice are thrown. The game tree
4649
includes chance nodes along with min and max nodes. [Figure 5.11]"""
@@ -66,21 +69,19 @@ def chance_node(state, action):
6669
return game.utility(res_state, player)
6770
sum_chances = 0
6871
num_chances = 21
69-
dice_rolls = list(itertools.combinations_with_replacement([1, 2, 3, 4, 5, 6], 2))
70-
if res_state.to_move == 'W':
71-
for val in dice_rolls:
72-
game.dice_roll = (-val[0], -val[1])
73-
sum_chances += max_value(res_state,
74-
(-val[0], -val[1])) * (1/36 if val[0] == val[1] else 1/18)
75-
elif res_state.to_move == 'B':
76-
for val in dice_rolls:
77-
game.dice_roll = val
78-
sum_chances += min_value(res_state, val) * (1/36 if val[0] == val[1] else 1/18)
72+
for val in dice_rolls:
73+
game.dice_roll = tuple(map((direction[res_state.to_move]).__mul__, val))
74+
util = 0
75+
if res_state.to_move == player:
76+
util = max_value(res_state, game.dice_roll)
77+
else:
78+
util = min_value(res_state, game.dice_roll)
79+
sum_chances += util * (1/36 if val[0] == val[1] else 1/18)
7980
return sum_chances / num_chances
8081

8182
# Body of expectiminimax:
8283
return argmax(game.actions(state),
83-
key=lambda a: chance_node(state, a))
84+
key=lambda a: chance_node(state, a), default=None)
8485

8586

8687
def alphabeta_search(state, game):
@@ -181,18 +182,21 @@ def query_player(game, state):
181182
game.display(state)
182183
print("available moves: {}".format(game.actions(state)))
183184
print("")
184-
move_string = input('Your move? ')
185-
try:
186-
move = eval(move_string)
187-
except NameError:
188-
move = move_string
185+
move = None
186+
if game.actions(state):
187+
move_string = input('Your move? ')
188+
try:
189+
move = eval(move_string)
190+
except NameError:
191+
move = move_string
192+
else:
193+
print('no legal moves: passing turn to next player')
189194
return move
190195

191196

192197
def random_player(game, state):
193198
"""A player that chooses a legal move at random."""
194-
return random.choice(game.actions(state))
195-
199+
return random.choice(game.actions(state)) if game.actions(state) else None
196200

197201
def alphabeta_player(game, state):
198202
return alphabeta_search(state, game)
@@ -396,47 +400,45 @@ class Backgammon(Game):
396400

397401
def __init__(self):
398402
"""Initial state of the game"""
399-
self.dice_roll = (-random.randint(1, 6), -random.randint(1, 6))
403+
self.dice_roll = tuple(map((direction['W']).__mul__, random.choice(dice_rolls)))
400404
# TODO : Add bar to Board class where a blot is placed when it is hit.
401-
point = {'W':0, 'B':0}
402-
self.board = [point.copy() for index in range(24)]
403-
self.board[0]['B'] = self.board[23]['W'] = 2
404-
self.board[5]['W'] = self.board[18]['B'] = 5
405-
self.board[7]['W'] = self.board[16]['B'] = 3
406-
self.board[11]['B'] = self.board[12]['W'] = 5
407-
self.allow_bear_off = {'W': False, 'B': False}
408-
405+
point = {'W' : 0, 'B' : 0}
406+
board = [point.copy() for index in range(24)]
407+
board[0]['B'] = board[23]['W'] = 2
408+
board[5]['W'] = board[18]['B'] = 5
409+
board[7]['W'] = board[16]['B'] = 3
410+
board[11]['B'] = board[12]['W'] = 5
411+
self.allow_bear_off = {'W' : False, 'B' : False}
409412
self.initial = GameState(to_move='W',
410-
utility=0,
411-
board=self.board,
412-
moves=self.get_all_moves(self.board, 'W'))
413+
utility=0,
414+
board=board,
415+
moves=self.get_all_moves(board, 'W'))
413416

414417
def actions(self, state):
415-
"""Returns a list of legal moves for a state."""
418+
"""Return a list of legal moves for a state."""
416419
player = state.to_move
417420
moves = state.moves
418421
if len(moves) == 1 and len(moves[0]) == 1:
419422
return moves
420423
legal_moves = []
421424
for move in moves:
422425
board = copy.deepcopy(state.board)
423-
if self.is_legal_move(move, self.dice_roll, player):
426+
if self.is_legal_move(board, move, self.dice_roll, player):
424427
legal_moves.append(move)
425428
return legal_moves
426429

427430
def result(self, state, move):
428431
board = copy.deepcopy(state.board)
429432
player = state.to_move
430-
self.move_checker(move[0], self.dice_roll[0], player)
433+
self.move_checker(board, move[0], self.dice_roll[0], player)
431434
if len(move) == 2:
432-
self.move_checker(move[1], self.dice_roll[1], player)
435+
self.move_checker(board, move[1], self.dice_roll[1], player)
433436
to_move = ('W' if player == 'B' else 'B')
434437
return GameState(to_move=to_move,
435438
utility=self.compute_utility(board, move, player),
436439
board=board,
437440
moves=self.get_all_moves(board, to_move))
438441

439-
440442
def utility(self, state, player):
441443
"""Return the value to player; 1 for win, -1 for loss, 0 otherwise."""
442444
return state.utility if player == 'W' else -state.utility
@@ -452,7 +454,7 @@ def get_all_moves(self, board, player):
452454
all_points = board
453455
taken_points = [index for index, point in enumerate(all_points)
454456
if point[player] > 0]
455-
if self.checkers_at_home(player) == 1:
457+
if self.checkers_at_home(board, player) == 1:
456458
return [(taken_points[0], )]
457459
moves = list(itertools.permutations(taken_points, 2))
458460
moves = moves + [(index, index) for index, point in enumerate(all_points)
@@ -463,32 +465,28 @@ def display(self, state):
463465
"""Display state of the game."""
464466
board = state.board
465467
player = state.to_move
466-
print("Current State : ")
468+
print("current state : ")
467469
for index, point in enumerate(board):
468-
if point['W'] != 0 or point['B'] != 0:
469-
print("Point : ", index, " W : ", point['W'], " B : ", point['B'])
470-
print("To play : ", player)
470+
print("point : ", index, " W : ", point['W'], " B : ", point['B'])
471+
print("to play : ", player)
471472

472473
def compute_utility(self, board, move, player):
473474
"""If 'W' wins with this move, return 1; if 'B' wins return -1; else return 0."""
474-
count = 0
475+
util = {'W' : 1, 'B' : '-1'}
475476
for idx in range(0, 24):
476-
count = count + board[idx][player]
477-
if player == 'W' and count == 0:
478-
return 1
479-
if player == 'B' and count == 0:
480-
return -1
481-
return 0
477+
if board[idx][player] > 0:
478+
return 0
479+
return util[player]
482480

483-
def checkers_at_home(self, player):
481+
def checkers_at_home(self, board, player):
484482
"""Return the no. of checkers at home for a player."""
485483
sum_range = range(0, 7) if player == 'W' else range(17, 24)
486484
count = 0
487485
for idx in sum_range:
488-
count = count + self.board[idx][player]
486+
count = count + board[idx][player]
489487
return count
490488

491-
def is_legal_move(self, start, steps, player):
489+
def is_legal_move(self, board, start, steps, player):
492490
"""Move is a tuple which contains starting points of checkers to be
493491
moved during a player's turn. An on-board move is legal if both the destinations
494492
are open. A bear-off move is the one where a checker is moved off-board.
@@ -497,31 +495,31 @@ def is_legal_move(self, start, steps, player):
497495
dest_range = range(0, 24)
498496
move1_legal = move2_legal = False
499497
if dest1 in dest_range:
500-
if self.is_point_open(player, self.board[dest1]):
501-
self.move_checker(start[0], steps[0], player)
498+
if self.is_point_open(player, board[dest1]):
499+
self.move_checker(board, start[0], steps[0], player)
502500
move1_legal = True
503501
else:
504502
if self.allow_bear_off[player]:
505-
self.move_checker(start[0], steps[0], player)
503+
self.move_checker(board, start[0], steps[0], player)
506504
move1_legal = True
507505
if not move1_legal:
508506
return False
509507
if dest2 in dest_range:
510-
if self.is_point_open(player, self.board[dest2]):
508+
if self.is_point_open(player, board[dest2]):
511509
move2_legal = True
512510
else:
513511
if self.allow_bear_off[player]:
514512
move2_legal = True
515513
return move1_legal and move2_legal
516514

517-
def move_checker(self, start, steps, player):
515+
def move_checker(self, board, start, steps, player):
518516
"""Move a checker from starting point by a given number of steps"""
519517
dest = start + steps
520518
dest_range = range(0, 24)
521-
self.board[start][player] -= 1
519+
board[start][player] -= 1
522520
if dest in dest_range:
523-
self.board[dest][player] += 1
524-
if self.checkers_at_home(player) == 15:
521+
board[dest][player] += 1
522+
if self.checkers_at_home(board, player) == 15:
525523
self.allow_bear_off[player] = True
526524

527525
def is_point_open(self, player, point):
@@ -530,3 +528,19 @@ def is_point_open(self, player, point):
530528
move a checker to a point only if it is open."""
531529
opponent = 'B' if player == 'W' else 'W'
532530
return point[opponent] <= 1
531+
532+
def play_game(self, *players):
533+
"""Play backgammon."""
534+
state = self.initial
535+
while True:
536+
for player in players:
537+
saved_dice_roll = self.dice_roll
538+
move = player(self, state)
539+
self.dice_roll = saved_dice_roll
540+
if move is not None:
541+
state = self.result(state, move)
542+
self.dice_roll = tuple(map((direction[player]).__mul__,
543+
random.choice(dice_rolls)))
544+
if self.terminal_test(state):
545+
self.display(state)
546+
return self.utility(state, self.to_move(self.initial))

0 commit comments

Comments
 (0)