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 ) @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 a test for this # TODO: Make sure that the bars fit, 0 represents the -1 player and 25 represents the 1 player # index 26 is player 1 home, index 27 is player -1 home @staticmethod def board_features_to_pubeval(board, player): if player == -1: board = Board.flip(board) board = list(board) positives = [x if x > 0 else 0 for x in board] negatives = [x if x < 0 else 0 for x in board] board.append(15 - sum(positives)) board.append(-15 - sum(negatives)) return board @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: 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 apply_moves_to_board(board, player, moves): for move in moves: from_idx, to_idx = move.split("/") board[int(from_idx)] -= int(player) board[int(to_idx)] += int(player) return board @staticmethod def calculate_legal_states(board, player, roll): # Find all points with checkers on them belonging to the player # Iterate through each index and check if it's a possible move given the 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) if len(idxs_with_checkers) == 0: return [board] 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] board_list = list(filter(None, boards)) # Remove None-values # if len(board_list) == 0: # return [board] return board_list # Problem with cal_moves: Method can return empty list (should always contain at least same board). # *Update*: Seems to be fixed. # ------------------ # 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 #print("initial board: ", board) #print("roll:", roll) boards = calc_moves(board, roll[0]) #print("boards after first die: ", boards) for die in roll[1:]: # Calculate boards resulting from second move nested_boards = [calc_moves(board, die) for board in boards] #print("nested boards: ", nested_boards) boards = [board for boards in nested_boards for board in boards] # What the fuck #for board in boards: # print(board) # print("type__:",type(board)) # Add resulting unique boards to set of legal boards resulting from roll #print("printing boards from calculate_legal_states: ", boards) legal_moves = legal_moves | set(boards) # print("legal moves: ", legal_moves) if len(legal_moves) == 0: legal_moves = { tuple(board) } return legal_moves @staticmethod def pretty(board): def black(count): return "\033[0;30m\033[47m{}\033[0m\033[47m".format(count) def white(count): return "\033[0;31m\033[47m{}\033[0m\033[47m".format(count) temp = [] for x in board: if x > 0: temp.append(" {}".format(x)) elif x < 0: temp.append("{}".format(x)) else: temp.append(" ") return """ 13 14 15 16 17 18 19 20 21 22 23 24 +--------------------------------------------------------------------------+ | {12}| {11}| {10}| {9}| {8}| {7}| bar -1: {0} | {6}| {5}| {4}| {3}| {2}| {1}| end -1: TODO| |---|---|---|---|---|---|------------|---|---|---|---|---|---| | | {13}| {14}| {15}| {16}| {17}| {18}| bar 1: {25} | {19}| {20}| {21}| {22}| {23}| {24}| 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[25] -= player else: board[0] -= player board[to_idx] = 0 return board # TODO: Moving in from bar is handled by the representation # TODONE: Handle bearing off from_idx = move[0] #print("from_idx: ", from_idx) to_idx = move[1] #print("to_idx: ", to_idx) # pdb.set_trace() 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 tuple(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) @staticmethod def flip(board): return tuple((-x for x in reversed(board)))