Пример #1
0
    def generate_first_move(
            self, tiles: List[Tile], board_state: BoardState
    ) -> Result[Tuple[BoardPosition, Tile, PortID]]:
        """
        Generate the first move by choosing from the given list of tiles. Returns the move along with
        the port that the player's token should be placed on. `set_color` and `set_rule_checker` will
        be called prior to this method.

        This strategy just chooses the first valid move checking positions clockwise starting from (1,0)
        and checking ports clockwise from top left.

        :param tiles:           The set of tile options for the first move
        :param board_state:     The state of the current board
        :return:                A result containing a tuple containing the board position, tile, and port ID
                                for the player's initial move
        """
        if len(tiles) != EXPECTED_TILE_COUNT_INITIAL_MOVE:
            return error(
                f"Strategy.generate_first_move given {len(tiles)} (expected {EXPECTED_TILE_COUNT_INITIAL_MOVE})"
            )

        tile = tiles[2]

        # Check the top of the board (exclusive of 0,0) for valid positions
        y = MIN_BOARD_COORDINATE
        for x in range(MIN_BOARD_COORDINATE + 1, MAX_BOARD_COORDINATE + 1):
            r_move = self._find_valid_move(board_state, BoardPosition(x, y))
            if r_move.is_ok():
                pos, port = r_move.value()
                return ok((pos, tile, port))

        # Check the right side of the board for valid positions
        x = MAX_BOARD_COORDINATE
        for y in range(MIN_BOARD_COORDINATE, MAX_BOARD_COORDINATE + 1):
            r_move = self._find_valid_move(board_state, BoardPosition(x, y))
            if r_move.is_ok():
                pos, port = r_move.value()
                return ok((pos, tile, port))

        # Check the bottom of the board for valid positions
        y = MAX_BOARD_COORDINATE
        for x in reversed(range(MIN_BOARD_COORDINATE,
                                MAX_BOARD_COORDINATE + 1)):
            r_move = self._find_valid_move(board_state, BoardPosition(x, y))
            if r_move.is_ok():
                pos, port = r_move.value()
                return ok((pos, tile, port))

        # Check the left side of the board for valid positions (inclusive of 0,0)
        x = MIN_BOARD_COORDINATE
        for y in reversed(range(MIN_BOARD_COORDINATE,
                                MAX_BOARD_COORDINATE + 1)):
            r_move = self._find_valid_move(board_state, BoardPosition(x, y))
            if r_move.is_ok():
                pos, port = r_move.value()
                return ok((pos, tile, port))

        # No move found
        return error("Failed to find a valid initial move!")
Пример #2
0
def test_exceptions() -> None:
    with pytest.raises(ValueError):
        result.ok("a") == result.ok("b")
    with pytest.raises(ValueError):
        if result.ok("a"):
            pass
    with pytest.raises(ValueError):
        hash(result.ok("a"))
Пример #3
0
    def _move_player_along_path(self, player: ColorString) -> Result[bool]:
        """
        Move the given player along their path until they hit the end of a path or the edge of a board. Returns a
        result containing a boolean that indicates whether or not the player hit the edge of the board.

        If the player's path forms an infinite loop, removes the player from the board.

        :param player:  The color of the player
        :return:        An result containing whether the player hit the edge of the board or an error
        """
        seen_pos_port: Set[Tuple[BoardPosition, PortID]] = set()

        while True:
            r = self._board_state.get_position_of_player(player)
            if r.is_error():
                return error(
                    "failed to move player %s along path: %s" % (player, r.error())
                )
            pos, port = r.value()

            if (pos, port) in seen_pos_port:
                for observer in self._observers:
                    observer.player_entered_loop(player)
                # They entered an infinite loop and must be removed
                return ok(True)
            seen_pos_port.add((pos, port))

            r2 = self._board_state.calculate_adjacent_position_of_player(player)
            if r2.is_error():
                return error(
                    "failed to move player %s along path: %s" % (player, r.error())
                )
            next_pos = r2.value()
            if next_pos is None:
                # They hit the edge of the board so remove them from the list of live players
                return ok(True)

            next_tile = self._board_state.get_tile(next_pos)

            if next_tile is None:
                # They didn't hit the edge of the board so they don't need to be removed
                return ok(False)

            next_port = Port.get_adjoining_port(port)

            next_next_port = next_tile.get_port_connected_to(next_port)

            self._board_state = self._board_state.with_live_players(
                self._board_state.live_players.set(player, (next_pos, next_next_port))
            )
Пример #4
0
    def _get_check_intermediate_move(
            self, color: ColorString, board: Board, tiles: List[Tile],
            player: PlayerInterface) -> Result[IntermediateMove]:
        """
        Gets the intermediate move from the player and checks whether it is valid based on the rulechecker. If any errors, the player is added as a cheater.
        Returns an error if cheating, or the chosen move if it is valid
        """
        r_move = self._handle_player_timeout(
            color, lambda: player.generate_move(deepcopy(tiles),
                                                board.get_board_state()))
        if r_move.is_error():
            self._cheaters.add(color)
            return error(r_move.error())

        intermediate_move = IntermediateMove(r_move.value(), color)
        r_rule = self._rule_checker.validate_move(board.get_board_state(),
                                                  tiles, intermediate_move)

        for observer in self._observers:
            observer.intermediate_move_played(color, tiles,
                                              board.get_board_state(),
                                              intermediate_move,
                                              r_rule.is_ok())

        if r_rule.is_error():
            self._cheaters.add(color)
            return error(r_rule.error())

        return ok(intermediate_move)
Пример #5
0
    def _run_game_initial_turns(self, board: Board) -> Result[None]:
        """
        Run the first step of a Tsuro game: prompting every player for their initial move. Apply the
        changes to this board to the fields contained within this referee.

        :param board:   The board to run the game on
        :return:        A result containing either None or an error
        """
        r_components = self._confirm_all_components()
        if r_components.is_error(): return r_components

        for color, player in self._players.items():
            tiles = self._get_tiles(3)
            for observer in self._observers:
                observer.initial_move_offered(color, tiles,
                                              board.get_board_state())

            r_initial_move = self._get_check_initial_move(
                board, color, player, tiles)
            if r_initial_move.is_error(): continue

            r = board.initial_move(r_initial_move.value())
            if r.is_error():
                return error(r.error())

        self._remove_cheaters(board)
        return ok(None)
Пример #6
0
    def _run_round(self) -> Result[Optional[TournamentResult]]:
        """
        Run a single round of a Tsuro tournament and update self.players based off of
        the results of the tournament.

        :return:    If the tournament is over, return the Results of the single
        game, otherwise return a Result containing None. 
        """
        games_r = self.bracket_strategy.bucket_players(
            set((player_id, player_age) for player, player_id, player_age in self.players))
        if games_r.is_error(): return error(games_r.error())

        for observer in self._observers: observer.games_created(games_r.value())

        if len(games_r.value()) == 1:
            return self._run_last_game(games_r.value())
        else:  
            game_results_r = self._run_games(games_r.value())
            if game_results_r.is_error(): return error(game_results_r.error())

            self._handle_eliminated_players(game_results_r)
            
            for game_result in game_results_r.value():
                winners, game_cheaters = game_result
                self.cheaters.extend(game_cheaters)

            return ok(None)
Пример #7
0
    def generate_first_move(
        self, tiles: List[Tile], board_state: BoardState
    ) -> Result[Tuple[BoardPosition, Tile, PortID]]:
        """
        Generate the first move by choosing from the given list of 3 tiles. Returns the move along with
        the port that the player's token should be placed on. `set_color` and `set_rule_checker` will
        be called prior to this method.

        This strategy just chooses the first valid move checking positions counter-clockwise from (0,0)
        exclusive and checking ports counter-clockwise from top left.

        :param tiles:           The set of tile options for the first move
        :param board_state:     The state of the current board
        :return:                A result containing a tuple containing the board position, tile, and port ID
                                for the player's initial move
        """
        tile = tiles[2]

        move = self._first_legal_posn_on_vertical_side(MIN_BOARD_COORDINATE,
            range(MIN_BOARD_COORDINATE + 1, MAX_BOARD_COORDINATE), board_state, tile)
        if not move:
            move = self._first_legal_posn_on_horizontal_side(MAX_BOARD_COORDINATE,
                range(MIN_BOARD_COORDINATE, MAX_BOARD_COORDINATE + 1), board_state, tile)
        if not move:
            move = self._first_legal_posn_on_vertical_side(MAX_BOARD_COORDINATE,
                reversed(range(MIN_BOARD_COORDINATE, MAX_BOARD_COORDINATE)), board_state, tile)
        if not move:
            move = self._first_legal_posn_on_horizontal_side(MIN_BOARD_COORDINATE,
                reversed(range(MIN_BOARD_COORDINATE + 1, MAX_BOARD_COORDINATE)), board_state, tile)

        if move:
            return ok(move)
        else:
            return error("Failed to find a valid initial move!")
Пример #8
0
    def _run_games(
        self, games: Set[FrozenSet[PlayerID]]
    ) -> Result[List[TournamentResult]]:
        """
        Run a series of games and return the results of the games

        :param games:   A set of games represented as a set of sets where the inner sets are the players that should play in a given game
        :return:        A Result containing a list of game results
        """
        tournament_results = []
        for player_ids in games:
            ref = self._initialize_ref()
            
            sorted_player_ids = list(sorted(player_ids, key=self._get_age_by_id))
            players = [self._get_player_by_id(player_id) for player_id in sorted_player_ids]
            
            colors_r = ref.set_players(players)
            if colors_r.is_error(): return error(colors_r.error())

            for observer in self._observers: observer.game_started(player_ids)

            game_r = ref.run_game()
            if game_r.is_error(): return error(game_r.error())
        
            for observer in self._observers: observer.game_completed(player_ids, game_r.value())

            tournament_results.append(translate_color_to_pids(game_r.value(), sorted_player_ids, colors_r.value()))

        return ok(tournament_results)
Пример #9
0
    def is_legal_for_condition(self, legality_check, board_state,
                               tile_choices: List[Tile],
                               move: IntermediateMove,
                               error_message) -> Result[None]:
        """
        Checks whether a move is legal against a certain legality check (ie loop or suicide) in that 
        the move is only legal if it passes that check, or if all options fail that check.

        :param board_state:     The board state to apply the move to
        :param move:            The intermediate move being applied
        :param legality_check:  The function that checks the validity of the move against a certain rule
        :param tile_choices:    The tile options provided
        """
        legal_r = legality_check(board_state, move)
        if legal_r.is_error():
            return error(legal_r.error())
        if legal_r.value():
            for tile_choice in tile_choices:
                for rotated_tile_choice in tile_choice.all_rotations():
                    legal_r = legality_check(
                        board_state,
                        IntermediateMove(rotated_tile_choice, move.player))
                    if legal_r.is_error():
                        return error(legal_r.error())
                    if not legal_r.value():
                        return error(
                            f"player chose a {error_message}: {rotated_tile_choice}"
                        )
        return ok(None)
Пример #10
0
    def validate_move(self, board_state: BoardState, tile_choices: List[Tile],
                      move: IntermediateMove) -> Result[None]:
        """
        Validate the given intermediate move when the player was given the choice of tile_choices.

        :param board_state:     The board state the move is being applied to
        :param tile_choices:    The list of tiles the player was allowed to choose from
        :param move:            The intermediate move to validate
        :return:                A result containing either None (if the move is valid) or an error
                                if the move is invalid. If the move is invalid, the error contains
                                a description of why it is invalid.
        """
        conditions_r = self.check_valid_move_params(
            tile_choices, move.tile, EXPECTED_TILE_COUNT_INTERMEDIATE_MOVE)
        if conditions_r.is_error(): return conditions_r

        board = Board(deepcopy(board_state))
        r = board.validate_intermediate_move(move)
        if r.is_error():
            return r

        loop_r = self.is_legal_for_condition(self.move_creates_loop,
                                             board_state, tile_choices, move,
                                             LOOP_ERROR)
        if loop_r.is_error(): return loop_r

        suicide_r = self.is_legal_for_condition(self.is_move_suicidal,
                                                board_state, tile_choices,
                                                move, SUICIDE_ERROR)
        if suicide_r.is_error(): return suicide_r

        return ok(None)
Пример #11
0
    def set_players(
            self, players: List[PlayerInterface]) -> Result[List[ColorString]]:
        """
        Add the given players (ordered by age, decreasing) to the game. Ties for age can be represented in either order.
        Returns an error if `len(players)` is greater than 5 or less than 3, or if the method has already been called.

        :param players: Players for this referee to use in a game. Allows only 3-5 players.
        :return:        The list of colors that will be assigned to those players.
        """
        if self._players:
            return error("players have already been set for this game.")
        if len(players) < 3 or len(players) > 5:
            return error(
                f"there must be between 3 and 5 players, not {len(players)}.")
        if len(set(players)) != len(players):
            return error(
                f"the given set of players contains duplicates (or players that do not "
                f"implement __hash__, __eq__)")

        assigned_colors = AllColors[:len(players)]

        self._players = {
            color: silenced_object(player)
            for color, player in zip(assigned_colors, players)
        }

        for observer in self._observers:
            observer.players_added(assigned_colors)
        return ok(assigned_colors)
Пример #12
0
 def _handle_initial_received(self, initial: List) -> Result[Tuple[BoardPosition, Tile, PortID]]:
     """
     Returns the initial placement given the initial message.
     """
     board_posn = BoardPosition(initial[2], initial[3])
     tile = tile_pattern_to_tile(initial[0][0], initial[0][1])
     port = network_port_id_to_port_id(initial[1])
     return ok((board_posn, tile, port))
Пример #13
0
 def send_message(self, msg: JSON) -> Result[None]:
     """
     Send the given JSON value to stdout via this JSON stream
     :param msg:     The JSON value to send
     :return:        A result indicating whether or not it was sent successfully
     """
     print(json_dump(msg))
     return ok(None)
Пример #14
0
    def generate_move(self, tiles: List[Tile], board_state: BoardState) -> Result[Tile]:
        """
        Try all tiles in all rotations clockwise starting with the second tile then the first, returning the first legal tile.
        If no tiles orientations are valid, return the second tile without rotation.

        :param tiles:           The list of 2 tile options
        :param board_state:     The state of the current board
        :return:                A result containing the tile that will be placed for the given player
        """
        for tile in reversed(tiles):
            for rot_tile in tile.all_rotations():
                r_illegal = self.rule_checker.is_move_illegal(
                    board_state, IntermediateMove(rot_tile, self.color)
                )
                if not r_illegal.is_error() and not r_illegal.value():
                    return ok(rot_tile)

        return ok(tiles[1])
Пример #15
0
 def _confirm_all_components(self) -> Result[None]:
     """
     Checks that all components required for the referee to run (players, rules, tile iterator) exist
     """
     if not self._players:
         return error("must add players to this referee")
     if not self._rule_checker:
         return error("must add a rule checker to this referee")
     if not self._tile_iterator:
         return error("must add a tile iterator to this referee")
     return ok(None)
Пример #16
0
    def get_position_of_player(
            self, player: ColorString) -> Result[Tuple[BoardPosition, PortID]]:
        """
        Get the position of the player specified by a color. Returns a result containing their position
        on the board and the port they're on, or an error if they are not alive.

        :param player:  The player to retrieve
        :return:        A result containing their position and port or an error
        """
        if player in self._live_players:
            return ok(self._live_players[player])
        return error("player %s is not on the board" % player)
Пример #17
0
    def _run_not_enough_players(self) -> Result[TournamentResult]:
        """
        If there is an insufficient number of players to run a game for the
        tournament, all players are winners.
        """
        winners = [pid for player, pid, age in self.players]
        self._notify_winners(winners)
         
        for observer in self._observers:
            observer.tournament_completed(([winners], set(self.cheaters)))

        return ok(([winners], set(self.cheaters)))
Пример #18
0
 def send_message(self, msg: JSON) -> Result[None]:
     """
     Send the given JSON value to a remote server via this JSON stream
     :param msg:     The JSON value to send
     :return:        A result indicating whether or not it was sent successfully
     """
     if self.conn is None:
         return error(
             "connection is not initialized, cannot send message (ensure that this NetworkJSONStream was "
             "made via one of the factory methods)")
     send_from_conn(self.conn, msg)
     return ok(None)
Пример #19
0
    def intermediate_move(self, move: IntermediateMove) -> Result[None]:
        """
        Place the given tile at the given position on this board.

        :param move:        The intermediate move to be placed
        :return:            A result containing an error if the move is invalid or a result containing the value None
                            if there was no error and the tile was placed successfully
        """
        validate_result = self.validate_intermediate_move(move)
        if validate_result.is_error():
            return validate_result

        pos_r = self._board_state.calculate_adjacent_position_of_player(move.player)

        if pos_r.is_error():
            return error(
                "cannot place tile for player %s: %s" % (move.player, pos_r.error())
            )
        pos = pos_r.value()

        if pos is None:
            return error(
                f"cannot place tile for player {move.player} since it would go off the edge of the board (this should "
                f"never happen!)"
            )

        if self._board_state.get_tile(pos) is not None:
            return error(
                f"cannot place for player {move.player} since it would be on top of an existing tile (this should"
                f"never happen!)"
            )

        # Assignment 6 states that if a move causes a loop it is legal (and the board must support it)
        # but that the expected behavior is to remove the players on the loop, not the place tile,
        # and accept the move.
        orig_board_state = deepcopy(self._board_state)
        temp_logging_observer = LoggingObserver()
        self.add_observer(temp_logging_observer)
        self._board_state = self._board_state.with_tile(move.tile, pos)
        r = self._move_all_players_along_paths()
        if r.is_error():
            return r
        if temp_logging_observer.entered_loop:
            # Placing this tile caused someone to enter a loop. So we undo any changes, delete the
            # people that were in the loop, and return ok
            self._board_state = orig_board_state
            for player in temp_logging_observer.entered_loop:
                self.remove_player(player)
        self.remove_observer(temp_logging_observer)
        return ok(None)
Пример #20
0
    def generate_move(self, tiles: List[Tile],
                      board_state: BoardState) -> Result[Tile]:
        """
        Try all tiles in all rotations clockwise, returning the first legal tile.
        If no tiles are valid, return the first tile without rotation.

        :param tiles:           The list of tile options
        :param board_state:     The state of the current board
        :return:                A result containing the tile that will be placed for the given player
        """
        if len(tiles) != EXPECTED_TILE_COUNT_INTERMEDIATE_MOVE:
            return error(
                f"Strategy.generate_move given {len(tiles)} (expected {EXPECTED_TILE_COUNT_INTERMEDIATE_MOVE})"
            )

        for tile in tiles:
            for rot_tile in tile.all_rotations():
                r_illegal = self.rule_checker.is_move_illegal(
                    board_state, IntermediateMove(rot_tile, self.color))
                if not r_illegal.is_error() and not r_illegal.value():
                    return ok(rot_tile)

        return ok(tiles[0])
Пример #21
0
    def generate_move(self, tiles: List[Tile],
                      board_state: BoardState) -> Result[Tile]:
        """
        Always choose the first tile without rotation.

        :param tiles:           The set of tile options for the first move
        :param board_state:     The state of the current board
        :return:                A result containing the tile that will be placed for the given player
        """
        if len(tiles) != EXPECTED_TILE_COUNT_INTERMEDIATE_MOVE:
            return error(
                f"Strategy.generate_move given {len(tiles)} (expected {EXPECTED_TILE_COUNT_INTERMEDIATE_MOVE})"
            )

        return ok(tiles[0])
Пример #22
0
    def _find_valid_port(self, board_state: BoardState,
                         pos: BoardPosition) -> Result[PortID]:
        """
        Find a valid port on the board state for a move at the given position

        :param board_state:     The board state to apply the move to
        :param pos:             The position for the move
        :return:                A result containing the port or an error if there is no valid port to play on at the
                                given position
        """
        for port in Port.all():
            if PhysicalConstraintChecker.is_valid_initial_port(
                    board_state, pos, port).is_ok():
                return ok(port)
        return error("No valid ports on given tile.")
Пример #23
0
    def validate_intermediate_move(self, move: IntermediateMove) -> Result[None]:
        """
        Validate the given intermediate move, ensuring that the player is still alive
        before making the move.

        :param move:        The intermediate move to validate
        :return:            A result containing an error if the move is invalid or a result containing the value None
                            if there was no error
        """
        if move.player not in self._board_state.live_players:
            return error(
                f"cannot place a tile for player {move.player} since they are not alive"
            )

        return ok(None)
Пример #24
0
def test_result() -> None:
    r = result.ok("a")
    assert r.is_ok()
    assert not r.is_error()
    assert r.value() == "a"
    with pytest.raises(result.ResultMisuseException):
        r.error()
    assert str(r) == repr(r) == "Result(val='a')"

    r = result.error("msg")
    assert not r.is_ok()
    assert r.is_error()
    assert r.error() == "msg"
    with pytest.raises(result.ResultMisuseException):
        r.value()
    assert str(r) == repr(r) == "Result(error='msg')"
Пример #25
0
def rcv_from_conn(conn) -> Result[JSON]:
    """
    Receives a message using the given connection
    """
    all_data: List[bytes] = []
    while True:
        data = conn.recv(1)
        if not data:
            return error(
                f"{CLOSED_INPUT_PREFIX} cannot read message from socket because "
                f"the socket is closed")
        all_data.append(data)
        try:
            return ok(json.loads(b"".join(all_data).decode("utf-8")))
        except json.JSONDecodeError:
            pass
Пример #26
0
    def move_creates_loop(self, board_state: BoardState,
                          move: IntermediateMove) -> Result[bool]:
        """
        Returns whether or not the given move creates a loop for anyone on the board. Note that this returns False if a
        loop is created that no player is on.

        :param board_state:     The board state to apply the move to
        :param move:            The intermediate move being applied
        :return:                A Result containing whether or not the move creates a loop for anyone on the baord
        """
        logging_observer = LoggingObserver()
        board = Board(deepcopy(board_state))
        board.add_observer(logging_observer)
        r = board.intermediate_move(move)
        if r.is_error():
            return error(r.error())
        return ok(len(logging_observer.entered_loop) > 0)
Пример #27
0
    def _run_last_game(self, game: Set[FrozenSet[PlayerID]]) -> Result[TournamentResult]:
        """
        Handles the results of the last game of the tournament.
        
        :param game:    A set of games, represented by sets of player IDs, 
        which is length 1 becaues it is the last game.
        :return:    The winners and cheaters of the tournament after the game is complete. 
        """
        game_results_r = self._run_games(game)
      
        if game_results_r.is_error(): 
            return error(game_results_r.error())

        winners = game_results_r.value()[0][0]
        cheaters = game_results_r.value()[0][1]
        self.cheaters.extend(cheaters)
        return ok((winners, set(self.cheaters)))
Пример #28
0
 def receive_message(self) -> Result[JSON]:
     """
     Receive a JSON message from stdin via this JSON stream
     :return:    A Result containing the received JSON message or an error. The error contains the string
                 `CLOSED_INPUT_PREFIX` if the error is due to a closed input
     """
     data = ""
     while True:
         new_char = sys.stdin.read(1)
         if new_char == "":
             return error(
                 f"{CLOSED_INPUT_PREFIX} cannot read message from stdin because sys.stdin is closed"
             )
         data += new_char
         try:
             return ok(json.loads(data))
         except json.JSONDecodeError:
             pass
Пример #29
0
    def _generate_game_result(self, board: Board) -> Result[GameResult]:
        """
        Generate a GameResult once a game of Tsuro is complete based off of the data in self.cheaters,
        self.players_eliminated_in_round, and board. Must only be called once the game is over and 0 or 1
        players remain on the board.

        :param board:   The board at the end of the game
        :return:        The game result which contains a leaderboard and a list of cheaters
        """
        # Add the last man standing to the list of eliminated players
        self._leaderboard.append(set(board.live_players.keys()))
        leaderboard = [x for x in reversed(self._leaderboard) if x]

        # Return the leaderboard and the cheaters and notify observers
        results = deepcopy((leaderboard, self._cheaters))
        for observer in self._observers:
            observer.game_result(results)
        return ok(results)
Пример #30
0
    def _find_valid_move(
        self, board_state: BoardState, pos: BoardPosition
    ) -> Result[Tuple[BoardPosition, PortID]]:
        """
        Find a valid move on the board state at the given position

        :param board_state:     The board state to apply the move to
        :param pos:             The position for the move
        :return:                A result containing the position and port or an error if there is no valid move at
                                the given position
        """
        r = PhysicalConstraintChecker.is_valid_initial_position(board_state, pos)
        if r.is_error():
            return error(r.error())
        r_port = self._find_valid_port(board_state, pos)
        if r_port.is_error():
            return error(r_port.error())
        return ok((pos, r_port.value()))