def get_player_actions_from(self, player: Player) -> List[Action]: """ For the given player, find all possible moves for each of this players penguins. :param player: The player whose possible moves will be calculated. :return: List[Action] representing all possible moves for this Player's penguins """ if player is None: return None if player == 0: return None player_actions = [] for penguin in player.penguins: reachable_tiles = self.board.get_reachable_tiles( penguin.row, penguin.col, [ penguin for _, player in self.players.items() for penguin in player.penguins ]) potential_actions = [] for reachable_tile in reachable_tiles: potential_actions.append( Action(Coordinate(penguin.row, penguin.col), Coordinate(reachable_tile.row, reachable_tile.col), player.color)) player_actions.extend(potential_actions) return player_actions
def test_notify_player_placement_invalid_movement(self): state = self.setup() state = state.add_penguin(row=0, col=0, color="red") state = state.add_penguin(row=1, col=1, color="white") state = state.finalize() expected_action = Action(start=Coordinate(row=0, col=0), end=Coordinate(row=3, col=2), player_color="red") not_expected_action = Action(start=Coordinate(row=1, col=1), end=Coordinate(row=2, col=2), player_color="white") mocked_strategy_player1 = GenericStrategyComponent() mocked_strategy_player1.choose_action_from_state = MagicMock( return_value=expected_action) mocked_strategy_player2 = GenericStrategyComponent() mocked_strategy_player2.choose_action_from_state = MagicMock( return_value=not_expected_action) player_1_component = PlayerComponent(strategy=mocked_strategy_player1) player_2_component = PlayerComponent(strategy=mocked_strategy_player2) ref = Referee(players=[player_1_component, player_2_component], num_rows=4, num_cols=3) expected = state.kick_player(player_to_kick=state.current_player) ref.state = state ref.end_penguin_placement() ref.notify_players_movement() actual = ref.state self.assertTrue(actual.is_equal(expected))
def test_get_reachable_tile_coordinates_by_direction(self): model = FishBoard(num_rows=4, num_cols=3) tile = model.board[1][2] direction = 'bottom_left_coordinate' expected = [Coordinate(row=2, col=2), Coordinate(row=3, col=1)] self.assertEqual( model.get_reachable_tile_coordinates_by_direction(tile, direction), expected)
def test_take_action_failure(self): state, *_ = self.setup() tree = GameStateTree(state) self.assertEqual( tree.take_action( Action(start=Coordinate(0, 1), end=Coordinate(2, 2), player_color="red")), "Player color red does not own penguin at row 0, col 1")
def test_get_reachable_tiles(self): model = FishBoard(num_rows=5, num_cols=4) expected_reachable = [ Coordinate(row=0, col=1), Coordinate(row=1, col=1), Coordinate(row=0, col=2), Coordinate(row=3, col=1), Coordinate(row=4, col=2), Coordinate(row=4, col=1), Coordinate(row=3, col=0), Coordinate(row=4, col=0), Coordinate(row=1, col=0), Coordinate(row=0, col=0) ] self.assertEqual(model.get_reachable_tiles(2, 1), expected_reachable)
def __init__(self, num_rows, num_cols, board: List[List[Tile]] = None): """ :param num_rows: the number of rows :param num_cols: the number of cols """ if num_rows <= 0 or num_cols <= 0: raise ValueError( f"[FishGame] : invalid board dimensions: ({num_cols}, {num_rows})" ) self.num_rows = num_rows self.num_cols = num_cols if board is None: self.board: List[List[Tile]] = [] for r in range(self.num_rows): row = [] for c in range(self.num_cols): tile = Tile.from_coordinate(coordinate=Coordinate(row=r, col=c), num_fish=0, num_rows=num_rows, num_cols=self.num_cols) row.append(tile) self.board.append(row) else: self.board = board self.neighboring_coordinates = [ 'top_coordinate', 'top_right_coordinate', 'bottom_right_coordinate', 'bottom_coordinate', 'bottom_left_coordinate', 'top_left_coordinate' ]
def add_penguin(self, row, col, color): """ Add a penguin for the given player id at the given row and column :param row: the row of the tile to add penguin on :param col: the col of the tile to add penguin on :param color: the color who will be adding the penguin :return: Updated GameState with added penguin """ is_valid, err = self.can_add_penguin(row=row, col=col, color=color) if not is_valid: raise ValueError(err) players = [] for player in self.players.values(): if player.color == color: players.append( Player.from_dict( { **player._asdict(), **{ "penguins": player.penguins + [(Coordinate(row, col))] } })) else: players.append(player) return FishGameState(board=self.board, num_players=self.num_players, players=players, current_player=self.next_player(), phase=self.phase)
def test_create_choose_action_base_case_no_children_for_maximizing_player( self): """ If the player has turns > 0 but has no children - end-game - then return the current's player's previous action """ state, *_ = self.setup() production_tree = GameStateTree(state, previous_action=None) expected = Action( player_color=production_tree.state.current_player.color, start=Coordinate(0, 0), end=Coordinate(1, 0)) production_tree.previous_action = expected actual = GenericStrategyComponent.choose_action(tree=production_tree, num_turns=2) self.assertEqual(expected, actual)
def test_create_choose_action_base_case_0_turns(self): """ If the player has 0 turns and it is the currents player turn, then the previous action is return. """ state, *_ = self.setup() production_tree = GameStateTree(state, previous_action=None) production_tree.get_children = MagicMock(return_value=None) expected = Action( player_color=production_tree.state.current_player.color, start=Coordinate(0, 0), end=Coordinate(1, 0)) production_tree.previous_action = expected actual = GenericStrategyComponent.choose_action(tree=production_tree, num_turns=0) self.assertEqual(expected, actual)
def test_add_penguin_success(self): board = FishBoard(4, 3) factory = FishGameState(board=board, num_players=2, players=TestGameState.player_list, phase=GameStatePhase.INITIAL, current_player=TestGameState.player_list[0]) factory = factory.add_penguin(0, 0, "red") expected_penguins = [Coordinate(0, 0)] self.assertEqual(factory.players["red"].penguins, expected_penguins)
def test_create_choose_action_base_case_no_children_for_non_maximizing_player( self): """ If the player has turns > 0 but has no children - end-game - and this is not the maximizing player, then we return None. """ state, *_ = self.setup() production_tree = GameStateTree(state, previous_action=None) production_tree.get_children = MagicMock(return_value=[]) production_tree.previous_action = Action( player_color=production_tree.state.current_player.color, start=Coordinate(0, 0), end=Coordinate(1, 0)) expected = None _, actual = GenericStrategyComponent.minimax(tree=production_tree, num_rounds=2, maximizing_player=Player( "white", 0, 0, [])) self.assertEqual(expected, actual)
def test_create_choose_action_recursive_case_is_maximizing_twice(self): """ In a recursive case where 1 round is played and the all leave nodes are the maximizing players, the minimum of these leave nodes is return. """ state, *_ = self.setup() production_tree = GameStateTree(state, previous_action=None) expected = Action( player_color=production_tree.state.current_player.color, start=Coordinate(0, 0), end=Coordinate(1, 0)) children = [child for child in production_tree.get_children()] for idx, child in enumerate(children): second_players_children = [ second_players_child for second_players_child in child.get_children() ] for jdx, second_players_child in enumerate( second_players_children): if jdx == 1 and idx == 1: score = 0 child.previous_action = expected else: score = 10 modified_player = Player.from_dict({ **second_players_child.state.current_player._asdict(), **{ 'score': score } }) second_players_child.state.current_player = modified_player second_players_child.get_children = MagicMock(return_value=[]) second_players_child.state.check_any_player_can_move = MagicMock( return_value=False) production_tree.get_children = MagicMock(return_value=children) actual = GenericStrategyComponent.choose_action(tree=production_tree, num_turns=2) self.assertEqual(expected, actual)
def place_penguin(state: FishGameState) -> Coordinate: """ Place a penguin in the next available place in the given state's board according to a zig-zag pattern that begins in the top left corner. :param state: The state that will have penguins added to it. :return: The updated state after adding the penguin in the correct place. """ for r, row in enumerate(state.board.board): for c, col in enumerate(row): if state.validate_input(state.current_player.color, r, c)[0]: return Coordinate(r, c) raise ValueError("No available tiles for penguin placement.")
def test_create_choose_action_recursive_case_is_maximizing_once(self): """ In a scenario where all the children of the maximizing player are leaves - aka end-games - than a tie-breaker decides which action to take. """ state, *_ = self.setup() production_tree = GameStateTree(state, previous_action=None) expected = Action( player_color=production_tree.state.current_player.color, start=Coordinate(0, 0), end=Coordinate(1, 0)) children = [child for child in production_tree.get_children()] for idx, child in enumerate(children): if idx == 1: child.previous_action = expected child.get_children = MagicMock(return_value=[]) production_tree.get_children = MagicMock(return_value=children) actual = GenericStrategyComponent.choose_action(tree=production_tree, num_turns=2) self.assertEqual(expected, actual)
def test_move_penguin(self): board = FishBoard(4, 3) factory = FishGameState(board=board, num_players=3, players=TestGameState.player_list, phase=GameStatePhase.INITIAL, current_player=TestGameState.player_list[0]) factory = factory.add_penguin(row=0, col=1, color="red") factory = factory.add_penguin(row=0, col=0, color="white") factory = factory.add_penguin(row=3, col=0, color="brown") game_state = factory.finalize() game_state = game_state.move_penguin("red", 0, 1, 2, 1) expected_penguins = [Coordinate(row=2, col=1)] self.assertEqual(game_state.players["red"].penguins, expected_penguins)
def test_notify_player_placement_invalid(self): state = self.setup() expected_placement = Coordinate(row=5, col=5) mocked_strategy_player1 = GenericStrategyComponent() mocked_strategy_player1.place_penguin = MagicMock( return_value=expected_placement) player_1_component = PlayerComponent(strategy=mocked_strategy_player1) player_2_component = PlayerComponent() ref = Referee(players=[player_1_component, player_2_component], num_rows=4, num_cols=3) expected = state.kick_player(player_to_kick=state.current_player) ref.notify_player_placement() self.assertTrue(ref.state.is_equal(expected))
def is_reachable_from(self, start_row, start_col, end_row, end_col, unreachable_tiles: List[Coordinate] = None): """ Given the row,col information, determine whether the ending row and column is reachable from the starting row and column. Reachability is determined if the end row, col follows a straight path from the starting row and column. Holes or the provided unreachable tiles act as barriers meaning that tiles on or beyond them are not reachable. :param start_row: the starting row :param start_col: the starting column :param end_row: the ending row :param end_col: the ending cool :param unreachable_tiles: a list of Coordinate that are unreachable at it and past it. :return: a boolean representing if the ending row, col is reachable. """ return Coordinate(end_row, end_col) in self.get_reachable_tiles( start_row, start_col, unreachable_tiles)
def move_penguin(self, color, start_row, start_col, end_row, end_col): """ Move penguin for player with color from start position to end position if possible. :param color: the color of the player who wants to move the penguin :param start_row: the current row of the penguin to be moved :param start_col: the current col of the penguin to be moved :param end_row: the desired row to which the penguin will be moved :param end_col: the desired col to which the penguin will be moved :return: a new FishGameState with the action taken. """ is_valid, err = self.can_move_penguin(color, start_row, start_col, end_row, end_col) if not is_valid: raise ValueError(err) players = [ Player.from_dict({ **self.players[color]._asdict(), **{ "score": (self.players[color].score + self.board.retrieve_num_fish( start_row, start_col)) }, **{ "penguins": [ Coordinate(end_row, end_col) if penguin.row == start_row and penguin.col == start_col else penguin for penguin in self.players[color].penguins ] } }) if player.color == color else player for player in self.players.values() ] board = self.board.create_hole(start_row, start_col) return FishGameState(board=board, num_players=self.num_players, players=players, current_player=self.next_player(), phase=self.phase)
def test_create_board(self): model = FishBoard(num_rows=4, num_cols=3) expected_board = [[ Tile(coordinate=Coordinate(row=0, col=0), is_tile=True, num_fish=0, top_coordinate=None, top_left_coordinate=None, top_right_coordinate=None, bottom_coordinate=(2, 0), bottom_left_coordinate=None, bottom_right_coordinate=(1, 0)), Tile(coordinate=Coordinate(row=0, col=1), is_tile=True, num_fish=0, top_coordinate=None, top_left_coordinate=None, top_right_coordinate=None, bottom_coordinate=(2, 1), bottom_left_coordinate=(1, 0), bottom_right_coordinate=(1, 1)), Tile(coordinate=Coordinate(row=0, col=2), is_tile=True, num_fish=0, top_coordinate=None, top_left_coordinate=None, top_right_coordinate=None, bottom_coordinate=(2, 2), bottom_left_coordinate=(1, 1), bottom_right_coordinate=(1, 2)) ], [ Tile(coordinate=Coordinate(row=1, col=0), is_tile=True, num_fish=0, top_coordinate=None, top_left_coordinate=(0, 0), top_right_coordinate=(0, 1), bottom_coordinate=(3, 0), bottom_left_coordinate=(2, 0), bottom_right_coordinate=(2, 1)), Tile(coordinate=Coordinate(row=1, col=1), is_tile=True, num_fish=0, top_coordinate=None, top_left_coordinate=(0, 1), top_right_coordinate=(0, 2), bottom_coordinate=(3, 1), bottom_left_coordinate=(2, 1), bottom_right_coordinate=(2, 2)), Tile(coordinate=Coordinate(row=1, col=2), is_tile=True, num_fish=0, top_coordinate=None, top_left_coordinate=(0, 2), top_right_coordinate=None, bottom_coordinate=(3, 2), bottom_left_coordinate=(2, 2), bottom_right_coordinate=None) ], [ Tile(coordinate=Coordinate(row=2, col=0), is_tile=True, num_fish=0, top_coordinate=(0, 0), top_left_coordinate=None, top_right_coordinate=(1, 0), bottom_coordinate=None, bottom_left_coordinate=None, bottom_right_coordinate=(3, 0)), Tile(coordinate=Coordinate(row=2, col=1), is_tile=True, num_fish=0, top_coordinate=(0, 1), top_left_coordinate=(1, 0), top_right_coordinate=(1, 1), bottom_coordinate=None, bottom_left_coordinate=(3, 0), bottom_right_coordinate=(3, 1)), Tile(coordinate=Coordinate(row=2, col=2), is_tile=True, num_fish=0, top_coordinate=(0, 2), top_left_coordinate=(1, 1), top_right_coordinate=(1, 2), bottom_coordinate=None, bottom_left_coordinate=(3, 1), bottom_right_coordinate=(3, 2)) ], [ Tile(coordinate=Coordinate(row=3, col=0), is_tile=True, num_fish=0, top_coordinate=(1, 0), top_left_coordinate=(2, 0), top_right_coordinate=(2, 1), bottom_coordinate=None, bottom_left_coordinate=None, bottom_right_coordinate=None), Tile(coordinate=Coordinate(row=3, col=1), is_tile=True, num_fish=0, top_coordinate=(1, 1), top_left_coordinate=(2, 1), top_right_coordinate=(2, 2), bottom_coordinate=None, bottom_left_coordinate=None, bottom_right_coordinate=None), Tile(coordinate=Coordinate(row=3, col=2), is_tile=True, num_fish=0, top_coordinate=(1, 2), top_left_coordinate=(2, 2), top_right_coordinate=None, bottom_coordinate=None, bottom_left_coordinate=None, bottom_right_coordinate=None) ]] self.assertEqual(model.num_rows, 4) self.assertEqual(model.num_cols, 3) self.assertEqual(model.board, expected_board)
parser.add_argument('--board-demo1', dest="board_demo1", action="store_true") parser.add_argument('--board-demo2', dest="board_demo2", action="store_true") parser.add_argument('--board-demo3', dest="board_demo3", action="store_true") parser.add_argument('--state-demo1', dest="state_demo1", action="store_true") parser.add_argument('--state-demo2', dest="state_demo2", action="store_true") args = parser.parse_args() return args if __name__ == '__main__': args = parse_arguments() if args.board_demo1: controller = board.Demo1() elif args.board_demo2: controller = board.Demo2(2) elif args.board_demo3: controller = board.Demo3(holes=[ Coordinate(row=1, col=0), Coordinate(row=0, col=1), ]) elif args.state_demo1: controller = state.Demo1() elif args.state_demo2: controller = state.Demo2() else: raise ValueError("Must provide demo number") controller.run()