From c4d3abaa539223f6d99ddbe944ec1a9a826fbd2b Mon Sep 17 00:00:00 2001 From: AdityaDaflapurkar Date: Sun, 1 Apr 2018 20:25:58 +0530 Subject: [PATCH 1/5] Resolve recursion issue in Backgammon class --- games.py | 52 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/games.py b/games.py index f129ecd1d..444bcc745 100644 --- a/games.py +++ b/games.py @@ -61,7 +61,9 @@ def min_value(state, dice_roll): return v def chance_node(state, action): + print(action, game.dice_roll) res_state = game.result(state, action) + game.display(res_state) if game.terminal_test(res_state): return game.utility(res_state, player) sum_chances = 0 @@ -79,6 +81,7 @@ def chance_node(state, action): return sum_chances / num_chances # Body of expectiminimax: + print(game.dice_roll) return argmax(game.actions(state), key=lambda a: chance_node(state, a)) @@ -246,6 +249,7 @@ def play_game(self, *players): state = self.initial while True: for player in players: + self.display(state) move = player(self, state) state = self.result(state, move) if self.terminal_test(state): @@ -399,17 +403,17 @@ def __init__(self): self.dice_roll = (-random.randint(1, 6), -random.randint(1, 6)) # TODO : Add bar to Board class where a blot is placed when it is hit. point = {'W':0, 'B':0} - self.board = [point.copy() for index in range(24)] - self.board[0]['B'] = self.board[23]['W'] = 2 - self.board[5]['W'] = self.board[18]['B'] = 5 - self.board[7]['W'] = self.board[16]['B'] = 3 - self.board[11]['B'] = self.board[12]['W'] = 5 + board = [point.copy() for index in range(24)] + board[0]['B'] = board[23]['W'] = 2 + board[5]['W'] = board[18]['B'] = 5 + board[7]['W'] = board[16]['B'] = 3 + board[11]['B'] = board[12]['W'] = 5 self.allow_bear_off = {'W': False, 'B': False} self.initial = GameState(to_move='W', utility=0, - board=self.board, - moves=self.get_all_moves(self.board, 'W')) + board=board, + moves=self.get_all_moves(board, 'W')) def actions(self, state): """Returns a list of legal moves for a state.""" @@ -420,16 +424,16 @@ def actions(self, state): legal_moves = [] for move in moves: board = copy.deepcopy(state.board) - if self.is_legal_move(move, self.dice_roll, player): + if self.is_legal_move(board, move, self.dice_roll, player): legal_moves.append(move) return legal_moves def result(self, state, move): board = copy.deepcopy(state.board) player = state.to_move - self.move_checker(move[0], self.dice_roll[0], player) + self.move_checker(board, move[0], self.dice_roll[0], player) if len(move) == 2: - self.move_checker(move[1], self.dice_roll[1], player) + self.move_checker(board, move[1], self.dice_roll[1], player) to_move = ('W' if player == 'B' else 'B') return GameState(to_move=to_move, utility=self.compute_utility(board, move, player), @@ -452,7 +456,7 @@ def get_all_moves(self, board, player): all_points = board taken_points = [index for index, point in enumerate(all_points) if point[player] > 0] - if self.checkers_at_home(player) == 1: + if self.checkers_at_home(board, player) == 1: return [(taken_points[0], )] moves = list(itertools.permutations(taken_points, 2)) moves = moves + [(index, index) for index, point in enumerate(all_points) @@ -480,15 +484,15 @@ def compute_utility(self, board, move, player): return -1 return 0 - def checkers_at_home(self, player): + def checkers_at_home(self, board, player): """Return the no. of checkers at home for a player.""" sum_range = range(0, 7) if player == 'W' else range(17, 24) count = 0 for idx in sum_range: - count = count + self.board[idx][player] + count = count + board[idx][player] return count - def is_legal_move(self, start, steps, player): + def is_legal_move(self, board, start, steps, player): """Move is a tuple which contains starting points of checkers to be moved during a player's turn. An on-board move is legal if both the destinations are open. A bear-off move is the one where a checker is moved off-board. @@ -497,31 +501,31 @@ def is_legal_move(self, start, steps, player): dest_range = range(0, 24) move1_legal = move2_legal = False if dest1 in dest_range: - if self.is_point_open(player, self.board[dest1]): - self.move_checker(start[0], steps[0], player) + if self.is_point_open(player, board[dest1]): + self.move_checker(board, start[0], steps[0], player) move1_legal = True else: if self.allow_bear_off[player]: - self.move_checker(start[0], steps[0], player) + self.move_checker(board, start[0], steps[0], player) move1_legal = True if not move1_legal: return False if dest2 in dest_range: - if self.is_point_open(player, self.board[dest2]): + if self.is_point_open(player, board[dest2]): move2_legal = True else: if self.allow_bear_off[player]: move2_legal = True return move1_legal and move2_legal - def move_checker(self, start, steps, player): + def move_checker(self, board, start, steps, player): """Move a checker from starting point by a given number of steps""" dest = start + steps dest_range = range(0, 24) - self.board[start][player] -= 1 + board[start][player] -= 1 if dest in dest_range: - self.board[dest][player] += 1 - if self.checkers_at_home(player) == 15: + board[dest][player] += 1 + if self.checkers_at_home(board, player) == 15: self.allow_bear_off[player] = True def is_point_open(self, player, point): @@ -530,3 +534,7 @@ def is_point_open(self, player, point): move a checker to a point only if it is open.""" opponent = 'B' if player == 'W' else 'W' return point[opponent] <= 1 + +if __name__ == "__main__": + bgm = Backgammon() + print(bgm.play_game(expectiminimax_player, query_player)) From b7632395e1f25cc479cd828ed08bd7feef7a4700 Mon Sep 17 00:00:00 2001 From: AdityaDaflapurkar Date: Sun, 1 Apr 2018 22:30:36 +0530 Subject: [PATCH 2/5] Handle empty action list in player functions --- games.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/games.py b/games.py index 444bcc745..d6cfb6284 100644 --- a/games.py +++ b/games.py @@ -83,7 +83,7 @@ def chance_node(state, action): # Body of expectiminimax: print(game.dice_roll) return argmax(game.actions(state), - key=lambda a: chance_node(state, a)) + key=lambda a: chance_node(state, a)) if len(game.actions(state)) > 0 else None def alphabeta_search(state, game): @@ -184,18 +184,21 @@ def query_player(game, state): game.display(state) print("available moves: {}".format(game.actions(state))) print("") - move_string = input('Your move? ') - try: - move = eval(move_string) - except NameError: - move = move_string + move = None + if len(game.actions(state)) > 0: + move_string = input('Your move? ') + try: + move = eval(move_string) + except NameError: + move = move_string + else: + print('No legal moves. Passing turn to next player.') return move def random_player(game, state): """A player that chooses a legal move at random.""" - return random.choice(game.actions(state)) - + return random.choice(game.actions(state)) if len(game.actions(state)) > 0 else None def alphabeta_player(game, state): return alphabeta_search(state, game) @@ -409,7 +412,6 @@ def __init__(self): board[7]['W'] = board[16]['B'] = 3 board[11]['B'] = board[12]['W'] = 5 self.allow_bear_off = {'W': False, 'B': False} - self.initial = GameState(to_move='W', utility=0, board=board, @@ -440,7 +442,6 @@ def result(self, state, move): board=board, moves=self.get_all_moves(board, to_move)) - def utility(self, state, player): """Return the value to player; 1 for win, -1 for loss, 0 otherwise.""" return state.utility if player == 'W' else -state.utility From a07ee02bbfc75d26d32c4b96d5fedb99acb87a46 Mon Sep 17 00:00:00 2001 From: AdityaDaflapurkar Date: Mon, 2 Apr 2018 00:16:28 +0530 Subject: [PATCH 3/5] Add play_game method for backgammon --- games.py | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/games.py b/games.py index d6cfb6284..0c4804dc3 100644 --- a/games.py +++ b/games.py @@ -61,9 +61,7 @@ def min_value(state, dice_roll): return v def chance_node(state, action): - print(action, game.dice_roll) res_state = game.result(state, action) - game.display(res_state) if game.terminal_test(res_state): return game.utility(res_state, player) sum_chances = 0 @@ -81,9 +79,8 @@ def chance_node(state, action): return sum_chances / num_chances # Body of expectiminimax: - print(game.dice_roll) return argmax(game.actions(state), - key=lambda a: chance_node(state, a)) if len(game.actions(state)) > 0 else None + key=lambda a: chance_node(state, a)) if game.actions(state) else None def alphabeta_search(state, game): @@ -185,20 +182,20 @@ def query_player(game, state): print("available moves: {}".format(game.actions(state))) print("") move = None - if len(game.actions(state)) > 0: + if game.actions(state): move_string = input('Your move? ') try: move = eval(move_string) except NameError: move = move_string else: - print('No legal moves. Passing turn to next player.') + print('no legal moves: passing turn to next player') return move def random_player(game, state): """A player that chooses a legal move at random.""" - return random.choice(game.actions(state)) if len(game.actions(state)) > 0 else None + return random.choice(game.actions(state)) if game.actions(state) else None def alphabeta_player(game, state): return alphabeta_search(state, game) @@ -252,7 +249,6 @@ def play_game(self, *players): state = self.initial while True: for player in players: - self.display(state) move = player(self, state) state = self.result(state, move) if self.terminal_test(state): @@ -413,12 +409,12 @@ def __init__(self): board[11]['B'] = board[12]['W'] = 5 self.allow_bear_off = {'W': False, 'B': False} self.initial = GameState(to_move='W', - utility=0, + utility=0, board=board, moves=self.get_all_moves(board, 'W')) def actions(self, state): - """Returns a list of legal moves for a state.""" + """Return a list of legal moves for a state.""" player = state.to_move moves = state.moves if len(moves) == 1 and len(moves[0]) == 1: @@ -468,11 +464,11 @@ def display(self, state): """Display state of the game.""" board = state.board player = state.to_move - print("Current State : ") + print("current state : ") for index, point in enumerate(board): if point['W'] != 0 or point['B'] != 0: - print("Point : ", index, " W : ", point['W'], " B : ", point['B']) - print("To play : ", player) + print("point : ", index, " W : ", point['W'], " B : ", point['B']) + print("to play : ", player) def compute_utility(self, board, move, player): """If 'W' wins with this move, return 1; if 'B' wins return -1; else return 0.""" @@ -536,6 +532,20 @@ def is_point_open(self, player, point): opponent = 'B' if player == 'W' else 'W' return point[opponent] <= 1 -if __name__ == "__main__": - bgm = Backgammon() - print(bgm.play_game(expectiminimax_player, query_player)) + def play_game(self, *players): + """Play backgammon.""" + state = self.initial + while True: + for player in players: + saved_dice_roll = self.dice_roll + move = player(self, state) + self.dice_roll = saved_dice_roll + if move is not None: + state = self.result(state, move) + if state.to_move == 'W': + self.dice_roll = (-random.randint(1, 6), -random.randint(1, 6)) + else: + self.dice_roll = (random.randint(1, 6), random.randint(1, 6)) + if self.terminal_test(state): + self.display(state) + return self.utility(state, self.to_move(self.initial)) From b7fe9d01ca3b5c4c4e1eec8505d4c87bb8058162 Mon Sep 17 00:00:00 2001 From: AdityaDaflapurkar Date: Thu, 5 Apr 2018 01:33:25 +0530 Subject: [PATCH 4/5] Refactor functions --- games.py | 47 +++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/games.py b/games.py index 0c4804dc3..275830a6c 100644 --- a/games.py +++ b/games.py @@ -41,6 +41,9 @@ def min_value(state): # ______________________________________________________________________________ +dice_rolls = list(itertools.combinations_with_replacement([1, 2, 3, 4, 5, 6], 2)) +direction = {'W' : -1, 'B' : 1} + def expectiminimax(state, game): """Return the best move for a player after dice are thrown. The game tree includes chance nodes along with min and max nodes. [Figure 5.11]""" @@ -66,16 +69,14 @@ def chance_node(state, action): return game.utility(res_state, player) sum_chances = 0 num_chances = 21 - dice_rolls = list(itertools.combinations_with_replacement([1, 2, 3, 4, 5, 6], 2)) - if res_state.to_move == 'W': - for val in dice_rolls: - game.dice_roll = (-val[0], -val[1]) - sum_chances += max_value(res_state, - (-val[0], -val[1])) * (1/36 if val[0] == val[1] else 1/18) - elif res_state.to_move == 'B': - for val in dice_rolls: - game.dice_roll = val - sum_chances += min_value(res_state, val) * (1/36 if val[0] == val[1] else 1/18) + for val in dice_rolls: + game.dice_roll = tuple(map((direction[res_state.to_move]).__mul__, val)) + util = 0 + if res_state.to_move == player: + util = max_value(res_state, game.dice_roll) + else: + util = min_value(res_state, game.dice_roll) + sum_chances += util * (1/36 if val[0] == val[1] else 1/18) return sum_chances / num_chances # Body of expectiminimax: @@ -399,15 +400,15 @@ class Backgammon(Game): def __init__(self): """Initial state of the game""" - self.dice_roll = (-random.randint(1, 6), -random.randint(1, 6)) + self.dice_roll = tuple(map((direction['W']).__mul__, random.choice(dice_rolls))) # TODO : Add bar to Board class where a blot is placed when it is hit. - point = {'W':0, 'B':0} + point = {'W' : 0, 'B' : 0} board = [point.copy() for index in range(24)] board[0]['B'] = board[23]['W'] = 2 board[5]['W'] = board[18]['B'] = 5 board[7]['W'] = board[16]['B'] = 3 board[11]['B'] = board[12]['W'] = 5 - self.allow_bear_off = {'W': False, 'B': False} + self.allow_bear_off = {'W' : False, 'B' : False} self.initial = GameState(to_move='W', utility=0, board=board, @@ -466,20 +467,16 @@ def display(self, state): player = state.to_move print("current state : ") for index, point in enumerate(board): - if point['W'] != 0 or point['B'] != 0: - print("point : ", index, " W : ", point['W'], " B : ", point['B']) + print("point : ", index, " W : ", point['W'], " B : ", point['B']) print("to play : ", player) def compute_utility(self, board, move, player): """If 'W' wins with this move, return 1; if 'B' wins return -1; else return 0.""" - count = 0 + util = {'W' : 1, 'B' : '-1'} for idx in range(0, 24): - count = count + board[idx][player] - if player == 'W' and count == 0: - return 1 - if player == 'B' and count == 0: - return -1 - return 0 + if board[idx][player] > 0: + return 0 + return util[player] def checkers_at_home(self, board, player): """Return the no. of checkers at home for a player.""" @@ -542,10 +539,8 @@ def play_game(self, *players): self.dice_roll = saved_dice_roll if move is not None: state = self.result(state, move) - if state.to_move == 'W': - self.dice_roll = (-random.randint(1, 6), -random.randint(1, 6)) - else: - self.dice_roll = (random.randint(1, 6), random.randint(1, 6)) + self.dice_roll = tuple(map((direction[player]).__mul__, + random.choice(dice_rolls))) if self.terminal_test(state): self.display(state) return self.utility(state, self.to_move(self.initial)) From a4cb9433f1fd241ea72b6be88097b0591b4b9838 Mon Sep 17 00:00:00 2001 From: AdityaDaflapurkar Date: Fri, 6 Apr 2018 20:01:19 +0530 Subject: [PATCH 5/5] Update argmax function call --- games.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/games.py b/games.py index 275830a6c..23e785bab 100644 --- a/games.py +++ b/games.py @@ -81,7 +81,7 @@ def chance_node(state, action): # Body of expectiminimax: return argmax(game.actions(state), - key=lambda a: chance_node(state, a)) if game.actions(state) else None + key=lambda a: chance_node(state, a), default=None) def alphabeta_search(state, game):