diff --git a/board.py b/board.py index d32197c..c32340d 100644 --- a/board.py +++ b/board.py @@ -1,3 +1,4 @@ +import quack import numpy as np import itertools @@ -12,15 +13,9 @@ class Board: @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 + return quack.idxs_with_checkers_of_player(board, player) - - # 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): @@ -40,19 +35,19 @@ class Board: def board_features_quack(board, player): board = list(board) board += ([1, 0] if np.sign(player) > 0 else [0, 1]) - return np.array(board).reshape(1, -1) + return np.array(board).reshape(1,28) # quack-fat @staticmethod def board_features_quack_fat(board, player): - 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)) - board += ([1, 0] if np.sign(player) > 0 else [0, 1]) - return np.array(board).reshape(1,-1) - + return np.array(quack.board_features_quack_fat(board,player)).reshape(1,30) + # 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)) + # board += ([1, 0] if np.sign(player) > 0 else [0, 1]) + # return np.array(board).reshape(1,30) # quack-fatter @staticmethod @@ -68,7 +63,7 @@ class Board: board.append(15 - sum(positives)) board.append(-15 - sum(negatives)) board += ([1, 0] if np.sign(player) > 0 else [0, 1]) - return np.array(board).reshape(1, -1) + return np.array(board).reshape(1, 30) # tesauro @staticmethod @@ -97,9 +92,62 @@ class Board: board_rep += bar_trans(board, player) board_rep += (15 - Board.num_of_checkers_for_player(board, player),) - board_rep += ([1,0] if cur_player == 1 else [1,0]) + board_rep += ([1, 0] if cur_player == 1 else [0, 1]) - return np.array(board_rep).reshape(1,198) + return np.array(board_rep).reshape(1, 198) + + + @staticmethod + def board_features_tesauro_fat(board, cur_player): + def ordinary_trans(val, player): + abs_val = val*player + if abs_val <= 0: + return (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + elif abs_val == 1: + return (1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + elif abs_val == 2: + return (1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + elif abs_val == 3: + return (1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + elif abs_val == 4: + return (1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + elif abs_val == 5: + return (1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + elif abs_val == 6: + return (1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0) + elif abs_val == 7: + return (1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0) + elif abs_val == 8: + return (1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0) + elif abs_val == 9: + return (1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0) + elif abs_val == 10: + return (1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0) + elif abs_val == 11: + return (1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0) + elif abs_val == 12: + return (1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0) + elif abs_val == 13: + return (1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0) + elif abs_val == 14: + return (1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0) + elif abs_val == 15: + return (1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) + + def bar_trans(board, player): + if player == 1: return (abs(board[0]/2),) + elif player == -1: return (abs(board[25]/2),) + + board_rep = [] + for player in [1, -1]: + for x in board[1:25]: + board_rep += ordinary_trans(x, player) + board_rep += bar_trans(board, player) + board_rep += (15 - Board.num_of_checkers_for_player(board, player),) + + board_rep += ([1, 0] if cur_player == 1 else [0, 1]) + + return np.array(board_rep).reshape(1, len(board_rep)) @staticmethod @@ -124,98 +172,15 @@ class Board: # Calculate how many pieces there must be in the home state and divide it by 15 features.append((15 - sum) / 15) features += ([1,0] if np.sign(cur_player) > 0 else [0,1]) - test = np.array(features).reshape(1,-1) + test = np.array(features) #print("TEST:",test) - return test + return test.reshape(1,198) @staticmethod def is_move_valid(board, player, face_value, move): - if face_value == 0: - return True - else: - 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 - # print("is_forward:",is_forward_move()) - # print("face_value:",face_value_match_move_length()) - # print("Checkes_at_from:",checkers_at_from_idx()) - # print("no_block:",no_block_at_to_idx()) - - 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 ]) + return quack.is_move_valid(board, player, face_value, move) @staticmethod def any_move_valid(board, player, roll): @@ -255,40 +220,37 @@ class Board: @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 + def apply_moves_to_board(board, player, move): + from_idx = move[0] + to_idx = move[1] + board = list(board) + board[from_idx] -= player + + if (to_idx < 1 or to_idx > 24): + return + + if (board[to_idx] * player == -1): + + if (player == 1): + board[25] -= player + else: + board[0] -= player + + board[to_idx] = 0 + + board[to_idx] += player + + return tuple(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: + if face_value == 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] - # print("pls:",boards) - board_list = list(filter(None, boards)) # Remove None-values - # if len(board_list) == 0: - # return [board] - # print("board list:", board_list) - return board_list + return quack.calc_moves(board, player, face_value) # Problem with cal_moves: Method can return empty list (should always contain at least same board). # *Update*: Seems to be fixed. @@ -302,23 +264,17 @@ class Board: 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] + #print("Permuts:",dice_permutations) # print("Dice permuts:",dice_permutations) 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) @@ -347,9 +303,9 @@ class Board: return """ 13 14 15 16 17 18 19 20 21 22 23 24 +--------------------------------------------------------------------------+ -| {13}| {14}| {15}| {16}| {17}| {18}| bar -1: {25} | {19}| {20}| {21}| {22}| {23}| {24}| 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}| bar 1: {0} | {6}| {5}| {4}| {3}| {2}| {1}| end 1: TODO| +| {12}| {11}| {10}| {9}| {8}| {7}| bar 1: {0} | {6}| {5}| {4}| {3}| {2}| {1}| end -1: TODO| +--------------------------------------------------------------------------+ 12 11 10 9 8 7 6 5 4 3 2 1 """.format(*temp) @@ -357,42 +313,8 @@ class Board: @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 + return quack.do_move(board, player, move) - # 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): diff --git a/test.py b/test.py index 6c9c130..0c6d8a6 100644 --- a/test.py +++ b/test.py @@ -141,6 +141,56 @@ class TestIsMoveValid(unittest.TestCase): # TODO: More tests for bearing off are needed + def test_bear_off_non_backmost(self): + board = ( 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, + 0 ) + self.assertEqual(Board.is_move_valid(board, 1, 2, (23, 25)), True) + self.assertEqual(Board.is_move_valid(board, 1, 1, (24, 25)), True) + self.assertEqual(Board.is_move_valid(board, 1, 2, (24, 26)), False) + + def test_bear_off_quadrant_limits_white(self): + board = ( 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1, + 0 ) + self.assertEqual(Board.is_move_valid(board, 1, 2, (23, 25)), False) + self.assertEqual(Board.is_move_valid(board, 1, 1, (24, 25)), False) + + def test_bear_off_quadrant_limits_black(self): + board = ( 0, + -1, -1, -1, -1, -1, -1, + -1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0 ) + self.assertEqual(Board.is_move_valid(board, -1, 2, (2, 0)), False) + self.assertEqual(Board.is_move_valid(board, -1, 1, (1, 0)), False) + + def test_bear_off_quadrant_limits_white_2(self): + board = ( 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 1, + 0 ) + self.assertEqual(Board.is_move_valid(board, 1, 1, (24, 25)), True) + + def test_bear_off_quadrant_limits_black_2(self): + board = ( 0, + -1, 0, 0, 0, 0, -1, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0 ) + self.assertEqual(Board.is_move_valid(board, -1, 1, (1, 0)), True) + + class TestNumOfChecker(unittest.TestCase): def test_simple_1(self): board = ( 0, @@ -687,6 +737,23 @@ class TestBoardFlip(unittest.TestCase): self.assertTrue((Board.board_features_tesauro(board, 1) == np.array(expected).reshape(1, 198)).all()) + def test_pubeval_features(self): + board = Board.initial_state + + expected = (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, + 0, 0) + + import numpy as np + self.assertTrue((Board.board_features_to_pubeval(board, 1) == + np.array(expected).reshape(1, 28)).all()) + self.assertTrue((Board.board_features_to_pubeval(board, -1) == + np.array(expected).reshape(1, 28)).all()) + def test_tesauro_bars(self): board = list(Board.initial_state) board[1] = 0