import numpy as np import itertools class Board: initial_state = ( 0, 2, 0, 0, 0, 0, -5, 0, -3, 0, 0, 0, 5, -5, 0, 0, 0, 3, 0, 5, 0, 0, 0, 0, -2, 0 ) # TODO: Remember to handle pushing other pieces to home # TODO: Also remember that a player can't move backwards and the one # player goes from 1-47 while the other goes from 47-1 # Remember to handle edge case when we're on the last moves and you may go # from position 22 -> 24 on a 6, if you have no pieces behind 22. Simply # check if any are behind if you're circle or if any are higher if you are # X, then it can be allowed. # Also, the check_move will also fail when you're attempting to leave a # bar. A fix of this is of course to check if the from_idx = bar and if so, # allow some extra stuff! @staticmethod def idxs_with_checkers_of_player(board, player): idxs = [] for idx, checker_count in enumerate(board): if checker_count * player >= 1: idxs.append(idx) return idxs # TODO: write tests # FIXME: make sure to disallow backwards movement # TODO: implement double roll feature (4 dice if dice are equal) # TODO: implement moving checkers into home (bearing off) # TODO: handle barring in a more elengant way # TODO: allow bearing off with non-exact die roll if this is only possible # move # TODO: Allow not doing anything if and only if no alternatives @staticmethod def is_move_valid(board, player, face_value, move): def sign(a): return (a > 0) - (a < 0) from_idx = move[0] to_idx = move[1] to_state = None from_state = board[from_idx] delta = to_idx - from_idx direction = sign(delta) bearing_off = None # FIXME: Use get instead of array-like indexing if to_idx >= 1 and to_idx <= 24: to_state = board[to_idx] bearing_off = False else: # Bearing off to_state = 0 bearing_off = True # print("_"*20) # print("board:", board) # print("to_idx:", to_idx, "board[to_idx]:", board[to_idx], "to_state:", to_state) # print("+"*20) def is_forward_move(): return direction == player def face_value_match_move_length(): return abs(delta) == face_value def bear_in_if_checker_on_bar(): if player == 1: bar = 0 else: bar = 25 bar_state = board[bar] if bar_state != 0: return from_idx == bar else: return True def checkers_at_from_idx(): return sign(from_state) == player def no_block_at_to_idx(): if -sign(to_state) == player: return abs(to_state) == 1 else: return True def can_bear_off(): checker_idxs = Board.idxs_with_checkers_of_player(board, player) def is_moving_backmost_checker(): if player == 1: return all([(idx >= from_idx) for idx in checker_idxs]) else: return all([(idx <= from_idx) for idx in checker_idxs]) def all_checkers_in_last_quadrant(): if player == 1: return all([(idx >= 19) for idx in checker_idxs]) else: return all([(idx >= 6) for idx in checker_idxs]) return all([ is_moving_backmost_checker(), all_checkers_in_last_quadrant() ]) # TODO # TODO: add switch here instead of wonky ternary in all return all([ is_forward_move(), face_value_match_move_length(), bear_in_if_checker_on_bar(), checkers_at_from_idx(), no_block_at_to_idx(), can_bear_off() if bearing_off else True ]) @staticmethod def any_move_valid(board, player, roll): for die in roll: idxs = Board.idxs_with_checkers_of_player(board, player) for idx in idxs: if Board.is_move_valid(board, player, die, (idx, idx + (die * player))): return True return False @staticmethod def num_of_checkers_for_player(board,player): return player * sum([board[idx] for idx in Board.idxs_with_checkers_of_player(board, player)]) @staticmethod def outcome(board): def all_checkers_in_first_quadrant(player): checker_idxs = Board.idxs_with_checkers_of_player(board, player) if player == 1: return all([(idx <= 6) for idx in checker_idxs]) else: return all([(idx >= 19) for idx in checker_idxs]) winner = None for player in [1, -1]: if Board.idxs_with_checkers_of_player(board, player) == []: winner = player if winner == None: return None #backgammon = all_checkers_in_first_quadrant(-winner) gammon = Board.num_of_checkers_for_player(board, -winner) == 15 score = 2 if gammon else 1 return {winner: score, -winner: -score} @staticmethod def calculate_legal_states(board, player, roll): # Find all pips with things on them belonging to the player # Iterate through each index and check if it's a possible move given the roll # If player is O, then check for idx + roll # If player is X, then check for idx - roll # TODO: make sure that it is not possible to do nothing on first part of # turn and then do something with the second die def calc_moves(board, face_value): idxs_with_checkers = Board.idxs_with_checkers_of_player(board, player) boards = [(Board.do_move(board, player, (idx, idx + (face_value * player))) if Board.is_move_valid(board, player, face_value, (idx, idx + (face_value * player))) else None) for idx in idxs_with_checkers] return list(filter(None, boards)) # Remove None-values # ------------------ # 1. Determine if dice have identical face value # 2. Iterate through remaining dice legal_moves = set() if not Board.any_move_valid(board, player, roll): return { board } dice_permutations = list(itertools.permutations(roll)) if roll[0] != roll[1] else [[roll[0]]*4] for roll in dice_permutations: # Calculate boards resulting from first move boards = calc_moves(board, roll[0]) for die in roll[1:]: # Calculate boards resulting from second move nested_boards = [calc_moves(board, die) for board in boards] boards = [board for boards in nested_boards for board in boards] # Add resulting unique boards to set of legal boards resulting from roll legal_moves = legal_moves | set(boards) return legal_moves @staticmethod def pretty(board): temp = [] for x in board: if x >= 0: temp.append(" {}".format(x)) else: temp.append("{}".format(x)) return """ 13 14 15 16 17 18 19 20 21 22 23 24 -------------------------------------------------------------------------- | {11}| {10}| {9}| {8}| {7}| {6}| bar -1: {24} | {5}| {4}| {3}| {2}| {1}| {0}| end -1: TODO| |---|---|---|---|---|---|-----------|---|---|---|---|---|---| | {12}| {13}| {14}| {15}| {16}| {17}| bar 1: {25} | {18}| {19}| {20}| {21}| {22}| {23}| end 1: TODO| -------------------------------------------------------------------------- 12 11 10 9 8 7 6 5 4 3 2 1 """.format(*temp) @staticmethod def do_move(board, player, move): # Implies that move is valid; make sure to check move validity before calling do_move(...) def move_to_bar(board, to_idx): board = list(board) if player == 1: board[0] += player else: board[25] += player board[to_idx] = 0 return tuple(board) # TODO: Moving in from bar is handled by the representation # TODONE: Handle bearing off from_idx = move[0] to_idx = move[1] board = list(board) # Make mutable copy of board # 'Lift' checker board[from_idx] -= player # Handle bearing off if to_idx < 1 or to_idx > 24: return board # Handle hitting checkers if board[to_idx] * player == -1: board = move_to_bar(board, to_idx) # Put down checker board[to_idx] += player return tuple(board)