diff --git a/games.py b/games.py index f129ecd1d..23e785bab 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,21 +69,19 @@ 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: return argmax(game.actions(state), - key=lambda a: chance_node(state, a)) + key=lambda a: chance_node(state, a), default=None) def alphabeta_search(state, game): @@ -181,18 +182,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 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') 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 game.actions(state) else None def alphabeta_player(game, state): return alphabeta_search(state, game) @@ -396,23 +400,22 @@ 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} - 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 - self.allow_bear_off = {'W': False, 'B': False} - + 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.initial = GameState(to_move='W', - utility=0, - board=self.board, - moves=self.get_all_moves(self.board, 'W')) + 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: @@ -420,23 +423,22 @@ 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), 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 @@ -452,7 +454,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) @@ -463,32 +465,28 @@ 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.""" - 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, 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 +495,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 +528,19 @@ 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 + + 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) + 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))