예제 #1
0
 def _generate_node(self, game_state: GameState) -> GameNode:
     """Generate the node with game_state and also its entire subtree. The node is also inserted into the
     corresponding layer.
     Assumption: a node with game_state does not yet exist in the tree (to avoid empty recursive calls)
     :param game_state: a valid game state, that is a descendant of the root game state
     :return: the generated node
     """
     # init
     assert self.find(game_state) is None
     node = GameNode(game_state)
     # generate all child nodes, look for a winning == -1 flag
     minus1_found = False
     for s_game_state in game_state.normalized_successors():
         s_node = self.find(s_game_state)
         if s_node is None:  # recursive guard
             s_node = self._generate_node(s_game_state)  # recursive call
         node.children.append(s_node)
         assert s_node.winning != 0
         if s_node.winning == -1:
             minus1_found = True
     # set the winning flag
     if minus1_found:
         node.winning = 1
     else:
         node.winning = -1
     # insert this node
     n = game_state.get_total_count()
     self.layers[n].insert(node)
     # count nodes
     self.node_count += 1
     # result
     return node
예제 #2
0
 def test_2normalize(self):
     logger.info("test_2normalize")
     rows = [1, 0, 3, 2, 1]
     gs = GameState(rows)
     self.assertEqual(gs.get_rows(), rows)
     self.assertEqual(str(gs), "[10321]")
     self.assertFalse(gs.is_normalized())
     p = gs.normalize()
     self.assertTrue(gs.is_normalized())
     self.assertEqual(gs.get_rows(), [0, 1, 1, 2, 3])
     gs.denormalize(p)
     self.assertEqual(gs.get_rows(), rows)
예제 #3
0
 def test_1GameNode(self):
     logger.info("test_1GameNode")
     gs1 = GameState([1, 2, 2, 4, 4])
     gs2 = GameState([1, 2, 3, 3, 4])
     node1 = GameNode(gs1)
     node2 = GameNode(gs2)
     node3 = node2
     node3.winning = -1
     self.assertTrue(node1 < node2)
     self.assertTrue(node2 == node3)
     logger.info(
         f"node1:{str(node1)}, node2:{str(node2)}, node3:{str(node3)}")
예제 #4
0
 def all_levels(self, level):
     gs = GameState([0, 0, 1, 0, 0])
     gm, cont = solve(gs, level)
     self.assertTrue(gm is None and cont == -1)
     gs = GameState([1, 0, 1, 0, 0])
     gm, cont = solve(gs, level)
     self.assertTrue(gm.row_index in [0, 2] and gm.match_count == 1
                     and cont == 0)
     gs = GameState([0, 0, 3, 0, 0])
     gm, cont = solve(gs, level)
     self.assertTrue(gm.row_index == 2 and 1 <= gm.match_count <= 2
                     and ((gm.match_count == 1) == (cont > 0)))
예제 #5
0
 def test_1init(self):
     gs = GameState([1, 2, 3, 4, 5])
     try:
         solve(gs, 3)
         self.assertTrue(False)
     except models.solver.Error:
         pass
예제 #6
0
 def test_4best(self):
     set_current_tree(GameState([1, 2, 3, 4, 5]))
     self.all_levels(2)
     # winning states
     gs = GameState([0, 2, 1, 1, 1])
     gm, cont = solve(gs, 2)
     self.assertTrue(gm.row_index == 1 and gm.match_count == 2
                     and cont == 3)
     gs = GameState([1, 2, 3, 4, 5])  # see logs of 12345 in test_game_trees
     gm, cont = solve(gs, 2)
     self.assertTrue(cont == 3)
     # looser states
     gs = GameState([0, 1, 1, 0, 1])
     gm, cont = solve(gs, 2)
     self.assertTrue(gm.match_count == 1 and cont == 2)
     gs = GameState([1, 2, 0, 4, 3])  # see logs of 01234 in test_game_trees
     gm, cont = solve(gs, 2)
     self.assertTrue(gm.match_count == 1 and cont == 2)
예제 #7
0
 def test_3most_first(self):
     self.all_levels(1)
     gs = GameState([1, 2, 3, 4, 5])
     gm, cont = solve(gs, 1)
     self.assertTrue(gm.row_index == 4 and gm.match_count == 3
                     and cont == 1)
     gs = GameState([0, 0, 3, 2, 1])
     gm, cont = solve(gs, 1)
     self.assertTrue(gm.row_index == 2 and gm.match_count == 3
                     and cont == 1)
     gs = GameState([0, 2, 1, 1, 1])
     gm, cont = solve(gs, 1)
     self.assertTrue(gm.row_index == 1 and gm.match_count == 2
                     and cont == 1)
     gs = GameState([0, 2, 0, 0, 1])
     gm, cont = solve(gs, 1)
     self.assertTrue(gm.row_index == 1 and gm.match_count == 2
                     and cont == 0)
예제 #8
0
def next_move(rows_state, level):
    """Compute the next move from a given state.

    Method: GET

    Request:
    /next_move [/<rows_state> [/<level>] ]
    Examples:
        /next_move
        /next_move/10340
        /next_move/10340/2
    <rows_state> is a sequence of digits of 0..5.
    Digit at index k must be <= k+1 (where first index is k=0).
    <level> is integer in 0..2

    Response: see also doc of return value of solver.solve.
        One of the 3 json strings:
        1. {“gameContinues“:-1}
           Meaning: "You won".
        2. {“gameContinues“:c, "rowIndex":i, "numberOfMatches":n}
           where c in 0..3, i in 0..4, n in 1..3.
           Meaning:
             The move of the app is taking n matches from the row with index i.
             c == 0: "the App won". There is only 1 match left.
             c == 1: game continues, i.e. more than 1 match left. No further information.
             c == 2: game continues and you have a safe strategy to win.
             c == 3: game continues and your opponent (the App) has a safe strategy to win.
        3. {"error" : message}
           where message is a string describing the error.
           Meaning:
           A software error occurred while the app executed the request.
    """
    result = {}
    try:
        # log
        logging.info(f"next_move, rows {rows_state}, level {level}")
        # check and convert input
        rows = list(rows_state)
        game_states.Error.check(all([('0' <= rows[k] <= '5') for k in range(len(rows))]),
                                "rows_state must contain digits in 0..5")
        rows = [int(rows[k]) for k in range(len(rows))]
        game_state = GameState(rows)
        # compute next move
        game_move, game_continues = solver.solve(game_state, level)
        # compose result
        result["gameContinues"] = game_continues
        if game_continues >= 0:
            result["rowIndex"] = game_move.row_index
            result["numberOfMatches"] = game_move.match_count
    except (solver.Error, game_states.Error) as e:
        result["error"] = str(e)
    finally:
        pass  # no return here, see PEP 601
    # return result
    logging.info(f"next_move, result {result}")
    return json.dumps(result)
예제 #9
0
 def test_2GameLayer(self):
     logger.info("test_2GameLayer")
     layer = GameLayer(5)
     self.assertEqual(layer.nodes, [])
     gs1 = GameState([0, 0, 0, 2, 3])  # gs1
     node = layer.find(gs1)
     self.assertTrue(node is None)
     node1 = GameNode(gs1)
     layer.insert(node1)
     node = layer.find(gs1)
     self.assertTrue(node == node1)
     gs2 = GameState([0, 1, 1, 1, 2])  # gs1 < gs2
     node = layer.find(gs2)
     self.assertTrue(node is None)
     node2 = GameNode(gs2)
     layer.insert(node2)
     node = layer.find(gs2)
     self.assertTrue(node == node2)
     gs3 = GameState([0, 0, 0, 0, 5])  # gs3 < gs1
     node = layer.find(gs3)
     self.assertTrue(node is None)
     node3 = GameNode(gs3)
     layer.insert(node3)
     node = layer.find(gs3)
     self.assertTrue(node == node3)
     gs4 = GameState([0, 0, 0, 1, 4])  # gs3 < gs4 < gs1
     node = layer.find(gs4)
     self.assertTrue(node is None)
     node4 = GameNode(gs4)
     layer.insert(node4)
     node = layer.find(gs4)
     self.assertTrue(node == node4)
     gs5 = GameState([1, 1, 1, 1, 1])  # gs3 < gs4 < gs1 < gs2 < gs5
     node = layer.find(gs5)
     self.assertTrue(node is None)
     node5 = GameNode(gs5)
     layer.insert(node5)
     node = layer.find(gs5)
     self.assertTrue(node == node5)
     self.assertTrue(layer.is_sorted_lt())
     logger.info("layer.nodes:" + str(layer.nodes))
예제 #10
0
 def test_1init(self):
     logger.info("test_1init")
     try:
         GameState('12345')
         self.assertTrue(False)
     except Error:
         pass
     try:
         GameState(list('123'))
         # GameState(['2', '3'])
         self.assertTrue(False)
     except Error:
         pass
     try:
         GameState([1, 2, 3, 'a', 5])
         self.assertTrue(False)
     except Error:
         pass
     try:
         GameState([0, 3, 4, 5, 5])
         self.assertTrue(False)
     except Error:
         pass
     try:
         GameState([1, 3, 3, 4, 5])
         self.assertTrue(False)
     except Error:
         pass
     try:
         GameState([0, 0, 0, 0, 0])
         self.assertTrue(False)
     except Error:
         pass
예제 #11
0
 def test_4select_move(self):
     logger.info("test_4select_move")
     # 00123
     gs = GameState([0, 0, 1, 2, 3])
     tree = GameTree(gs)
     self.assertEqual(tree.root_node.winning, -1)
     # 1st  move
     game_move, winning = tree.root_node.select_move()
     self.assertEqual(winning, 1)
     new_game_state = tree.root_node.game_state.make_move(game_move)
     new_node = tree.find(new_game_state)
     self.assertEqual(new_node.winning, 1)
     self.assertEqual(new_node.game_state.get_total_count(),
                      tree.root_node.game_state.get_total_count() - 1)
     # 2nd move
     game_move, winning = new_node.select_move()
     self.assertEqual(winning, -1)
     new_game_state = new_node.game_state.make_move(game_move)
     new_node = tree.find(new_game_state)
     self.assertEqual(new_node.winning, -1)
     # 12345
     gs = GameState([1, 2, 3, 4, 5])
     tree = GameTree(gs)
     self.assertEqual(tree.root_node.winning, 1)  # see logs above
     # 1st  move
     game_move, winning = tree.root_node.select_move()
     self.assertEqual(winning, -1)
     new_game_state = tree.root_node.game_state.make_move(game_move)
     new_node = tree.find(new_game_state)
     self.assertEqual(new_node.winning, -1)
     # 2nd move
     game_move, winning = new_node.select_move()
     self.assertEqual(winning, 1)
     new_game_state = new_node.game_state.make_move(game_move)
     new_node = tree.find(new_game_state)
     self.assertEqual(new_node.winning, 1)
예제 #12
0
 def __init__(self, game_state: GameState):
     """Create the tree whose root-node contains game_state. """
     # for tests and logs only
     self.node_count: int = 0
     # generate layers
     self.layers: List[GameLayer] = []
     self.total_count: int = game_state.get_total_count()
     for n in range(self.total_count + 1):
         self.layers.append(GameLayer(n))
     # generate root node -- and recursively all nodes
     self.root_node: GameNode = self._generate_node(game_state)
     # checks
     assert self.node_count == sum(
         [len(layer.nodes) for layer in self.layers])
     assert all([layer.is_sorted_lt() for layer in self.layers])
예제 #13
0
 def test_3ordering(self):
     logger.info("test_3ordering")
     gs_max = GameState([1, 2, 3, 4, 5])
     gs1 = GameState([1, 0, 0, 0, 0])
     gs2 = GameState([0, 1, 0, 0, 0])
     gs_min = GameState([0, 0, 0, 0, 1])
     self.assertTrue(gs_min < gs2)
     self.assertTrue(gs2 < gs1)
     self.assertTrue(gs1 < gs_max)
     gs3 = GameState([1, 2, 0, 4, 0])
     gs4 = GameState([1, 1, 3, 4, 5])
     self.assertTrue(gs_min < gs4 < gs3 < gs_max)
     self.assertTrue(gs_min <= gs4 <= gs3 <= gs_max)
예제 #14
0
 def test_5get_move(self):
     logger.info("test_5get_move")
     gs1 = GameState([1, 2, 3, 4, 5])
     gs2 = GameState([1, 2, 2, 3, 5])
     self.assertEqual(gs1.get_move(gs2), GameMove(3, 2))
     gs1 = GameState([0, 0, 1, 1, 1])
     gs2 = GameState([0, 0, 0, 1, 1])
     gm = gs1.get_move(gs2)
     self.assertTrue(gm.match_count == 1 and gm.row_index in range(2, 5))
     gs1 = GameState([1, 2, 3, 4, 5])
     gs2 = GameState([0, 2, 3, 4, 5])
     self.assertEqual(gs1.get_move(gs2), GameMove(0, 1))
     gs1 = GameState([1, 2, 3, 4, 5])
     gs2 = GameState([1, 2, 2, 3, 4])
     self.assertEqual(gs1.get_move(gs2), GameMove(4, 3))
예제 #15
0
 def test_2random(self):
     self.all_levels(0)
     gs = GameState([0, 0, 0, 4, 5])
     gm, cont = solve(gs, 0)
     self.assertTrue(gm.row_index in [3, 4] and 1 <= gm.match_count <= 3
                     and cont > 0)
예제 #16
0
    try:
        # log
        logging.info(f"next_move, rows {rows_state}, level {level}")
        # check and convert input
        rows = list(rows_state)
        game_states.Error.check(all([('0' <= rows[k] <= '5') for k in range(len(rows))]),
                                "rows_state must contain digits in 0..5")
        rows = [int(rows[k]) for k in range(len(rows))]
        game_state = GameState(rows)
        # compute next move
        game_move, game_continues = solver.solve(game_state, level)
        # compose result
        result["gameContinues"] = game_continues
        if game_continues >= 0:
            result["rowIndex"] = game_move.row_index
            result["numberOfMatches"] = game_move.match_count
    except (solver.Error, game_states.Error) as e:
        result["error"] = str(e)
    finally:
        pass  # no return here, see PEP 601
    # return result
    logging.info(f"next_move, result {result}")
    return json.dumps(result)


mylogconfig.simplest()
set_current_tree(GameState([1, 2, 3, 4, 5]))

if __name__ == '__main__':
    app.run()
예제 #17
0
 def __init__(self, game_state: GameState):
     assert game_state.is_normalized()
     self.game_state: GameState = game_state
     self.winning: int = 0
     self.children: List[GameNode] = []
예제 #18
0
 def find(self, game_state: GameState) -> GameNode or None:
     """Return the the tree-node containing game_state, None if not found."""
     n = game_state.get_total_count()
     assert n <= self.total_count
     node = self.layers[n].find(game_state)
     return node
예제 #19
0
 def test_3GameTree(self):
     logger.info("test_3GameTree")
     # the most simple game
     gs = GameState([0, 0, 0, 0, 1])
     tree = GameTree(gs)
     self.assertEqual(tree.node_count, 1)
     self.assertEqual(tree.root_node.winning, -1)
     # some simple situations
     gs = GameState([0, 0, 0, 1, 1])
     tree = GameTree(gs)
     self.assertEqual(tree.node_count, 2)
     self.assertEqual(tree.root_node.winning, 1)
     gs = GameState([0, 0, 1, 1, 1])
     tree = GameTree(gs)
     self.assertEqual(tree.node_count, 3)
     self.assertEqual(tree.root_node.winning, -1)
     gs = GameState([0, 0, 0, 0, 2])
     tree = GameTree(gs)
     self.assertEqual(tree.node_count, 2)
     self.assertEqual(tree.root_node.winning, 1)
     gs = GameState([0, 0, 0, 0, 5])
     tree = GameTree(gs)
     self.assertEqual(tree.node_count, 5)
     self.assertEqual(tree.root_node.winning, -1)
     gs = GameState([0, 0, 0, 2, 3])
     tree = GameTree(gs)
     self.assertEqual(len(tree.layers[4].nodes), 2)
     self.assertEqual(len(tree.layers[3].nodes), 2)
     self.assertEqual(len(tree.layers[2].nodes), 2)
     self.assertEqual(tree.node_count, 8)
     self.assertEqual(tree.root_node.winning, 1)
     gs = GameState([0, 0, 0, 2, 2])
     tree = GameTree(gs)
     self.assertEqual(tree.root_node.winning, -1)
     gs = GameState([0, 1, 1, 2, 2])
     tree = GameTree(gs)
     self.assertEqual(tree.root_node.winning, -1)
     # the "start" situations with 2 to 5 rows
     gs = GameState([0, 0, 0, 1, 2])
     tree = GameTree(gs)
     logger.info("tree root: " + str(tree.root_node) +
                 f" children: {[str(c) for c in tree.root_node.children]}")
     self.assertEqual(tree.node_count, 4)
     self.assertEqual(tree.root_node.winning, 1)
     gs = GameState([0, 0, 1, 2, 3])
     tree = GameTree(gs)
     logger.info("tree root: " + str(tree.root_node) +
                 f" children: {[str(c) for c in tree.root_node.children]}")
     self.assertEqual(len(tree.layers[5].nodes), 3)  # 00023,00113,00122
     self.assertEqual(len(tree.layers[4].nodes), 3)  # 00013,00022,00112
     self.assertEqual(len(tree.layers[3].nodes), 3)  # 00003,00012,00111
     self.assertEqual(len(tree.layers[2].nodes), 2)  # 00002,00011
     self.assertEqual(tree.node_count, 13)
     self.assertEqual(tree.root_node.winning, -1)
     gs = GameState([0, 1, 2, 3, 4])
     tree = GameTree(gs)
     logger.info("tree root: " + str(tree.root_node) +
                 f" children: {[str(c) for c in tree.root_node.children]}")
     logger.info(f"node count: {tree.node_count}")
     logger.info(f"winning: {tree.root_node.winning}")
     gs = GameState([1, 2, 3, 4, 5])
     tree = GameTree(gs)
     logger.info("tree root: " + str(tree.root_node) +
                 f" children: {[str(c) for c in tree.root_node.children]}")
     logger.info(f"node count: {tree.node_count}")
     logger.info(f"winning: {tree.root_node.winning}")
예제 #20
0
 def test_4successors_and_make_move(self):
     logger.info("test_4successors_and_make_move")
     # possible move
     self.assertTrue(
         GameState([1, 0, 0, 3, 0]).is_possible_move(GameMove(3, 3)))
     self.assertTrue(
         GameState([1, 0, 0, 3, 0]).is_possible_move(GameMove(0, 1)))
     self.assertFalse(
         GameState([1, 0, 0, 3, 0]).is_possible_move(GameMove(0, 2)))
     self.assertFalse(
         GameState([1, 0, 0, 3, 0]).is_possible_move(GameMove(1, 1)))
     self.assertFalse(
         GameState([0, 0, 0, 3, 0]).is_possible_move(GameMove(3, 3)))
     # make move
     self.assertEqual(
         GameState([1, 2, 3, 4, 5]).make_move(GameMove(1, 2)),
         GameState([1, 0, 3, 4, 5]))
     self.assertEqual(
         GameState([0, 0, 0, 3, 0]).make_move(GameMove(3, 2)),
         GameState([0, 0, 0, 1, 0]))
     self.assertEqual(
         GameState([1, 0, 0, 3, 0]).make_move(GameMove(0, 1)),
         GameState([0, 0, 0, 3, 0]))
     # normalized successors
     self.assertEqual(
         GameState([0, 0, 0, 0, 1]).normalized_successors(), [])
     self.assertEqual(
         sorted(GameState([0, 0, 0, 0, 3]).normalized_successors()),
         sorted([GameState([0, 0, 0, 0, 2]),
                 GameState([0, 0, 0, 0, 1])]))
     self.assertEqual(
         sorted(GameState([0, 0, 1, 2, 2]).normalized_successors()),
         sorted([
             GameState([0, 0, 0, 2, 2]),
             GameState([0, 0, 1, 1, 2]),
             GameState([0, 0, 0, 1, 2])
         ]))
     self.assertEqual(
         len(GameState([1, 2, 3, 4, 5]).normalized_successors()), 5 + 4 + 3)
     self.assertEqual(
         sorted(GameState([1, 2, 3, 4, 5]).normalized_successors()),
         sorted([
             GameState([0, 2, 3, 4, 5]),
             GameState([1, 1, 3, 4, 5]),
             GameState([1, 2, 2, 4, 5]),
             GameState([1, 2, 3, 3, 5]),
             GameState([1, 2, 3, 4, 4]),
             GameState([0, 1, 3, 4, 5]),
             GameState([1, 1, 2, 4, 5]),
             GameState([1, 2, 2, 3, 5]),
             GameState([1, 2, 3, 3, 4]),
             GameState([0, 1, 2, 4, 5]),
             GameState([1, 1, 2, 3, 5]),
             GameState([1, 2, 2, 3, 4])
         ]))
예제 #21
0
def solve(game_state: GameState, level: int) -> (GameMove or None, int):
    """Compute the next move.
    :param game_state: a valid game_state
    :param level: the smartness level, must be in 0..2.
    :return: result[0] the game-move
             result[1] game continues, int in [-1, 0, 1, 2, 3]
                       -1 : "You won". Occurs when input game_state contains exactly 1 match.
                            In this case, result[0] == None. In all other cases result[0] != None.
                        0 : "I won". Occurs when game_state after making the move specified by result[0]
                            contains exactly 1 match.
                        1 : game continues -- no further information
                        2 : game continues -- you have a safe strategy to win.
                        3 : game continues -- your opponent (i.e. I) has a safe strategy to win
    :raise: Error, if level invalid.
    """
    Error.check(0 <= level <= 2, "level must be an integer in 0..2")
    rows = game_state.get_rows()

    # sub functions

    def random_move() -> GameMove:
        """Choose randomly one of the possible moves."""
        non_zeros = [k for k in range(5)
                     if rows[k] > 0]  # all indices with value > 0
        row_index = rand.choice(non_zeros)  # choose such index
        max_n = min(
            3,
            rows[row_index])  # max number of matches to be taken at this index
        if len(
                non_zeros
        ) == 1:  # special case: only this row has matches --> must not make_move all
            max_n = min(max_n, rows[row_index] - 1)
        match_count = rand.randint(
            1, max_n)  # choose number of matches at this index
        return GameMove(row_index, match_count)

    def most_first() -> GameMove:
        """Choose a row with the most matches and take as many matches as possible."""
        p = game_state.normalize()
        sorted_rows = game_state.get_rows()
        match_count = min(3, sorted_rows[4])  # max number of matches
        if sorted_rows[
                3] == 0:  # special case: only this row has matches --> must not take all
            match_count = min(match_count, sorted_rows[4] - 1)
        row_index = p(4)
        return GameMove(row_index, match_count)

    def best_move() -> (GameMove, int):
        """Choose best possible move, if several exist, choose one randomly.
        Return also the winning flag of the resulting node.
        """
        p = game_state.normalize()
        node = current_tree().find(game_state)
        _game_move, _winning = node.select_move()
        _game_move.row_index = p(_game_move.row_index)
        return _game_move, _winning

    # todo: new intermediate level between 1 and 2: start randomly, switch to best when more than half of
    # the matches have been taken.

    # main body continued

    # init to "you won"
    game_move = None
    game_continues = -1
    # not "you won"
    if sum(rows) > 1:
        # choose an algorithm
        winning = 0
        if level == 0:
            game_move = random_move()
        elif level == 1:
            game_move = most_first()
        else:
            game_move, winning = best_move()
        assert 1 <= game_move.match_count <= min(3, rows[game_move.row_index])
        # check whether I won or game continues
        assert sum(rows) - game_move.match_count > 0
        if sum(rows) - game_move.match_count > 1:
            game_continues = 1
            if winning == 1:
                game_continues = 2
            elif winning == -1:
                game_continues = 3
        else:  # I won
            game_continues = 0
    # you won
    else:
        assert sum(rows) == 1
    return game_move, game_continues