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
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)
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)}")
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)))
def test_1init(self): gs = GameState([1, 2, 3, 4, 5]) try: solve(gs, 3) self.assertTrue(False) except models.solver.Error: pass
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)
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)
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)
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))
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
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)
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])
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)
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))
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)
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()
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] = []
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
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}")
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]) ]))
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