示例#1
0
def _find_action_to_neighboring_tile(state: State, to_pos: Position) -> Action:
    """
    Finds the action the second player can take to move to one of the tiles
    that neighbors the first player's ending position if it exists.  
    Otherwise, None is returned. 
    
    Actions will be searched for by checking neighboring
    tiles in the following order: North, Northeast, Southeast, South, Southwest, Northwest.
    
    If there are multiple avatars that can reach neighboring tiles, a tie breaking algorithm
    will choose the action with the top-most row of the from position, the left-most column
    of the from position, the top-most row of the to position, and the left-most column
    of the to position in that order.

    :param state: the current state resulting from the first player successfully executing their desired action
    :param to_pos: the position of the tile that the first player landed on after executing their action
    :return: an Action the second player can take to get to a neighboring tile or None if no such action exists
    """
    if not isinstance(state, State):
        raise TypeError("Expected State for state.")
    if not isinstance(to_pos, Position):
        raise TypeError("Expected Position for to_pos.")

    # Get player placements from state
    player_placements = state.placements

    # Second player's color
    second_player_color = state.player_order[0]

    # Search each direction and see if any of player 2's positions can reach a neighboring tile
    # in that direction
    for direction in directions_to_try:
        # The new target position for player 2 in the current direction
        new_pos = _get_next_position(to_pos, direction)

        # List to store the potential valid actions to reach the neighboring tile in the current direction
        valid_actions = []

        for pos in player_placements[second_player_color]:
            try:
                # The action to be tried
                new_action = Action(pos, new_pos)

                # Try the action
                gt = GameTree(state)
                gt.try_action(new_action)

                # If this point is reached with no exception, it is a valid action.
                valid_actions.append(new_action)
            except InvalidActionException:
                pass
            except GameNotRunningException:
                return None

        # If any valid actions were found, either return one of them or tiebreak
        if len(valid_actions) > 0:
            return min(valid_actions)

    return None
示例#2
0
def _get_next_state(state: State, from_pos: Position,
                    to_pos: Position) -> State:
    """
    Produces the subsequent game state from trying to move the first player's avatars
    from the given start position to the given end position. If the given action is illegal,
    an exception will be thrown. If the game has not been started fully yet, None will be returned.

    :param state: state containing starting player list and board as extracted by initialize_state
    :param from_pos: the starting position for the first player's desired action
    :param to_pos: the ending position for the first player's desired action
    :return: resulting state from executing Action(from_pos, to_pos)
    """
    if not isinstance(state, State):
        raise TypeError("Expected State for state.")
    if not isinstance(from_pos, Position):
        raise TypeError("Expected Position for from_pos.")
    if not isinstance(to_pos, Position):
        raise TypeError("Expected Position for to_pos.")

    # Initialize player placements
    player_placements = state.placements

    # Get first player color (it's currently the first player's turn)
    first_player_color = state.current_player

    # Verify that from_pos is one of player 1's current positions
    if from_pos not in player_placements[first_player_color]:
        raise InvalidActionException(
            "Expected from_pos to be one of player 1's current placements")

    # Attempt action
    try:
        new_state = GameTree.try_action(GameTree(state),
                                        Action(from_pos, to_pos))
        return new_state
    except InvalidActionException as e:
        raise e
    except GameNotRunningException:
        pass

    return None
 def test_try_action_fail5(self):
     # Tests a failing try_action due to action involves moving to an already occupied tile
     with self.assertRaises(InvalidActionException):
         GameTree.try_action(GameTree(self.__state2),
                             Action(Position(4, 0), Position(3, 0)))
 def test_try_action_fail4(self):
     # Tests a failing try_action due to action involves moving thru another character
     with self.assertRaises(InvalidActionException):
         GameTree.try_action(GameTree(self.__state2),
                             Action(Position(4, 0), Position(2, 1)))
 def test_try_action_fail3(self):
     # Tests a failing try_action due to action being out of turn (it involves moving someone
     # else but the current player's avatar, despite otherwise being legal)
     with self.assertRaises(InvalidActionException):
         GameTree.try_action(GameTree(self.__state2),
                             Action(Position(0, 1), Position(2, 1)))
 def test_try_action_fail2(self):
     # Tests a failing try_action due to action being invalid (not accessible via a straight
     # line path)
     with self.assertRaises(InvalidActionException):
         GameTree.try_action(GameTree(self.__state2),
                             Action(Position(3, 0), Position(0, 0)))
class GameTreeTests(unittest.TestCase):
    def __init__(self, *args, **kwargs):
        super(GameTreeTests, self).__init__(*args, **kwargs)

        # Initialize boards
        self.__board1 = Board.homogeneous(5, 5, 3)
        self.__board2 = Board.homogeneous(3, 5, 2)
        self.__board3 = Board({
            Position(0, 0): Tile(5),
            Position(0, 1): Tile(3),
            Position(0, 2): Tile(2),
            Position(1, 0): Tile(2),
            Position(1, 1): Tile(3),
            Position(1, 2): Tile(2),
            Position(2, 0): Tile(3),
            Position(2, 1): Tile(4),
            Position(2, 2): Tile(1),
            Position(3, 0): Tile(1),
            Position(3, 1): Tile(1),
            Position(3, 2): Tile(5),
            Position(4, 0): Tile(2),
            Position(4, 1): Tile(3),
            Position(4, 2): Tile(4)
        })

        # Initialize some players for testing
        self.__p1 = PlayerEntity("John", Color.RED)
        self.__p2 = PlayerEntity("George", Color.WHITE)
        self.__p3 = PlayerEntity("Gary", Color.BLACK)
        self.__p4 = PlayerEntity("Jeanine", Color.BROWN)
        self.__p5 = PlayerEntity("Obama", Color.RED)
        self.__p6 = PlayerEntity("Fred", Color.BROWN)
        self.__p7 = PlayerEntity("Stewart", Color.WHITE)
        self.__p8 = PlayerEntity("Bobby Mon", Color.BLACK)
        self.__p9 = PlayerEntity("Bob Ross", Color.WHITE)
        self.__p10 = PlayerEntity("Eric Khart", Color.BROWN)
        self.__p11 = PlayerEntity("Ionut", Color.RED)

        # ========================== STATE 1 ==========================

        # Initialize a premature state
        self.__state1 = State(self.__board1,
                              [self.__p1, self.__p2, self.__p3, self.__p4])

        # ========================== STATE 2 ==========================

        # Initialize a finalized state where at least two more rounds are possible
        self.__state2 = State(self.__board1, [self.__p1, self.__p2, self.__p3])
        # Place all avatars
        # Player 1 place
        self.__state2.place_avatar(Color.RED, Position(4, 0))
        # Player 2 place
        self.__state2.place_avatar(Color.WHITE, Position(0, 1))
        # Player 3 place
        self.__state2.place_avatar(Color.BLACK, Position(2, 2))
        # Player 1 place
        self.__state2.place_avatar(Color.RED, Position(1, 0))
        # Player 2 place
        self.__state2.place_avatar(Color.WHITE, Position(2, 0))
        # Player 3 place
        self.__state2.place_avatar(Color.BLACK, Position(3, 2))
        # Player 1 place
        self.__state2.place_avatar(Color.RED, Position(1, 1))
        # Player 2 place
        self.__state2.place_avatar(Color.WHITE, Position(4, 1))
        # Player 3 place
        self.__state2.place_avatar(Color.BLACK, Position(3, 0))

        # Make up tree for this state
        self.__tree2 = GameTree(self.__state2)

        # ========================== STATE 3 ==========================
        # Setup state that is one move away from game over
        self.__state3 = State(
            self.__board2,
            players=[self.__p5, self.__p6, self.__p7, self.__p8])

        # Set up the board with placements s.t. only 2 moves can be made
        # Player 1
        self.__state3.place_avatar(Color.RED, Position(3, 0))
        # Player 2
        self.__state3.place_avatar(Color.BROWN, Position(0, 0))
        # Player 3
        self.__state3.place_avatar(Color.WHITE, Position(1, 0))
        # Player 4
        self.__state3.place_avatar(Color.BLACK, Position(2, 0))
        # Player 1
        self.__state3.place_avatar(Color.RED, Position(3, 1))
        # Player 2
        self.__state3.place_avatar(Color.BROWN, Position(0, 1))
        # Player 3
        self.__state3.place_avatar(Color.WHITE, Position(1, 1))
        # Player 4
        self.__state3.place_avatar(Color.BLACK, Position(2, 1))
        # Make move 1 for p1
        self.__state3.move_avatar(Position(3, 1), Position(4, 1))

        # Make up tree for this state
        self.__tree3 = GameTree(self.__state3)

        # ========================== STATE 4 ==========================
        # Setup state that is game over
        self.__state4 = copy.deepcopy(self.__state3)

        # Make final move
        self.__state4.move_avatar(Position(2, 0), Position(4, 0))

        # Make up tree for this state
        self.__tree4 = GameTree(self.__state4)

        # ========================== STATE 5 ==========================
        # Setup state that includes heterogeneous board
        self.__state5 = State(self.__board3,
                              players=[self.__p9, self.__p10, self.__p11])

        # Player 1
        self.__state5.place_avatar(Color.WHITE, Position(2, 0))
        # Player 2
        self.__state5.place_avatar(Color.BROWN, Position(0, 1))
        # Player 3
        self.__state5.place_avatar(Color.RED, Position(0, 2))
        # Player 1
        self.__state5.place_avatar(Color.WHITE, Position(1, 0))
        # Player 2
        self.__state5.place_avatar(Color.BROWN, Position(1, 2))
        # Player 3
        self.__state5.place_avatar(Color.RED, Position(0, 0))
        # Player 1
        self.__state5.place_avatar(Color.WHITE, Position(3, 1))
        # Player 2
        self.__state5.place_avatar(Color.BROWN, Position(2, 1))
        # Player 3
        self.__state5.place_avatar(Color.RED, Position(3, 2))

        # Make up tree for this state
        self.__tree5 = GameTree(self.__state5)

    def test_init_fail1(self):
        # Tests failing constructor due to an invalid state
        with self.assertRaises(TypeError):
            GameTree("mickey mouse")

    @unittest.skip(
        "state does not check for the number of remaining avatars to place anymore"
    )
    def test_init_fail2(self):
        # Tests failing constructor due to state not being 'started' (or due
        # to not everyone having placed their avatars).
        with self.assertRaises(GameNotRunningException):
            GameTree(self.__state1)

    def test_init_success(self):
        # Tests successful game tree constructor
        GameTree(self.__state2)

    def test_get_next1(self):
        # Tests the get_next() of a GameTree where at least two more rounds
        # are possible.
        # It's player 1's turn in the state corresponding to this tree
        self.assertEqual(self.__tree2.state.current_player, Color.RED)

        # Make sure it has generated all possible connecting trees
        self.assertSequenceEqual(self.__state2.get_possible_actions(),
                                 self.__tree2.all_possible_actions)

        # Cycle over child trees, check current player, and check move log to make sure
        # move has been made for the state.
        for action, tree in self.__tree2.get_next():
            # Make sure it's player 2's turn in the state corresponding to this tree
            self.assertEqual(tree.state.current_player, Color.WHITE)

            # Make sure action was the last action that happened
            self.assertEqual(action, tree.state.move_log[-1])

    def test_get_next2(self):
        # Tests the get_next() out of a GameTree where one more round is possible
        # It's player 4's turn in the state corresponding to this tree
        self.assertEqual(self.__tree3.state.current_player, Color.BLACK)

        # Make sure it has generated all possible connecting trees
        self.assertSequenceEqual(self.__tree3.all_possible_actions,
                                 [Action(Position(2, 0), Position(4, 0))])

        # Cycle over child trees, check current player, and check move log to make sure
        # move has been made for the state.
        for action, tree in self.__tree3.get_next():
            # Make sure it's player 11's turn in the state corresponding to this tree
            self.assertEqual(tree.state.current_player, Color.BLACK)

            # Make sure action was the last action that happened
            self.assertEqual(action, tree.state.move_log[-1])

            # Make sure it's game over
            self.assertFalse(tree.state.can_anyone_move())

            # Make sure no more child states are possible for it is game
            # over
            self.assertSequenceEqual(tree.all_possible_actions, [])

    def test_get_next3(self):
        # Tests the get_next() of a GameTree where no more rounds is possible (aka
        # game over state).
        for _, _ in self.__tree4.get_next():
            self.assertTrue(False)

        # Make sure is has not generated any more trees (as there are no more
        # possible moves)
        self.assertSequenceEqual(self.__tree4.all_possible_actions, [])

    def test_get_next4(self):
        # Tests the get_next() out of a GameTree and that of its children
        # It's player 1's turn in the state corresponding to this tree
        self.assertEqual(self.__tree2.state.current_player, Color.RED)

        # Cycle over child trees, check current player, and check move log to make sure
        # move has been made for the state.
        for action1, tree1 in self.__tree2.get_next():
            # Make sure it has generated all possible connecting edges
            self.assertSequenceEqual(tree1.state.get_possible_actions(),
                                     tree1.all_possible_actions)

            for action2, tree2 in self.__tree2.get_next():
                # Make sure it has generated all possible connecting edges for current
                # child tree
                self.assertSequenceEqual(tree2.state.get_possible_actions(),
                                         tree2.all_possible_actions)

                # Make sure it's player 2's turn in the state corresponding to this tree
                self.assertEqual(tree2.state.current_player, Color.WHITE)

                # Make sure action was the last action that happened
                self.assertEqual(action2, tree2.state.move_log[-1])

    def test_try_action_fail1(self):
        # Tests a failing try_action due to action being invalid (type-wise)
        with self.assertRaises(TypeError):
            self.__tree2.try_action(Position(0, 0), Position(1, 0))

    def test_try_action_fail2(self):
        # Tests a failing try_action due to action being invalid (not accessible via a straight
        # line path)
        with self.assertRaises(InvalidActionException):
            GameTree.try_action(GameTree(self.__state2),
                                Action(Position(3, 0), Position(0, 0)))

    def test_try_action_fail3(self):
        # Tests a failing try_action due to action being out of turn (it involves moving someone
        # else but the current player's avatar, despite otherwise being legal)
        with self.assertRaises(InvalidActionException):
            GameTree.try_action(GameTree(self.__state2),
                                Action(Position(0, 1), Position(2, 1)))

    def test_try_action_fail4(self):
        # Tests a failing try_action due to action involves moving thru another character
        with self.assertRaises(InvalidActionException):
            GameTree.try_action(GameTree(self.__state2),
                                Action(Position(4, 0), Position(2, 1)))

    def test_try_action_fail5(self):
        # Tests a failing try_action due to action involves moving to an already occupied tile
        with self.assertRaises(InvalidActionException):
            GameTree.try_action(GameTree(self.__state2),
                                Action(Position(4, 0), Position(3, 0)))

    def test_try_action_success(self):
        # Tests a successful try_action where a valid action gets executed
        valid_action = Action(Position(1, 0), Position(4, 2))
        new_state = self.__tree2.try_action(valid_action)

        # Make sure the valid action we gave it got executed and is at the top
        # of the move log
        self.assertEqual(valid_action, new_state.move_log[-1])
        # Make sure it's second player's turn now
        self.assertEqual(new_state.current_player, Color.WHITE)

    def test_apply_to_child_states_fail1(self):
        # Tests apply_to_child_states failing due to invalid function (type-wise)
        with self.assertRaises(TypeError):
            self.__tree2.apply_to_child_states(lambda state, _: state)

    def test_apply_to_child_states_success1(self):
        # Tests successful apply_to_child_states where each child state maps to
        # that state's current player
        result = GameTree.apply_to_child_states(
            GameTree(self.__state2), lambda state: state.current_player)

        # Resulting array should consist of the next player's id the same number of times
        # as there are reachable states
        self.assertSequenceEqual(result, [Color.WHITE] * 7)

    def test_apply_to_child_states_success2(self):
        # Tests successful apply_to_child_states where each child state maps to
        # that a given player's score (homogeneous board)
        result = GameTree.apply_to_child_states(
            GameTree(self.__state2),
            lambda state: state.get_player_score(Color.RED))

        # Score should be the same (5) all around since this is a homogeneous board with
        # 5 fish to each tile
        self.assertSequenceEqual(result, 7 * [5])

    def test_apply_to_child_states_success3(self):
        # Tests successful apply_to_child_states where each child state maps to
        # that a given player's score (heterogeneous board)
        result = GameTree.apply_to_child_states(
            GameTree(self.__state5),
            lambda state: state.get_player_score(Color.WHITE))

        self.assertSequenceEqual(result, [3, 3, 3, 2, 1, 1, 1, 1])
示例#8
0
class Referee(object):
    """
    PURPOSE:        This class implements a Referee for the game fish. It is purported to provide all the
                    functionality required for running a game from placements to moves and game end. It
                    reports game updates and the final report (at game end) to observers. The latter includes a list
                    of cheating and failing players, as well as a leader board of all rule-abiding players.

                    A cheating player is one that attempts to perform either an illegal placement (placing on an already
                    occupied tile or outside the bounds of the board) or an illegal move (moving via a path that
                    is unclear of holes or avatars, moving to an occupied tile, moving across corners or tiles that
                    are not accessible in a straight line across parallel hexagon edges, moving in-place, and moving
                    outside the bounds of the board). This determination is for placements is made using the State and
                    whereas the one for moves (or actions) is made using the GameTree. Both State and GameTree will
                    raise appropriate exceptions to indicate abnormal conditions should any occur.

                    A failing player is one that fails to return either a placement or an action. More specifically,
                    if a player returns an object of the wrong type (something that is not a Position for
                    get_placement or something that is not an Action for get_action), it is marked as failing.
                    Similarly, if a player takes takes more than PLAYER_TIMEOUT seconds to respond to the referee or
                    throws any exception(s), it is marked out as failing.

                    The referee will prompt the players for moves and placements by passing a deep copy of its
                    game state. This means that exogenous players will not be able to affect the state maintained
                    by the referee.

                    The referee will remove the cheating and failing players' avatars from the game and prevent them
                    from taking any more turns (that includes placing and moving).

    INTERPRETATION: The referee could best be described as the engine that runs a game of Fish. It receives
                    a list of IPlayer objects that is sorted by age and row and column dimensions of the board from
                    the tournament manager, sets up a homogeneous game board of the specified size and assigns each
                    player a color. The board the referee creates is homogeneous (has the same random number of fish on
                    each tile) and may have holes in it (see paragraph on "difficulty factor"). When signaled to kick
                    off the game (via start()) it then prompts each player for a placement by having them return a
                    Position object containing the row and column number they wish to place their avatar. After it
                    finishes prompting users for placements it prompts each movable player for an Action object
                    (made up of a Position describing the place on the board the move is made from and another
                    describing the place the move is made to).

                    To setup the game board, the referee applies a "difficulty factor" - a Natural number that speaks to
                    the maximum number of Tiles the referee will try to remove. This factor is adjustable and can be
                    leveraged to make a game more challenging or less so. The referee will randomly pick the tiles to
                    remove and may even end up removing 0 tiles for a difficulty factor D > 0.

                    The referee also maintains a master copy of the game State which it updates throughout the course of
                    the game to reflect the players' positions, score etc.. Given the state's design, the current player
                    in it will always be unstuck (stuck players are automatically skipped) unless all players are stuck,
                    in which case the referee ends the game.

                    It also provides functionality that external observers can employ to follow the game. An
                    observer or tournament manager subscribe via `subscribe_game_updates` to receive an update with
                    the latest game state every time it changes (this happens whenever a player makes a placement or
                    move, or is kicked). They can also subscribe to an end game report via `subscribe_final_game_report`
                    to receive a copy of the final game report.

                    The final game report encompasses a list of the cheating players, a list of the failing
                    players and a list of dictionary objects sorted in decreasing order of score,
                    each object containing a rule-abiding player's name, color and score.

                    Here's an example of what the report may look like:

                    {
                        'cheating_players': [IPlayer],
                        'failing_players': [IPlayer],
                        'leaderboard': [
                            {'name': 'Winner', 'color': Color.BLACK, 'score': 99},
                            {'name': 'Runner-up', 'color': Color.WHITE, 'score': 40}
                        ]

                    }

                    Upon determining that no more moves can be made (by calling can_anyone_move() on the internal state)
                    , the referee ends the game and provides all players and subscribed observers with the final game
                    report.

                    At initialization, the referee is given a list of IPlayer objects with undefined colors (.color =
                    Color.UNDEFINED). After assigning colors, the referee creates a PlayerEntity for each object,
                    which contains the essential information needed for identification in the game (namely name,
                    color and placements). All other information pertaining to a player is scrapped. The referee
                    starts running the game (from the placement phase onwards) when start() is called on it (presumably
                    the tournament manager would call it to kick off the game).

                    Throughout the game, every time the internal game state is altered, the game tree is updated,
                    players are synchronized (by calling sync on them with the game state) and observers are notified
                    with a version of the latest one. This keeps all parties informed and the game tree up to date for
                    rule-checking.

    DEFINITIONS:    A losing player is one that does not obtain the largest number of fish in the game, or is one that
                    cheats or fails.

                    A winning player is one that obtains the largest number of fish in the game (and does not cheat).
                    There can be multiple winning players if multiple players obtain the same largest number of fish
                    in the game.
    """
    DEBUG = False

    # Initialize difficulty factor
    DIFFICULTY_FACTOR = 2
    # Initialize player timeout (number of seconds a player is allowd to take to make a move/placement)
    PLAYER_TIMEOUT = 1

    def __init__(self, rows: int, cols: int, players: [IPlayer], fish_no: int = None) -> None:
        """
        Initializes a referee for a game with a board of size row x col and a given (ordered) list of IPlayer
        objects.

        :param rows: row dimension of the board
        :param cols: column dimension of the board
        :param players: list of IPlayer objects sorted in increasing order of age
        :param fish_no: Number of fish to be placed on each tile on the board
        :return: None
        """
        # Validate params
        if not isinstance(rows, int) or rows <= 0:
            raise TypeError('Expected positive int for rows!')

        if not isinstance(cols, int) or cols <= 0:
            raise TypeError('Expected positive int for cols!')

        if not isinstance(players, list):
            raise TypeError('Expected list for players!')

        # Make sure list consists of only IPlayer objects
        if not all(isinstance(x, IPlayer) for x in players):
            raise TypeError('All player list objects have to of type IPlayer!')

        # Make sure we weren't given too many players
        if len(players) < ct.MIN_PLAYERS or len(players) > ct.MAX_PLAYERS:
            raise ValueError(f'Invalid player length; length has to be between {ct.MIN_PLAYERS} and'
                             f' {ct.MAX_PLAYERS}')

        # Make sure dimensions are large enough to accommodate all players
        if cols * rows < len(players):
            raise ValueError('Board dimensions are too small to accomodate all players!')

        # Make sure fish is between 1 and 5 or is equal to None (default value, means that the user didn't specify a
        # fish number.
        if fish_no is not None and \
                (not isinstance(fish_no, int) or fish_no < ct.MIN_FISH_PER_TILE or fish_no > ct.MAX_FISH_PER_TILE):
            raise ValueError('Expected positive int between 1 and 5 inclusive for fish!')

        # Set properties
        self.__players: [IPlayer] = players
        self.__avatars_per_player = 6 - len(players)

        # Make up list of IPlayer holding failing players
        self.__failing_players: [IPlayer] = []
        # Make up list of IPlayer holding cheating players
        self.__cheating_players: [IPlayer] = []

        # Initialize game update callbacks as a list of callable items called every time
        # the state of the game changes
        self.__game_update_callbacks = []

        # Initializes game over callbacks as a list of callable items called at the end
        # of the game together with the game report
        self.__game_over_callbacks = []

        # Make up a board
        self.__board = self.__make_board(cols, rows, fish_no)

        # Send player's color information
        self.__notify_player_colors()

        # Make up state from board & list of PlayerEntity objects
        self.__state = State(self.__board, [PlayerEntity(p.name, p.color) for p in players])
        # Initialize game tree placeholder
        self.__game_tree = None

        # Make up flag to indicate whether the game has started
        self.__started = False
        # Make up flag to indicate whether the game has ended
        self.__game_over = False
        # Initialize empty game report that will be fleshed out at game end
        self.__report = {}
        # Initialize empty list of IPlayer to hold winners (player(s) with the highest score in the game)
        self.__winners = []
        # Initialize empty list of IPlayer to hold losers
        self.__losers = []

    @property
    def game_over(self) -> bool:
        """
        Tells whether the game run by this referee has ended.
        """
        return self.__game_over

    @property
    def game_report(self) -> dict:
        """
        Retrieves game report for the game.
        """
        return self.__report.copy()

    @property
    def winners(self) -> [IPlayer]:
        """
        Retrieves the winners in this game.
        """
        return self.__winners

    @property
    def losers(self) -> [IPlayer]:
        """
        Retrieves the losers in this game.
        """
        return self.__losers

    @property
    def state(self) -> State:
        """
        Retrieves the current game state in this game.
        """
        return self.__state

    def start(self) -> None:
        """
        This method starts the game by first running a series of placement rounds and then
        prompting each player to make a move until game end. At game end, it provides all pertinent
        parties with a copy of the game report.

        :return: None
        """
        # Return if we already started
        if self.__started:
            return

        # RUN ON A SEPARATE THREAD
        # Indicate that game has started
        self.__started = True
        # Run placement rounds
        if self.__run_placements():
            # Initialize game tree for rule checking
            self.__game_tree = GameTree(self.__state)
            # Run game
            self.__run_game()

        # End game
        self.__fire_game_over()

    @property
    def players(self) -> [IPlayer]:
        """
        Returns (copy) collection of players referee oversees.
        """
        return pickle.loads(pickle.dumps(self.__players))

    @property
    def cheating_players(self) -> [IPlayer]:
        """
        Returns collection of IPlayer objects corresponding to cheating
        players.

        :return: resulting list of Color
        """
        return self.__cheating_players

    @property
    def failing_players(self) -> [IPlayer]:
        """
        Returns collection of IPlayer objects corresponding to failing
        players.

        :return: resulting list of Color
        """
        return self.__failing_players

    @property
    def started(self) -> bool:
        """
        Returns boolean flag indicating whether the referee has started
        the game. A game is started when the referee prompts the first player to
        make a placement.

        :return: boolean flag indicating the above
        """
        return self.__started

    def __notify_player_colors(self):
        """
        Assign each player the color that correspond to their position in the player list and notify
        each player which colors they will be playing against. If player's fail to acknowledge the color
        messages, their are marked as failing players.
        :return: None
        """
        # Assign each player the color that correspond to their position in the player list
        game_colors = []
        for index, p in enumerate(self.__players):
            ack = utils.timed_call(Referee.PLAYER_TIMEOUT, p, 'set_color', args=(Color(index),))
            game_colors.append(Color(index))
            # if the player doesn't ack, they are a failing player
            if ack is None or not ack:
                self.__failing_players.append(p)

        # Notify each player which colors they will be playing against
        for player in self.__players:
            colors = [color for color in game_colors if color != player.color]
            ack = utils.timed_call(Referee.PLAYER_TIMEOUT, player, 'notify_opponent_colors', args=tuple([colors]))
            # if the player doesn't ack, they are a failing player
            if ack is None or not ack:
                self.__failing_players.append(player)

    def __make_board(self, cols: int, rows: int, fish_no: int) -> Board:
        """
        Makes a board with the given dimensions. It also applies a difficulty factor to
        the board by removing at most DIFFICULTY_FACTOR tiles. What and how many tiles
        is something determined randomly.

        :param cols: number of columns for the board
        :param rows: number of rows for the board
        :param fish: number of fish to be placed on each tile on the board
        :return: resulting Board object
        """

        # number of fish as a range or set number
        fish_no = randrange(ct.MIN_FISH_PER_TILE, ct.MAX_FISH_PER_TILE) if fish_no is None else fish_no

        # Make up board
        board = Board.homogeneous(fish_no, rows, cols)
        # Determine number of tiles to remove given difficulty factor
        tiles_to_remove = min(Referee.DIFFICULTY_FACTOR,
                              rows * cols - len(self.__players) * self.__avatars_per_player)

        for k in range(tiles_to_remove):
            # Generate random row of tile to remove
            random_row = randrange(0, rows - 1)
            # Generate random col of tile to remove
            random_col = randrange(0, cols - 1)
            # Make up location of tile to remove
            tile_location = Position(random_row, random_col)

            # If it's a hole, skip
            if board.get_tile(tile_location).is_hole:
                continue

            # Remove tile
            board.remove_tile(tile_location)

        # Return resulting board
        return board

    def __run_placements(self) -> bool:
        """
        Runs placements rounds until everyone has placed their avatars. Players may
        get removed in the process for either failing or cheating. If all players
        get removed then the function returns False to indicate there is no point
        in pressing forward with the game. Otherwise, it returns True.

        :return: boolean indicating whether any players remain
        """

        # Determine how many avatars there are to place
        avatars_to_place = self.__avatars_per_player * len(self.__players)

        # Prompt players to place until we've exhausted all avatars
        while avatars_to_place > 0:
            # Cycle over players and have them provide a Position object describing where they
            # wish to place their avatars
            for p in self.__players:
                # Check if player has either failed or cheated; if they have, skip 'em over
                if p in self.__failing_players or p in self.__cheating_players:
                    avatars_to_place -= 1
                    continue

                # Get placement for player using a deep copy of state
                placement = utils.timed_call(Referee.PLAYER_TIMEOUT, p, 'get_placement',
                                             args=(self.__state.deepcopy(),))

                # Validate placement received
                if not isinstance(placement, Position):
                    # If it's not a Position, mark out player as failing & remove player from
                    # state
                    self.__kick_player(p, PlayerKickReason.FAILING)
                    # Decrement avatars needed to be placed
                    avatars_to_place -= 1
                    continue

                try:
                    # Try to place on board
                    self.__state.place_avatar(p.color, placement)
                except InvalidPositionException:
                    # Position is out-of-bounds, already occupied or a hole. Mark player
                    # as cheating & remove player from state.
                    self.__kick_player(p, PlayerKickReason.CHEATING)
                    # Decrement avatars needed to be placed
                    avatars_to_place -= 1
                    continue

                if Referee.DEBUG:
                    print(f'got placement of {placement} from player {p.color}')

                self.__fire_game_state_changed()
                # Decrement avatars needed to be placed
                avatars_to_place -= 1

        # Check if any players remain after placement (everyone might have gotten kicked)
        return self.__state.players_no != 0

    def __kick_player(self, player_obj: IPlayer, reason: PlayerKickReason):
        """
        Kicks provided Player from the game.

        :param player_obj: IPlayer object to kick
        :param reason: reason (str) they're being kicked
        """
        # Validate params
        if not isinstance(player_obj, IPlayer):
            raise TypeError('Expected IPlayer object for player_obj!')

        if not isinstance(reason, PlayerKickReason):
            raise TypeError('Expected PlayerKickReason for reason!')

        if Referee.DEBUG:
            print(f'Kicking {player_obj.color} for reason {reason}')

        if reason == PlayerKickReason.CHEATING:
            self.__cheating_players.append(player_obj)
        else:
            self.__failing_players.append(player_obj)

        # Notify player WHY they're being kicked
        player_obj.kick(reason.name)
        # Remove player from state
        self.__state.remove_player(player_obj.color)
        # Trigger event
        self.__fire_game_state_changed()

    def __run_game(self) -> None:
        """
        This method runs the game after placement by prompting each active player
        for an action. A player is active if they can move and have not been removed from
        the game.

        :return: None
        """
        # Run game by prompting players for actions until nobody can move
        while self.__state.can_anyone_move():
            self.__run_turn()

    def __run_turn(self):
        """
        This method runs a single turn by prompting the current player in the internal state
        to make a move.
        """
        current_player_obj = self.__get_player_by_color(self.__state.current_player)
        try:
            # Get action from player using a deep copy of state
            action = utils.timed_call(Referee.PLAYER_TIMEOUT, current_player_obj, 'get_action',
                                       args=(self.__state.deepcopy(),))

            # If call was not successful or anything but an Action object was returned, the player failed
            if not isinstance(action, Action):
                self.__kick_player(current_player_obj, PlayerKickReason.FAILING)
            else:
                # Use game tree to validate action (will throw InvalidPositionException if
                # action is illegal)
                self.__state = self.__game_tree.try_action(action)

                if Referee.DEBUG:
                    print(f'{current_player_obj.color} just moved from {action.src} to {action.dst}')
                self.__fire_game_state_changed()
        except AssertionError as e:
            # Raise assertion errors are these are used for testing
            raise e
        except InvalidActionException:
            self.__kick_player(current_player_obj, PlayerKickReason.CHEATING)

    def __get_player_by_color(self, color: Color) -> IPlayer:
        """
        Retrieves IPlayer object with provided color.

        :param color: Color of player to retrieve
        :return: associated IPlayer object
        """
        # Validate params
        if not isinstance(color, Color):
            raise TypeError('Expected Color for color!')

        for p in self.__players:
            if p.color == color:
                return p

        raise NonExistentPlayerException()

    def __fire_game_state_changed(self):
        """
        Signals that the game state has changed and it is time to update the game tree, sync all the players and
        notify all subscribed observers about the new state. It notifies observers so by calling their provided
        callbacks on a copy of the latest game state.
        """
        # Update game tree
        self.__game_tree = GameTree(self.__state)

        # Notify all parties subscribed for game updates
        state_to_broadcast = self.__state.deepcopy()

        # Cycle over players and sync them
        for p in self.__players:
            p.sync(state_to_broadcast)

        # Cycle over game update callbacks and call each one
        # with a copy of the latest state
        for callback in self.__game_update_callbacks:
            try:
                callback(state_to_broadcast)
            except AssertionError as e:
                # Raise assertion exceptions are these are used for testing
                raise e
            except Exception as e:
                print(f'Exception occurred, removing observer: {e}')

    def __get_game_report(self) -> dict:
        """
        Retrieves the final game report. It encompasses a list of the cheating players' colors,
        a list of the failing players' colors and a list of dictionary objects sorted in decreasing
        order of score, each object containing the respective player's name, color and score.

        Here's an example of what the report may look like:

        {
            'cheating_players': [Color.BROWN],
            'failing_players': [Color.RED],
            'leaderboard': [
                {'name': 'Winner', 'color': Color.BLACK, 'score': 99},
                {'name': 'Runner-up', 'color': Color.WHITE, 'score': 40}
            ]

        }

        :return: resulting dict object
        """
        # Make up array to hold leaderboard
        leaderboard = []

        # Cycle over rule-abiding players and collect their name, color & score
        for p in self.__state.players:
            # Only add player to leaderboard if they were rule-abiding
            if p not in self.__failing_players and p not in self.__cheating_players:
                leaderboard.append({'name': p.name, 'color': p.color, 'score': p.score})

        # Sort leader board in decreasing order of score
        leaderboard.sort(key=operator.itemgetter('score'), reverse=True)

        # Return report
        return {
            'cheating_players': self.__cheating_players,
            'failing_players': self.__failing_players,
            'leaderboard': leaderboard
        }

    def __get_player_by_name(self, name: str) -> IPlayer:
        for p in self.__players:
            if p.name == name:
                return p

    def __fire_game_over(self) -> None:
        """
        Signals the game is over and dispatches the final game report to all subscribed
        observers.
        """
        # Retrieve report
        self.__report = self.__get_game_report()

        # Set flag
        self.__game_over = True

        # Determine highest score in the game
        max_score = max([p['score'] for p in self.__report['leaderboard']]) \
            if len(self.__report['leaderboard']) > 0 else 0

        # Determine names of winners
        winner_names = [p['name'] for p in self.__report['leaderboard'] if p['score'] == max_score]
        loser_names = [p['name'] for p in self.__report['leaderboard'] if p['score'] < max_score]

        # Determine winners with the highest scores by name
        self.__winners = [self.__get_player_by_name(name) for name in winner_names]
        # Determine losers by adding players with scores < highest_score, failing & cheating players
        self.__losers = [self.__get_player_by_name(name) for name in loser_names]

        self.__losers.extend(self.__report['failing_players'])
        self.__losers.extend(self.__report['cheating_players'])

        if Referee.DEBUG:
            print(f'Game over report: {self.__report}')

        # Give each player a copy of the report
        for player in self.__players:
            player.game_over(self.__report['leaderboard'], self.__report['cheating_players'],
                             self.__report['failing_players'])

        # Cycle over game update callbacks and call each observer with the report
        for callback in self.__game_over_callbacks:
            try:
                callback(self.__report)
            except AssertionError as e:
                # Raise assertion exceptions are these are used for testing
                raise e
            except Exception as e:
                # Callback has failed
                if Referee.DEBUG:
                    print(f'Game over callback has failed: {e}')

    def subscribe_game_updates(self, callback: 'Callable') -> None:
        """
        Subscribes caller for game state updates by way of a callable
        object that is called with a copy of the internal game state
        every time said game state changes. i.e. callback(state)

        :param callback: callback function call state on
        :return: None
        """
        # Validate params
        if not callable(callback):
            raise TypeError('Expected callable for callback!')

        # Add to list of callbacks
        self.__game_update_callbacks.append(callback)

    def subscribe_final_game_report(self, callback: 'Callable'):
        """
        Subscribes observers for the final game report by way of a callable
        object that is called with a copy of the final game report
        when the game ends. i.e. callback(report).

        :param callback: callback function call report on
        :return: None
        """
        # Validate params
        if not callable(callback):
            raise TypeError('Expected callable for callback!')

        # Add to list of callbacks
        self.__game_over_callbacks.append(callback)