def test_extreme_aspect_ratios(self):
        test_cases = [(0.1, (1000, 100), [10, 100], (500, 0), (450, 45)),
                      (1.0, (1000, 100), [100, 100], (500, 0), (450, 0)),
                      (10.0, (1000, 100), [100, 10], (500, 45), (495, 0)),
                      (10.0, (100, 1000), [50, 5], (50, 497.5), (47.5, 475)),
                      (1.0, (100, 1000), [50, 50], (50, 475), (25, 475)),
                      (0.1, (100, 1000), [10, 100], (50, 450), (0, 495)),
                      (0.1, (1000, 1000), [100, 1000], (500, 0), (0, 450)),
                      (1.0, (1000, 1000), [500, 500], (500, 250), (250, 250)),
                      (10.0, (1000, 1000), [500, 50], (500, 475), (475, 250))]

        for i, test_case in enumerate(test_cases):
            ratio, size, card_size, talon_pos, trump_pos = test_case
            talon_widget = TalonWidget(aspect_ratio=ratio)
            talon_widget.size = size
            talon_widget.size_hint = None, None
            self.render(talon_widget)

            talon_card = _get_test_card(ratio)
            talon_card.pos = 12, 34
            talon_card.size = 56, 56 / ratio
            trump_card = _get_test_card(ratio)
            trump_card.pos = 98, 76
            trump_card.size = 54, 54 / ratio

            talon_widget.set_trump_card(trump_card)
            talon_widget.push_card(talon_card)
            self.advance_frames(1)
            self.assertEqual(talon_pos, talon_card.pos, msg=i)
            self.assertEqual(card_size, talon_card.size, msg=i)
            self.assertEqual(0, talon_card.rotation, msg=i)
            self.assertEqual(trump_pos, trump_card.pos, msg=i)
            self.assertEqual(card_size, trump_card.size, msg=i)
            self.assertEqual(90, trump_card.rotation, msg=i)
    def test_close_talon(self):
        talon_widget = TalonWidget(aspect_ratio=0.5)
        self.render(talon_widget)

        trump_card = _get_test_card()
        talon_widget.set_trump_card(trump_card)
        talon_card = _get_test_card()
        talon_widget.push_card(talon_card)
        self.advance_frames(1)

        self.assertFalse(talon_widget.closed)
        self.assert_is_drawn_on_top(talon_card, trump_card)
        self.assertNotEqual(talon_card.center, trump_card.center)
        self.assertEqual(90, trump_card.rotation)

        talon_widget.closed = True
        self.advance_frames(1)
        self.assertTrue(talon_widget.closed)
        self.assert_is_drawn_on_top(trump_card, talon_card)
        self.assertEqual(talon_card.center, trump_card.center)
        self.assertAlmostEqual(10, trump_card.rotation, places=0)

        talon_widget.closed = False
        self.advance_frames(1)
        self.assertFalse(talon_widget.closed)
        self.assert_is_drawn_on_top(talon_card, trump_card)
        self.assertNotEqual(talon_card.center, trump_card.center)
        self.assertAlmostEqual(90, trump_card.rotation, places=0)
 def test_delta_pct(self):
     talon_widget = TalonWidget(aspect_ratio=0.5, delta_pct=0.1)
     self.render(talon_widget)
     card_1 = _get_test_card()
     card_2 = _get_test_card()
     card_3 = _get_test_card()
     talon_widget.push_card(card_1)
     talon_widget.push_card(card_2)
     talon_widget.push_card(card_3)
     self.advance_frames(1)
     self.assertEqual((dp(160), dp(0)), card_1.pos)
     self.assertEqual((dp(172), dp(24)), card_2.pos)
     self.assertEqual((dp(184), dp(48)), card_3.pos)
Ejemplo n.º 4
0
 def _init_talon_widget(self):
     self._talon = TalonWidget(delta_pct=0.005)
     self._talon.size_hint = 1, 1
     self._talon.pos_hint = {'x': 0, 'y': 0}
     self.ids.talon_placeholder.add_widget(self._talon)
Ejemplo n.º 5
0
class GameWidget(FloatLayout, Player, metaclass=GameWidgetMeta):
    """The main widget used to view/play a game of Schnapsen."""

    # pylint: disable=too-many-instance-attributes

    def __init__(self, game_options: Optional[GameOptions] = None, **kwargs):
        """
    Instantiates a new GameWidget and all its children widgets. All the widgets
    are empty (i.e., no cards).
    """
        # Store or initialize the game options. This has to be done before the call
        # to Widget.__init__() so the options are available when the game_widget.kv
        # file is parsed.
        self._game_options = game_options or GameOptions()

        super().__init__(**kwargs)

        # Dictionary used to store all the cards widgets.
        self._cards: Dict[Card, CardWidget] = {}

        # A reference to the area where the cards are moved when one player plays a
        # card.
        self._play_area = self.ids.play_area.__self__
        # Store the current play area size in order to update the position of the
        # already played cards accordingly, when the window is resized.
        self._prev_play_area_size = self._play_area.size[
            0], self._play_area.size[1]
        self._prev_play_area_pos = self._play_area.pos[0], self._play_area.pos[
            1]
        self._play_area.bind(size=lambda *_: self._update_play_area_cards())

        # The cards in the players' hands, sorted in display order.
        self._sorted_cards: Optional[PlayerPair[List[Card]]] = None

        # Widgets that store the cards.
        self._player_card_widgets: Optional[PlayerPair[CardSlotsLayout]] = None
        self._tricks_widgets: Optional[PlayerPair[CardSlotsLayout]] = None
        self._talon: Optional[TalonWidget] = None

        # Image that displays the trump suit, if the talon is empty.
        self._trump_suit_image: Optional[Image] = None

        # Labels used to display the trick points and game points.
        self._trick_score_labels: PlayerPair[Label] = PlayerPair(
            one=self.ids.human_trick_score_label.__self__,
            two=self.ids.computer_trick_score_label.__self__)
        self._game_score_labels: PlayerPair[Label] = PlayerPair(
            one=self.ids.human_game_score_label.__self__,
            two=self.ids.computer_game_score_label.__self__)

        # Stores the callback that is passed by the GameController when it requests
        # a new player action.
        self._action_callback: Optional[Callable[[PlayerAction], None]] = None

        # When a player action is requested, this dict stores the default action
        # associated to each card that can be double clicked.
        self._actions: Dict[CardWidget, PlayerAction] = {}

        # AnimationController that coordinates all card animations.
        self._animation_controller = AnimationController()
        self.fbind('size', self._cancel_animations)

        self._init_widgets()

    def _init_widgets(self):
        self._init_cards()
        self._init_tricks_widgets()
        self._init_cards_in_hand_widgets()
        self._init_talon_widget()
        self.do_layout()

    def _init_cards(self):
        self._cards = CardWidget.create_widgets_for_all_cards(
            path=self._game_options.cards_path)
        for card_widget in self._cards.values():
            card_widget.bind(on_card_moved=self._on_card_moved)

    def _init_talon_widget(self):
        self._talon = TalonWidget(delta_pct=0.005)
        self._talon.size_hint = 1, 1
        self._talon.pos_hint = {'x': 0, 'y': 0}
        self.ids.talon_placeholder.add_widget(self._talon)

    def _init_cards_in_hand_widgets(self):
        computer_cards = CardSlotsLayout(rows=1,
                                         cols=5,
                                         spacing=0.1,
                                         align_top=False)
        self.ids.computer_cards_placeholder.add_widget(computer_cards)
        computer_cards.size_hint = None, None
        human_cards = CardSlotsLayout(rows=1,
                                      cols=5,
                                      spacing=0.1,
                                      align_top=True)
        self.ids.human_cards_placeholder.add_widget(human_cards)
        human_cards.bind(size=computer_cards.setter('size'))
        self._player_card_widgets = PlayerPair(one=human_cards,
                                               two=computer_cards)

    def _init_tricks_widgets(self):
        computer_tricks = CardSlotsLayout(rows=2,
                                          cols=8,
                                          spacing=-0.2,
                                          align_top=True)
        computer_tricks.size_hint = 1, 1
        self.ids.computer_tricks_placeholder.add_widget(computer_tricks)
        human_tricks = CardSlotsLayout(rows=2,
                                       cols=8,
                                       spacing=-0.2,
                                       align_top=True)
        human_tricks.size_hint = 1, 1
        self.ids.human_tricks_placeholder.add_widget(human_tricks)
        self._tricks_widgets = PlayerPair(one=human_tricks,
                                          two=computer_tricks)

    def _init_trump_suit_image(self, suit: Suit):
        image_filename = str(suit.name.lower())[0] + ".png"
        image_full_path = os.path.join(self._game_options.cards_path,
                                       image_filename)
        self._trump_suit_image = Image(source=image_full_path)
        self._trump_suit_image.size_hint = None, None
        self._trump_suit_image.opacity = 0
        self.ids.talon_placeholder.add_widget(self._trump_suit_image)

        def center_trump_image_inside_talon_widget(*_):
            size = min(self._talon.size) / 2
            self._trump_suit_image.size = size, size
            self._trump_suit_image.center = self._talon.center

        center_trump_image_inside_talon_widget()
        self._talon.bind(center=center_trump_image_inside_talon_widget)
        self._talon.bind(size=center_trump_image_inside_talon_widget)

    def _cancel_animations(self, *_):
        if self._animation_controller.is_running:
            self._animation_controller.cancel()

    def reset(self) -> None:
        """
    Resets the GameWidget and leaves it ready to be initialized from a new game
    state.
    """
        self._trigger_layout.cancel()
        self._cancel_animations()
        _delete_widget(self._player_card_widgets.one)
        _delete_widget(self._player_card_widgets.two)
        self._player_card_widgets = None
        _delete_widget(self._talon)
        if self._trump_suit_image is not None:
            _delete_widget(self._trump_suit_image)
            self._trump_suit_image = None
        _delete_widget(self._tricks_widgets.one)
        _delete_widget(self._tricks_widgets.two)
        self._tricks_widgets = None
        for card_widget in self._cards.values():
            _delete_widget(card_widget)
        self._cards = {}
        self._sorted_cards = None
        self._init_widgets()

    @property
    def cards(self) -> Dict[Card, CardWidget]:
        return self._cards

    @property
    def talon_widget(self) -> TalonWidget:
        return self._talon

    @property
    def tricks_widgets(self) -> PlayerPair[CardSlotsLayout]:
        """
    Returns the pair of widgets used to display the tricks won by each player.
    """
        return self._tricks_widgets

    @property
    def player_card_widgets(self) -> PlayerPair[CardSlotsLayout]:
        """
    Returns the pair of widgets used to display the cards held by each player.
    """
        return self._player_card_widgets

    @property
    def play_area(self) -> FloatLayout:
        """
    Returns a reference to the widget representing the area where the cards are
    played during a trick.
    """
        return self._play_area

    @property
    def trick_score_labels(self) -> PlayerPair[Label]:
        """
    Returns the pair of labels used to display the trick points for each player.
    """
        return self._trick_score_labels

    @property
    def game_score_labels(self) -> PlayerPair[Label]:
        """
    Returns the pair of labels used to display the game points for each player.
    """
        return self._game_score_labels

    @property
    def trump_suit_image(self) -> Optional[Image]:
        """
    Returns the image used to display the trump suit when the talon is empty.
    """
        return self._trump_suit_image

    def init_from_game_state(
        self,
        game_state: GameState,
        done_callback: Closure,
        game_score: PlayerPair[int] = PlayerPair(0, 0)
    ) -> None:
        """
    Updates this GameWidget such that it represents the game state provided as
    an argument. It does not hold a reference to the game_state object. This
    GameWidget will not update itself automatically if subsequent changes are
    performed on the game_state object.
    :param game_state The initial game_state that this widget should represent.
    :param done_callback The closure that should be called once the GameWidget
    has finished initializing itself.
    :param game_score The Bummerl game score.
    """
        # Init the cards for each player.
        self._sorted_cards = PlayerPair(
            _sort_cards_for_player(game_state.cards_in_hand.one, PlayerId.ONE),
            _sort_cards_for_player(game_state.cards_in_hand.two, PlayerId.TWO))
        self._update_cards_in_hand_after_animation()

        # Init the won tricks for each player.
        for player in PlayerId:
            for trick in game_state.won_tricks[player]:
                self._cards[trick.one].visible = True
                self._tricks_widgets[player].add_card(self._cards[trick.one])
                self._cards[trick.two].visible = True
                self._tricks_widgets[player].add_card(self._cards[trick.two])

        # Init the trump card and the talon.
        if game_state.trump_card is not None:
            self._talon.set_trump_card(self._cards[game_state.trump_card])
        for i, card in enumerate(reversed(game_state.talon)):
            card_widget = self._cards[card]
            card_widget.visible = False
            if i != 0:
                card_widget.shadow = False
            self._talon.push_card(card_widget)

        if game_state.is_talon_closed:
            self._talon.closed = True

        # Init the scores.
        self.on_score_modified(game_state.trick_points)
        self._update_game_score(game_score)

        # Set the trump image source here since we know what the trump suit is, but
        # only make it visible when the talon is empty.
        self._init_trump_suit_image(game_state.trump)

        # If a card is already played check if it was a simple card play or a
        # marriage announcement and execute the corresponding action.
        for player in PlayerId:
            card = game_state.current_trick[player]
            if card is None:
                continue
            if card.suit in game_state.marriage_suits[player] and \
                card.card_value in [CardValue.QUEEN, CardValue.KING] and \
                card.marriage_pair in game_state.cards_in_hand[player]:
                action = AnnounceMarriageAction(player, card)
            else:
                action = PlayCardAction(player, card)
            # pylint: disable=cell-var-from-loop
            Clock.schedule_once(
                lambda *_: self.on_action(action, done_callback), -1)
            # pylint: enable=cell-var-from-loop
            return

        # If we didn't call on_action() above, we are done with the initialization.
        # If animations are enabled, we just flip the cards in the human player's
        # hand.
        Clock.schedule_once(
            lambda *_: self._flip_human_player_cards(game_state, done_callback
                                                     ), -1)

    def _flip_human_player_cards(self, game_state: GameState,
                                 done_callback: Closure) -> None:
        for card in game_state.cards_in_hand.one:
            card_widget = self._cards[card]
            card_widget.visible = False
            card_widget.check_aspect_ratio(False)
            self._player_card_widgets.one.remove_card(card_widget)
            self.add_widget(card_widget)
            animation = card_widget.get_flip_animation(
                self._game_options.draw_cards_duration, True)
            self._add_animation(card_widget, animation)
        self._animation_controller.start(
            lambda: self._update_cards_in_hand_after_animation(done_callback))

    def on_score_modified(self, score: PlayerPair[int]) -> None:
        """
    This method should be called whenever the trick points need to be updated.
    :param score: The updated value for trick points.
    """
        score_template = "[color=%s]Trick points: %s[/color]"
        color = _get_trick_points_color(score.one)
        self._trick_score_labels.one.text = score_template % (color, score.one)
        color = _get_trick_points_color(score.two)
        self._trick_score_labels.two.text = score_template % (color, score.two)

    def _update_game_score(self, score: PlayerPair[int]) -> None:
        assert 0 <= score.one < 7 and 0 <= score.two < 7, "Invalid game score"
        score_template = "[color=%s]Game points: %s[/color]"
        color = _get_game_points_color(score.one)
        self._game_score_labels.one.text = score_template % (color,
                                                             7 - score.one)
        color = _get_game_points_color(score.two)
        self._game_score_labels.two.text = score_template % (color,
                                                             7 - score.two)

    def do_layout(self, *args, **kwargs) -> None:
        """
    This function is called when a layout is called by a trigger. That means
    whenever the position, the size or the children of this layout change.
    """
        self.ids.computer_tricks_placeholder.height = 0.25 * self.height
        self.ids.human_tricks_placeholder.height = \
          self.ids.computer_tricks_placeholder.height
        self.ids.human_tricks_placeholder.y = 0.10 * self.height
        self.ids.fill_area.height = 0.10 * self.height
        self.ids.talon_placeholder.height = 0.30 * self.height
        self.ids.menu_placeholder.height = self.height * (1 - 0.25 - 0.25 -
                                                          0.1 - 0.3)
        super().do_layout(*args, **kwargs)

    def _get_trump_jack_widget(self) -> CardWidget:
        trump_jack = Card(suit=self._talon.trump_card.card.suit,
                          card_value=CardValue.JACK)
        trump_jack_widget = self._cards[trump_jack]
        return trump_jack_widget

    def _animate_exchange_trump_card(self, player: PlayerId) -> None:
        trump_jack_widget = self._get_trump_jack_widget()
        trump_jack_widget.grayed_out = False
        card_slots_widget = self._player_card_widgets[player]
        row, col = card_slots_widget.remove_card(trump_jack_widget)
        assert row is not None and col is not None, \
          "Trump Jack not in player's hand"
        trump_card_widget = self._talon.remove_trump_card()
        self.add_widget(trump_card_widget, index=len(self.children))
        self.add_widget(trump_jack_widget)

        exchange_pos = self._get_trump_exchange_pos()
        duration = self._game_options.exchange_trump_duration
        trump_jack_animation = Animation(center_x=exchange_pos[0],
                                         center_y=exchange_pos[1],
                                         duration=duration / 2)
        if not trump_jack_widget.visible:
            trump_jack_widget.check_aspect_ratio(False)
            trump_jack_animation &= trump_jack_widget.get_flip_animation(
                duration / 2, False)
        trump_jack_animation += Animation(rotation=90,
                                          center_x=trump_card_widget.center_x,
                                          center_y=trump_card_widget.center_y,
                                          duration=duration / 4)
        self._add_animation(trump_jack_widget, trump_jack_animation)

        cards_in_hand = _get_card_list(card_slots_widget)
        cards_in_hand.append(trump_card_widget.card)
        cards_in_hand[-1].public = True
        assert len(cards_in_hand) == 5, \
          "Cannot exchange trump with less then five cards in hand"
        self._sorted_cards[player] = _sort_cards_for_player(
            cards_in_hand, player)

        def bring_trump_card_to_front(*_):
            self.remove_widget(trump_card_widget)
            self.add_widget(trump_card_widget)
            self.remove_widget(trump_jack_widget)
            self.add_widget(trump_jack_widget, index=len(self.children))

        trump_card_col = self._sorted_cards[player].index(
            trump_card_widget.card)
        pos = card_slots_widget.get_card_pos(0, trump_card_col)
        pos = pos[0] + trump_jack_widget.width / 2, \
              pos[1] + trump_jack_widget.height / 2
        trump_card_animation = Animation(center_x=exchange_pos[0],
                                         center_y=exchange_pos[1],
                                         rotation=0,
                                         duration=duration / 4)
        trump_card_animation.bind(on_complete=bring_trump_card_to_front)
        trump_card_animation += Animation(center_x=pos[0],
                                          center_y=pos[1],
                                          duration=duration / 2)
        self._add_animation(trump_card_widget, trump_card_animation)
        self._animate_cards_for_player(player,
                                       duration=duration / 4,
                                       skip_cards=[trump_card_widget.card])

    def _get_trump_exchange_pos(self):
        exchange_pos = self._talon.pos[0], \
                       self._talon.pos[1] + self._talon.height / 2
        return exchange_pos

    def _exchange_trump_card(self, player: PlayerId) -> None:
        card_children = _remove_card_children(self)
        assert len(card_children) == 2
        jack_card_children = [
            child for child in card_children
            if child.card.card_value == CardValue.JACK
        ]
        assert len(jack_card_children) == 1
        trump_jack_widget = jack_card_children[0]
        trump_jack_widget.check_aspect_ratio(True)
        self._talon.set_trump_card(trump_jack_widget)

        card_children.remove(trump_jack_widget)
        trump_card_widget = card_children[0]
        trump_card_widget.rotation = 0
        self._player_card_widgets[player].add_card(trump_card_widget)
        if player == PlayerId.ONE:
            trump_card_widget.grayed_out = True

        self._update_cards_in_hand_for_player_after_animation(player)

    def _animate_talon_closed(self):
        trump_card_widget = self._talon.remove_trump_card()
        self.add_widget(trump_card_widget, index=len(self.children))
        exchange_pos = self._get_trump_exchange_pos()
        duration = self._game_options.close_talon_duration
        trump_card_animation = Animation(center_x=exchange_pos[0],
                                         center_y=exchange_pos[1],
                                         rotation=40,
                                         duration=duration / 2)

        def bring_trump_card_to_front(*_):
            self.remove_widget(trump_card_widget)
            self.add_widget(trump_card_widget)

        trump_card_animation.bind(on_complete=bring_trump_card_to_front)
        pos = self._talon.top_card().center
        trump_card_animation += Animation(center_x=pos[0],
                                          center_y=pos[1],
                                          rotation=10,
                                          duration=duration / 2)
        self._add_animation(trump_card_widget, trump_card_animation)

    def _get_card_pos_delta(self, player) -> Tuple[float, float]:
        """
    Position delta that should be used to avoid card overlaps when multiple
    cards should be moved roughly to the same position.

    Examples:
    * separate cards that are played to the center of the play area by double
    clicking on them instead of dragging;
    * separate the cards in a marriage when it is announced.

    :param player: The player that plays the card. For PlayerId.ONE the deltas
    move the second card towards the bottom-left corner. For PlayerId.TWO the
    deltas move the second card towards the upper-right corner.
    """
        delta = self._player_card_widgets[player].card_size
        if player == PlayerId.ONE:
            delta = -delta[0], -delta[1]
        return 0.2 * delta[0], 0.2 * delta[1]

    def _animate_card_to_play_area(
            self,
            player: PlayerId,
            card: Card,
            center: Optional[Tuple[float, float]] = None) -> None:
        card_widget = self._cards[card]
        duration = self._game_options.play_card_duration
        if card_widget.parent is not self._play_area:
            card_widget.grayed_out = False
            card_slots_widget = self._player_card_widgets[player]
            pos = card_slots_widget.remove_card(card_widget)
            assert pos[0] is not None, "Player %s does not hold %s" % (player,
                                                                       card)
            self.add_widget(card_widget)
            if center is None:
                center = self._get_default_play_location(player)
            animation = Animation(center_x=center[0],
                                  center_y=center[1],
                                  duration=duration)
            if not card_widget.visible:
                card_widget.check_aspect_ratio(False)
                animation &= card_widget.get_flip_animation(duration, False)
            self._add_animation(card_widget, animation)

    def _move_player_card_to_play_area(
            self,
            player: PlayerId,
            card: Card,
            center: Optional[Tuple[float, float]] = None) -> None:
        """
    Move the card given as argument from the player's hand to the play area.
    :param player: The player holding the card to be moved.
    :param card: The card to be moved.
    :param center: The position of the center of the card after the move.
    """
        card_widget = self._cards[card]
        if card_widget.parent != self._play_area:
            logging.info("GameWidget: Moving %s to play area.", card)
            if center is None:
                center = self._get_default_play_location(player)
            self.remove_widget(card_widget)
            self._play_area.add_widget(card_widget)
            card_widget.visible = True
            card_widget.check_aspect_ratio(True)
            card_widget.size = self.player_card_widgets.one.card_size
            card_widget.center = center[0], center[1]

    def _get_default_play_location(self, player: PlayerId) -> Tuple[int, int]:
        """
    Returns the coordinates where a card should be moved when it is played
    without using dragging (i.e., it's a card played by the computer or the user
    double-clicked it instead of dragging it).
    """
        pos = self._play_area.center
        for widget in self._play_area.children:
            if isinstance(widget, CardWidget):
                pos = widget.center
                delta = self._get_card_pos_delta(player)
                pos = pos[0] + delta[0], pos[1] + delta[1]
                break
        return pos[0], pos[1]

    def _update_play_area_cards(self) -> None:
        """
    Whenever the size of the play area changes (because the size of the
    GameWidget changes), we resize the cards to match the size of the cards in
    the player's hand and we reposition them proportionally to the new size of
    the play area.
    """
        for widget in self._play_area.children:
            if not isinstance(widget, CardWidget):
                continue
            new_center_x = (widget.center[0] - self._prev_play_area_pos[0]) / \
                           self._prev_play_area_size[0] * self._play_area.size[0]
            new_center_y = (widget.center[1] - self._prev_play_area_pos[1]) / \
                           self._prev_play_area_size[1] * self._play_area.size[1]
            widget.size = self.player_card_widgets.one.card_size
            widget.center = self._play_area.pos[0] + new_center_x, \
                            self._play_area.pos[1] + new_center_y
        self._prev_play_area_size = self._play_area.size[
            0], self._play_area.size[1]
        self._prev_play_area_pos = self._play_area.pos[0], self._play_area.pos[
            1]

    def on_action(self, action: PlayerAction, done_callback: Closure) -> None:
        """
    This method should be called whenever a new player action was performed in a
    game of Schnapsen, in order to update the state of the widget accordingly.
    :param action: The latest action performed by one of the players.
    :param done_callback: The closure to be called once the GameWidget is done
    updating itself according to the player action.
    """
        logging.info("GameWidget: on_action: %s", action)
        if isinstance(action, ExchangeTrumpCardAction):
            self._animate_exchange_trump_card(action.player_id)
        elif isinstance(action, CloseTheTalonAction):
            self._animate_talon_closed()
        elif isinstance(action, PlayCardAction):
            self._animate_card_to_play_area(action.player_id, action.card)
        elif isinstance(action, AnnounceMarriageAction):
            center = self._get_default_play_location(action.player_id)
            delta = self._get_card_pos_delta(action.player_id)
            pair_center = center[0] + delta[0], center[1] + delta[1]
            self._animate_card_to_play_area(action.player_id,
                                            action.card.marriage_pair,
                                            pair_center)
            self._animate_card_to_play_area(action.player_id, action.card)
        else:
            assert False, "Should not reach this code"
        assert not self._animation_controller.is_running
        self._animation_controller.start(
            lambda: self._on_action_after_animations(action, done_callback))

    def _on_action_after_animations(self, action: PlayerAction,
                                    done_callback: Closure) -> None:
        if isinstance(action, ExchangeTrumpCardAction):
            self._exchange_trump_card(action.player_id)
        elif isinstance(action, CloseTheTalonAction):
            card_children = _remove_card_children(self)
            assert len(card_children) == 1, card_children
            trump_card_widget = card_children[0]
            self._talon.set_trump_card(trump_card_widget)
            self._talon.closed = True
        elif isinstance(action, PlayCardAction):
            self._move_player_card_to_play_area(action.player_id, action.card)
        elif isinstance(action, AnnounceMarriageAction):
            center = self._get_default_play_location(action.player_id)
            delta = self._get_card_pos_delta(action.player_id)
            pair_center = center[0] + delta[0], center[1] + delta[1]
            self._move_player_card_to_play_area(action.player_id,
                                                action.card.marriage_pair,
                                                pair_center)
            self._move_player_card_to_play_area(action.player_id, action.card,
                                                center)
        else:
            assert False, "Should not reach this code"  # pragma: no cover
        done_callback()

    # pylint: disable=too-many-arguments
    def on_trick_completed(self, trick: Trick, winner: PlayerId,
                           cards_in_hand: PlayerPair[List[Card]],
                           draw_new_cards: bool,
                           done_callback: Closure) -> None:
        """
    This method should be called whenever a trick is completed in a game of
    Schnapsen, in order to update the state of this GameWidget accordingly.
    :param trick: The trick that just got completed.
    :param winner: The player that won the trick.
    :param cards_in_hand: The updated list of cards that each player holds.
    :param draw_new_cards: If True, it means that each player drew a new card
    from the talon at the end of the trick.
    :param done_callback Closure to be called when the GameWidget has finished
    updating itself based on this trick-completed event.
    """
        assert not self._animation_controller.is_running

        # Move the cards from the play area to self and keep the same z-order.
        for child in reversed(self._play_area.children):
            if not isinstance(child, CardWidget):
                continue
            if child.card not in [trick.one, trick.two]:
                continue
            self._play_area.remove_widget(child)
            self.add_widget(child)

        tricks_widget = self._tricks_widgets[winner]
        duration = self._game_options.trick_completed_duration

        # Animate the first card.
        row, col = tricks_widget.first_free_slot
        pos = tricks_widget.get_card_pos(row, col)
        size = tricks_widget.card_size
        card_widget = self._cards[trick.one]
        card_widget.check_aspect_ratio(False)
        self._add_animation(
            card_widget,
            Animation(x=pos[0],
                      y=pos[1],
                      width=size[0],
                      height=size[1],
                      duration=duration))

        # Animate the second card.
        pos = tricks_widget.get_card_pos(row, col + 1)
        card_widget = self._cards[trick.two]
        card_widget.check_aspect_ratio(False)
        self._add_animation(
            card_widget,
            Animation(x=pos[0],
                      y=pos[1],
                      width=size[0],
                      height=size[1],
                      duration=duration))

        # Update the list of sorted cards for each player.
        self._sorted_cards = PlayerPair(
            _sort_cards_for_player(cards_in_hand.one, PlayerId.ONE),
            _sort_cards_for_player(cards_in_hand.two, PlayerId.TWO))

        self._animation_controller.start(
            lambda: self._on_trick_completed_after_animations(
                trick, winner, draw_new_cards, done_callback))

    # pydgdgdlint: disable=too-many-arguments
    def _on_trick_completed_after_animations(self, trick: Trick,
                                             winner: PlayerId,
                                             draw_new_cards: bool,
                                             done_callback: Closure) -> None:
        tricks_widget = self._tricks_widgets[winner]

        for card in [trick.one, trick.two]:
            card_widget = self._cards[card]
            self.remove_widget(card_widget)
            tricks_widget.add_card(card_widget)
            card_widget.check_aspect_ratio(True)

        if draw_new_cards:
            first_card = self._talon.pop_card()
            self.add_widget(first_card)
            second_card = self._talon.pop_card()
            if second_card is None:
                # TODO(ui): If talon is empty maybe show opponent cards in hand as well.
                second_card = self._talon.remove_trump_card()
            self.add_widget(second_card)
        self._update_cards_in_hand(done_callback)

    def _update_cards_in_hand(self, done_callback: Closure) -> None:
        duration = self._game_options.draw_cards_duration
        self._animate_cards_for_player(PlayerId.ONE, duration)
        self._animate_cards_for_player(PlayerId.TWO, duration)
        self._animation_controller.start(
            lambda: self._update_cards_in_hand_after_animation(done_callback))

    def _animate_cards_for_player(self,
                                  player: PlayerId,
                                  duration: float,
                                  skip_cards: List[Card] = None) -> None:
        cards_slots_widget = self._player_card_widgets[player]
        for i, card in enumerate(self._sorted_cards[player]):
            if skip_cards is not None and card in skip_cards:
                continue
            card_widget = self._cards[card]
            card_widget.do_translation = False
            card_widget.shadow = True
            if cards_slots_widget.at(0, i) == card_widget:
                continue
            pos = cards_slots_widget.get_card_pos(0, i)
            size = cards_slots_widget.card_size
            card_widget.check_aspect_ratio(False)
            animation_params = {
                'x': pos[0],
                'y': pos[1],
                'width': size[0],
                'height': size[1],
                'duration': duration
            }
            if card_widget.rotation != 0:
                animation_params['rotation'] = 0
            animation = Animation(**animation_params)
            if player == PlayerId.ONE and not card_widget.visible:
                animation &= card_widget.get_flip_animation(duration, False)
            self._add_animation(card_widget, animation)

    def _update_cards_in_hand_after_animation(
            self, done_callback: Optional[Closure] = None) -> None:
        self._update_cards_in_hand_for_player_after_animation(PlayerId.ONE)
        self._update_cards_in_hand_for_player_after_animation(PlayerId.TWO)
        if self._talon.top_card(
        ) is None and self._trump_suit_image is not None:
            self._trump_suit_image.opacity = 1
        if done_callback is not None:
            done_callback()

    def _update_cards_in_hand_for_player_after_animation(
            self, player: PlayerId):
        for col in range(5):
            self._player_card_widgets[player].remove_card_at(0, col)
        _remove_card_children(self)
        _remove_card_children(self._play_area)

        for i, card in enumerate(self._sorted_cards[player]):
            card_widget = self._cards[card]
            card_widget.do_translation = False
            card_widget.shadow = True
            card_widget.rotation = 0
            if player == PlayerId.ONE:
                card_widget.visible = True
                card_widget.grayed_out = True
            else:
                card_widget.visible = (
                    card.public or self._game_options.computer_cards_visible)
            self._player_card_widgets[player].add_card(card_widget, 0, i)
            card_widget.check_aspect_ratio(True)

    def request_next_action(
            self,
            game_view: GameState,
            callback: Callable[[PlayerAction], None],
            game_points: Optional[PlayerPair[int]] = None) -> None:
        available_actions = get_available_actions(game_view)
        logging.info("GameWidget: Action requested. Available actions are: %s",
                     available_actions)
        self._action_callback = callback

        for action in available_actions:
            if isinstance(action, ExchangeTrumpCardAction):
                self._bind_card_action(self._talon.trump_card, action)
            elif isinstance(action, CloseTheTalonAction):
                self._bind_card_action(self._talon.top_card(), action)
            elif isinstance(action, (AnnounceMarriageAction, PlayCardAction)):
                card = self._cards[action.card]
                card.grayed_out = False
                self._bind_card_action(card, action)
                card.do_translation = True
                if isinstance(action, AnnounceMarriageAction):
                    card.bind(
                        on_transform_with_touch=self._on_transform_with_touch)
            else:  # pragma: no cover
                assert False, "Should not be reachable"

    def _bind_card_action(self, card: CardWidget,
                          action: PlayerAction) -> None:
        self._actions[card] = action
        card.bind(on_double_tap=self._card_action_callback)

    def _card_action_callback(self, card_widget: CardWidget) -> None:
        """
    Function executed when a card is double clicked.
    :param card_widget: The card that was double-clicked.
    """
        self._reply_with_action(self._actions[card_widget])

    def _reply_with_action(self, action: PlayerAction) -> None:
        """
    This method is executed once the player decided which is the next action
    they want to play. We call the callback provided by the GameController when
    it called request_next_action() and we clear all the other card actions.
    :param action: The action that the player decided to execute.
    """
        logging.info("GameWidget: Executing action %s", action)
        for card_widget in self._actions:
            if card_widget.parent is self._player_card_widgets.one:
                card_widget.grayed_out = True
        self._clear_actions()
        callback = self._action_callback
        self._action_callback = None
        callback(action)

    def _clear_actions(self):
        """
    Remove all actions associated to a card. Unbinds all double-click callbacks.
    """
        for card_widget in self._actions:
            card_widget.unbind(on_double_tap=self._card_action_callback)
            card_widget.unbind(
                on_transform_with_touch=self._on_transform_with_touch)
            card_widget.do_translation = False
        self._actions = {}

    def _on_card_moved(self, card_widget: CardWidget,
                       center: Tuple[int, int]) -> None:
        # If the card is dragged onto the play area, play it.
        if self._play_area.collide_point(*center):
            logging.info("GameWidget: Card %s was dragged to the playing area",
                         card_widget)

            action = self._actions[card_widget]

            # If the player announces a marriage, first move the un-played card to the
            # play area, so it will be displayed under the card that is played.
            if isinstance(action, AnnounceMarriageAction):
                marriage_pair_widget = self._cards[
                    card_widget.card.marriage_pair]
                self._player_card_widgets.one.remove_card(marriage_pair_widget)
                self._play_area.add_widget(marriage_pair_widget)

            self._player_card_widgets.one.remove_card(card_widget)
            self._play_area.add_widget(card_widget)
            self._reply_with_action(action)
            return

        # If the trump jack is dragged onto the trump card, exchange the trump card,
        # if this action is available.
        if self._talon.trump_card is not None:
            if card_widget == self._get_trump_jack_widget():
                action = self._actions.get(self._talon.trump_card, None)
                if action is not None:
                    if self._talon.trump_card.collide_point(*center):
                        self._reply_with_action(action)
                        return

        # If the card is dragged anywhere else, trigger a call to do_layout() which
        # will bring the dragged card back to the player's hand before the next
        # frame is drawn.
        self._player_card_widgets.one.trigger_layout()

    def _on_transform_with_touch(self, card_widget: CardWidget, _) -> None:
        """
    This method is called whenever a marriage card is dragged by the user to a
    new position, so we can update the position of the marriage pair card
    accordingly.
    :param card_widget: The CardWidget that got dragged.
    """
        pos = card_widget.pos
        delta = self._get_card_pos_delta(PlayerId.ONE)
        pos = pos[0] + delta[0], pos[1] + delta[1]
        marriage_pair_widget = self._cards[card_widget.card.marriage_pair]
        marriage_pair_widget.pos = pos

    def _add_animation(self, card_widget: CardWidget,
                       animation: Animation) -> None:
        if not self._game_options.enable_animations:
            return
        self._animation_controller.add_card_animation(card_widget, animation)
    def test_trump_card_z_index_relative_to_other_talon_cards(self):
        talon_widget = TalonWidget(aspect_ratio=0.5)
        self.render(talon_widget)

        card_1 = _get_test_card()
        talon_widget.push_card(card_1)
        trump_card = _get_test_card()
        talon_widget.set_trump_card(trump_card)
        self.assert_is_drawn_on_top(card_1, trump_card)
        card_2 = _get_test_card()
        talon_widget.push_card(card_2)
        self.assert_is_drawn_on_top(card_1, trump_card)
        self.assert_is_drawn_on_top(card_2, trump_card)
        card_3 = _get_test_card()
        talon_widget.push_card(card_3)
        self.assert_is_drawn_on_top(card_1, trump_card)
        self.assert_is_drawn_on_top(card_2, trump_card)
        self.assert_is_drawn_on_top(card_3, trump_card)
        talon_widget.remove_trump_card()
        talon_widget.set_trump_card(trump_card)
        self.assert_is_drawn_on_top(card_1, trump_card)
        self.assert_is_drawn_on_top(card_2, trump_card)
        self.assert_is_drawn_on_top(card_3, trump_card)
        talon_widget.closed = True
        self.assert_is_drawn_on_top(trump_card, card_1)
        self.assert_is_drawn_on_top(trump_card, card_2)
        self.assert_is_drawn_on_top(trump_card, card_3)
        talon_widget.remove_trump_card()
        talon_widget.set_trump_card(trump_card)
        self.assert_is_drawn_on_top(trump_card, card_1)
        self.assert_is_drawn_on_top(trump_card, card_2)
        self.assert_is_drawn_on_top(trump_card, card_3)
        talon_widget.pop_card()
        self.assert_is_drawn_on_top(trump_card, card_1)
        self.assert_is_drawn_on_top(trump_card, card_2)
        talon_widget.push_card(card_3)
        self.assert_is_drawn_on_top(trump_card, card_1)
        self.assert_is_drawn_on_top(trump_card, card_2)
        self.assert_is_drawn_on_top(trump_card, card_3)
        talon_widget.closed = False
        self.assert_is_drawn_on_top(card_1, trump_card)
        self.assert_is_drawn_on_top(card_2, trump_card)
        self.assert_is_drawn_on_top(card_3, trump_card)
    def test_add_and_remove_cards(self):
        talon_widget = TalonWidget(aspect_ratio=0.5)
        self.render(talon_widget)

        self.assertIsNone(talon_widget.top_card())
        self.assertIsNone(talon_widget.pop_card())
        with self.assertRaisesRegex(AssertionError,
                                    "Card widget cannot be None"):
            # noinspection PyTypeChecker
            talon_widget.push_card(None)

        card_1 = _get_test_card()
        talon_widget.push_card(card_1)
        self.assertIs(card_1, talon_widget.top_card())
        card_2 = _get_test_card()
        talon_widget.push_card(card_2)
        self.assertIs(card_2, talon_widget.top_card())
        card_3 = _get_test_card()
        talon_widget.push_card(card_3)
        self.assertIs(card_3, talon_widget.top_card())

        self.assertIs(card_3, talon_widget.pop_card())
        self.assertIs(card_2, talon_widget.top_card())
        self.assertIs(card_2, talon_widget.pop_card())
        self.assertIs(card_1, talon_widget.top_card())
        self.assertIs(card_1, talon_widget.pop_card())
        self.assertIsNone(talon_widget.top_card())
        self.assertIsNone(talon_widget.pop_card())
    def test_coordinate_systems(self):
        # TODO(ui): Revisit this. It doesn't work properly without a window?
        for layout_class in [RelativeLayout, FloatLayout]:
            msg = str(layout_class.__name__)

            talon_widget = TalonWidget(aspect_ratio=0.5)
            talon_widget.size = 100, 800
            talon_widget.size_hint = None, None
            talon_widget.pos = 0, 0

            layout = layout_class()
            layout.size = 200, 800
            layout.pos = 37, 37
            layout.size_hint = None, None
            layout.add_widget(talon_widget)
            self.render(layout)

            talon_card = _get_test_card()
            talon_card.pos = 12, 34
            talon_card.size = 56, 112
            trump_card = _get_test_card()
            trump_card.pos = 98, 76
            trump_card.size = 54, 108

            self.assertEqual((12, 34), talon_card.pos, msg=msg)
            self.assertEqual([56, 112], talon_card.size, msg=msg)
            self.assertEqual(0, talon_card.rotation, msg=msg)
            self.assertEqual((98, 76), trump_card.pos, msg=msg)
            self.assertEqual([54, 108], trump_card.size, msg=msg)
            self.assertEqual(0, trump_card.rotation, msg=msg)

            talon_widget.push_card(talon_card)
            talon_widget.set_trump_card(trump_card)
            self.advance_frames(1)
            self.assertEqual((50, 350), talon_card.pos, msg=msg)
            self.assertEqual([50, 100], talon_card.size, msg=msg)
            self.assertEqual(0, talon_card.rotation, msg=msg)
            self.assertEqual((0, 375), trump_card.pos, msg=msg)
            self.assertEqual([50, 100], trump_card.size, msg=msg)
            self.assertEqual(90, trump_card.rotation, msg=msg)

            talon_widget.pos = 50, 50
            self.advance_frames(1)
            # The bottom left corner of the talon_card is at:
            #   (100, 400) inside the talon_widget;
            #   (150, 450) inside the layout;
            #   (187, 487) inside the window.
            self.assertEqual((100, 400), talon_card.pos, msg=msg)
            self.assertEqual((100, 400), talon_card.to_parent(0, 0), msg=msg)
            self.assertEqual((150, 450),
                             talon_widget.to_parent(*talon_card.pos, True),
                             msg=msg)
            self.assertEqual((187, 487),
                             talon_card.to_window(0, 0, False, True),
                             msg=msg)
            self.assertEqual((187, 487),
                             talon_widget.to_window(*talon_card.pos, False,
                                                    True),
                             msg=msg)
            self.assertEqual((187, 487),
                             layout.to_parent(*talon_widget.to_parent(
                                 *talon_card.pos, True),
                                              relative=True),
                             msg=msg)
            self.assertEqual([50, 100], talon_card.size, msg=msg)
            self.assertEqual(0, talon_card.rotation, msg=msg)

            # The bottom left corner of the trump card (after it is rotated) is at:
            #   (50, 425) inside the talon_widget;
            #   (100, 475) inside the layout;
            #   (137, 512) inside the window.
            self.assertEqual((50, 425), trump_card.pos)
            self.assertEqual((100, 475),
                             talon_widget.to_parent(*trump_card.pos, True))
            # In local coordinates we have to specify the top-left corner of the card,
            # which will be the bottom-left corner after the rotation.
            self.assertEqual((137, 512),
                             trump_card.to_window(0, trump_card.size[1], False,
                                                  True))
            self.assertEqual((137, 512),
                             talon_widget.to_window(*trump_card.pos, False,
                                                    True))
            self.assertEqual([50, 100], trump_card.size)
            self.assertEqual(90, trump_card.rotation)
    def test_set_and_remove_trump_card(self):
        talon_widget = TalonWidget(aspect_ratio=0.5)
        self.render(talon_widget)

        self.assertIsNone(talon_widget.trump_card)
        with self.assertRaisesRegex(AssertionError, "No trump card set"):
            talon_widget.remove_trump_card()
        with self.assertRaisesRegex(AssertionError,
                                    "Trump card cannot be set to None"):
            # noinspection PyTypeChecker
            talon_widget.set_trump_card(None)
        trump_card = _get_test_card()
        trump_card.visible = False
        talon_widget.set_trump_card(trump_card)
        self.assertEqual(trump_card, talon_widget.trump_card)
        self.assertTrue(trump_card.visible)
        with self.assertRaisesRegex(AssertionError,
                                    "Trump card is already set"):
            talon_widget.set_trump_card(_get_test_card())
        self.assertIs(trump_card, talon_widget.remove_trump_card())
        self.assertIsNone(talon_widget.trump_card)
        with self.assertRaisesRegex(AssertionError, "No trump card set"):
            talon_widget.remove_trump_card()
    def test_use_all_width(self):
        # pylint: disable=too-many-statements
        talon_widget = TalonWidget(aspect_ratio=0.5)
        talon_widget.size = 100, 800
        talon_widget.pos = 0, 0
        talon_widget.size_hint = None, None
        self.render(talon_widget)

        talon_card = _get_test_card()
        talon_card.pos = 12, 34
        talon_card.size = 56, 112
        trump_card = _get_test_card()
        trump_card.pos = 98, 76
        trump_card.size = 54, 108

        self.assertEqual((12, 34), talon_card.pos)
        self.assertEqual([56, 112], talon_card.size)
        self.assertEqual(0, talon_card.rotation)
        self.assertEqual((98, 76), trump_card.pos)
        self.assertEqual([54, 108], trump_card.size)
        self.assertEqual(0, trump_card.rotation)

        talon_widget.push_card(talon_card)
        self.advance_frames(1)
        self.assertEqual((50, 350), talon_card.pos)
        self.assertEqual([50, 100], talon_card.size)
        self.assertEqual(0, talon_card.rotation)
        self.assertEqual((98, 76), trump_card.pos)
        self.assertEqual([54, 108], trump_card.size)
        self.assertEqual(0, trump_card.rotation)

        talon_widget.set_trump_card(trump_card)
        self.advance_frames(1)
        self.assertEqual((50, 350), talon_card.pos)
        self.assertEqual([50, 100], talon_card.size)
        self.assertEqual(0, talon_card.rotation)
        self.assertEqual((0, 375), trump_card.pos)
        self.assertEqual([50, 100], trump_card.size)
        self.assertEqual(90, trump_card.rotation)

        talon_widget.size = 10, 80
        self.advance_frames(1)
        self.assertEqual((5, 35), talon_card.pos)
        self.assertEqual([5, 10], talon_card.size)
        self.assertEqual(0, talon_card.rotation)
        self.assertEqual((0, 37.5), trump_card.pos)
        self.assertEqual([5, 10], trump_card.size)
        self.assertEqual(90, trump_card.rotation)

        talon_widget.pos = 50, 50
        self.advance_frames(1)
        self.assertEqual((55, 85), talon_card.pos)
        self.assertEqual([5, 10], talon_card.size)
        self.assertEqual(0, talon_card.rotation)
        self.assertEqual((50, 87.5), trump_card.pos)
        self.assertEqual([5, 10], trump_card.size)
        self.assertEqual(90, trump_card.rotation)

        self.assertIs(trump_card, talon_widget.remove_trump_card())
        self.assertIs(talon_card, talon_widget.pop_card())
        talon_widget.pos = 0, 0
        self.advance_frames(1)
        self.assertEqual((55, 85), talon_card.pos)
        self.assertEqual([5, 10], talon_card.size)
        self.assertEqual(0, talon_card.rotation)
        self.assertEqual((50, 87.5), trump_card.pos)
        self.assertEqual([5, 10], trump_card.size)
        self.assertEqual(90, trump_card.rotation)
 def test_can_close_talon_widget_without_setting_a_trump_card(self):
     talon_widget = TalonWidget(aspect_ratio=0.5)
     self.render(talon_widget)
     talon_widget.closed = True
     talon_widget.closed = False
    def test_setting_closed_to_the_current_value_does_not_trigger_layout(self):
        talon_widget = TalonWidget(aspect_ratio=0.5)
        self.render(talon_widget)

        trump_card = _get_test_card()
        talon_widget.set_trump_card(trump_card)
        talon_card = _get_test_card()
        talon_card.visible = False
        talon_widget.push_card(talon_card)
        self.advance_frames(1)

        mock = Mock()
        # pylint: disable=protected-access
        talon_widget._trigger_layout = mock
        # pylint: enable=protected-access
        mock.assert_not_called()
        self.assertFalse(talon_widget.closed)
        talon_widget.closed = False
        mock.assert_not_called()
        talon_widget.closed = True
        mock.assert_called_once()
        mock.reset_mock()
        talon_widget.closed = True
        mock.assert_not_called()
        talon_widget.closed = False
        mock.assert_called_once()