Ejemplo n.º 1
0
def main9():
    game_so_far = [
        'g1', 'd5', 'e7', 'b5', 'e1', 'e3', 'i4', 'g4', 'b6', 'e4', 'f5', 'a5',
        'c5', 'e8', 'c3', 'd3', 'f4', 'd2'
    ]
    # Moves by property:
    # SpaceProperies.WHITE_LOSE: ['c4', 'd6', 'f1', 'f3', 'f6']
    # ERROR: SpaceProperies.BLACK_SINGLE_CHECK: ['b1', 'b3', 'c6', 'd7', 'e6', 'g3'] # b3 is not a check... it is already blocked.
    # SpaceProperies.BLACK_WIN: ['d4']
    # SpaceProperies.BLACK_LOSE: ['c1', 'c2', 'd1', 'e2', 'e5', 'f2', 'f3']
    # ERROR: SpaceProperies.GAME_OVER: ['f7', 'h1']
    # SpaceProperies.WHITE_SINGLE_CHECK: ['d4', 'e5', 'f2']

    #game_so_far =  ['e8', 'a1', 'a2', 'a3', 'f1', 'a4', 'c1']
    # Moves by property:
    # SpaceProperies.BLACK_LOSE: ['a5']
    # game_so_far = ['d3']

    move_stack = [
        'd2', 'b2', 'd4', 'f5', 'd5', 'd3', 'b5', 'c5', 'f1', 'e2', 'f2', 'a1',
        'b1', 'c7', 'c6', 'e8', 'g6', 'f6', 'b6', 'd6', 'a3', 'g1'
    ]
    game_so_far = [
        'g2', 'c2', 'g7', 'i5', 'd1', 'e6', 'c4', 'a2', 'b3', 'e5', 'g4', 'g5',
        'c1'
    ]
    yavalath_engine.Render(board=yavalath_engine.HexBoard(),
                           moves=game_so_far).render_image("debug.png")
    classifier = blue_player.NextMoveClassifier(game_so_far, verbose=True)
    classifier.compute_moves_by_property()
Ejemplo n.º 2
0
    def compute_signature_and_properties_slow(arms):
        """arms is a 6-tuple of the values in the arms pointing out from 'e5'.  Use the 'e5' condition vector and
        compute the signature after setting the peices according to 'arms'.  Then, determine the properties tuple,
        and map the signature to that tuple."""
        assert len(arms) == 6, "Expects 6 arms, but {} were given".format(len(arms))
        game_vec = common.moves_to_board_vector([])[:-1]
        e5_condition_vector = NextMoveClassifier.get_condition_vector_for_space('e5')
        for direction in range(6):
            for distance in range(3):
                token = arms[direction][distance]
                next_space = yavalath_engine.HexBoard().next_space_in_dir('e5', direction=direction, distance=1+distance)
                assert next_space is not None, "This algo should always be able to find the next space from 'e5'"
                next_space_index = NextMoveClassifier.space_to_index[next_space]
                game_vec[next_space_index] = token  # -1, 0, or +1
        assert len(e5_condition_vector) == len(game_vec), "The condition vector should match the board length, but {} != {}".format(len(e5_condition_vector), len(game_vec))
        e5_signature = sum(e5_condition_vector * game_vec)[0]
        # Now decide what moving at 'e5' does.

        properties = collections.defaultdict(int)

        # If any arms are the same pieces, this is GAME_OVER_ALREADY
        for arm in arms:
            if arm[0] == arm[1] == arm[2] == 1 or arm[0] == arm[1] == arm[2] == -1:
                properties[SpaceProperies.GAME_OVER] = 1  # The count of GAME_OVER doesn't matter.
        if not SpaceProperies.GAME_OVER in properties:
            for token in [-1, 1]:
                NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(properties, arms[0], arms[3], token)
                NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(properties, arms[1], arms[4], token)
                NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(properties, arms[2], arms[5], token)

        # TODO: Here is the best place to simplify the properties to get one condition for black and one for white.

        return e5_signature, properties
Ejemplo n.º 3
0
    def test_chico_crash_1(self):
        # This is the same bug as the suicide-block...
        game_so_far = ['d5', 'g7', 'g5', 'e3', 'h3', 'i5', 'h2']
        yavalath_engine.Render(
            yavalath_engine.HexBoard(),
            game_so_far).render_image("test_chico_crash_1.png")
        cutoff = game_so_far + [
            'i4', 'h5', 'h4', 'e5', 'f5', 'g3', 'f4', 'g4', 'i2', 'i3', 'f2'
        ]
        yavalath_engine.Render(
            yavalath_engine.HexBoard(),
            cutoff).render_image("test_chico_crash_1_cutoff.png")

        game_so_far = game_so_far + [
            'i4', 'h5', 'h4', 'e5', 'f5', 'g3', 'f4'
        ]  # blue2 plays at 'g4' to block, but it is suicidal

        move = player(game_so_far, depth=2, verbose=False)
        self.assertNotEqual(move[0], 'f2')
Ejemplo n.º 4
0
def moves_to_board_vector(moves):
    board = yavalath_engine.HexBoard()
    space_to_index = {space: i for i, space in enumerate(board.spaces)}
    N = 61 + 1
    result = numpy.zeros((N, 1), dtype="i1")
    for turn, move in enumerate(moves):
        # TODO: Handle 'swap'
        result[space_to_index[move]] = 1 if turn % 2 == 0 else -1
    result[61] = 1  # To account for the level of indicators.
    return result
Ejemplo n.º 5
0
 def compute_signature(arms):
     game_vec = common.moves_to_board_vector([])[:-1]
     for direction in range(6):
         for distance in range(3):
             token = arms[direction][distance]
             next_space = yavalath_engine.HexBoard().next_space_in_dir('e5', direction=direction, distance=1+distance)
             next_space_index = NextMoveClassifier.space_to_index[next_space]
             game_vec[next_space_index] = token  # -1, 0, or +1
     e5_condition_vector = NextMoveClassifier.get_condition_vector_for_space('e5')
     e5_signature = sum(e5_condition_vector * game_vec)[0]
     return e5_signature
Ejemplo n.º 6
0
 def test_renderer_debug(self):
     move_stack = [
         'd2', 'b2', 'd4', 'f5', 'd5', 'd3', 'b5', 'c5', 'f1', 'e2', 'f2',
         'a1', 'b1', 'c7', 'c6', 'e8', 'g6', 'f6', 'b6', 'd6', 'a3', 'g1'
     ]
     game_so_far = [
         'g2', 'c2', 'g7', 'i5', 'd1', 'e6', 'c4', 'a2', 'b3', 'e5', 'g4',
         'g5', 'c1'
     ] + move_stack
     yavalath_engine.Render(board=yavalath_engine.HexBoard(),
                            moves=game_so_far).render_image("debug.png")
Ejemplo n.º 7
0
 def get_condition_vector_for_space(space):
     # Initialize the condition matrix.  It has 61 condition vectors, one for each board space.  There are up to 18
     # non-zero entries in the vector, each a power of 3.
     result = numpy.zeros((NUMBER_OF_BOARD_SPACES, 1), dtype="i4") # TODO: Try performance with i8
     power_of_three = 1
     for direction in range(6):
         for distance in range(3):
             next_space = yavalath_engine.HexBoard().next_space_in_dir(space, direction=direction, distance=distance+1)
             if next_space is not None:  # Check if the horizon space is off the board.
                 next_space_index = NextMoveClassifier.space_to_index[next_space]
                 result[next_space_index] = power_of_three
             power_of_three *= 3  # Always do this, even if the space was off the board
     return result
Ejemplo n.º 8
0
    def __init__(self):
        self.moves = list()
        self.white_moves = list()
        self.black_moves = list()
        self.swapped = False
        self.board = yavalath_engine.HexBoard()
        self.space_to_index = {
            space: i
            for i, space in enumerate(self.board.spaces)
        }
        self.index_to_space = self.board.spaces

        # The views array is indexed first by the space index, then by direction.
        self.views = self.empty_view()
Ejemplo n.º 9
0
def player(game_so_far, board=yavalath_engine.HexBoard(), depth=0):
    """This is my attempt at an AI-driven player.  Likely I'll have many variants."""
    # First basic strategy, heading toward minimax.  Score all moves on the current board.
    #return best_move(board, game_so_far, 1)[1]
    options = set(board.spaces) - set(game_so_far)
    #options_with_scores = [(simple_score_for_option(game_so_far, o), o) for o in options]
    options_with_scores = [(minimax_score_for_option(board, game_so_far, o,
                                                     depth), o)
                           for o in options]
    max_move = max(options_with_scores)
    max_score = max_move[0]
    next_move = random.choice(
        [s for s in options_with_scores if s[0] == max_score])
    logger.debug("Selected move with score of: {}".format(next_move))
    # print("dta Game so far:", game_so_far)
    # print("dta Selected move with score of: {}".format(next_move))
    return next_move[1]
Ejemplo n.º 10
0
 def compute_signature_and_properties_for_space(self, space):
     """Uses the current board and target space to return the signature for that space, and the computed (not cached)
     properties for that space.  This is for testing."""
     arms = list()
     board = yavalath_engine.HexBoard()
     space_to_index = {space: i for i, space in enumerate(board.spaces)}
     arms = list()
     for direction in range(6):
         arm = list()
         for distance in range(3):
             next_space = board.next_space_in_dir(space, direction, distance+1)
             if next_space is None:
                 token = 0
             else:
                 next_space_index = space_to_index[next_space]
                 token = self.game_vec[next_space_index][0]
             arm.append(token)
         arms.append(tuple(arm))
     return NextMoveClassifier.compute_signature_and_properties(tuple(arms))
Ejemplo n.º 11
0
 def test_game_so_far(game_so_far):
     board = yavalath_engine.HexBoard()
     state = blue_player.GameState(game_so_far)
     self.assertEqual(
         len(state.options), 61 - len(game_so_far),
         "The number of options is wrong for game: {}.".format(
             game_so_far))
     options = [space_to_index[space] for space in state.options]
     self.assertEqual(
         state.white_potential_moves.shape, (62, 62 - len(game_so_far))
     )  # 62 rows for the spaces + "1", 61-len(game) columns for potential moves + the "no move"
     for board_space in range(
             61):  # Always 61, since the board size is fixed
         for potential_move_index in range(len(
                 state.options)):  # Just the number of options
             expected = 1 if board_space == options[
                 potential_move_index] else 0
             if board.spaces[board_space] in game_so_far:  # a1
                 expected = 1 if board_space in state.white_move_indices else -1
             self.assertEqual(
                 state.white_potential_moves[board_space,
                                             potential_move_index],
                 expected,
                 "Mismatch for board_space:{}, potential_move_index:{}".
                 format(board_space, potential_move_index))
     for board_space in range(61):
         potential_move_index = len(state.options)
         expected = 1 if board_space in state.white_move_indices else -1 if board_space in state.black_move_indices else 0
         self.assertEqual(
             state.white_potential_moves[board_space,
                                         potential_move_index],
             expected,
             "Mismatch for 'no move' column, board_space:{}, potential_move_index:{}"
             .format(board_space, potential_move_index))
     for potential_move_index in range(len(
             state.options)):  # Just the number of options
         board_space = 61
         self.assertEqual(
             state.white_potential_moves[board_space,
                                         potential_move_index], 1,
             "Mismatch for 'offset' row, board_space:{}, potential_move_index:{}"
             .format(board_space, potential_move_index))
Ejemplo n.º 12
0
    def compute_signature_and_properties(arms):
        """arms is a 6-tuple of the values in the arms pointing out from 'e5'.  Use the 'e5' condition vector and
        compute the signature after setting the peices according to 'arms'.  Then, determine the properties tuple,
        and map the signature to that tuple."""
        assert len(arms) == 6, "Expects 6 arms, but {} were given".format(len(arms))
        game_vec = common.moves_to_board_vector([])[:-1]
        for direction in range(6):
            for distance in range(3):
                token = arms[direction][distance]
                next_space = yavalath_engine.HexBoard().next_space_in_dir('e5', direction=direction, distance=1+distance)
                assert next_space is not None, "This algo should always be able to find the next space from 'e5'"
                next_space_index = NextMoveClassifier.space_to_index[next_space]
                game_vec[next_space_index] = token  # -1, 0, or +1
        e5_condition_vector = NextMoveClassifier.get_condition_vector_for_space('e5')
        assert len(e5_condition_vector) == len(game_vec), "The condition vector should match the board length, but {} != {}".format(len(e5_condition_vector), len(game_vec))
        e5_signature = sum(e5_condition_vector * game_vec)[0]
        # Now decide what moving at 'e5' does.

        # If any arms are the same pieces, this is GAME_OVER_ALREADY
        result = None
        for arm in arms:
            if arm[0] == arm[1] == arm[2] == 1 or arm[0] == arm[1] == arm[2] == -1:
                result = (SpaceProperies.GAME_OVER,)


        if result is None:
            # Get the white result
            r1 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[0], arms[3], 1)
            r2 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[1], arms[4], 1)
            r3 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[2], arms[5], 1)
            white_result = NextMoveClassifier.white_result_from_arm_results(r1, r2, r3)

            # Get the black result
            r1 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[0], arms[3], -1)
            r2 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[1], arms[4], -1)
            r3 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[2], arms[5], -1)
            black_result = NextMoveClassifier.black_result_from_arm_results(r1, r2, r3)

            result = (white_result, black_result)

        return e5_signature, result
Ejemplo n.º 13
0
 def test_connected_along_axis(self):
     board = yavalath_engine.HexBoard()
     self.assertEqual(
         yavalath_engine._connected_along_axis(board, "e6",
                                               ["e4", "a1", "e5", "a2"], 0),
         3)
     self.assertEqual(
         3,
         yavalath_engine._connected_along_axis(board, "g1", ["e1", "f1"],
                                               1))
     self.assertEqual(
         3,
         yavalath_engine._connected_along_axis(
             board, "g7", ['e7', 'c1', 'f7', 'e9', 'e6', 'h6'], 1))
     self.assertEqual(
         3,
         yavalath_engine._connected_along_axis(board, "c3", [
             'f8', 'd8', 'b6', 'f6', 'c1', 'd2', 'f3', 'g4', 'c2', 'd5',
             'f4', 'e7', 'g6', 'e8', 'i5', 'h1', 'h4', 'd4', 'd6', 'a4',
             'e4', 'h6'
         ], 0))
Ejemplo n.º 14
0
    def __init__(self, game_so_far, verbose=True):
        self.verbose = verbose
        self.options = sorted(list(set(yavalath_engine.HexBoard().spaces) - set(game_so_far)))
        self.option_index_to_board_index = [NextMoveClassifier.space_to_index[s] for s in self.options]
        self.open_option_indices = set(range(len(self.options)))  # Column index into the potential move matrices
        self.game_vec = common.moves_to_board_vector(game_so_far)[:NUMBER_OF_BOARD_SPACES]

        # Compute the condition matrix.  I only care about condition vectors for empty spaces.
        all_condition_vectors = [self.get_condition_vector_for_space(o) for o in self.options]
        matrix_shape = (len(self.options), NUMBER_OF_BOARD_SPACES)
        self.condition_matrix = numpy.matrix(numpy.array(list(itertools.chain(*all_condition_vectors))).reshape(matrix_shape), dtype='i4')

        # Initialize the signatures now.  Manage them with add_move/undo_move.
        self.signatures = self.condition_matrix * self.game_vec

        # Takes days to compute the table
        self.signature_table, self.properties_table = NextMoveClassifier.load_signature_table()
        self.properties_table = numpy.array(self.properties_table)
        self.signature_table = numpy.array(self.signature_table)

        self.compute_moves_by_property()
Ejemplo n.º 15
0
    def test_next_space(self):
        b = yavalath_engine.HexBoard()
        self.assertEqual("e6", b.next_space("e5", 0, 1))
        self.assertEqual("e4", b.next_space("e5", 0, -1))
        self.assertEqual("f5", b.next_space("e5", 1, 1))
        self.assertEqual("d4", b.next_space("e5", 1, -1))
        self.assertEqual("d5", b.next_space("e5", -1, 1))
        self.assertEqual("f4", b.next_space("e5", -1, -1))

        self.assertEqual("d6", b.next_space("d5", 0, 1))
        self.assertEqual("d4", b.next_space("d5", 0, -1))
        self.assertEqual("e6", b.next_space("d5", 1, 1))
        self.assertEqual("c4", b.next_space("d5", 1, -1))
        self.assertEqual("c5", b.next_space("d5", -1, 1))
        self.assertEqual("e5", b.next_space("d5", -1, -1))

        self.assertEqual("f6", b.next_space("f5", 0, 1))
        self.assertEqual("f4", b.next_space("f5", 0, -1))
        self.assertEqual("g5", b.next_space("f5", 1, 1))
        self.assertEqual("e5", b.next_space("f5", 1, -1))
        self.assertEqual("e6", b.next_space("f5", -1, 1))
        self.assertEqual("g4", b.next_space("f5", -1, -1))

        self.assertEqual("f1", b.next_space("g1", 1, -1))
Ejemplo n.º 16
0
# numpy plan... each board is an Nx1, b vector of 1,0,-1 values.  (For Yavalath, N=61)
# An indicator is a 1xN vector, v such that v*b <= 1.  v*b == 1 implies the condition is met.
# Integer math is faster than floating point math.  So, let v*b == L (level) imply the condition is met.
# Add one more element to b, value 1, and v, value -L.  Then v*b == 0 implies the condition is met.
# Potential moves can then be represented by the boards they create, giving an Nxk matrix, P, representing
# the potential boards.
# All win conditions and loss conditions can be represented in a single M x N matrix, C.  Then, the matrix
# product C*P gives an M*P matrix, telling which conditions hold for each potential board.
# Potentialy, there are multiple M to classify "I Win", "I lose", "I check", "I must block"
#
# For starters, let's get utility functions to translate a set of moves to a board vector and a set of
# condition vectors that determine wins/losses.
#
# Every potential board is the current board plus a move vector for a single move.
#============================================================================================================
board = yavalath_engine.HexBoard()

# ==== The horizon condition vectors approach ===
# IDEA: What if I define a kernel method that applies to the whole board.  For any space, we know whether it is a win,
# loss, check, or block (it could be more than one of these) based on just the 2 spaces in any direction.  Maybe I put
# 3^n on each of those kernel entries, leading to a unique number for each possibility.  There are 12 surrounding
# spaces, for 3^12 values = 531441.  This isn't bad.  Have a lookup array of those values into a condition table.  This
# approach would mean single condition matrix, of 61 condition vectors.  If I do a horizon of 3 spaces, I get a bigger
# lookup table, 3**18 = 387420489, which is big, but not too big for memory.  400MB of my budget.  This would allow me
# to determine checks and multi-checks as well.

# Maybe nos is when I abstract out the board/condition updates from the move selection.  A classified board has this
# interface:
#    __init__(game_so_far)
#    options # list of allowable moves (the output string, like 'e1')
#    column_index_to_board_index  # Maps from the potential move number to the board index of that move
Ejemplo n.º 17
0
class NextMoveClassifier():
    @staticmethod
    @functools.lru_cache()
    def keys_for_token(token):
        if token == 1:
            return SpaceProperies.WHITE_WIN, SpaceProperies.WHITE_LOSE, SpaceProperies.WHITE_DOUBLE_CHECK, SpaceProperies.WHITE_SINGLE_CHECK
        else:
            return SpaceProperies.BLACK_WIN, SpaceProperies.BLACK_LOSE, SpaceProperies.BLACK_DOUBLE_CHECK, SpaceProperies.BLACK_SINGLE_CHECK

    @staticmethod
    @functools.lru_cache()
    def find_wins_and_checks_for_token_and_opp_arms(left_arm, right_arm, token):
        """Add to 'properites'.  I plan to call this for each token, for each of the 3 axes.  The properties
        count how many axes have each condition.
        TODO: This needs to improve... really, there is one condition for white and black, either:
           WIN, LOSE, DOUBLE_CHECK, or SINGLE_CHECK, in that order.
        Only one of each color.  That's how to label the space.
        """
        properties = dict()
        line = (left_arm[2], left_arm[1], left_arm[0], token, right_arm[0], right_arm[1], right_arm[2])
        win_key, lose_key, double_check_key, single_check_key = NextMoveClassifier.keys_for_token(token)

        # Detect outright wins
        if (left_arm[1] == left_arm[0] == token == right_arm[0]) or (left_arm[0] == token == right_arm[0] == right_arm[1]):
            return win_key

        # And now outright losses
        if (left_arm[1] == left_arm[0] == token) or (left_arm[0] == token == right_arm[0]) or (token == right_arm[0] == right_arm[1]):
            return lose_key

        # And now a double-check on this axis
        if (left_arm[2] == left_arm[1] == right_arm[1] == right_arm[2] == token) and (left_arm[0] == right_arm[0] == 0):
            return double_check_key

        if (left_arm[1] == token == right_arm[0] == right_arm[2]) and (left_arm[0] == right_arm[1] == 0):
            return double_check_key

        if (left_arm[2] == left_arm[0] == token == right_arm[1]) and (left_arm[1] == right_arm[0] == 0):
            return double_check_key

        # And now single-checks on this axis, possibly upgrading them to double-checks
        if (left_arm[1] == token and left_arm[0] == 0 and right_arm[0] == token) \
                or (left_arm[0] == token and right_arm[0] == 0 and right_arm[1] == token) \
                or (left_arm[2] == left_arm[1] == token and left_arm[0] == 0) \
                or (left_arm[2] == left_arm[0] == token and left_arm[1] == 0) \
                or (right_arm[2] == right_arm[1] == token and right_arm[0] == 0) \
                or (right_arm[2] == right_arm[0] == token and right_arm[1] == 0):
            return single_check_key

    @staticmethod
    @functools.lru_cache()
    def white_result_from_arm_results(r1, r2, r3):
        arm_results = (r1, r2, r3)
        if SpaceProperies.WHITE_WIN in arm_results:
            white_result = SpaceProperies.WHITE_WIN
        elif SpaceProperies.WHITE_LOSE in arm_results:
            white_result = SpaceProperies.WHITE_LOSE
        elif SpaceProperies.WHITE_DOUBLE_CHECK in arm_results:
            white_result = SpaceProperies.WHITE_DOUBLE_CHECK
        else:
            check_count = arm_results.count(SpaceProperies.WHITE_SINGLE_CHECK)
            double_check_count = arm_results.count(SpaceProperies.WHITE_DOUBLE_CHECK)
            total_check_count = 2*double_check_count + check_count
            
            if total_check_count > 2:
                white_result = SpaceProperies.WHITE_TRIPLE_CHECK
            elif total_check_count > 1:
                white_result = SpaceProperies.WHITE_DOUBLE_CHECK
            elif total_check_count == 1:
                white_result = SpaceProperies.WHITE_SINGLE_CHECK
            else:
                white_result = None
        return white_result

    @staticmethod
    @functools.lru_cache()
    def black_result_from_arm_results(r1, r2, r3):
        arm_results = (r1, r2, r3)
        if SpaceProperies.BLACK_WIN in arm_results:
            black_result = SpaceProperies.BLACK_WIN
        elif SpaceProperies.BLACK_LOSE in arm_results:
            black_result = SpaceProperies.BLACK_LOSE
        elif SpaceProperies.BLACK_DOUBLE_CHECK in arm_results:
            black_result = SpaceProperies.BLACK_DOUBLE_CHECK
        else:
            check_count = arm_results.count(SpaceProperies.BLACK_SINGLE_CHECK)
            double_check_count = arm_results.count(SpaceProperies.BLACK_DOUBLE_CHECK)
            total_check_count = 2*double_check_count + check_count
            
            if total_check_count > 2:
                black_result = SpaceProperies.BLACK_TRIPLE_CHECK
            elif total_check_count > 1:
                black_result = SpaceProperies.BLACK_DOUBLE_CHECK
            elif total_check_count == 1:
                black_result = SpaceProperies.BLACK_SINGLE_CHECK
            else:
                black_result = None
        return black_result

    @staticmethod
    def signature_index_to_arms(signature):
        tokens = list()
        for i in range(18):
            modulus = signature % 3
            token = [-1, 0, 1][modulus]
            tokens.append(token)
            signature = int(signature / 3)
        return tuple(tokens[0:3]), tuple(tokens[3:6]), tuple(tokens[6:9]), tuple(tokens[9:12]), tuple(tokens[12:15]), tuple(tokens[15:18])

    @staticmethod
    def compute_signature(arms):
        game_vec = common.moves_to_board_vector([])[:-1]
        for direction in range(6):
            for distance in range(3):
                token = arms[direction][distance]
                next_space = yavalath_engine.HexBoard().next_space_in_dir('e5', direction=direction, distance=1+distance)
                next_space_index = NextMoveClassifier.space_to_index[next_space]
                game_vec[next_space_index] = token  # -1, 0, or +1
        e5_condition_vector = NextMoveClassifier.get_condition_vector_for_space('e5')
        e5_signature = sum(e5_condition_vector * game_vec)[0]
        return e5_signature

    @staticmethod
    def compute_signature_and_properties(arms):
        """arms is a 6-tuple of the values in the arms pointing out from 'e5'.  Use the 'e5' condition vector and
        compute the signature after setting the peices according to 'arms'.  Then, determine the properties tuple,
        and map the signature to that tuple."""
        assert len(arms) == 6, "Expects 6 arms, but {} were given".format(len(arms))
        game_vec = common.moves_to_board_vector([])[:-1]
        for direction in range(6):
            for distance in range(3):
                token = arms[direction][distance]
                next_space = yavalath_engine.HexBoard().next_space_in_dir('e5', direction=direction, distance=1+distance)
                assert next_space is not None, "This algo should always be able to find the next space from 'e5'"
                next_space_index = NextMoveClassifier.space_to_index[next_space]
                game_vec[next_space_index] = token  # -1, 0, or +1
        e5_condition_vector = NextMoveClassifier.get_condition_vector_for_space('e5')
        assert len(e5_condition_vector) == len(game_vec), "The condition vector should match the board length, but {} != {}".format(len(e5_condition_vector), len(game_vec))
        e5_signature = sum(e5_condition_vector * game_vec)[0]
        # Now decide what moving at 'e5' does.

        # If any arms are the same pieces, this is GAME_OVER_ALREADY
        result = None
        for arm in arms:
            if arm[0] == arm[1] == arm[2] == 1 or arm[0] == arm[1] == arm[2] == -1:
                result = (SpaceProperies.GAME_OVER,)


        if result is None:
            # Get the white result
            r1 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[0], arms[3], 1)
            r2 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[1], arms[4], 1)
            r3 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[2], arms[5], 1)
            white_result = NextMoveClassifier.white_result_from_arm_results(r1, r2, r3)

            # Get the black result
            r1 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[0], arms[3], -1)
            r2 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[1], arms[4], -1)
            r3 = NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(arms[2], arms[5], -1)
            black_result = NextMoveClassifier.black_result_from_arm_results(r1, r2, r3)

            result = (white_result, black_result)

        return e5_signature, result

    @staticmethod
    def find_wins_and_checks_for_token_and_opp_arms_slow(properties, left_arm, right_arm, token):
        """Add to 'properites'.  I plan to call this for each token, for each of the 3 axes.  The properties
        count how many axes have each condition.
        TODO: This needs to improve... really, there is one condition for white and black, either:
           WIN, LOSE, DOUBLE_CHECK, or SINGLE_CHECK, in that order.
        Only one of each color.  That's how to label the space.
        """
        lose_key = SpaceProperies.WHITE_LOSE if token == 1 else SpaceProperies.BLACK_LOSE
        win_key = SpaceProperies.WHITE_WIN if token == 1 else SpaceProperies.BLACK_WIN
        single_check_key = SpaceProperies.WHITE_SINGLE_CHECK if token == 1 else SpaceProperies.BLACK_SINGLE_CHECK
        double_check_key = SpaceProperies.WHITE_DOUBLE_CHECK if token == 1 else SpaceProperies.BLACK_DOUBLE_CHECK
        line = list(reversed(left_arm)) + [token,] + list(right_arm)

        # Detect outright wins
        if (line[1] == line[2] == line[3] == line[4]) or (line[2] == line[3] == line[4] == line[5]):
            properties[win_key] = 1
            if lose_key in properties:
                del properties[lose_key]

        # And now outright losses
        elif (line[1] == line[2] == line[3]) or (line[2] == line[3] == line[4]) or (line[3] == line[4] == line[5]):
            if win_key not in properties:
                properties[lose_key] = 1

        # Only consider checks if there are no outright wins or losses
        elif win_key not in properties and lose_key not in properties:
            # And now a double-check on this axis
            if line == [token, token, 0, token, 0, token, token]:
                properties[double_check_key] = 1
                if single_check_key in properties:
                    del properties[single_check_key]

            # And now single-checks on this axis, possibly upgrading them to double-checks
            elif (line[1] == token and line[2] ==0 and line[4] == token) or (line[2] == token and line[4] == 0 and line[5] == token) \
                or (line[0] == line[1] == token) or (line[6] == line[5] == token):
                key = SpaceProperies.WHITE_SINGLE_CHECK if token == 1 else SpaceProperies.BLACK_SINGLE_CHECK
                # If already have a single check, change it to double.  If we already have a double, don't add a single.
                if key in properties:
                    del properties[key]
                    properties[double_check_key] = 1
                elif double_check_key not in properties:
                    properties[key] = 1

    @staticmethod
    @functools.lru_cache()
    def get_condition_vector_for_space(space):
        # Initialize the condition matrix.  It has 61 condition vectors, one for each board space.  There are up to 18
        # non-zero entries in the vector, each a power of 3.
        result = numpy.zeros((NUMBER_OF_BOARD_SPACES, 1), dtype="i4") # TODO: Try performance with i8
        power_of_three = 1
        for direction in range(6):
            for distance in range(3):
                next_space = yavalath_engine.HexBoard().next_space_in_dir(space, direction=direction, distance=distance+1)
                if next_space is not None:  # Check if the horizon space is off the board.
                    next_space_index = NextMoveClassifier.space_to_index[next_space]
                    result[next_space_index] = power_of_three
                power_of_three *= 3  # Always do this, even if the space was off the board
        return result

    @staticmethod
    def compute_signature_and_properties_slow(arms):
        """arms is a 6-tuple of the values in the arms pointing out from 'e5'.  Use the 'e5' condition vector and
        compute the signature after setting the peices according to 'arms'.  Then, determine the properties tuple,
        and map the signature to that tuple."""
        assert len(arms) == 6, "Expects 6 arms, but {} were given".format(len(arms))
        game_vec = common.moves_to_board_vector([])[:-1]
        e5_condition_vector = NextMoveClassifier.get_condition_vector_for_space('e5')
        for direction in range(6):
            for distance in range(3):
                token = arms[direction][distance]
                next_space = yavalath_engine.HexBoard().next_space_in_dir('e5', direction=direction, distance=1+distance)
                assert next_space is not None, "This algo should always be able to find the next space from 'e5'"
                next_space_index = NextMoveClassifier.space_to_index[next_space]
                game_vec[next_space_index] = token  # -1, 0, or +1
        assert len(e5_condition_vector) == len(game_vec), "The condition vector should match the board length, but {} != {}".format(len(e5_condition_vector), len(game_vec))
        e5_signature = sum(e5_condition_vector * game_vec)[0]
        # Now decide what moving at 'e5' does.

        properties = collections.defaultdict(int)

        # If any arms are the same pieces, this is GAME_OVER_ALREADY
        for arm in arms:
            if arm[0] == arm[1] == arm[2] == 1 or arm[0] == arm[1] == arm[2] == -1:
                properties[SpaceProperies.GAME_OVER] = 1  # The count of GAME_OVER doesn't matter.
        if not SpaceProperies.GAME_OVER in properties:
            for token in [-1, 1]:
                NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(properties, arms[0], arms[3], token)
                NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(properties, arms[1], arms[4], token)
                NextMoveClassifier.find_wins_and_checks_for_token_and_opp_arms(properties, arms[2], arms[5], token)

        # TODO: Here is the best place to simplify the properties to get one condition for black and one for white.

        return e5_signature, properties

    @staticmethod
    def do_one_piece_of_work(arm1, arm2, arm_possibilities, signature_to_properties_index, properties_table, verbose=False):
        """Iterate over all products where the last of 6 arms is fixed.  I can then parallelize the work based on
        that last arm value."""
        # Each process makes its own tables.  I'll write merge routines later.
        start = time.time()
        for i, arms in enumerate(itertools.product(arm_possibilities, arm_possibilities, arm_possibilities, arm_possibilities)):
            arms = list(arms)
            arms.append(arm1)
            arms.append(arm2)
            signature, properties = NextMoveClassifier.compute_signature_and_properties(arms)
            signature_index = NextMoveClassifier.SIGNATURE_OFFSET + signature
            # assert 0 <= signature_index < 2*NextMoveClassifier.SIGNATURE_OFFSET+1, "Invalid signature: {}".format(signature_index)
            # assert properties in properties_table, "Unforutnately, {} is not in the properties_table".format(properties)
            signature_to_properties_index[signature_index] = properties_table.index(properties)
            if verbose and i % 10000 == 0:
                print("PID:{}, Arms: {}, {}, Done with step {}, duration so far: {}".format(os.getpid(), arm1, arm2, i, time.time() - start))

    @staticmethod
    @functools.lru_cache()
    def all_valid_properties():
        """This returns the list of all expected tuples that can come out of the compute_signature_and_properties function.
        """
        valid_black_properties = [None, SpaceProperies.BLACK_WIN, SpaceProperies.BLACK_LOSE, SpaceProperies.BLACK_SINGLE_CHECK, SpaceProperies.BLACK_DOUBLE_CHECK]
        valid_white_properties = [None, SpaceProperies.WHITE_WIN, SpaceProperies.WHITE_LOSE, SpaceProperies.WHITE_SINGLE_CHECK, SpaceProperies.WHITE_DOUBLE_CHECK]
        result = [(SpaceProperies.GAME_OVER,)] + list(itertools.product(valid_white_properties, valid_black_properties))
        return result

    @staticmethod
    def worker(worker_id, work_queue, done_queue):
        filename = "data/signature_table_worker_{}.dat".format(worker_id)
        filepath = pathlib.Path(filename)
        properties_table = NextMoveClassifier.all_valid_properties()
        if not filepath.exists():
            signature_to_properties_index = numpy.zeros(2*NextMoveClassifier.SIGNATURE_OFFSET+1, dtype='i1') - 1 # Initialize everything to sentinel, -1
            print("This worker has not done work before.")
        else:
            signature_to_properties_index, loaded_properties_table = pickle.load(open(filepath.as_posix(), 'rb'))
            assert loaded_properties_table == properties_table
            print("Loaded the worker state {}, {}".format(len(signature_to_properties_index), len(properties_table)))

        one_arm_possibilities = list(itertools.product([-1,0,1], [-1,0,1], [-1,0,1]))
        verbose = True or (worker_id == 0)
        try:
            while True:
                task = work_queue.get(timeout=60)
                print("Worker: {}, Starting {} at {} on PID: {}".format(worker_id, task, time.time(), os.getpid()))
                arm1, arm2 = task
                NextMoveClassifier.do_one_piece_of_work(arm1, arm2, one_arm_possibilities, signature_to_properties_index, properties_table, verbose)
                with open(filepath.as_posix(), 'wb') as file_obj:
                    pickle.dump(file=file_obj, obj=(signature_to_properties_index, properties_table))
                    print("Commit to file:{}".format(filename))
                done_queue.put(task)
        except queue.Empty:
            if verbose:
                print("Queue is empty")

    @staticmethod
    def compute_signature_table(tasks=None, processes=4):
        """Starts a multi-processed computation of the signature/properties tables.  Data will be put in 'data/'.  If
        'tasks' is passed in as an iterable of pairs of arms, that will be used instead of the full desired set of
        27 x 27 arm paris."""
        # I also need to initialize the condition outcomes array.  This has 3**18 entries.  The product of a condition
        # vector and a board vector gives a value in [-N, N] where N = sum([3**i for i in range(18)]) = 193710244
        # Call this number the signature of the space, given the board.
        # (Of course, most of these won't get hit, as it's not possible to have 3 black in a row on one of the arms, for
        # instance.  That would already be a loss for black.)

        done_task_filename = "data/complete_tasks.dat"
        done_task_filepath = pathlib.Path(done_task_filename)
        if not done_task_filepath.exists():
            done_tasks = list()
            print("Nothing done yet.")
        else:
            done_tasks = pickle.load(open(done_task_filepath.as_posix(), 'rb'))
            print("loaded {} done tasks: {}".format(len(done_tasks), done_tasks))

        # Iterate over all possible sets of arms
        work_queue = multiprocessing.Queue()
        done_queue = multiprocessing.Queue()
        if tasks is None:
            one_arm_possibilities = list(itertools.product([-1,0,1], [-1,0,1], [-1,0,1]))
            for arm1, arm2 in itertools.product(one_arm_possibilities, one_arm_possibilities):
                task = (arm1, arm2)
                if task not in done_tasks:
                    work_queue.put((arm1, arm2))  # Make 27*27 tasks
        else:
            for task in tasks:
                work_queue.put(task)

        # Make some workers
        processes = [multiprocessing.Process(target=NextMoveClassifier.worker, args=[id, work_queue, done_queue]) for id in range(processes)]
        for p in processes:
            p.start()

        while not work_queue.empty():
            try:
                while True:
                    done_task = done_queue.get(timeout=60)
                    done_tasks.append(done_task)
                    print("Waiter... done_task: {}".format(done_task))
                    with open(done_task_filepath.as_posix(), 'wb') as file_obj:
                        pickle.dump(file=file_obj, obj=done_tasks)
            except queue.Empty:
                pass

        for p in processes:
            p.join()

        # One more pass to mark things as done
        try:
            while True:
                done_task = done_queue.get(timeout=1)
                done_tasks.append(done_task)
                print("Waiter... done_task: {}".format(done_task))
                with open(done_task_filepath.as_posix(), 'wb') as file_obj:
                    pickle.dump(file=file_obj, obj=done_tasks)
        except queue.Empty:
            pass

        return True

    @staticmethod
    @functools.lru_cache()
    def load_signature_table():
        filename = pathlib.Path(__file__).parent / "signature_table.dat"
        print("Loading signature table from: {}.  Exists: {}".format(filename.as_posix(), filename.exists()))
        return pickle.load(open(filename.as_posix(), "rb"))

    @staticmethod
    def get_board_properties(game_so_far, verbose=False):
        c = NextMoveClassifier(game_so_far, verbose=verbose)
        c.compute_moves_by_property()
        return c.moves_by_property

    space_to_index = {space: i for i, space in enumerate(yavalath_engine.HexBoard().spaces)}
    SIGNATURE_OFFSET = sum([3**i for i in range(18)])  # Add this to all signatures to make them >= 0.

    def __init__(self, game_so_far, verbose=True):
        self.verbose = verbose
        self.options = sorted(list(set(yavalath_engine.HexBoard().spaces) - set(game_so_far)))
        self.option_index_to_board_index = [NextMoveClassifier.space_to_index[s] for s in self.options]
        self.open_option_indices = set(range(len(self.options)))  # Column index into the potential move matrices
        self.game_vec = common.moves_to_board_vector(game_so_far)[:NUMBER_OF_BOARD_SPACES]

        # Compute the condition matrix.  I only care about condition vectors for empty spaces.
        all_condition_vectors = [self.get_condition_vector_for_space(o) for o in self.options]
        matrix_shape = (len(self.options), NUMBER_OF_BOARD_SPACES)
        self.condition_matrix = numpy.matrix(numpy.array(list(itertools.chain(*all_condition_vectors))).reshape(matrix_shape), dtype='i4')

        # Initialize the signatures now.  Manage them with add_move/undo_move.
        self.signatures = self.condition_matrix * self.game_vec

        # Takes days to compute the table
        self.signature_table, self.properties_table = NextMoveClassifier.load_signature_table()
        self.properties_table = numpy.array(self.properties_table)
        self.signature_table = numpy.array(self.signature_table)

        self.compute_moves_by_property()

    def compute_moves_by_property(self):
        """Populate the move sets that will be useful for making decisions on how to move.
        Assumes black and white potential move matrices are current, and the condition_matrix is computed"""
        move_properties = self.properties_table[self.signature_table[self.signatures + NextMoveClassifier.SIGNATURE_OFFSET]]

        self.moves_by_property = collections.defaultdict(set)
        for option_index, properties in enumerate(move_properties):
            for property_key in properties:
                for sub_key in property_key:
                    self.moves_by_property[sub_key].add(option_index)

        if self.verbose:
            print("Moves by property:")
            for key, option_index_list in self.moves_by_property.items():
                if key is None:
                    continue
                option_list = [self.options[i] for i in option_index_list]
                print("{}: {}".format(key, sorted(option_list)))

    def add_move(self, option_index, token):
        board_index = self.option_index_to_board_index[option_index]
        # assert self.game_vec[board_index,0] == 0, "Attempting to add a move to a non-empty space: {}, {}".format(self.game_vec, board_index)
        # assert option_index in self.open_option_indices, "Attept to add a move that is not available."
        self.game_vec[board_index] = token
        self.open_option_indices.remove(option_index)

        # This is the logical: self.signatures = self.condition_matrix * self.game_vec
        # Optimized, I only need to change by one row in the condition matrix
        self.signatures += self.condition_matrix[:, board_index] * token  # Add the correct column, multiplied by the new token
        # if numpy.max(self.signatures) + NextMoveClassifier.SIGNATURE_OFFSET >= 387420489:
        #     raise Exception("Broken")
        self.compute_moves_by_property()

    def undo_move(self, option_index):
        board_index = self.option_index_to_board_index[option_index]
        # assert option_index not in self.open_option_indices, "Attept to add a undo a move that is already available."
        self.open_option_indices.add(option_index)
        token = self.game_vec[board_index][0]
        # assert token in (-1, 1), "Attempting to remove a move from an empty space: {}, {}".format(self.game_vec, board_index)
        self.game_vec[board_index] = 0
        self.signatures -= self.condition_matrix[:, board_index] * token
        # if numpy.max(self.signatures) + NextMoveClassifier.SIGNATURE_OFFSET >= 387420489:
        #     raise Exception("Broken")
        self.compute_moves_by_property()

    def compute_signature_and_properties_for_space(self, space):
        """Uses the current board and target space to return the signature for that space, and the computed (not cached)
        properties for that space.  This is for testing."""
        arms = list()
        board = yavalath_engine.HexBoard()
        space_to_index = {space: i for i, space in enumerate(board.spaces)}
        arms = list()
        for direction in range(6):
            arm = list()
            for distance in range(3):
                next_space = board.next_space_in_dir(space, direction, distance+1)
                if next_space is None:
                    token = 0
                else:
                    next_space_index = space_to_index[next_space]
                    token = self.game_vec[next_space_index][0]
                arm.append(token)
            arms.append(tuple(arm))
        return NextMoveClassifier.compute_signature_and_properties(tuple(arms))
Ejemplo n.º 18
0
def get_linear_condition_matrix(kernel, delta=0):
    """Applies a linear kernel as much as it can across the board, and returns a condition matrix for this kernel.

    For example, the kernel for winning is (1,1,1,1).  The resulting condition matrix is the set of win conditions.
    The losing kernel is (1,1,1).  The resulting matrix is the set of lose conditions.

    Args:
        kernel:
            Tuple, which will be multiplied by the token values on the board.
        delta:
            By default, the condition is met only if the same piece is at all spaces in the kernel.  For a win, the
            sum must be 4 (or -4) for the condition to be met.  However, for a check (kernel 2,1,1,2), it is useful to
            have the 'condition met' be 5 (or -5), since this can only happen if there are 3 of the same color with
            a space, the check space, between.  In this case, the delta is -1.
    Returns:
        numpy.matrix, with <# conditions> rows and <1 + # board spaces> (= 62) columns.
    """
    """Returns an M x 62 matrix, C, of the win conditions.  Multiply C*P and look for zeros."""
    board = yavalath_engine.HexBoard()
    axis_n_starts = ['e1', 'f1', 'g1', 'h1', 'i1', 'i2', 'i3', 'i4', 'i5']
    axix_0_starts = ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1', 'i1']
    axix_p_starts = ['a5', 'a4', 'a3', 'a2', 'a1', 'b1', 'c1', 'd1', 'e1']
    result = list()
    for coord_u in range(1, 10):
        v_width = 9 - abs(coord_u - 5)
        for v_start_pos in range(v_width + 1 - len(kernel)):
            uvK_tuples = list()
            for kernel_offset, kernel_value in enumerate(kernel):
                coord_v = v_start_pos + 1 + kernel_offset
                uvK_tuples.append((coord_u, coord_v, kernel_value))

            # TODO: Should this be global?
            space_to_index = {
                space: i
                for i, space in enumerate(yavalath_engine.HexBoard().spaces)
            }

            def uvK_condition_in_direction(uvK_tuples, axis_starts, axis):
                N = 61 + 1
                one_condition = numpy.zeros((N, 1), dtype="i1")
                for u, v, K in uvK_tuples:
                    space = board.next_space_in_dir(axis_starts[u - 1], axis,
                                                    v - 1)
                    board_index = space_to_index[space]
                    one_condition[board_index] = K
                one_condition[N - 1] = -sum(kernel) - delta
                return one_condition

            # Add one condition in each of the 3 directions.  The one starting at (coord_u, v_start_pos)
            result.append(
                uvK_condition_in_direction(uvK_tuples, axix_0_starts,
                                           0))  # Direction 1
            result.append(
                uvK_condition_in_direction(uvK_tuples, axix_p_starts,
                                           1))  # Direction 2
            result.append(
                uvK_condition_in_direction(uvK_tuples, axis_n_starts,
                                           5))  # Direction 3

    return numpy.matrix(
        numpy.array(list(itertools.chain(*result))).reshape(
            (len(result), result[0].shape[0])))
Ejemplo n.º 19
0
    print("Result:", r)


def main3():
    v1 = blue_player.get_linear_condition_matrix((1, 1, 1, 1))
    v2 = blue_player.get_win_conditions()
    print("v1 == v2:", (v1 == v2).all())

    v1 = blue_player.get_linear_condition_matrix((1, 1, 1))
    v2 = blue_player.get_loss_conditions()
    print("v1 == v2:", (v1 == v2).all())


space_to_index = {
    space: i
    for i, space in enumerate(yavalath_engine.HexBoard().spaces)
}


def get_board_hash(white_move_indices, black_move_indices):
    key = (tuple(sorted(white_move_indices)),
           tuple(sorted(black_move_indices)))
    return hash(key)


def get_board_hash2(white_move_indices, black_move_indices):
    white_bitset = int()
    for move in white_move_indices:
        white_bitset |= (1 << move)
    black_bitset = int()
    for move in black_move_indices:
Ejemplo n.º 20
0
class Conditions():
    win_conditions = get_win_conditions()
    loss_conditions = get_loss_conditions()
    check_conditions = get_check_conditions()
    board = yavalath_engine.HexBoard()
    space_to_index = {space: i for i, space in enumerate(board.spaces)}
Ejemplo n.º 21
0
def random_player(game_so_far):
    board = yavalath_engine.HexBoard()
    return random.choice(list(set(board.spaces) - set(game_so_far)))