예제 #1
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)
예제 #2
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)
예제 #3
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)
예제 #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(self) -> Result[GameResult]:
        """
        Run an entire game of Tsuro with the players that have been added to this referee. Returns the result
        of the game.

        A list of players, a rule checker, and a tile iterator must have already been set on this referee
        prior to calling run_game.

        :return: The GameResult at the end of the game, or an error if something goes wrong.
        """
        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")

        self._initialize_players()
        board = Board()

        # Run the initial turns
        r = self._run_game_initial_turns(board)
        if r.is_error():
            return error(r.error())

        # Run the intermediate turns
        while True:
            if len(board.live_players) <= 1:
                break

            r = self._run_game_single_round(board)
            if r.is_error():
                return error(r.error())

        return self._generate_game_result(board)
예제 #6
0
파일: rules.py 프로젝트: feliciazhang/tsuro
    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)
예제 #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 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!")
예제 #8
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)
예제 #9
0
파일: board.py 프로젝트: feliciazhang/tsuro
    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))
            )
예제 #10
0
파일: board.py 프로젝트: feliciazhang/tsuro
    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)
예제 #11
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!")
예제 #12
0
 def receive_message(self) -> Result[JSON]:
     """
     Receive a JSON message from 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
     """
     return error("receive_message not implemented in JSONStream interface")
예제 #13
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)
예제 #14
0
 def send_message(self, msg: JSON) -> Result[None]:
     """
     Send the given JSON value over this JSON stream
     :param msg:     The JSON value to send
     :return:        A result indicating whether or not it was sent successfully
     """
     return error("send_message not implemented in JSONStream interface")
예제 #15
0
 def send_message(self, msg: JSON) -> Result[None]:
     """
     Return an error since StringJSONStream does not support sending a message
     :param _:   A message that is not sent
     :return:    A result containing an error
     """
     return error(
         "StringJSONStream does not implemented sending of messages")
예제 #16
0
 def generate_first_move(
         self, tiles: List[Tile], board_state: BoardState
 ) -> Result[Tuple[BoardPosition, Tile, PortID]]:
     if self.first:
         if self.crash:
             raise Exception("Crash!")
         else:
             return error("Error!")
     return FirstS().generate_first_move(tiles, board_state)
예제 #17
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()))
예제 #18
0
파일: rules.py 프로젝트: feliciazhang/tsuro
    def check_valid_move_params(self, tile_choices: List[Tile], tile: Tile,
                                expected: int) -> Result[None]:
        """
        Checks whether the tile that the player has chosen is one of the given tiles and that
        the number of tiles given is the expected number for the type of turn.
        If either condition is invalid, an error is returned

        :param tile_choices:    The list of tiles the player was allowed to choose from
        :param tile:            The chosen tile to validate
        """
        if len(tile_choices) != expected:
            return error(
                f"cannot validate move with {len(tile_choices)} tile choices "
                f"(expected {expected})")
        if tile not in tile_choices:
            return error(
                f"tile {tile} is not in the list of tiles {tile_choices} the player was given"
            )
        return ok(None)
예제 #19
0
 def receive_message(self) -> Result[JSON]:
     """
     Receive a JSON message from a remote server 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
     """
     if self.conn is None:
         return error(
             "connection is not initialized, cannot receive message (ensure that this NetworkJSONStream "
             "was made via one of the factory methods)")
     return rcv_from_conn(self.conn)
예제 #20
0
    def generate_move(self, tiles: List[Tile],
                      board_state: BoardState) -> Result[Tile]:
        """
        Generate a move by choosing from the given list of tiles and returning the move. `set_color`,
        `set_rule_checker`, and `generate_first_move` will be called prior to this method.

        :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
        """
        return error("Strategy does not implement method generate_move!")
예제 #21
0
 def inner(*args: Any, **kwargs: Any) -> Any:
     if os.environ.get(TYPE_CHECKING_VAR):
         try:
             return typechecked(func)(*args, **kwargs)  # type: ignore
         except TypeError as exc:
             return_type = func.__annotations__.get("return", None)
             if getattr(return_type, "__origin__", None) == Result:
                 return error(str(exc))
             else:
                 raise exc
     return func(*args, **kwargs)  # type: ignore
예제 #22
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)
예제 #23
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)
예제 #24
0
    def is_valid_initial_position(board_state: "BoardState",
                                  pos: BoardPosition) -> result.Result[None]:
        """
        Returns whether the given position is a valid position to place a tile on for an initial move.

        :param board_state:     The current state of the board
        :param pos:             The board position to check
        :return:                A result containing an error if it is invalid otherwise a result containing None
        """
        if board_state.get_tile(pos) is not None:
            return result.error(
                f"cannot place tile at position {pos} since there is already a tile at that position"
            )
        if not pos.is_edge():
            return result.error(
                f"cannot make an initial move at position {pos} since it is not on the edge"
            )
        if not board_state.surrounding_positions_are_empty(pos):
            return result.error(
                f"cannot make an initial move at position {pos} since the surrounding tiles are not all empty"
            )
        return ok(None)
예제 #25
0
 def calculate_adjacent_position_of_player(
         self, player: ColorString) -> Result[Optional[BoardPosition]]:
     """
     Calculate the Board Position adjacent to the tile and port that specified player is on.
     :param player:  The color of the player to calculate a position relative to
     :return:        A result containing the board position if the adjacent position is a valid position. None
                     if the adjacent position is off the edge of the board. Or an error.
     """
     if player not in self._live_players:
         return error("player %s is not a live player" % player)
     current_pos_r = self.get_position_of_player(player)
     if current_pos_r.is_error():
         return error(current_pos_r.error())
     current_pos, current_port = (  # pylint: disable=unpacking-non-sequence
         current_pos_r.value())
     if current_port in [Port.TopLeft, Port.TopRight]:
         if current_pos.y - 1 >= 0:
             return ok(BoardPosition(x=current_pos.x, y=current_pos.y - 1))
         else:
             return ok(None)
     elif current_port in [Port.RightTop, Port.RightBottom]:
         if current_pos.x + 1 <= MAX_BOARD_COORDINATE:
             return ok(BoardPosition(x=current_pos.x + 1, y=current_pos.y))
         else:
             return ok(None)
     elif current_port in [Port.BottomLeft, Port.BottomRight]:
         if current_pos.y + 1 <= MAX_BOARD_COORDINATE:
             return ok(BoardPosition(x=current_pos.x, y=current_pos.y + 1))
         else:
             return ok(None)
     elif current_port in [Port.LeftBottom, Port.LeftTop]:
         if current_pos.x - 1 >= MIN_BOARD_COORDINATE:
             return ok(BoardPosition(x=current_pos.x - 1, y=current_pos.y))
         else:
             return ok(None)
     else:
         return error("could not match current_port %s to a direction" %
                      current_port)
예제 #26
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.

        :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
        """
        return error("Strategy does not implement method generate_first_move!")
예제 #27
0
파일: rules.py 프로젝트: feliciazhang/tsuro
    def is_move_suicidal(self, board_state: BoardState,
                         move: IntermediateMove) -> Result[bool]:
        """
        Returns whether the given intermediate move is suicidal. A suicidal move is a move that
        results in the player who placed the move leaving the board but NOT if a loop is caused.

        :param board_state:     The board state the move is being applied to
        :param move:            The intermediate move to check for suicideness
        :return:                A result containing a boolean or an error. If it contains a value
                                the boolean specifies whether or not the move is suicidal.
        """
        logging_observer = LoggingObserver()
        if move.player not in board_state.live_players:
            return error(
                f"player {move.player} is not alive thus the move cannot be suicidal"
            )
        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(move.player not in board.live_players
                  and len(logging_observer.entered_loop) <= 0)
예제 #28
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])
예제 #29
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.")
예제 #30
0
파일: board.py 프로젝트: feliciazhang/tsuro
    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)