schnapsen.game

In this module you will find all parts related to playing a game of Schnapsen.

   1"""
   2In this module you will find all parts related to playing a game of Schnapsen.
   3"""
   4
   5from __future__ import annotations
   6
   7from abc import ABC, abstractmethod
   8import contextlib
   9from dataclasses import dataclass, field
  10from enum import Enum
  11from io import StringIO
  12from random import Random
  13import sys
  14from typing import Generator, Iterable, Optional, Union, cast, Any
  15from .deck import CardCollection, OrderedCardCollection, Card, Rank, Suit
  16import itertools
  17
  18
  19class Bot(ABC):
  20    """
  21    The Bot baseclass. Derive your own bots from this class and implement the get_move method to use it in games.
  22    Besides the get_move method, it is also possible to override notify_trump_exchange and notify_game_end to get notified when these events happen.
  23
  24    :param name: (str): Optionally, specify a name for the bot. Defaults to None.
  25    """
  26
  27    def __init__(self, name: Optional[str] = None) -> None:
  28        if name:
  29            self.__name = name
  30
  31    @abstractmethod
  32    def get_move(self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
  33        """
  34        Get the move this Bot wants to play. This is the method that gets called by the engine to get the bot's next move.
  35        If this Bot is leading, the leader_move will be None. If this both is following, the leader_move will contain the move the opponent just played
  36
  37        :param perspective: (PlayerPerspective): The PlayerPerspective which contains the information on the current state of the game from the perspective of this player
  38        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if this bot is the leader.
  39        """
  40
  41    def notify_trump_exchange(self, move: TrumpExchange) -> None:
  42        """
  43        The engine will call this method when a trump exchange is made.
  44        Overide this method to get notified about trump exchanges. Note that this notification is sent to both bots.
  45
  46        :param move: (TrumpExchange): the Trump Exchange move that was played.
  47        """
  48
  49    def notify_game_end(self, won: bool, perspective: PlayerPerspective) -> None:
  50        """
  51        The engine will call this method when the game ends.
  52        Override this method to get notified about the end of the game.
  53
  54        :param won: (bool): Did this bot win the game?
  55        :param perspective: (PlayerPerspective) The final perspective of the game.
  56        """
  57
  58    def __str__(self) -> str:
  59        """
  60        A string representation of the Bot. If the bot was constructed with a name, it will be that name.
  61        Otherwise it will be the class name and the memory address of the bot.
  62
  63        :returns: (str): A string representation of the bot.
  64        """
  65        return self.__name if hasattr(self, '_Bot__name') else super().__str__()
  66
  67
  68class Move(ABC):
  69    """
  70    A single move during a game. There are several types of move possible: normal moves, trump exchanges, and marriages.
  71    They are implmented in classes inheriting from this class.
  72    """
  73
  74    cards: list[Card]  # implementation detail: The creation of this list is defered to the derived classes in _cards()
  75    """The cards played in this move"""
  76
  77    def is_regular_move(self) -> bool:
  78        """
  79        Is this Move a regular move (not a mariage or trump exchange)
  80
  81        :returns: a bool indicating whether this is a regular move
  82        """
  83        return False
  84
  85    def as_regular_move(self) -> RegularMove:
  86        """Returns this same move but as a RegularMove."""
  87        raise AssertionError("as_regular_move called on a Move which is not a regular move. Check with is_regular_move first.")
  88
  89    def is_marriage(self) -> bool:
  90        """
  91        Is this Move a marriage?
  92
  93        :returns: a bool indicating whether this move is a marriage
  94        """
  95        return False
  96
  97    def as_marriage(self) -> Marriage:
  98        """Returns this same move but as a Marriage."""
  99        raise AssertionError("as_marriage called on a Move which is not a Marriage. Check with is_marriage first.")
 100
 101    def is_trump_exchange(self) -> bool:
 102        """
 103        Is this Move a trump exchange move?
 104
 105        :returns: a bool indicating whether this move is a trump exchange
 106        """
 107        return False
 108
 109    def as_trump_exchange(self) -> TrumpExchange:
 110        """Returns this same move but as a TrumpExchange."""
 111        raise AssertionError("as_trump_exchange called on a Move which is not a TrumpExchange. Check with is_trump_exchange first.")
 112
 113    def __getattribute__(self, __name: str) -> Any:
 114        if __name == "cards":
 115            # We call the method to compute the card list
 116            return object.__getattribute__(self, "_cards")()
 117        return object.__getattribute__(self, __name)
 118
 119    @abstractmethod
 120    def _cards(self) -> list[Card]:
 121        """
 122        Get the list of cards in this move. This method should not be called direcly, use the cards property instead.
 123        Private abstract method for other classes to inherit and/or override.
 124        """
 125
 126    @abstractmethod
 127    def __eq__(self, __o: object) -> bool:
 128        """
 129        Compares two moves with each other. Two moves are equal in case they are of the same type and if they contain the same cards.
 130        Abstract method for derived classes to override.
 131        """
 132
 133
 134@dataclass(frozen=True)
 135class RegularMove(Move):
 136    """A regular move in the game"""
 137
 138    card: Card
 139    """The card which is played"""
 140
 141    def _cards(self) -> list[Card]:
 142        return [self.card]
 143
 144    @staticmethod
 145    def from_cards(cards: Iterable[Card]) -> list[Move]:
 146        """Create an iterable of Moves from an iterable of cards."""
 147        return [RegularMove(card) for card in cards]
 148
 149    def is_regular_move(self) -> bool:
 150        return True
 151
 152    def as_regular_move(self) -> RegularMove:
 153        return self
 154
 155    def __repr__(self) -> str:
 156        return f"RegularMove(card={self.card})"
 157
 158    def __eq__(self, __o: object) -> bool:
 159        if not isinstance(__o, RegularMove):
 160            return False
 161        return self.card == __o.card
 162
 163
 164@dataclass(frozen=True)
 165class TrumpExchange(Move):
 166    """A move that implements the exchange of the trump card for a Jack of the same suit."""
 167
 168    jack: Card
 169    """The Jack which will be placed at the bottom of the Talon"""
 170
 171    def __post_init__(self) -> None:
 172        """
 173        Asserts that the card is a Jack
 174        """
 175        assert self.jack.rank is Rank.JACK, f"The rank card {self.jack} used to initialize the {TrumpExchange.__name__} was not Rank.JACK"
 176
 177    def is_trump_exchange(self) -> bool:
 178        """
 179        Returns True if this is a trump exchange.
 180        """
 181        return True
 182
 183    def as_trump_exchange(self) -> TrumpExchange:
 184        """
 185        Returns this same move but as a TrumpExchange.
 186
 187        """
 188        return self
 189
 190    def _cards(self) -> list[Card]:
 191        return [self.jack]
 192
 193    def __repr__(self) -> str:
 194        return f"TrumpExchange(jack={self.jack})"
 195
 196    def __eq__(self, __o: object) -> bool:
 197        if not isinstance(__o, TrumpExchange):
 198            return False
 199        return self.jack == __o.jack
 200
 201
 202@dataclass(frozen=True)
 203class Marriage(Move):
 204    """
 205    A Move representing a marriage in the game. This move has two cards, a king and a queen of the same suit.
 206    Right after the marriage is played, the player must play either the queen or the king.
 207    Because it can only be beneficial to play the king, it is chosen automatically.
 208    This Regular move is part of this Move already and does not have to be played separatly.
 209    """
 210
 211    queen_card: Card
 212    """The queen card of this marriage"""
 213
 214    king_card: Card
 215    """The king card of this marriage"""
 216
 217    suit: Suit = field(init=False, repr=False, hash=False)
 218    """The suit of this marriage, gets derived from the suit of the queen and king."""
 219
 220    def __post_init__(self) -> None:
 221        """
 222        Ensures that the suits of the fields all have the same suit and are a king and a queen.
 223        Finally, sets the suit field.
 224        """
 225        assert self.queen_card.rank is Rank.QUEEN, f"The rank card {self.queen_card} used to initialize the {Marriage.__name__} was not Rank.QUEEN"
 226        assert self.king_card.rank is Rank.KING, f"The rank card {self.king_card} used to initialize the {Marriage.__name__} was not Rank.KING"
 227        assert self.queen_card.suit == self.king_card.suit, f"The cards used to inialize the Marriage {self.queen_card} and {self.king_card} so not have the same suit."
 228        object.__setattr__(self, "suit", self.queen_card.suit)
 229
 230    def is_marriage(self) -> bool:
 231        return True
 232
 233    def as_marriage(self) -> Marriage:
 234        return self
 235
 236    def underlying_regular_move(self) -> RegularMove:
 237        """
 238        Get the regular move which was played because of the marriage. In this engine this is always the queen card.
 239
 240        :returns: (RegularMove): The regular move which was played because of the marriage.
 241        """
 242        # this limits you to only have the queen to play after a marriage, while in general you would have a choice.
 243        # This is not an issue since playing the king give you the highest score.
 244        return RegularMove(self.king_card)
 245
 246    def _cards(self) -> list[Card]:
 247        return [self.queen_card, self.king_card]
 248
 249    def __repr__(self) -> str:
 250        return f"Marriage(queen_card={self.queen_card}, king_card={self.king_card})"
 251
 252    def __eq__(self, __o: object) -> bool:
 253        if not isinstance(__o, Marriage):
 254            return False
 255        return self.queen_card == __o.queen_card and self.king_card == self.king_card
 256
 257
 258class Hand(CardCollection):
 259    """
 260    The cards in the hand of a player. These are the cards which the player can see and which he can play with in the turn.
 261
 262    :param cards: (Iterable[Card]): The cards to be added to the hand
 263    :param max_size: (int): The maximum number of cards the hand can contain. If the number of cards goes beyond, an Exception is raised. Defaults to 5.
 264
 265    :attr cards: The cards in the hand - initialized from the cards parameter.
 266    :attr max_size: The maximum number of cards the hand can contain - initialized from the max_size parameter.
 267    """
 268
 269    def __init__(self, cards: Iterable[Card], max_size: int = 5) -> None:
 270        self.max_size = max_size
 271        cards = list(cards)
 272        assert len(cards) <= max_size, f"The number of cards {len(cards)} is larger than the maximum number fo allowed cards {max_size}"
 273        self.cards = cards
 274
 275    def remove(self, card: Card) -> None:
 276        """
 277        Remove one occurence of the card from this hand
 278
 279        :param card: (Card): The card to be removed from the hand.
 280        """
 281        try:
 282            self.cards.remove(card)
 283        except ValueError as ve:
 284            raise Exception(f"Trying to remove a card from the hand which is not in the hand. Hand is {self.cards}, trying to remove {card}") from ve
 285
 286    def add(self, card: Card) -> None:
 287        """
 288        Add a card to the Hand
 289
 290        :param card:  The card to be added to the hand
 291        """
 292        assert len(self.cards) < self.max_size, "Adding one more card to the hand will cause a hand with too many cards"
 293        self.cards.append(card)
 294
 295    def has_cards(self, cards: Iterable[Card]) -> bool:
 296        """
 297        Are all the cards contained in this Hand?
 298
 299        :param cards: An iterable of cards which need to be checked
 300        :returns: Whether all cards in the provided iterable are in this Hand
 301        """
 302        return all(card in self.cards for card in cards)
 303
 304    def copy(self) -> Hand:
 305        """
 306        Create a deep copy of this Hand
 307
 308        :returns: A deep copy of this hand. Changes to the original will not affect the copy and vice versa.
 309        """
 310        return Hand(list(self.cards), max_size=self.max_size)
 311
 312    def is_empty(self) -> bool:
 313        """
 314        Is the Hand emoty?
 315
 316        :returns: A bool indicating whether the hand is empty
 317        """
 318        return len(self.cards) == 0
 319
 320    def get_cards(self) -> list[Card]:
 321        """
 322        Returns the cards in the hand
 323
 324        :returns: (list[Card]): A defensive copy of the list of Cards in this Hand.
 325        """
 326        return list(self.cards)
 327
 328    def filter_suit(self, suit: Suit) -> list[Card]:
 329        """
 330        Return a list of all cards in the hand which have the specified suit.
 331
 332        :param suit: (Suit): The suit to filter on.
 333        :returns: (list(Card)): A list of cards which have the specified suit.
 334        """
 335        results: list[Card] = [card for card in self.cards if card.suit is suit]
 336        return results
 337
 338    def filter_rank(self, rank: Rank) -> list[Card]:
 339        """
 340        Return a list of all cards in the hand which have the specified rank.
 341
 342        :param suit: (Rank): The rank to filter on.
 343        :returns: (list(Card)): A list of cards which have the specified rank.
 344        """
 345        results: list[Card] = [card for card in self.cards if card.rank is rank]
 346        return results
 347
 348    def __repr__(self) -> str:
 349        return f"Hand(cards={self.cards}, max_size={self.max_size})"
 350
 351
 352class Talon(OrderedCardCollection):
 353    """
 354    The Talon contains the cards which have not yet been given to the players.
 355
 356    :param cards: The cards to be put on this talon, a defensive copy will be made.
 357    :param trump_suit: The trump suit of the Talon, important if there are no more cards to be taken.
 358    :attr _cards: The cards of the Talon (defined in super().__init__)
 359    :attr __trump_suit: The trump suit of the Talon.
 360    """
 361
 362    def __init__(self, cards: Iterable[Card], trump_suit: Optional[Suit] = None) -> None:
 363        """
 364        The cards of the Talon. The last card of the iterable is the bottommost card.
 365        The first one is the top card (which will be taken when/if a card is drawn)
 366        The Trump card is at the bottom of the Talon.
 367        The trump_suit can also be specified explicitly, which is important when the Talon is empty.
 368        If the trump_suit is specified and there are cards, then the suit of the bottommost card must be the same.
 369        """
 370        if cards:
 371            trump_card_suit = list(cards)[-1].suit
 372            assert not trump_suit or trump_card_suit == trump_suit, "If the trump suit is specified, and there are cards on the talon, the suit must be the same!"
 373            self.__trump_suit = trump_card_suit
 374        else:
 375            assert trump_suit, f"If an empty {Talon.__name__} is created, the trump_suit must be specified"
 376            self.__trump_suit = trump_suit
 377
 378        super().__init__(cards)
 379
 380    def copy(self) -> Talon:
 381        """
 382        Create an independent copy of this talon.
 383
 384        :returns: (Talon): A deep copy of this talon. Changes to the original will not affect the copy and vice versa.
 385        """
 386        # We do not need to make a copy of the cards as this happend in the constructor of Talon.
 387        return Talon(self._cards, self.__trump_suit)
 388
 389    def trump_exchange(self, new_trump: Card) -> Card:
 390        """
 391        perfom a trump-jack exchange. The card to be put as the trump card must be a Jack of the same suit.
 392        As a result, this Talon changed: the old trump is removed and the new_trump is at the bottom of the Talon
 393
 394        We also require that there must be two cards on the Talon, which is always the case in a normal game of Schnapsen
 395
 396        :param new_trump: (Card):The card to be put. It must be a Jack of the same suit as the card at the bottom
 397        :returns: (Card): The card which was at the bottom of the Talon before the exchange.
 398
 399        """
 400        assert new_trump.rank is Rank.JACK, f"the rank of the card used for the exchange {new_trump} is not a Rank.JACK"
 401        assert len(self._cards) >= 2, f"There must be at least two cards on the talon to do an exchange len = {len(self._cards)}"
 402        assert new_trump.suit is self._cards[-1].suit, f"The suit of the new card {new_trump} is not equal to the current bottom {self._cards[-1].suit}"
 403        old_trump = self._cards.pop(len(self._cards) - 1)
 404        self._cards.append(new_trump)
 405        return old_trump
 406
 407    def draw_cards(self, amount: int) -> list[Card]:
 408        """
 409        Draw a card from this Talon. This changes the talon.
 410
 411        param amount: (int): The number of cards to be drawn
 412        :returns: (Iterable[Card]): The cards drawn from this Talon.
 413        """
 414
 415        assert len(self._cards) >= amount, f"There are only {len(self._cards)} on the Talon, but {amount} cards are requested"
 416        draw = self._cards[:amount]
 417        self._cards = self._cards[amount:]
 418        return draw
 419
 420    def trump_suit(self) -> Suit:
 421        """
 422        Return the suit of the trump card, i.e., the bottommost card.
 423        This still works, even when the Talon has become empty.
 424
 425        :returns: (Suit): the trump suit of the Talon
 426        """
 427        return self.__trump_suit
 428
 429    def trump_card(self) -> Optional[Card]:
 430        """
 431        Returns the current trump card, i.e., the bottommost card.
 432        Or None in case this Talon is empty
 433
 434        :returns: (Card): The trump card, or None if the Talon is empty
 435        """
 436        if len(self._cards) > 0:
 437            return self._cards[-1]
 438        return None
 439
 440    def __repr__(self) -> str:
 441        """
 442        A string representation of the Talon.
 443
 444        :returns: (str): A string representation of the Talon.
 445        """
 446        return f"Talon(cards={self._cards}, trump_suit={self.__trump_suit})"
 447
 448
 449@dataclass(frozen=True)
 450class Trick(ABC):
 451    """
 452    A complete trick. This is, the move of the leader and if that was not an exchange, the move of the follower.
 453    """
 454
 455    cards: Iterable[Card] = field(init=False, repr=False, hash=False)
 456    """All cards used as part of this trick. This includes cards used in marriages"""
 457
 458    @abstractmethod
 459    def is_trump_exchange(self) -> bool:
 460        """
 461        Returns True if this is a trump exchange
 462
 463        :returns: True in case this was a trump exchange
 464        """
 465
 466    @abstractmethod
 467    def as_partial(self) -> PartialTrick:
 468        """
 469        Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts
 470
 471        :returns: The first part of this trick
 472        """
 473
 474    def __getattribute__(self, __name: str) -> Any:
 475        if __name == "cards":
 476            # We call the method to compute the card list
 477            return object.__getattribute__(self, "_cards")()
 478        return object.__getattribute__(self, __name)
 479
 480    @abstractmethod
 481    def _cards(self) -> Iterable[Card]:
 482        """
 483        Get all cards used in this tick. This method should not be called directly.
 484        Use the cards property instead.
 485
 486        :returns: (Iterable[Card]): All cards used in this trick.
 487        """
 488
 489
 490@dataclass(frozen=True)
 491class ExchangeTrick(Trick):
 492    """
 493    A Trick in which the player does a trump exchange.
 494    """
 495
 496    exchange: TrumpExchange
 497    """A trump exchange by the leading player"""
 498
 499    trump_card: Card
 500    """The card at the bottom of the talon"""
 501
 502    def is_trump_exchange(self) -> bool:
 503        """Returns True if this is a trump exchange"""
 504        return True
 505
 506    def as_partial(self) -> PartialTrick:
 507        """ Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts"""
 508        raise Exception("An Exchange Trick does not have a first part")
 509
 510    def _cards(self) -> Iterable[Card]:
 511        """Get all cards used in this tick. This method should not be called directly."""
 512        exchange = self.exchange.cards
 513        exchange.append(self.trump_card)
 514        return exchange
 515
 516
 517@dataclass(frozen=True)
 518class PartialTrick:
 519    """
 520    A partial trick is the move(s) played by the leading player.
 521    """
 522    leader_move: Union[RegularMove, Marriage]
 523    """The move played by the leader of the trick"""
 524
 525    def is_trump_exchange(self) -> bool:
 526        """Returns false to indicate that this trick is not a trump exchange"""
 527        return False
 528
 529    def __repr__(self) -> str:
 530        return f"PartialTrick(leader_move={self.leader_move})"
 531
 532
 533@dataclass(frozen=True)
 534class RegularTrick(Trick, PartialTrick):
 535    """
 536    A regular trick, with a move by the leader and a move by the follower
 537    """
 538    follower_move: RegularMove
 539    """The move played by the follower"""
 540
 541    def is_trump_exchange(self) -> bool:
 542        """Returns false to indicate that this trick is not a trump exchange"""
 543        return False
 544
 545    def as_partial(self) -> PartialTrick:
 546        """Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts"""
 547        return PartialTrick(self.leader_move)
 548
 549    def _cards(self) -> Iterable[Card]:
 550        """Get all cards used in this tick. This method should not be called directly."""
 551        return itertools.chain(self.leader_move.cards, self.follower_move.cards)
 552
 553    def __repr__(self) -> str:
 554        """A string representation of the Trick"""
 555        return f"RegularTrick(leader_move={self.leader_move}, follower_move={self.follower_move})"
 556
 557
 558@dataclass(frozen=True)
 559class Score:
 560    """
 561    The score of one of the bots. This consists of the current points and potential pending points because of an earlier played marriage.
 562    Note that the socre object is immutable and supports the `+` operator, so it can be used somewhat as a usual number.
 563    """
 564    direct_points: int = 0
 565    """The current number of points"""
 566    pending_points: int = 0
 567    """Points to be applied in the future because of a past marriage"""
 568
 569    def __add__(self, other: Score) -> Score:
 570        """
 571        Adds two scores together. Direct points and pending points are added separately.
 572
 573        :param other: (Score): The score to be added to the current one.
 574        :returns: (Score): A new score object with the points of the current score and the other combined
 575        """
 576        total_direct_points = self.direct_points + other.direct_points
 577        total_pending_points = self.pending_points + other.pending_points
 578        return Score(total_direct_points, total_pending_points)
 579
 580    def redeem_pending_points(self) -> Score:
 581        """
 582        Redeem the pending points
 583
 584        :returns: (Score):A new score object with the pending points added to the direct points and the pending points set to zero.
 585        """
 586        return Score(direct_points=self.direct_points + self.pending_points, pending_points=0)
 587
 588    def __repr__(self) -> str:
 589        """A string representation of the Score"""
 590        return f"Score(direct_points={self.direct_points}, pending_points={self.pending_points})"
 591
 592
 593class GamePhase(Enum):
 594    """
 595    An indicator about the phase of the game. This is used because in Schnapsen, the rules change when the game enters the second phase.
 596    """
 597    ONE = 1
 598    TWO = 2
 599
 600
 601@dataclass
 602class BotState:
 603    """A bot with its implementation and current state in a game"""
 604
 605    implementation: Bot
 606    hand: Hand
 607    score: Score = field(default_factory=Score)
 608    won_cards: list[Card] = field(default_factory=list)
 609
 610    def get_move(self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
 611        """
 612        Gets the next move from the bot itself, passing it the state.
 613        Does a quick check to make sure that the hand has the cards which are played. More advanced checks are performed outside of this call.
 614
 615        :param state: (PlayerPerspective): The PlayerGameState which contains the information on the current state of the game from the perspective of this player
 616        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
 617        :returns: The move the played
 618        """
 619        move = self.implementation.get_move(perspective, leader_move=leader_move)
 620        assert move is not None, f"The bot {self.implementation} returned a move which is None"
 621        if not isinstance(move, Move):
 622            raise AssertionError(f"The bot {self.implementation} returned an object which is not a Move, got {move}")
 623        return move
 624
 625    def copy(self) -> BotState:
 626        """
 627        Makes a deep copy of the current state.
 628
 629        :returns: (BotState): The deep copy.
 630        """
 631        new_bot = BotState(
 632            implementation=self.implementation,
 633            hand=self.hand.copy(),
 634            score=self.score,  # does not need a copy because it is not mutable
 635            won_cards=list(self.won_cards),
 636        )
 637        return new_bot
 638
 639    def __repr__(self) -> str:
 640        return f"BotState(implementation={self.implementation}, hand={self.hand}, "\
 641               f"score={self.score}, won_cards={self.won_cards})"
 642
 643
 644@dataclass(frozen=True)
 645class Previous:
 646    """
 647    Information about the previous GameState.
 648    This object can be used to access the history which lead to the current GameState
 649    """
 650
 651    state: GameState
 652    """The previous state of the game. """
 653    trick: Trick
 654    """The trick which led to the current Gamestate from the Previous state"""
 655    leader_remained_leader: bool
 656    """Did the leader of remain the leader."""
 657
 658
 659@dataclass
 660class GameState:
 661    """
 662    The current state of the game, as seen by the game engine.
 663    This contains all information about the positions of the cards, bots, scores, etc.
 664    The bot must not get direct access to this information because it would allow it to cheat.
 665    """
 666    leader: BotState
 667    """The current leader, i.e., the one who will play the first move in the next trick"""
 668    follower: BotState
 669    """The current follower, i.e., the one who will play the second move in the next trick"""
 670    trump_suit: Suit = field(init=False)
 671    """The trump suit in this game. This information is also in the Talon."""
 672    talon: Talon
 673    """The talon, containing the cards not yet in the hand of the player and the trump card at the bottom"""
 674    previous: Optional[Previous]
 675    """The events which led to this GameState, or None, if this is the initial GameState (or previous tricks and states are unknown)"""
 676
 677    def __getattribute__(self, __name: str) -> Any:
 678        if __name == "trump_suit":
 679            # We get it from the talon
 680            return self.talon.trump_suit()
 681        return object.__getattribute__(self, __name)
 682
 683    def copy_for_next(self) -> GameState:
 684        """
 685        Make a copy of the gamestate, modified such that the previous state is this state, but the previous trick is not filled yet.
 686        This is used to create a GameState which will be modified to become the next gamestate.
 687
 688        :returns: (Gamestate): A copy of the gamestate, with the previous trick not filled yet.
 689        """
 690        # We intentionally do no initialize the previous information. It is not known yet
 691        new_state = GameState(
 692            leader=self.leader.copy(),
 693            follower=self.follower.copy(),
 694            talon=self.talon.copy(),
 695            previous=None
 696        )
 697        return new_state
 698
 699    def copy_with_other_bots(self, new_leader: Bot, new_follower: Bot) -> GameState:
 700        """
 701        Make a copy of the gamestate, modified such that the bots are replaced by the provided ones.
 702        This is used to continue playing an existing GameState with different bots.
 703
 704        :param new_leader: (Bot): The new leader
 705        :param new_follower: (Bot): The new follower
 706        :returns: (Gamestate): A copy of the gamestate, with the bots replaced.
 707        """
 708        new_state = GameState(
 709            leader=self.leader.copy(),
 710            follower=self.follower.copy(),
 711            talon=self.talon.copy(),
 712            previous=self.previous
 713        )
 714        new_state.leader.implementation = new_leader
 715        new_state.follower.implementation = new_follower
 716        return new_state
 717
 718    def game_phase(self) -> GamePhase:
 719        """What is the current phase of the game
 720
 721        :returns: GamePhase.ONE or GamePahse.TWO indicating the current phase
 722        """
 723        if self.talon.is_empty():
 724            return GamePhase.TWO
 725        return GamePhase.ONE
 726
 727    def are_all_cards_played(self) -> bool:
 728        """Returns True in case the players have played all their cards and the game is has come to an end
 729
 730        :returns: (bool): True if all cards have been played, False otherwise
 731        """
 732        return self.leader.hand.is_empty() and self.follower.hand.is_empty() and self.talon.is_empty()
 733
 734    def __repr__(self) -> str:
 735        return f"GameState(leader={self.leader}, follower={self.follower}, "\
 736               f"talon={self.talon}, previous={self.previous})"
 737
 738
 739class PlayerPerspective(ABC):
 740    """
 741    The perspective a player has on the state of the game. This only gives access to the partially observable information.
 742    The Bot gets passed an instance of this class when it gets requested a move by the GamePlayEngine
 743
 744    This class has several convenience methods to get more information about the current state.
 745
 746    :param state: (GameState): The current state of the game
 747    :param engine: (GamePlayEngine): The engine which is used to play the game5
 748    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.5
 749    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
 750    """
 751
 752    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
 753        self.__game_state = state
 754        self.__engine = engine
 755
 756    @abstractmethod
 757    def valid_moves(self) -> list[Move]:
 758        """
 759        Get a list of all valid moves the bot can play at this point in the game.
 760
 761        Design note: this could also return an Iterable[Move], but list[Move] was chosen to make the API easier to use.
 762        """
 763
 764    def get_game_history(self) -> list[tuple[PlayerPerspective, Optional[Trick]]]:
 765        """
 766        The game history from the perspective of the player. This means all the past PlayerPerspective this bot has seen, and the Tricks played.
 767        This only provides access to cards the Bot is allowed to see.
 768
 769        :returns: (list[tuple[PlayerPerspective, Optional[Trick]]]): The PlayerPerspective and Tricks in chronological order, index 0 is the first round played. Only the last Trick will be None.
 770        The last pair will contain the current PlayerGameState.
 771        """
 772
 773        # We reconstruct the history backwards.
 774        game_state_history: list[tuple[PlayerPerspective, Optional[Trick]]] = []
 775        # We first push the current state to the end
 776        game_state_history.insert(0, (self, None))
 777
 778        current_leader = self.am_i_leader()
 779        current = self.__game_state.previous
 780
 781        while current:
 782            # If we were leader, and we remained, then we were leader before
 783            # If we were follower, and we remained, then we were follower before
 784            # If we were leader, and we did not remain, then we were follower before
 785            # If we were follower, and we did not remain, then we were leader before
 786            # This logic gets reflected by the negation of a xor
 787            current_leader = not current_leader ^ current.leader_remained_leader
 788
 789            current_player_perspective: PlayerPerspective
 790            if current_leader:
 791                current_player_perspective = LeaderPerspective(current.state, self.__engine)
 792            else:  # We are following
 793                if current.trick.is_trump_exchange():
 794                    current_player_perspective = ExchangeFollowerPerspective(current.state, self.__engine)
 795                else:
 796                    current_player_perspective = FollowerPerspective(current.state, self.__engine, current.trick.as_partial().leader_move)
 797            history_record = (current_player_perspective, current.trick)
 798            game_state_history.insert(0, history_record)
 799
 800            current = current.state.previous
 801        return game_state_history
 802
 803    @abstractmethod
 804    def get_hand(self) -> Hand:
 805        """Get the cards in the hand of the current player"""
 806
 807    @abstractmethod
 808    def get_my_score(self) -> Score:
 809        """Get the socre of the current player. The return Score object contains both the direct points and pending points from a marriage."""
 810
 811    @abstractmethod
 812    def get_opponent_score(self) -> Score:
 813        """Get the socre of the other player. The return Score object contains both the direct points and pending points from a marriage."""
 814
 815    def get_trump_suit(self) -> Suit:
 816        """Get the suit of the trump"""
 817        return self.__game_state.trump_suit
 818
 819    def get_trump_card(self) -> Optional[Card]:
 820        """Get the card which is at the bottom of the talon. Will be None if the talon is empty"""
 821        return self.__game_state.talon.trump_card()
 822
 823    def get_talon_size(self) -> int:
 824        """How many cards are still on the talon?"""
 825        return len(self.__game_state.talon)
 826
 827    def get_phase(self) -> GamePhase:
 828        """What is the pahse of the game? This returns a GamePhase object.
 829        You can check the phase by checking state.get_phase == GamePhase.ONE
 830        """
 831        return self.__game_state.game_phase()
 832
 833    @abstractmethod
 834    def get_opponent_hand_in_phase_two(self) -> Hand:
 835        """If the game is in the second phase, you can get the cards in the hand of the opponent.
 836        If this gets called, but the second pahse has not started yet, this will throw en Exception
 837        """
 838
 839    @abstractmethod
 840    def am_i_leader(self) -> bool:
 841        """Returns True if the bot is the leader of this trick, False if it is a follower."""
 842
 843    @abstractmethod
 844    def get_won_cards(self) -> CardCollection:
 845        """Get a list of all cards this Bot has won until now."""
 846
 847    @abstractmethod
 848    def get_opponent_won_cards(self) -> CardCollection:
 849        """Get the list of cards the opponent has won until now."""
 850
 851    def __get_own_bot_state(self) -> BotState:
 852        """Get the internal state object of this bot. This should not be used by a bot."""
 853        bot: BotState
 854        if self.am_i_leader():
 855            bot = self.__game_state.leader
 856        else:
 857            bot = self.__game_state.follower
 858        return bot
 859
 860    def __get_opponent_bot_state(self) -> BotState:
 861        """Get the internal state object of the other bot. This should not be used by a bot."""
 862        bot: BotState
 863        if self.am_i_leader():
 864            bot = self.__game_state.follower
 865        else:
 866            bot = self.__game_state.leader
 867        return bot
 868
 869    def seen_cards(self, leader_move: Optional[Move]) -> CardCollection:
 870        """Get a list of all cards your bot has seen until now
 871
 872        :param leader_move: (Optional[Move]):The move made by the leader of the trick. These cards have also been seen until now.
 873        :returns: (CardCollection): A list of all cards your bot has seen until now
 874        """
 875        bot = self.__get_own_bot_state()
 876
 877        seen_cards: set[Card] = set()  # We make it a set to remove duplicates
 878
 879        # in own hand
 880        seen_cards.update(bot.hand)
 881
 882        # the trump card
 883        trump = self.get_trump_card()
 884        if trump:
 885            seen_cards.add(trump)
 886
 887        # all cards which were played in Tricks (icludes marriages and Trump exchanges)
 888
 889        seen_cards.update(self.__past_tricks_cards())
 890        if leader_move is not None:
 891            seen_cards.update(leader_move.cards)
 892
 893        return OrderedCardCollection(seen_cards)
 894
 895    def __past_tricks_cards(self) -> set[Card]:
 896        """
 897        Gets the cards played in past tricks
 898
 899        :returns: (set[Card]): A set of all cards played in past tricks
 900        """
 901        past_cards: set[Card] = set()
 902        prev = self.__game_state.previous
 903        while prev:
 904            past_cards.update(prev.trick.cards)
 905            prev = prev.state.previous
 906        return past_cards
 907
 908    def get_known_cards_of_opponent_hand(self) -> CardCollection:
 909        """Get all cards which are in the opponents hand, but known to your Bot. This includes cards earlier used in marriages, or a trump exchange.
 910        All cards in the second pahse of the game.
 911
 912        :returns: (CardCollection): A list of all cards which are in the opponents hand, which are known to the bot.
 913        """
 914        opponent_hand = self.__get_opponent_bot_state().hand
 915        if self.get_phase() == GamePhase.TWO:
 916            return opponent_hand
 917        # We only disclose cards which have been part of a move, i.e., an Exchange or a Marriage
 918        past_trick_cards = self.__past_tricks_cards()
 919        return OrderedCardCollection(filter(lambda c: c in past_trick_cards, opponent_hand))
 920
 921    def get_engine(self) -> GamePlayEngine:
 922        """
 923        Get the GamePlayEngine in use for the current game.
 924        This engine can be used to retrieve all information about what kind of game we are playing,
 925        but can also be used to simulate alternative game rollouts.
 926
 927        :returns: (GamePlayEngine): The GamePlayEngine in use for the current game.
 928        """
 929        return self.__engine
 930
 931    def get_state_in_phase_two(self) -> GameState:
 932        """
 933        In phase TWO of the game, all information is known, so you can get the complete state
 934
 935        This removes the real bots from the GameState. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.
 936
 937        :retruns: (GameState): The GameState in phase two of the game - the active bots are replaced by dummy bots.
 938        """
 939
 940        if self.get_phase() == GamePhase.TWO:
 941            return self.__game_state.copy_with_other_bots(_DummyBot(), _DummyBot())
 942        raise AssertionError("You cannot get the state in phase one")
 943
 944    def make_assumption(self, leader_move: Optional[Move], rand: Random) -> GameState:
 945        """
 946        Takes the current imperfect information state and makes a random guess as to the position of the unknown cards.
 947        This also takes into account cards seen earlier during marriages played by the opponent, as well as potential trump jack exchanges
 948
 949        This removes the real bots from the GameState. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.
 950
 951        :param leader_move: (Optional[Move]): the optional already executed leader_move in the current trick. This card is guaranteed to be in the hand of the leader in the returned GameState.
 952        :param rand: (Random):the source of random numbers to do the random assignment of unknown cards
 953
 954        :returns: GameState: A perfect information state object.
 955        """
 956        opponent_hand = self.__get_opponent_bot_state().hand.copy()
 957
 958        if leader_move is not None:
 959            assert all(card in opponent_hand for card in leader_move.cards), f"The specified leader_move {leader_move} is not in the hand of the opponent {opponent_hand}"
 960
 961        full_state = self.__game_state.copy_with_other_bots(_DummyBot(), _DummyBot())
 962        if self.get_phase() == GamePhase.TWO:
 963            return full_state
 964
 965        seen_cards = self.seen_cards(leader_move)
 966        full_deck = self.__engine.deck_generator.get_initial_deck()
 967
 968        opponent_hand = self.__get_opponent_bot_state().hand.copy()
 969        unseen_opponent_hand = list(filter(lambda card: card not in seen_cards, opponent_hand))
 970
 971        talon = full_state.talon
 972        unseen_talon = list(filter(lambda card: card not in seen_cards, talon))
 973
 974        unseen_cards = list(filter(lambda card: card not in seen_cards, full_deck))
 975        if len(unseen_cards) > 1:
 976            rand.shuffle(unseen_cards)
 977
 978        assert len(unseen_talon) + len(unseen_opponent_hand) == len(unseen_cards), "Logical error. The number of unseen cards in the opponents hand and in the talon must be equal to the number of unseen cards"
 979
 980        new_talon: list[Card] = []
 981        for card in talon:
 982            if card in unseen_talon:
 983                # take one of the random cards
 984                new_talon.append(unseen_cards.pop())
 985            else:
 986                new_talon.append(card)
 987
 988        full_state.talon = Talon(new_talon)
 989
 990        new_opponent_hand = []
 991        for card in opponent_hand:
 992            if card in unseen_opponent_hand:
 993                new_opponent_hand.append(unseen_cards.pop())
 994            else:
 995                new_opponent_hand.append(card)
 996        if self.am_i_leader():
 997            full_state.follower.hand = Hand(new_opponent_hand)
 998        else:
 999            full_state.leader.hand = Hand(new_opponent_hand)
1000
1001        assert len(unseen_cards) == 0, "All cards must be consumed by either the opponent hand or talon by now"
1002
1003        return full_state
1004
1005
1006class _DummyBot(Bot):
1007    """A bot used by PlayerPerspective.make_assumption to replace the real bots. This bot cannot play and will throw an Exception for everything"""
1008
1009    def get_move(self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1010        raise Exception("The GameState from make_assumption removes the real bots from the Game. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.")
1011
1012    def notify_game_end(self, won: bool, perspective: PlayerPerspective) -> None:
1013        raise Exception("The GameState from make_assumption removes the real bots from the Game. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.")
1014
1015    def notify_trump_exchange(self, move: TrumpExchange) -> None:
1016        raise Exception("The GameState from make_assumption removes the real bots from the Game. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.")
1017
1018
1019class LeaderPerspective(PlayerPerspective):
1020    """
1021    The playerperspective of the Leader.
1022
1023    :param state: (GameState): The current state of the game
1024    :param engine: (GamePlayEngine): The engine which is used to play the game
1025    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.
1026    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
1027    """
1028
1029    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1030        super().__init__(state, engine)
1031        self.__game_state = state
1032        self.__engine = engine
1033
1034    def valid_moves(self) -> list[Move]:
1035        """
1036        Get a list of all valid moves the bot can play at this point in the game.
1037
1038        :returns: (list[Move]): A list of all valid moves the bot can play at this point in the game.
1039        """
1040        moves = self.__engine.move_validator.get_legal_leader_moves(self.__engine, self.__game_state)
1041        return list(moves)
1042
1043    def get_hand(self) -> Hand:
1044        """
1045        Get the cards in the hand of the leader
1046
1047        :returns: (Hand): The cards in the hand of the leader
1048        """
1049        return self.__game_state.leader.hand.copy()
1050
1051    def get_my_score(self) -> Score:
1052        """
1053        Get the score of the leader
1054
1055        :returns: (Score): The score of the leader
1056        """
1057        return self.__game_state.leader.score
1058
1059    def get_opponent_score(self) -> Score:
1060        """
1061        Get the score of the follower
1062        """
1063        return self.__game_state.follower.score
1064
1065    def get_opponent_hand_in_phase_two(self) -> Hand:
1066        """
1067        Get the cards in the hand of the follower. This is only allowed in the second phase of the game.
1068
1069        :returns: (Hand): The cards in the hand of the follower
1070        """
1071        assert self.get_phase() == GamePhase.TWO, "Cannot get the hand of the opponent in pahse one"
1072        return self.__game_state.follower.hand.copy()
1073
1074    def am_i_leader(self) -> bool:
1075        """
1076        Returns True because this is the leader perspective
1077        """
1078        return True
1079
1080    def get_won_cards(self) -> CardCollection:
1081        """
1082        Get a list of all tricks the leader has won until now.
1083
1084        :returns: (CardCollection): A CardCollection of all tricks the leader has won until now.
1085        """
1086        return OrderedCardCollection(self.__game_state.leader.won_cards)
1087
1088    def get_opponent_won_cards(self) -> CardCollection:
1089        """
1090        Get a list of all tricks the follower has won until now.
1091
1092        :returns: (CardCollection): A CardCollection of all tricks the follower has won until now.
1093        """
1094
1095        return OrderedCardCollection(self.__game_state.follower.won_cards)
1096
1097    def __repr__(self) -> str:
1098        return f"LeaderPerspective(state={self.__game_state}, engine={self.__engine})"
1099
1100
1101class FollowerPerspective(PlayerPerspective):
1102    """
1103    The playerperspective of the Follower.
1104
1105    :param state: (GameState): The current state of the game
1106    :param engine: (GamePlayEngine): The engine which is used to play the game
1107    :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
1108    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.
1109    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
1110    :attr __leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
1111    """
1112
1113    def __init__(self, state: GameState, engine: GamePlayEngine, leader_move: Optional[Move]) -> None:
1114        super().__init__(state, engine)
1115        self.__game_state = state
1116        self.__engine = engine
1117        self.__leader_move = leader_move
1118
1119    def valid_moves(self) -> list[Move]:
1120        """
1121        Get a list of all valid moves the bot can play at this point in the game.
1122
1123        :returns: (list[Move]): A list of all valid moves the bot can play at this point in the game.
1124        """
1125
1126        assert self.__leader_move, "There is no leader move for this follower, so no valid moves."
1127        return list(self.__engine.move_validator.get_legal_follower_moves(self.__engine, self.__game_state, self.__leader_move))
1128
1129    def get_hand(self) -> Hand:
1130        """
1131        Get the cards in the hand of the follower
1132
1133        :returns: (Hand): The cards in the hand of the follower
1134        """
1135        return self.__game_state.follower.hand.copy()
1136
1137    def get_my_score(self) -> Score:
1138        """
1139        Get the score of the follower
1140
1141        :returns: (Score): The score of the follower
1142        """
1143        return self.__game_state.follower.score
1144
1145    def get_opponent_score(self) -> Score:
1146        """
1147        Get the score of the leader
1148
1149        :returns: (Score): The score of the leader
1150        """
1151
1152        return self.__game_state.leader.score
1153
1154    def get_opponent_hand_in_phase_two(self) -> Hand:
1155        """
1156        Get the cards in the hand of the leader. This is only allowed in the second phase of the game.
1157
1158        :returns: (Hand): The cards in the hand of the leader
1159        """
1160
1161        assert self.get_phase() == GamePhase.TWO, "Cannot get the hand of the opponent in pahse one"
1162        return self.__game_state.leader.hand.copy()
1163
1164    def am_i_leader(self) -> bool:
1165        """ Returns False because this is the follower perspective"""
1166        return False
1167
1168    def get_won_cards(self) -> CardCollection:
1169        """
1170        Get a list of all tricks the follower has won until now.
1171
1172        :returns: (CardCollection): A CardCollection of all tricks the follower has won until now.
1173        """
1174
1175        return OrderedCardCollection(self.__game_state.follower.won_cards)
1176
1177    def get_opponent_won_cards(self) -> CardCollection:
1178        """
1179        Get a list of all tricks the leader has won until now.
1180
1181        :returns: (CardCollection): A CardCollection of all tricks the leader has won until now.
1182        """
1183
1184        return OrderedCardCollection(self.__game_state.leader.won_cards)
1185
1186    def __repr__(self) -> str:
1187        return f"FollowerPerspective(state={self.__game_state}, engine={self.__engine}, leader_move={self.__leader_move})"
1188
1189
1190class ExchangeFollowerPerspective(PlayerPerspective):
1191    """
1192    A special PlayerGameState only used for the history of a game in which a Trump Exchange happened.
1193    This state is does not allow any moves.
1194
1195    :param state: (GameState): The current state of the game
1196    :param engine: (GamePlayEngine): The engine which is used to play the game
1197    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.
1198    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
1199
1200    """
1201
1202    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1203        self.__game_state = state
1204        super().__init__(state, engine)
1205
1206    def valid_moves(self) -> list[Move]:
1207        """
1208        Get a list of all valid moves the bot can play at this point in the game.
1209
1210        Design note: this could also return an Iterable[Move], but list[Move] was chosen to make the API easier to use.
1211
1212        :returns: (list[Move]): An empty list, because no moves are allowed in this state.
1213        """
1214        return []
1215
1216    def get_hand(self) -> Hand:
1217        """
1218        Get the cards in the hand of the follower
1219
1220        :returns: (Hand): The cards in the hand of the follower
1221        """
1222
1223        return self.__game_state.follower.hand.copy()
1224
1225    def get_my_score(self) -> Score:
1226        """
1227        Get the score of the follower
1228
1229        :returns: (Score): The score of the follower
1230        """
1231
1232        return self.__game_state.follower.score
1233
1234    def get_opponent_score(self) -> Score:
1235        """
1236        Get the score of the leader
1237
1238        :returns: (Score): The score of the leader
1239        """
1240
1241        return self.__game_state.leader.score
1242
1243    def get_trump_suit(self) -> Suit:
1244        """
1245        Get the suit of the trump
1246
1247        :returns: (Suit): The suit of the trump
1248        """
1249        return self.__game_state.trump_suit
1250
1251    def get_opponent_hand_in_phase_two(self) -> Hand:
1252        """
1253        Get the cards in the hand of the leader. This is only allowed in the second phase of the game.
1254
1255        :returns: (Hand): The cards in the hand of the leader
1256        """
1257        assert self.get_phase() == GamePhase.TWO, "Cannot get the hand of the opponent in pahse one"
1258        return self.__game_state.leader.hand.copy()
1259
1260    def get_opponent_won_cards(self) -> CardCollection:
1261        """
1262        Get a list of all tricks the leader has won until now.
1263
1264        :returns: (CardCollection): A CardCollection of all tricks the leader has won until now.
1265        """
1266        return OrderedCardCollection(self.__game_state.leader.won_cards)
1267
1268    def get_won_cards(self) -> CardCollection:
1269        """
1270        Get a list of all tricks the follower has won until now.
1271
1272        :returns: (CardCollection): A CardCollection of all tricks the follower has won until now.
1273        """
1274
1275        return OrderedCardCollection(self.__game_state.follower.won_cards)
1276
1277    def am_i_leader(self) -> bool:
1278        """ Returns False because this is the follower perspective"""
1279        return False
1280
1281
1282class WinnerPerspective(LeaderPerspective):
1283    """
1284    The gamestate given to the winner of the game at the very end
1285
1286    :param state: (GameState): The current state of the game
1287    :param engine: (GamePlayEngine): The engine which is used to play the game
1288    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.
1289    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
1290    """
1291
1292    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1293        self.__game_state = state
1294        self.__engine = engine
1295        super().__init__(state, engine)
1296
1297    def valid_moves(self) -> list[Move]:
1298        """raise an Exception because the game is over"""
1299        raise Exception("Cannot request valid moves from the final PlayerGameState because the game is over")
1300
1301    def __repr__(self) -> str:
1302        return f"WinnerGameState(state={self.__game_state}, engine={self.__engine})"
1303
1304
1305class LoserPerspective(FollowerPerspective):
1306    """
1307    The gamestate given to the loser of the game at the very end
1308
1309    :param state: (GameState): The current state of the game
1310    :param engine: (GamePlayEngine): The engine which is used to play the game
1311    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.
1312    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
1313    """
1314
1315    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1316        self.__game_state = state
1317        self.__engine = engine
1318        super().__init__(state, engine, None)
1319
1320    def valid_moves(self) -> list[Move]:
1321        """raise an Exception because the game is over"""
1322        raise Exception("Cannot request valid moves from the final PlayerGameState because the game is over")
1323
1324    def __repr__(self) -> str:
1325        return f"LoserGameState(state={self.__game_state}, engine={self.__engine})"
1326
1327
1328class DeckGenerator(ABC):
1329    """
1330    A Deckgenerator specifies how what the cards for a game are.
1331    """
1332
1333    @abstractmethod
1334    def get_initial_deck(self) -> OrderedCardCollection:
1335        """
1336        Get the intial deck of cards which are used in the game.
1337        This method must always return the same set of cards in the same order.
1338        """
1339
1340    @classmethod
1341    def shuffle_deck(cls, deck: OrderedCardCollection, rng: Random) -> OrderedCardCollection:
1342        """
1343        Shuffle the given deck of cards, using the random number generator as a source of randomness.
1344
1345        :param deck: (OrderedCardCollection): The deck to shuffle.
1346        :param rng: (Random): The source of randomness.
1347        :returns: (OrderedCardCollection): The shuffled deck.
1348        """
1349        the_cards = list(deck.get_cards())
1350        rng.shuffle(the_cards)
1351        return OrderedCardCollection(the_cards)
1352
1353
1354class SchnapsenDeckGenerator(DeckGenerator):
1355    """
1356    The deck generator for the game of Schnapsen. This generator always creates the same deck of cards, in the same order.
1357
1358    :attr deck: (list[Card]): The deck of cards generated.
1359    """
1360
1361    def __init__(self) -> None:
1362        self.deck: list[Card] = []
1363        for suit in Suit:
1364            for rank in [Rank.JACK, Rank.QUEEN, Rank.KING, Rank.TEN, Rank.ACE]:
1365                self.deck.append(Card.get_card(rank, suit))
1366
1367    def get_initial_deck(self) -> OrderedCardCollection:
1368        """
1369        Get the intial deck of cards which are used in the game.
1370
1371        :returns: (OrderedCardCollection): The deck of cards used in the game.
1372        """
1373        return OrderedCardCollection(self.deck)
1374
1375
1376class HandGenerator(ABC):
1377    """
1378    The HandGenerator specifies how the intial set of cards gets divided over the two player's hands and the talon
1379    """
1380    @abstractmethod
1381    def generateHands(self, cards: OrderedCardCollection) -> tuple[Hand, Hand, Talon]:
1382        """
1383        Divide the collection of cards over the two hands and the Talon
1384
1385        :param cards: The cards to be dealt
1386        :returns: Two hands of cards and the talon. The first hand is for the first player, i.e, the one who will lead the first trick.
1387        """
1388
1389
1390class SchnapsenHandGenerator(HandGenerator):
1391    """Class used for generating the hands for the game of Schnapsen"""
1392    @classmethod
1393    def generateHands(self, cards: OrderedCardCollection) -> tuple[Hand, Hand, Talon]:
1394        """
1395        Divide the collection of cards over the two hands and the Talon
1396
1397        :param cards: (OrderedCardCollection): The cards to be dealt
1398        :returns: (tuple[Hand, Hand, Talon]): Two hands of cards and the talon. The first hand is for the first player, i.e, the one who will lead the first trick.
1399        """
1400
1401        the_cards = list(cards.get_cards())
1402        hand1 = Hand([the_cards[i] for i in range(0, 10, 2)], max_size=5)
1403        hand2 = Hand([the_cards[i] for i in range(1, 11, 2)], max_size=5)
1404        rest = Talon(the_cards[10:])
1405        return (hand1, hand2, rest)
1406
1407
1408class TrickImplementer(ABC):
1409    """
1410    The TrickImplementer is a blueprint for classes that specify how tricks are palyed in the game.
1411    """
1412    @abstractmethod
1413    def play_trick(self, game_engine: GamePlayEngine, game_state: GameState) -> GameState:
1414        """
1415        Plays a single Trick the game by asking the bots in the game_state for their Moves,
1416        using the MoveRequester from the game_engine.
1417        These moves are then also validated using the MoveValidator of the game_engine.
1418        Finally, the trick is recorder in the history (previous field) of the returned GameState.
1419
1420        Note, the provided GameState does not get modified by this method.
1421
1422        :param game_engine: The engine used to preform the underlying actions of the Trick.
1423        :param game_state: The state of the game before the trick is played. Thi state will not be modified.
1424        :returns: The GameState after the trick is completed.
1425        """
1426
1427    @abstractmethod
1428    def play_trick_with_fixed_leader_move(self, game_engine: GamePlayEngine, game_state: GameState,
1429                                          leader_move: Move) -> GameState:
1430        """
1431        The same as play_trick, but also takes the leader_move to start with as an argument.
1432        """
1433
1434
1435class SchnapsenTrickImplementer(TrickImplementer):
1436    """
1437    Child of TrickImplementer, SchnapsenTrickImplementer specifies how tricks are played in the game.
1438    """
1439
1440    def play_trick(self, game_engine: GamePlayEngine, game_state: GameState) -> GameState:
1441        # TODO: Fix the docstring
1442        """
1443        Plays a single Trick the game by asking the bots in the game_state for their Moves,
1444        first asks the leader for their move, and then depending on the resulting gamestate
1445        in self.play_trick_with_fixed_leader_move, will ask (or not ask) the follower for a move.
1446
1447        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1448        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1449        :returns: (GameState): The GameState after the trick is completed.
1450        """
1451        leader_move = self.get_leader_move(game_engine, game_state)
1452        return self.play_trick_with_fixed_leader_move(game_engine=game_engine, game_state=game_state, leader_move=leader_move)
1453
1454    def play_trick_with_fixed_leader_move(self, game_engine: GamePlayEngine, game_state: GameState,
1455                                          leader_move: Move) -> GameState:
1456        """
1457        Plays a trick with a fixed leader move, in order to determine the follower move. Potentially asks the folower for a move.
1458
1459        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1460        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1461        :param leader_move: (Move): The move made by the leader of the trick.
1462        :returns: (GameState): The GameState after the trick is completed.
1463        """
1464        if leader_move.is_trump_exchange():
1465            next_game_state = game_state.copy_for_next()
1466            exchange = cast(TrumpExchange, leader_move)
1467            old_trump_card = game_state.talon.trump_card()
1468            assert old_trump_card, "There is no card at the bottom of the talon"
1469            self.play_trump_exchange(next_game_state, exchange)
1470            # remember the previous state
1471            next_game_state.previous = Previous(game_state, ExchangeTrick(exchange, old_trump_card), True)
1472            # The whole trick ends here.
1473            return next_game_state
1474
1475        # We have a PartialTrick, ask the follower for its move
1476        leader_move = cast(Union[Marriage, RegularMove], leader_move)
1477        follower_move = self.get_follower_move(game_engine, game_state, leader_move)
1478
1479        trick = RegularTrick(leader_move=leader_move, follower_move=follower_move)
1480        return self._apply_regular_trick(game_engine=game_engine, game_state=game_state, trick=trick)
1481
1482    def _apply_regular_trick(self, game_engine: GamePlayEngine, game_state: GameState, trick: RegularTrick) -> GameState:
1483        """
1484        Applies the given regular trick to the given game state, returning the resulting game state.
1485
1486        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1487        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1488        :param trick: (RegularTrick): The trick to be applied to the game state.
1489        :returns: (GameState): The GameState after the trick is completed.
1490        """
1491
1492        # apply the trick to the next_game_state
1493        # The next game state will be modified during this trick. We start from the previous state
1494        next_game_state = game_state.copy_for_next()
1495
1496        if trick.leader_move.is_marriage():
1497            marriage_move: Marriage = cast(Marriage, trick.leader_move)
1498            self._play_marriage(game_engine, next_game_state, marriage_move=marriage_move)
1499            regular_leader_move: RegularMove = cast(Marriage, trick.leader_move).underlying_regular_move()
1500        else:
1501            regular_leader_move = cast(RegularMove, trick.leader_move)
1502
1503        # # apply changes in the hand and talon
1504        next_game_state.leader.hand.remove(regular_leader_move.card)
1505        next_game_state.follower.hand.remove(trick.follower_move.card)
1506
1507        # We set the leader for the next state based on what the scorer decides
1508        next_game_state.leader, next_game_state.follower, leader_remained_leader = game_engine.trick_scorer.score(trick, next_game_state.leader, next_game_state.follower, next_game_state.trump_suit)
1509
1510        # important: the winner takes the first card of the talon, the loser the second one.
1511        # this also ensures that the loser of the last trick of the first phase gets the face up trump
1512        if not next_game_state.talon.is_empty():
1513            drawn = next_game_state.talon.draw_cards(2)
1514            next_game_state.leader.hand.add(drawn[0])
1515            next_game_state.follower.hand.add(drawn[1])
1516
1517        next_game_state.previous = Previous(game_state, trick=trick, leader_remained_leader=leader_remained_leader)
1518
1519        return next_game_state
1520
1521    def get_leader_move(self, game_engine: GamePlayEngine, game_state: GameState) -> Move:
1522        """
1523        Get the move of the leader of the trick.
1524
1525        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1526        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1527        :returns: (Move): The move of the leader of the trick.
1528        """
1529
1530        # ask first players move trough the requester
1531        leader_game_state = LeaderPerspective(game_state, game_engine)
1532        leader_move = game_engine.move_requester.get_move(game_state.leader, leader_game_state, None)
1533        if not game_engine.move_validator.is_legal_leader_move(game_engine, game_state, leader_move):
1534            raise Exception(f"Leader {game_state.leader.implementation} played an illegal move")
1535
1536        return leader_move
1537
1538    def play_trump_exchange(self, game_state: GameState, trump_exchange: TrumpExchange) -> None:
1539        """
1540        Apply a trump exchange to the given game state. This method modifies the game state.
1541
1542        :param game_state: (GameState): The state of the game before the trump exchange is played. This state will be modified.
1543        :param trump_exchange: (TrumpExchange): The trump exchange to be applied to the game state.
1544        """
1545        assert trump_exchange.jack.suit is game_state.trump_suit, \
1546            f"A trump exchange can only be done with a Jack of the same suit as the current trump. Got a {trump_exchange.jack} while the  Trump card is a {game_state.trump_suit}"
1547        # apply the changes in the gamestate
1548        game_state.leader.hand.remove(trump_exchange.jack)
1549        old_trump = game_state.talon.trump_exchange(trump_exchange.jack)
1550        game_state.leader.hand.add(old_trump)
1551        # We notify the both bots that an exchange happened
1552        game_state.leader.implementation.notify_trump_exchange(trump_exchange)
1553        game_state.follower.implementation.notify_trump_exchange(trump_exchange)
1554
1555    def _play_marriage(self, game_engine: GamePlayEngine, game_state: GameState, marriage_move: Marriage) -> None:
1556        """
1557        Apply a marriage to the given game state. This method modifies the game state.
1558
1559        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1560        :param game_state: (GameState): The state of the game before the marriage is played. This state will be modified.
1561        :param marriage_move: (Marriage): The marriage to be applied to the game state.
1562        """
1563
1564        score = game_engine.trick_scorer.marriage(marriage_move, game_state)
1565        game_state.leader.score += score
1566
1567    def get_follower_move(self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move) -> RegularMove:
1568        """
1569        Get the move of the follower of the trick.
1570
1571        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1572        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1573        :param leader_move: (Move): The move made by the leader of the trick.
1574        :returns: (RegularMove): The move of the follower of the trick.
1575        """
1576
1577        follower_game_state = FollowerPerspective(game_state, game_engine, leader_move)
1578
1579        follower_move = game_engine.move_requester.get_move(game_state.follower, follower_game_state, leader_move)
1580        if not game_engine.move_validator.is_legal_follower_move(game_engine, game_state, leader_move, follower_move):
1581            raise Exception(f"Follower {game_state.follower.implementation} played an illegal move")
1582        return cast(RegularMove, follower_move)
1583
1584
1585class MoveRequester:
1586    """
1587    An moveRequester captures the logic of requesting a move from a bot.
1588    This logic also determines what happens in case the bot is to slow, throws an exception during operation, etc
1589    """
1590
1591    @abstractmethod
1592    def get_move(self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1593        """
1594        Get a move from the bot, potentially applying timeout logic.
1595
1596        """
1597
1598
1599class SimpleMoveRequester(MoveRequester):
1600    """The SimplemoveRquester just asks the move, and does not time out"""
1601
1602    def get_move(self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1603        """
1604        Get a move from the bot
1605
1606        :param bot: (BotState): The bot to request the move from
1607        :param perspective: (PlayerPerspective): The perspective of the bot
1608        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
1609        """
1610        return bot.get_move(perspective, leader_move=leader_move)
1611
1612
1613class _DummyFile(StringIO):
1614    """
1615    A dummy file that does not write anything.
1616    """
1617
1618    def write(self, _: str) -> int:
1619        return 0
1620
1621    def flush(self) -> None:
1622        pass
1623
1624
1625class SilencingMoveRequester(MoveRequester):
1626    """
1627    This MoveRequester just asks the move, but before doing so it routes stdout to a dummy file
1628
1629    :param requester: (MoveRequester): The MoveRequester to use to request the move.
1630    """
1631
1632    def __init__(self, requester: MoveRequester) -> None:
1633        self.requester = requester
1634
1635    @contextlib.contextmanager
1636    @staticmethod
1637    def __nostdout() -> Generator[None, Any, None]:
1638        """
1639        A context manager that silences stdout
1640
1641        :returns: (Generator[None, Any, None]): A context manager that silences stdout
1642        """
1643
1644        save_stdout = sys.stdout
1645        sys.stdout = _DummyFile()
1646        yield
1647        sys.stdout = save_stdout
1648
1649    def get_move(self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1650        """
1651        Get a move from the bot, potentially applying timeout logic.
1652
1653        :param bot: (BotState): The bot to request the move from
1654        :param perspective: (PlayerPerspective): The perspective of the bot
1655        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
1656        :returns: (Move): The move returned by the bot.
1657        """
1658
1659        with SilencingMoveRequester.__nostdout():
1660            return self.requester.get_move(bot, perspective, leader_move)
1661
1662
1663class MoveValidator(ABC):
1664    """
1665    An object of this class can be used to check whether a move is valid.
1666    """
1667    @abstractmethod
1668    def get_legal_leader_moves(self, game_engine: GamePlayEngine, game_state: GameState) -> Iterable[Move]:
1669        """
1670        Get all legal moves for the current leader of the game.
1671
1672        :param game_engine: The engine which is playing the game
1673        :param game_state: The current state of the game
1674
1675        :returns: An iterable containing the current legal moves.
1676        """
1677
1678    def is_legal_leader_move(self, game_engine: GamePlayEngine, game_state: GameState, move: Move) -> bool:
1679        """
1680        Whether the provided move is legal for the leader to play.
1681        The basic implementation checks whether the move is in the legal leader moves.
1682        Derived classes might implement this more performantly.
1683        """
1684        assert move, 'The move played by the leader cannot be None'
1685        return move in self.get_legal_leader_moves(game_engine, game_state)
1686
1687    @abstractmethod
1688    def get_legal_follower_moves(self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move) -> Iterable[Move]:
1689        """
1690        Get all legal moves for the current follower of the game.
1691
1692        :param game_engine: The engine which is playing the game
1693        :param game_state: The current state of the game
1694        :param leader_move: The move played by the leader of the trick.
1695
1696        :returns: An iterable containing the current legal moves.
1697        """
1698
1699    def is_legal_follower_move(self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move, move: Move) -> bool:
1700        """
1701        Whether the provided move is legal for the follower to play.
1702        The basic implementation checks whether the move is in the legal fllower moves.
1703        Derived classes might implement this more performantly.
1704        """
1705        assert move, 'The move played by the follower cannot be None'
1706        assert leader_move, 'The move played by the leader cannot be None'
1707        return move in self.get_legal_follower_moves(game_engine=game_engine, game_state=game_state, leader_move=leader_move)
1708
1709
1710class SchnapsenMoveValidator(MoveValidator):
1711    """
1712    The move validator for the game of Schnapsen.
1713    """
1714
1715    def get_legal_leader_moves(self, game_engine: GamePlayEngine, game_state: GameState) -> Iterable[Move]:
1716        """
1717        Get all legal moves for the current leader of the game.
1718
1719        :param game_engine: The engine which is playing the game
1720        :param game_state: The current state of the game
1721
1722        :returns: An iterable containing the current legal moves.
1723        """
1724        # all cards in the hand can be played
1725        cards_in_hand = game_state.leader.hand
1726        valid_moves: list[Move] = [RegularMove(card) for card in cards_in_hand]
1727        # trump exchanges
1728        if not game_state.talon.is_empty():
1729            trump_jack = Card.get_card(Rank.JACK, game_state.trump_suit)
1730            if trump_jack in cards_in_hand:
1731                valid_moves.append(TrumpExchange(trump_jack))
1732        # mariages
1733        for card in cards_in_hand.filter_rank(Rank.QUEEN):
1734            king_card = Card.get_card(Rank.KING, card.suit)
1735            if king_card in cards_in_hand:
1736                valid_moves.append(Marriage(card, king_card))
1737        return valid_moves
1738
1739    def is_legal_leader_move(self, game_engine: GamePlayEngine, game_state: GameState, move: Move) -> bool:
1740        """
1741        Whether the provided move is legal for the leader to play.
1742
1743        :param game_engine: (GamePlayEngine): The engine which is playing the game
1744        :param game_state: (GameState): The current state of the game
1745        :param move: (Move): The move to check
1746
1747        :returns: (bool): Whether the move is legal
1748        """
1749        cards_in_hand = game_state.leader.hand
1750        if move.is_marriage():
1751            marriage_move = cast(Marriage, move)
1752            # we do not have to check whether they are the same suit because of the implementation of Marriage
1753            return marriage_move.queen_card in cards_in_hand and marriage_move.king_card in cards_in_hand
1754        if move.is_trump_exchange():
1755            if game_state.talon.is_empty():
1756                return False
1757            trump_move: TrumpExchange = cast(TrumpExchange, move)
1758            return trump_move.jack in cards_in_hand
1759        # it has to be a regular move
1760        regular_move = cast(RegularMove, move)
1761        return regular_move.card in cards_in_hand
1762
1763    def get_legal_follower_moves(self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move) -> Iterable[Move]:
1764        """
1765        Get all legal moves for the current follower of the game.
1766
1767        :param game_engine: (GamePlayEngine): The engine which is playing the game
1768        :param game_state: (GameState): The current state of the game
1769        :param leader_move: (Move): The move played by the leader of the trick.
1770
1771        :returns: (Iterable[Move]): An iterable containing the current legal moves.
1772        """
1773
1774        hand = game_state.follower.hand
1775        if leader_move.is_marriage():
1776            leader_card = cast(Marriage, leader_move).queen_card
1777        else:
1778            leader_card = cast(RegularMove, leader_move).card
1779        if game_state.game_phase() is GamePhase.ONE:
1780            # no need to follow, any card in the hand is a legal move
1781            return RegularMove.from_cards(hand.get_cards())
1782        # information from https://www.pagat.com/marriage/schnaps.html
1783        # ## original formulation ##
1784        # if your opponent leads a non-trump:
1785        #     you must play a higher card of the same suit if you can;
1786        #     failing this you must play a lower card of the same suit;
1787        #     if you have no card of the suit that was led you must play a trump;
1788        #     if you have no trumps either you may play anything.
1789        # If your opponent leads a trump:
1790        #     you must play a higher trump if possible;
1791        #     if you have no higher trump you must play a lower trump;
1792        #     if you have no trumps at all you may play anything.
1793        # ## implemented version, realizing that the rules for trump are overlapping with the normal case ##
1794        # you must play a higher card of the same suit if you can
1795        # failing this, you must play a lower card of the same suit;
1796        # --new--> failing this, if the opponen did not play a trump, you must play a trump
1797        # failing this, you can play anything
1798        leader_card_score = game_engine.trick_scorer.rank_to_points(leader_card.rank)
1799        # you must play a higher card of the same suit if you can;
1800        same_suit_cards = hand.filter_suit(leader_card.suit)
1801        if same_suit_cards:
1802            higher_same_suit, lower_same_suit = [], []
1803            for card in same_suit_cards:
1804                # TODO this is slightly ambigousm should this be >= ??
1805                higher_same_suit.append(card) if game_engine.trick_scorer.rank_to_points(card.rank) > leader_card_score else lower_same_suit.append(card)
1806            if higher_same_suit:
1807                return RegularMove.from_cards(higher_same_suit)
1808        # failing this, you must play a lower card of the same suit;
1809            elif lower_same_suit:
1810                return RegularMove.from_cards(lower_same_suit)
1811            raise AssertionError("Somethign is wrong in the logic here. There should be cards, but they are neither placed in the low, nor higher list")
1812        # failing this, if the opponen did not play a trump, you must play a trump
1813        trump_cards = hand.filter_suit(game_state.trump_suit)
1814        if leader_card.suit != game_state.trump_suit and trump_cards:
1815            return RegularMove.from_cards(trump_cards)
1816        # failing this, you can play anything
1817        return RegularMove.from_cards(hand.get_cards())
1818
1819
1820class TrickScorer(ABC):
1821    @abstractmethod
1822    def score(self, trick: RegularTrick, leader: BotState, follower: BotState, trump: Suit) -> tuple[BotState, BotState, bool]:
1823        """
1824        Score the trick for the given leader and follower. The returned bots are the same bots provided (not copies) and have the score of the trick applied.
1825        They are returned in order (new_leader, new_follower). If appropriate, also pending points have been applied.
1826        The boolean is True if the leading bot remained the same, i.e., the past leader remains the leader
1827        """
1828
1829    @abstractmethod
1830    def declare_winner(self, game_state: GameState) -> Optional[tuple[BotState, int]]:
1831        """return a bot and the number of points if there is a winner of this game already
1832
1833        :param game_state: The current state of the game
1834        :returns: The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
1835
1836        """
1837
1838    @abstractmethod
1839    def rank_to_points(self, rank: Rank) -> int:
1840        """Get the point value for a given rank
1841
1842        :param rank: the rank of a card for which you want to obtain the points
1843        :returns: The score for that card
1844        """
1845
1846    @abstractmethod
1847    def marriage(self, move: Marriage, gamestate: GameState) -> Score:
1848        """Get the score which the player obtains for the given marriage
1849
1850        :param move: The marriage for which to get the score
1851        :param gamestate: the current state of the game, usually used to get the trump suit
1852        :returns: The score for this marriage
1853        """
1854
1855
1856class SchnapsenTrickScorer(TrickScorer):
1857    """
1858    A TrickScorer that scores ac cording to the Schnapsen rules
1859    """
1860
1861    SCORES = {
1862        Rank.ACE: 11,
1863        Rank.TEN: 10,
1864        Rank.KING: 4,
1865        Rank.QUEEN: 3,
1866        Rank.JACK: 2,
1867    }
1868
1869    def rank_to_points(self, rank: Rank) -> int:
1870        """
1871        Convert a rank to the number of points it is worth.
1872
1873        :param rank: The rank to convert.
1874        :returns: The number of points the rank is worth.
1875        """
1876        return SchnapsenTrickScorer.SCORES[rank]
1877
1878    def marriage(self, move: Marriage, gamestate: GameState) -> Score:
1879        """
1880        Get the score which the player obtains for the given marriage
1881
1882        :param move: The marriage for which to get the score
1883        :param gamestate: the current state of the game, usually used to get the trump suit
1884        :returns: The score for this marriage
1885        """
1886
1887        if move.suit is gamestate.trump_suit:
1888            # royal marriage
1889            return Score(pending_points=40)
1890        # any other marriage
1891        return Score(pending_points=20)
1892
1893    def score(self, trick: RegularTrick, leader: BotState, follower: BotState, trump: Suit) -> tuple[BotState, BotState, bool]:
1894        """
1895        Score the trick for the given leader and follower. The returned bots are the same bots provided (not copies) and have the score of the trick applied.
1896
1897        :param trick: The trick to score
1898        :param leader: The botstate of the leader
1899        :param follower: The botstate of the follower
1900        :param trump: The trump suit
1901        :returns: The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
1902        """
1903
1904        if trick.leader_move.is_marriage():
1905            regular_leader_move: RegularMove = cast(Marriage, trick.leader_move).underlying_regular_move()
1906        else:
1907            regular_leader_move = cast(RegularMove, trick.leader_move)
1908
1909        leader_card = regular_leader_move.card
1910        follower_card = trick.follower_move.card
1911        assert leader_card != follower_card, f"The leader card {leader_card} and follower_card {follower_card} cannot be the same."
1912        leader_card_points = self.rank_to_points(leader_card.rank)
1913        follower_card_points = self.rank_to_points(follower_card.rank)
1914
1915        if leader_card.suit is follower_card.suit:
1916            # same suit, either trump or not
1917            if leader_card_points > follower_card_points:
1918                leader_wins = True
1919            else:
1920                leader_wins = False
1921        elif leader_card.suit is trump:
1922            # the follower suit cannot be trumps as per the previous condition
1923            leader_wins = True
1924        elif follower_card.suit is trump:
1925            # the leader suit cannot be trumps because of the previous conditions
1926            leader_wins = False
1927        else:
1928            # the follower did not follow the suit of the leader and did not play trumps, hence the leader wins
1929            leader_wins = True
1930        winner, loser = (leader, follower) if leader_wins else (follower, leader)
1931        # record the win
1932        winner.won_cards.extend([leader_card, follower_card])
1933        # apply the points
1934        points_gained = leader_card_points + follower_card_points
1935        winner.score += Score(direct_points=points_gained)
1936        # add winner's total of direct and pending points as their new direct points
1937        winner.score = winner.score.redeem_pending_points()
1938        return winner, loser, leader_wins
1939
1940    def declare_winner(self, game_state: GameState) -> Optional[tuple[BotState, int]]:
1941        """
1942        Declaring a winner uses the logic from https://web.archive.org/web/20230303074822/https://www.pagat.com/marriage/schnaps.html#out , but simplified, because we do not have closing of the Talon and no need to guess the scores.
1943        The following text adapted accordingly from that website.
1944
1945        If a player has 66 or more points, she scores points toward game as follows:
1946
1947            * one game point, if the opponent has made at least 33 points;
1948            * two game points, if the opponent has made fewer than 33 points, but has won at least one trick (opponent is said to be Schneider);
1949            * three game points, if the opponent has won no tricks (opponent is said to be Schwarz).
1950
1951        If play continued to the very last trick with the talon exhausted, the player who takes the last trick wins the hand, scoring one game point, irrespective of the number of card points the players have taken.
1952
1953        :param game_state: (GameState): The current gamestate
1954        :returns: (Optional[tuple[BotState, int]]): The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
1955        """
1956        if game_state.leader.score.direct_points >= 66:
1957            follower_score = game_state.follower.score.direct_points
1958            if follower_score == 0:
1959                return game_state.leader, 3
1960            elif follower_score >= 33:
1961                return game_state.leader, 1
1962            else:
1963                # second case in explaination above, 0 < score < 33
1964                assert follower_score < 66, "Found a follower score of more than 66, while the leader also had more than 66. This must never happen."
1965                return game_state.leader, 2
1966        elif game_state.follower.score.direct_points >= 66:
1967            raise AssertionError("Would declare the follower winner, but this should never happen in the current implementation")
1968        elif game_state.are_all_cards_played():
1969            return game_state.leader, 1
1970        else:
1971            return None
1972
1973
1974@dataclass
1975class GamePlayEngine:
1976    """
1977    The GamePlayengine combines the different aspects of the game into an engine that can execute games.
1978    """
1979    deck_generator: DeckGenerator
1980    hand_generator: HandGenerator
1981    trick_implementer: TrickImplementer
1982    move_requester: MoveRequester
1983    move_validator: MoveValidator
1984    trick_scorer: TrickScorer
1985
1986    def play_game(self, bot1: Bot, bot2: Bot, rng: Random) -> tuple[Bot, int, Score]:
1987        """
1988        Play a game between bot1 and bot2, using the rng to create the game.
1989
1990        :param bot1: The first bot playing the game. This bot will be the leader for the first trick.
1991        :param bot2: The second bot playing the game. This bot will be the follower for the first trick.
1992        :param rng: The random number generator used to shuffle the deck.
1993
1994        :returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.
1995        """
1996        cards = self.deck_generator.get_initial_deck()
1997        shuffled = self.deck_generator.shuffle_deck(cards, rng)
1998        hand1, hand2, talon = self.hand_generator.generateHands(shuffled)
1999
2000        leader_state = BotState(implementation=bot1, hand=hand1)
2001        follower_state = BotState(implementation=bot2, hand=hand2)
2002
2003        game_state = GameState(
2004            leader=leader_state,
2005            follower=follower_state,
2006            talon=talon,
2007            previous=None
2008        )
2009        winner, points, score = self.play_game_from_state(game_state=game_state, leader_move=None)
2010        return winner, points, score
2011
2012    def get_random_phase_two_state(self, rng: Random) -> GameState:
2013        """
2014        Get a random GameState in the second phase of the game.
2015
2016        :param rng: The random number generator used to shuffle the deck.
2017
2018        :returns: A GameState in the second phase of the game.
2019        """
2020
2021        class RandBot(Bot):
2022            def __init__(self, rand: Random, name: Optional[str] = None) -> None:
2023                super().__init__(name)
2024                self.rng = rand
2025
2026            def get_move(
2027                self,
2028                perspective: PlayerPerspective,
2029                leader_move: Optional[Move],
2030            ) -> Move:
2031                moves: list[Move] = perspective.valid_moves()
2032                move = self.rng.choice(moves)
2033                return move
2034
2035        while True:
2036            cards = self.deck_generator.get_initial_deck()
2037            shuffled = self.deck_generator.shuffle_deck(cards, rng)
2038            hand1, hand2, talon = self.hand_generator.generateHands(shuffled)
2039            leader_state = BotState(implementation=RandBot(rand=rng), hand=hand1)
2040            follower_state = BotState(implementation=RandBot(rand=rng), hand=hand2)
2041            game_state = GameState(
2042                leader=leader_state,
2043                follower=follower_state,
2044                talon=talon,
2045                previous=None
2046            )
2047            second_phase_state, _ = self.play_at_most_n_tricks(game_state, RandBot(rand=rng), RandBot(rand=rng), 5)
2048            winner = self.trick_scorer.declare_winner(second_phase_state)
2049            if winner:
2050                continue
2051            if second_phase_state.game_phase() == GamePhase.TWO:
2052                return second_phase_state
2053
2054    def play_game_from_state_with_new_bots(self, game_state: GameState, new_leader: Bot, new_follower: Bot, leader_move: Optional[Move]) -> tuple[Bot, int, Score]:
2055        """
2056        Continue a game  which might have started before with other bots, with new bots.
2057        The new bots are new_leader and new_follower.
2058        The leader move is an optional paramter which can be provided to force this first move from the leader.
2059
2060        :param game_state: The state of the game to start from
2061        :param new_leader: The bot which will take the leader role in the game.
2062        :param new_follower: The bot which will take the follower in the game.
2063        :param leader_move: if provided, the leader will be forced to play this move as its first move.
2064
2065        :returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.
2066        """
2067
2068        game_state_copy = game_state.copy_with_other_bots(new_leader=new_leader, new_follower=new_follower)
2069        return self.play_game_from_state(game_state_copy, leader_move=leader_move)
2070
2071    def play_game_from_state(self, game_state: GameState, leader_move: Optional[Move]) -> tuple[Bot, int, Score]:
2072        """
2073        Continue a game  which might have been started before.
2074        The leader move is an optional paramter which can be provided to force this first move from the leader.
2075
2076        :param game_state: The state of the game to start from
2077        :param leader_move: if provided, the leader will be forced to play this move as its first move.
2078
2079        :returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.
2080        """
2081        winner: Optional[BotState] = None
2082        points: int = -1
2083        while not winner:
2084            if leader_move is not None:
2085                # we continues from a game where the leading bot already did a move, we immitate that
2086                game_state = self.trick_implementer.play_trick_with_fixed_leader_move(game_engine=self, game_state=game_state, leader_move=leader_move)
2087                leader_move = None
2088            else:
2089                game_state = self.trick_implementer.play_trick(self, game_state)
2090            winner, points = self.trick_scorer.declare_winner(game_state) or (None, -1)
2091
2092        winner_state = WinnerPerspective(game_state, self)
2093        winner.implementation.notify_game_end(won=True, perspective=winner_state)
2094
2095        loser_state = LoserPerspective(game_state, self)
2096        game_state.follower.implementation.notify_game_end(False, perspective=loser_state)
2097
2098        return winner.implementation, points, winner.score
2099
2100    def play_one_trick(self, game_state: GameState, new_leader: Bot, new_follower: Bot) -> GameState:
2101        """
2102        Plays one tricks (including the one started by the leader, if provided) on a game which might have started before.
2103        The new bots are new_leader and new_follower.
2104
2105        This method does not make changes to the provided game_state.
2106
2107        :param game_state: The state of the game to start from
2108        :param new_leader: The bot which will take the leader role in the game.
2109        :param new_follower: The bot which will take the follower in the game.
2110
2111        :returns: The GameState reached and the number of steps actually taken.
2112        """
2113        state, rounds = self.play_at_most_n_tricks(game_state, new_leader, new_follower, 1)
2114        assert rounds == 1, f"We called play_at_most_n_tricks with rounds=1, but it returned not excactly 1 round, got {rounds} rounds."
2115        return state
2116
2117    def play_at_most_n_tricks(self, game_state: GameState, new_leader: Bot, new_follower: Bot, n: int) -> tuple[GameState, int]:
2118        """
2119        Plays up to n tricks (including the one started by the leader, if provided) on a game which might have started before.
2120        The number of tricks will be smaller than n in case the game ends before n tricks are played.
2121        The new bots are new_leader and new_follower.
2122
2123        This method does not make changes to the provided game_state.
2124
2125        :param game_state: The state of the game to start from
2126        :param new_leader: The bot which will take the leader role in the game.
2127        :param new_follower: The bot which will take the follower in the game.
2128        :param n: the maximum number of tricks to play
2129
2130        :returns: The GameState reached and the number of steps actually taken.
2131        """
2132        assert n >= 0, "Cannot play less than 0 rounds"
2133        game_state_copy = game_state.copy_with_other_bots(new_leader=new_leader, new_follower=new_follower)
2134
2135        winner: Optional[BotState] = None
2136        rounds_played = 0
2137        while not winner:
2138            if rounds_played == n:
2139                break
2140            game_state_copy = self.trick_implementer.play_trick(self, game_state_copy)
2141            winner, _ = self.trick_scorer.declare_winner(game_state_copy) or (None, -1)
2142            rounds_played += 1
2143        if winner:
2144            winner_state = WinnerPerspective(game_state_copy, self)
2145            winner.implementation.notify_game_end(won=True, perspective=winner_state)
2146
2147            loser_state = LoserPerspective(game_state_copy, self)
2148            game_state_copy.follower.implementation.notify_game_end(False, perspective=loser_state)
2149
2150        return game_state_copy, rounds_played
2151
2152    def __repr__(self) -> str:
2153        return f"GamePlayEngine(deck_generator={self.deck_generator}, "\
2154               f"hand_generator={self.hand_generator}, "\
2155               f"trick_implementer={self.trick_implementer}, "\
2156               f"move_requester={self.move_requester}, "\
2157               f"move_validator={self.move_validator}, "\
2158               f"trick_scorer={self.trick_scorer})"
2159
2160
2161class SchnapsenGamePlayEngine(GamePlayEngine):
2162    """
2163    A GamePlayEngine configured according to the rules of Schnapsen
2164    """
2165
2166    def __init__(self) -> None:
2167        super().__init__(
2168            deck_generator=SchnapsenDeckGenerator(),
2169            hand_generator=SchnapsenHandGenerator(),
2170            trick_implementer=SchnapsenTrickImplementer(),
2171            move_requester=SimpleMoveRequester(),
2172            move_validator=SchnapsenMoveValidator(),
2173            trick_scorer=SchnapsenTrickScorer()
2174        )
2175
2176    def __repr__(self) -> str:
2177        return super().__repr__()
class Bot(abc.ABC):
20class Bot(ABC):
21    """
22    The Bot baseclass. Derive your own bots from this class and implement the get_move method to use it in games.
23    Besides the get_move method, it is also possible to override notify_trump_exchange and notify_game_end to get notified when these events happen.
24
25    :param name: (str): Optionally, specify a name for the bot. Defaults to None.
26    """
27
28    def __init__(self, name: Optional[str] = None) -> None:
29        if name:
30            self.__name = name
31
32    @abstractmethod
33    def get_move(self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
34        """
35        Get the move this Bot wants to play. This is the method that gets called by the engine to get the bot's next move.
36        If this Bot is leading, the leader_move will be None. If this both is following, the leader_move will contain the move the opponent just played
37
38        :param perspective: (PlayerPerspective): The PlayerPerspective which contains the information on the current state of the game from the perspective of this player
39        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if this bot is the leader.
40        """
41
42    def notify_trump_exchange(self, move: TrumpExchange) -> None:
43        """
44        The engine will call this method when a trump exchange is made.
45        Overide this method to get notified about trump exchanges. Note that this notification is sent to both bots.
46
47        :param move: (TrumpExchange): the Trump Exchange move that was played.
48        """
49
50    def notify_game_end(self, won: bool, perspective: PlayerPerspective) -> None:
51        """
52        The engine will call this method when the game ends.
53        Override this method to get notified about the end of the game.
54
55        :param won: (bool): Did this bot win the game?
56        :param perspective: (PlayerPerspective) The final perspective of the game.
57        """
58
59    def __str__(self) -> str:
60        """
61        A string representation of the Bot. If the bot was constructed with a name, it will be that name.
62        Otherwise it will be the class name and the memory address of the bot.
63
64        :returns: (str): A string representation of the bot.
65        """
66        return self.__name if hasattr(self, '_Bot__name') else super().__str__()

The Bot baseclass. Derive your own bots from this class and implement the get_move method to use it in games. Besides the get_move method, it is also possible to override notify_trump_exchange and notify_game_end to get notified when these events happen.

Parameters
  • name: (str): Optionally, specify a name for the bot. Defaults to None.
@abstractmethod
def get_move( self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
32    @abstractmethod
33    def get_move(self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
34        """
35        Get the move this Bot wants to play. This is the method that gets called by the engine to get the bot's next move.
36        If this Bot is leading, the leader_move will be None. If this both is following, the leader_move will contain the move the opponent just played
37
38        :param perspective: (PlayerPerspective): The PlayerPerspective which contains the information on the current state of the game from the perspective of this player
39        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if this bot is the leader.
40        """

Get the move this Bot wants to play. This is the method that gets called by the engine to get the bot's next move. If this Bot is leading, the leader_move will be None. If this both is following, the leader_move will contain the move the opponent just played

Parameters
  • perspective: (PlayerPerspective): The PlayerPerspective which contains the information on the current state of the game from the perspective of this player
  • leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if this bot is the leader.
def notify_trump_exchange(self, move: TrumpExchange) -> None:
42    def notify_trump_exchange(self, move: TrumpExchange) -> None:
43        """
44        The engine will call this method when a trump exchange is made.
45        Overide this method to get notified about trump exchanges. Note that this notification is sent to both bots.
46
47        :param move: (TrumpExchange): the Trump Exchange move that was played.
48        """

The engine will call this method when a trump exchange is made. Overide this method to get notified about trump exchanges. Note that this notification is sent to both bots.

Parameters
  • move: (TrumpExchange): the Trump Exchange move that was played.
def notify_game_end(self, won: bool, perspective: PlayerPerspective) -> None:
50    def notify_game_end(self, won: bool, perspective: PlayerPerspective) -> None:
51        """
52        The engine will call this method when the game ends.
53        Override this method to get notified about the end of the game.
54
55        :param won: (bool): Did this bot win the game?
56        :param perspective: (PlayerPerspective) The final perspective of the game.
57        """

The engine will call this method when the game ends. Override this method to get notified about the end of the game.

Parameters
  • won: (bool): Did this bot win the game?
  • perspective: (PlayerPerspective) The final perspective of the game.
class Move(abc.ABC):
 69class Move(ABC):
 70    """
 71    A single move during a game. There are several types of move possible: normal moves, trump exchanges, and marriages.
 72    They are implmented in classes inheriting from this class.
 73    """
 74
 75    cards: list[Card]  # implementation detail: The creation of this list is defered to the derived classes in _cards()
 76    """The cards played in this move"""
 77
 78    def is_regular_move(self) -> bool:
 79        """
 80        Is this Move a regular move (not a mariage or trump exchange)
 81
 82        :returns: a bool indicating whether this is a regular move
 83        """
 84        return False
 85
 86    def as_regular_move(self) -> RegularMove:
 87        """Returns this same move but as a RegularMove."""
 88        raise AssertionError("as_regular_move called on a Move which is not a regular move. Check with is_regular_move first.")
 89
 90    def is_marriage(self) -> bool:
 91        """
 92        Is this Move a marriage?
 93
 94        :returns: a bool indicating whether this move is a marriage
 95        """
 96        return False
 97
 98    def as_marriage(self) -> Marriage:
 99        """Returns this same move but as a Marriage."""
100        raise AssertionError("as_marriage called on a Move which is not a Marriage. Check with is_marriage first.")
101
102    def is_trump_exchange(self) -> bool:
103        """
104        Is this Move a trump exchange move?
105
106        :returns: a bool indicating whether this move is a trump exchange
107        """
108        return False
109
110    def as_trump_exchange(self) -> TrumpExchange:
111        """Returns this same move but as a TrumpExchange."""
112        raise AssertionError("as_trump_exchange called on a Move which is not a TrumpExchange. Check with is_trump_exchange first.")
113
114    def __getattribute__(self, __name: str) -> Any:
115        if __name == "cards":
116            # We call the method to compute the card list
117            return object.__getattribute__(self, "_cards")()
118        return object.__getattribute__(self, __name)
119
120    @abstractmethod
121    def _cards(self) -> list[Card]:
122        """
123        Get the list of cards in this move. This method should not be called direcly, use the cards property instead.
124        Private abstract method for other classes to inherit and/or override.
125        """
126
127    @abstractmethod
128    def __eq__(self, __o: object) -> bool:
129        """
130        Compares two moves with each other. Two moves are equal in case they are of the same type and if they contain the same cards.
131        Abstract method for derived classes to override.
132        """

A single move during a game. There are several types of move possible: normal moves, trump exchanges, and marriages. They are implmented in classes inheriting from this class.

cards: list[schnapsen.deck.Card]

The cards played in this move

def is_regular_move(self) -> bool:
78    def is_regular_move(self) -> bool:
79        """
80        Is this Move a regular move (not a mariage or trump exchange)
81
82        :returns: a bool indicating whether this is a regular move
83        """
84        return False

Is this Move a regular move (not a mariage or trump exchange)

:returns: a bool indicating whether this is a regular move

def as_regular_move(self) -> RegularMove:
86    def as_regular_move(self) -> RegularMove:
87        """Returns this same move but as a RegularMove."""
88        raise AssertionError("as_regular_move called on a Move which is not a regular move. Check with is_regular_move first.")

Returns this same move but as a RegularMove.

def is_marriage(self) -> bool:
90    def is_marriage(self) -> bool:
91        """
92        Is this Move a marriage?
93
94        :returns: a bool indicating whether this move is a marriage
95        """
96        return False

Is this Move a marriage?

:returns: a bool indicating whether this move is a marriage

def as_marriage(self) -> Marriage:
 98    def as_marriage(self) -> Marriage:
 99        """Returns this same move but as a Marriage."""
100        raise AssertionError("as_marriage called on a Move which is not a Marriage. Check with is_marriage first.")

Returns this same move but as a Marriage.

def is_trump_exchange(self) -> bool:
102    def is_trump_exchange(self) -> bool:
103        """
104        Is this Move a trump exchange move?
105
106        :returns: a bool indicating whether this move is a trump exchange
107        """
108        return False

Is this Move a trump exchange move?

:returns: a bool indicating whether this move is a trump exchange

def as_trump_exchange(self) -> TrumpExchange:
110    def as_trump_exchange(self) -> TrumpExchange:
111        """Returns this same move but as a TrumpExchange."""
112        raise AssertionError("as_trump_exchange called on a Move which is not a TrumpExchange. Check with is_trump_exchange first.")

Returns this same move but as a TrumpExchange.

@dataclass(frozen=True)
class RegularMove(Move):
135@dataclass(frozen=True)
136class RegularMove(Move):
137    """A regular move in the game"""
138
139    card: Card
140    """The card which is played"""
141
142    def _cards(self) -> list[Card]:
143        return [self.card]
144
145    @staticmethod
146    def from_cards(cards: Iterable[Card]) -> list[Move]:
147        """Create an iterable of Moves from an iterable of cards."""
148        return [RegularMove(card) for card in cards]
149
150    def is_regular_move(self) -> bool:
151        return True
152
153    def as_regular_move(self) -> RegularMove:
154        return self
155
156    def __repr__(self) -> str:
157        return f"RegularMove(card={self.card})"
158
159    def __eq__(self, __o: object) -> bool:
160        if not isinstance(__o, RegularMove):
161            return False
162        return self.card == __o.card

A regular move in the game

RegularMove(card: schnapsen.deck.Card)

The card which is played

@staticmethod
def from_cards(cards: Iterable[schnapsen.deck.Card]) -> list[Move]:
145    @staticmethod
146    def from_cards(cards: Iterable[Card]) -> list[Move]:
147        """Create an iterable of Moves from an iterable of cards."""
148        return [RegularMove(card) for card in cards]

Create an iterable of Moves from an iterable of cards.

def is_regular_move(self) -> bool:
150    def is_regular_move(self) -> bool:
151        return True

Is this Move a regular move (not a mariage or trump exchange)

:returns: a bool indicating whether this is a regular move

def as_regular_move(self) -> RegularMove:
153    def as_regular_move(self) -> RegularMove:
154        return self

Returns this same move but as a RegularMove.

@dataclass(frozen=True)
class TrumpExchange(Move):
165@dataclass(frozen=True)
166class TrumpExchange(Move):
167    """A move that implements the exchange of the trump card for a Jack of the same suit."""
168
169    jack: Card
170    """The Jack which will be placed at the bottom of the Talon"""
171
172    def __post_init__(self) -> None:
173        """
174        Asserts that the card is a Jack
175        """
176        assert self.jack.rank is Rank.JACK, f"The rank card {self.jack} used to initialize the {TrumpExchange.__name__} was not Rank.JACK"
177
178    def is_trump_exchange(self) -> bool:
179        """
180        Returns True if this is a trump exchange.
181        """
182        return True
183
184    def as_trump_exchange(self) -> TrumpExchange:
185        """
186        Returns this same move but as a TrumpExchange.
187
188        """
189        return self
190
191    def _cards(self) -> list[Card]:
192        return [self.jack]
193
194    def __repr__(self) -> str:
195        return f"TrumpExchange(jack={self.jack})"
196
197    def __eq__(self, __o: object) -> bool:
198        if not isinstance(__o, TrumpExchange):
199            return False
200        return self.jack == __o.jack

A move that implements the exchange of the trump card for a Jack of the same suit.

TrumpExchange(jack: schnapsen.deck.Card)

The Jack which will be placed at the bottom of the Talon

def is_trump_exchange(self) -> bool:
178    def is_trump_exchange(self) -> bool:
179        """
180        Returns True if this is a trump exchange.
181        """
182        return True

Returns True if this is a trump exchange.

def as_trump_exchange(self) -> TrumpExchange:
184    def as_trump_exchange(self) -> TrumpExchange:
185        """
186        Returns this same move but as a TrumpExchange.
187
188        """
189        return self

Returns this same move but as a TrumpExchange.

@dataclass(frozen=True)
class Marriage(Move):
203@dataclass(frozen=True)
204class Marriage(Move):
205    """
206    A Move representing a marriage in the game. This move has two cards, a king and a queen of the same suit.
207    Right after the marriage is played, the player must play either the queen or the king.
208    Because it can only be beneficial to play the king, it is chosen automatically.
209    This Regular move is part of this Move already and does not have to be played separatly.
210    """
211
212    queen_card: Card
213    """The queen card of this marriage"""
214
215    king_card: Card
216    """The king card of this marriage"""
217
218    suit: Suit = field(init=False, repr=False, hash=False)
219    """The suit of this marriage, gets derived from the suit of the queen and king."""
220
221    def __post_init__(self) -> None:
222        """
223        Ensures that the suits of the fields all have the same suit and are a king and a queen.
224        Finally, sets the suit field.
225        """
226        assert self.queen_card.rank is Rank.QUEEN, f"The rank card {self.queen_card} used to initialize the {Marriage.__name__} was not Rank.QUEEN"
227        assert self.king_card.rank is Rank.KING, f"The rank card {self.king_card} used to initialize the {Marriage.__name__} was not Rank.KING"
228        assert self.queen_card.suit == self.king_card.suit, f"The cards used to inialize the Marriage {self.queen_card} and {self.king_card} so not have the same suit."
229        object.__setattr__(self, "suit", self.queen_card.suit)
230
231    def is_marriage(self) -> bool:
232        return True
233
234    def as_marriage(self) -> Marriage:
235        return self
236
237    def underlying_regular_move(self) -> RegularMove:
238        """
239        Get the regular move which was played because of the marriage. In this engine this is always the queen card.
240
241        :returns: (RegularMove): The regular move which was played because of the marriage.
242        """
243        # this limits you to only have the queen to play after a marriage, while in general you would have a choice.
244        # This is not an issue since playing the king give you the highest score.
245        return RegularMove(self.king_card)
246
247    def _cards(self) -> list[Card]:
248        return [self.queen_card, self.king_card]
249
250    def __repr__(self) -> str:
251        return f"Marriage(queen_card={self.queen_card}, king_card={self.king_card})"
252
253    def __eq__(self, __o: object) -> bool:
254        if not isinstance(__o, Marriage):
255            return False
256        return self.queen_card == __o.queen_card and self.king_card == self.king_card

A Move representing a marriage in the game. This move has two cards, a king and a queen of the same suit. Right after the marriage is played, the player must play either the queen or the king. Because it can only be beneficial to play the king, it is chosen automatically. This Regular move is part of this Move already and does not have to be played separatly.

Marriage(queen_card: schnapsen.deck.Card, king_card: schnapsen.deck.Card)
queen_card: schnapsen.deck.Card

The queen card of this marriage

king_card: schnapsen.deck.Card

The king card of this marriage

The suit of this marriage, gets derived from the suit of the queen and king.

def is_marriage(self) -> bool:
231    def is_marriage(self) -> bool:
232        return True

Is this Move a marriage?

:returns: a bool indicating whether this move is a marriage

def as_marriage(self) -> Marriage:
234    def as_marriage(self) -> Marriage:
235        return self

Returns this same move but as a Marriage.

def underlying_regular_move(self) -> RegularMove:
237    def underlying_regular_move(self) -> RegularMove:
238        """
239        Get the regular move which was played because of the marriage. In this engine this is always the queen card.
240
241        :returns: (RegularMove): The regular move which was played because of the marriage.
242        """
243        # this limits you to only have the queen to play after a marriage, while in general you would have a choice.
244        # This is not an issue since playing the king give you the highest score.
245        return RegularMove(self.king_card)

Get the regular move which was played because of the marriage. In this engine this is always the queen card.

:returns: (RegularMove): The regular move which was played because of the marriage.

class Hand(schnapsen.deck.CardCollection):
259class Hand(CardCollection):
260    """
261    The cards in the hand of a player. These are the cards which the player can see and which he can play with in the turn.
262
263    :param cards: (Iterable[Card]): The cards to be added to the hand
264    :param max_size: (int): The maximum number of cards the hand can contain. If the number of cards goes beyond, an Exception is raised. Defaults to 5.
265
266    :attr cards: The cards in the hand - initialized from the cards parameter.
267    :attr max_size: The maximum number of cards the hand can contain - initialized from the max_size parameter.
268    """
269
270    def __init__(self, cards: Iterable[Card], max_size: int = 5) -> None:
271        self.max_size = max_size
272        cards = list(cards)
273        assert len(cards) <= max_size, f"The number of cards {len(cards)} is larger than the maximum number fo allowed cards {max_size}"
274        self.cards = cards
275
276    def remove(self, card: Card) -> None:
277        """
278        Remove one occurence of the card from this hand
279
280        :param card: (Card): The card to be removed from the hand.
281        """
282        try:
283            self.cards.remove(card)
284        except ValueError as ve:
285            raise Exception(f"Trying to remove a card from the hand which is not in the hand. Hand is {self.cards}, trying to remove {card}") from ve
286
287    def add(self, card: Card) -> None:
288        """
289        Add a card to the Hand
290
291        :param card:  The card to be added to the hand
292        """
293        assert len(self.cards) < self.max_size, "Adding one more card to the hand will cause a hand with too many cards"
294        self.cards.append(card)
295
296    def has_cards(self, cards: Iterable[Card]) -> bool:
297        """
298        Are all the cards contained in this Hand?
299
300        :param cards: An iterable of cards which need to be checked
301        :returns: Whether all cards in the provided iterable are in this Hand
302        """
303        return all(card in self.cards for card in cards)
304
305    def copy(self) -> Hand:
306        """
307        Create a deep copy of this Hand
308
309        :returns: A deep copy of this hand. Changes to the original will not affect the copy and vice versa.
310        """
311        return Hand(list(self.cards), max_size=self.max_size)
312
313    def is_empty(self) -> bool:
314        """
315        Is the Hand emoty?
316
317        :returns: A bool indicating whether the hand is empty
318        """
319        return len(self.cards) == 0
320
321    def get_cards(self) -> list[Card]:
322        """
323        Returns the cards in the hand
324
325        :returns: (list[Card]): A defensive copy of the list of Cards in this Hand.
326        """
327        return list(self.cards)
328
329    def filter_suit(self, suit: Suit) -> list[Card]:
330        """
331        Return a list of all cards in the hand which have the specified suit.
332
333        :param suit: (Suit): The suit to filter on.
334        :returns: (list(Card)): A list of cards which have the specified suit.
335        """
336        results: list[Card] = [card for card in self.cards if card.suit is suit]
337        return results
338
339    def filter_rank(self, rank: Rank) -> list[Card]:
340        """
341        Return a list of all cards in the hand which have the specified rank.
342
343        :param suit: (Rank): The rank to filter on.
344        :returns: (list(Card)): A list of cards which have the specified rank.
345        """
346        results: list[Card] = [card for card in self.cards if card.rank is rank]
347        return results
348
349    def __repr__(self) -> str:
350        return f"Hand(cards={self.cards}, max_size={self.max_size})"

The cards in the hand of a player. These are the cards which the player can see and which he can play with in the turn.

Parameters
  • cards: (Iterable[Card]): The cards to be added to the hand
  • max_size: (int): The maximum number of cards the hand can contain. If the number of cards goes beyond, an Exception is raised. Defaults to 5.

:attr cards: The cards in the hand - initialized from the cards parameter. :attr max_size: The maximum number of cards the hand can contain - initialized from the max_size parameter.

Hand(cards: Iterable[schnapsen.deck.Card], max_size: int = 5)
270    def __init__(self, cards: Iterable[Card], max_size: int = 5) -> None:
271        self.max_size = max_size
272        cards = list(cards)
273        assert len(cards) <= max_size, f"The number of cards {len(cards)} is larger than the maximum number fo allowed cards {max_size}"
274        self.cards = cards
max_size
cards
def remove(self, card: schnapsen.deck.Card) -> None:
276    def remove(self, card: Card) -> None:
277        """
278        Remove one occurence of the card from this hand
279
280        :param card: (Card): The card to be removed from the hand.
281        """
282        try:
283            self.cards.remove(card)
284        except ValueError as ve:
285            raise Exception(f"Trying to remove a card from the hand which is not in the hand. Hand is {self.cards}, trying to remove {card}") from ve

Remove one occurence of the card from this hand

Parameters
  • card: (Card): The card to be removed from the hand.
def add(self, card: schnapsen.deck.Card) -> None:
287    def add(self, card: Card) -> None:
288        """
289        Add a card to the Hand
290
291        :param card:  The card to be added to the hand
292        """
293        assert len(self.cards) < self.max_size, "Adding one more card to the hand will cause a hand with too many cards"
294        self.cards.append(card)

Add a card to the Hand

Parameters
  • card: The card to be added to the hand
def has_cards(self, cards: Iterable[schnapsen.deck.Card]) -> bool:
296    def has_cards(self, cards: Iterable[Card]) -> bool:
297        """
298        Are all the cards contained in this Hand?
299
300        :param cards: An iterable of cards which need to be checked
301        :returns: Whether all cards in the provided iterable are in this Hand
302        """
303        return all(card in self.cards for card in cards)

Are all the cards contained in this Hand?

Parameters
  • cards: An iterable of cards which need to be checked :returns: Whether all cards in the provided iterable are in this Hand
def copy(self) -> Hand:
305    def copy(self) -> Hand:
306        """
307        Create a deep copy of this Hand
308
309        :returns: A deep copy of this hand. Changes to the original will not affect the copy and vice versa.
310        """
311        return Hand(list(self.cards), max_size=self.max_size)

Create a deep copy of this Hand

:returns: A deep copy of this hand. Changes to the original will not affect the copy and vice versa.

def is_empty(self) -> bool:
313    def is_empty(self) -> bool:
314        """
315        Is the Hand emoty?
316
317        :returns: A bool indicating whether the hand is empty
318        """
319        return len(self.cards) == 0

Is the Hand emoty?

:returns: A bool indicating whether the hand is empty

def get_cards(self) -> list[schnapsen.deck.Card]:
321    def get_cards(self) -> list[Card]:
322        """
323        Returns the cards in the hand
324
325        :returns: (list[Card]): A defensive copy of the list of Cards in this Hand.
326        """
327        return list(self.cards)

Returns the cards in the hand

:returns: (list[Card]): A defensive copy of the list of Cards in this Hand.

def filter_suit(self, suit: schnapsen.deck.Suit) -> list[schnapsen.deck.Card]:
329    def filter_suit(self, suit: Suit) -> list[Card]:
330        """
331        Return a list of all cards in the hand which have the specified suit.
332
333        :param suit: (Suit): The suit to filter on.
334        :returns: (list(Card)): A list of cards which have the specified suit.
335        """
336        results: list[Card] = [card for card in self.cards if card.suit is suit]
337        return results

Return a list of all cards in the hand which have the specified suit.

Parameters
  • suit: (Suit): The suit to filter on. :returns: (list(Card)): A list of cards which have the specified suit.
def filter_rank(self, rank: schnapsen.deck.Rank) -> list[schnapsen.deck.Card]:
339    def filter_rank(self, rank: Rank) -> list[Card]:
340        """
341        Return a list of all cards in the hand which have the specified rank.
342
343        :param suit: (Rank): The rank to filter on.
344        :returns: (list(Card)): A list of cards which have the specified rank.
345        """
346        results: list[Card] = [card for card in self.cards if card.rank is rank]
347        return results

Return a list of all cards in the hand which have the specified rank.

Parameters
  • suit: (Rank): The rank to filter on. :returns: (list(Card)): A list of cards which have the specified rank.
class Talon(schnapsen.deck.OrderedCardCollection):
353class Talon(OrderedCardCollection):
354    """
355    The Talon contains the cards which have not yet been given to the players.
356
357    :param cards: The cards to be put on this talon, a defensive copy will be made.
358    :param trump_suit: The trump suit of the Talon, important if there are no more cards to be taken.
359    :attr _cards: The cards of the Talon (defined in super().__init__)
360    :attr __trump_suit: The trump suit of the Talon.
361    """
362
363    def __init__(self, cards: Iterable[Card], trump_suit: Optional[Suit] = None) -> None:
364        """
365        The cards of the Talon. The last card of the iterable is the bottommost card.
366        The first one is the top card (which will be taken when/if a card is drawn)
367        The Trump card is at the bottom of the Talon.
368        The trump_suit can also be specified explicitly, which is important when the Talon is empty.
369        If the trump_suit is specified and there are cards, then the suit of the bottommost card must be the same.
370        """
371        if cards:
372            trump_card_suit = list(cards)[-1].suit
373            assert not trump_suit or trump_card_suit == trump_suit, "If the trump suit is specified, and there are cards on the talon, the suit must be the same!"
374            self.__trump_suit = trump_card_suit
375        else:
376            assert trump_suit, f"If an empty {Talon.__name__} is created, the trump_suit must be specified"
377            self.__trump_suit = trump_suit
378
379        super().__init__(cards)
380
381    def copy(self) -> Talon:
382        """
383        Create an independent copy of this talon.
384
385        :returns: (Talon): A deep copy of this talon. Changes to the original will not affect the copy and vice versa.
386        """
387        # We do not need to make a copy of the cards as this happend in the constructor of Talon.
388        return Talon(self._cards, self.__trump_suit)
389
390    def trump_exchange(self, new_trump: Card) -> Card:
391        """
392        perfom a trump-jack exchange. The card to be put as the trump card must be a Jack of the same suit.
393        As a result, this Talon changed: the old trump is removed and the new_trump is at the bottom of the Talon
394
395        We also require that there must be two cards on the Talon, which is always the case in a normal game of Schnapsen
396
397        :param new_trump: (Card):The card to be put. It must be a Jack of the same suit as the card at the bottom
398        :returns: (Card): The card which was at the bottom of the Talon before the exchange.
399
400        """
401        assert new_trump.rank is Rank.JACK, f"the rank of the card used for the exchange {new_trump} is not a Rank.JACK"
402        assert len(self._cards) >= 2, f"There must be at least two cards on the talon to do an exchange len = {len(self._cards)}"
403        assert new_trump.suit is self._cards[-1].suit, f"The suit of the new card {new_trump} is not equal to the current bottom {self._cards[-1].suit}"
404        old_trump = self._cards.pop(len(self._cards) - 1)
405        self._cards.append(new_trump)
406        return old_trump
407
408    def draw_cards(self, amount: int) -> list[Card]:
409        """
410        Draw a card from this Talon. This changes the talon.
411
412        param amount: (int): The number of cards to be drawn
413        :returns: (Iterable[Card]): The cards drawn from this Talon.
414        """
415
416        assert len(self._cards) >= amount, f"There are only {len(self._cards)} on the Talon, but {amount} cards are requested"
417        draw = self._cards[:amount]
418        self._cards = self._cards[amount:]
419        return draw
420
421    def trump_suit(self) -> Suit:
422        """
423        Return the suit of the trump card, i.e., the bottommost card.
424        This still works, even when the Talon has become empty.
425
426        :returns: (Suit): the trump suit of the Talon
427        """
428        return self.__trump_suit
429
430    def trump_card(self) -> Optional[Card]:
431        """
432        Returns the current trump card, i.e., the bottommost card.
433        Or None in case this Talon is empty
434
435        :returns: (Card): The trump card, or None if the Talon is empty
436        """
437        if len(self._cards) > 0:
438            return self._cards[-1]
439        return None
440
441    def __repr__(self) -> str:
442        """
443        A string representation of the Talon.
444
445        :returns: (str): A string representation of the Talon.
446        """
447        return f"Talon(cards={self._cards}, trump_suit={self.__trump_suit})"

The Talon contains the cards which have not yet been given to the players.

Parameters
  • cards: The cards to be put on this talon, a defensive copy will be made.
  • trump_suit: The trump suit of the Talon, important if there are no more cards to be taken. :attr _cards: The cards of the Talon (defined in super().__init__) :attr __trump_suit: The trump suit of the Talon.
Talon( cards: Iterable[schnapsen.deck.Card], trump_suit: Optional[schnapsen.deck.Suit] = None)
363    def __init__(self, cards: Iterable[Card], trump_suit: Optional[Suit] = None) -> None:
364        """
365        The cards of the Talon. The last card of the iterable is the bottommost card.
366        The first one is the top card (which will be taken when/if a card is drawn)
367        The Trump card is at the bottom of the Talon.
368        The trump_suit can also be specified explicitly, which is important when the Talon is empty.
369        If the trump_suit is specified and there are cards, then the suit of the bottommost card must be the same.
370        """
371        if cards:
372            trump_card_suit = list(cards)[-1].suit
373            assert not trump_suit or trump_card_suit == trump_suit, "If the trump suit is specified, and there are cards on the talon, the suit must be the same!"
374            self.__trump_suit = trump_card_suit
375        else:
376            assert trump_suit, f"If an empty {Talon.__name__} is created, the trump_suit must be specified"
377            self.__trump_suit = trump_suit
378
379        super().__init__(cards)

The cards of the Talon. The last card of the iterable is the bottommost card. The first one is the top card (which will be taken when/if a card is drawn) The Trump card is at the bottom of the Talon. The trump_suit can also be specified explicitly, which is important when the Talon is empty. If the trump_suit is specified and there are cards, then the suit of the bottommost card must be the same.

def copy(self) -> Talon:
381    def copy(self) -> Talon:
382        """
383        Create an independent copy of this talon.
384
385        :returns: (Talon): A deep copy of this talon. Changes to the original will not affect the copy and vice versa.
386        """
387        # We do not need to make a copy of the cards as this happend in the constructor of Talon.
388        return Talon(self._cards, self.__trump_suit)

Create an independent copy of this talon.

:returns: (Talon): A deep copy of this talon. Changes to the original will not affect the copy and vice versa.

def trump_exchange(self, new_trump: schnapsen.deck.Card) -> schnapsen.deck.Card:
390    def trump_exchange(self, new_trump: Card) -> Card:
391        """
392        perfom a trump-jack exchange. The card to be put as the trump card must be a Jack of the same suit.
393        As a result, this Talon changed: the old trump is removed and the new_trump is at the bottom of the Talon
394
395        We also require that there must be two cards on the Talon, which is always the case in a normal game of Schnapsen
396
397        :param new_trump: (Card):The card to be put. It must be a Jack of the same suit as the card at the bottom
398        :returns: (Card): The card which was at the bottom of the Talon before the exchange.
399
400        """
401        assert new_trump.rank is Rank.JACK, f"the rank of the card used for the exchange {new_trump} is not a Rank.JACK"
402        assert len(self._cards) >= 2, f"There must be at least two cards on the talon to do an exchange len = {len(self._cards)}"
403        assert new_trump.suit is self._cards[-1].suit, f"The suit of the new card {new_trump} is not equal to the current bottom {self._cards[-1].suit}"
404        old_trump = self._cards.pop(len(self._cards) - 1)
405        self._cards.append(new_trump)
406        return old_trump

perfom a trump-jack exchange. The card to be put as the trump card must be a Jack of the same suit. As a result, this Talon changed: the old trump is removed and the new_trump is at the bottom of the Talon

We also require that there must be two cards on the Talon, which is always the case in a normal game of Schnapsen

Parameters
  • new_trump: (Card): The card to be put. It must be a Jack of the same suit as the card at the bottom :returns: (Card): The card which was at the bottom of the Talon before the exchange.
def draw_cards(self, amount: int) -> list[schnapsen.deck.Card]:
408    def draw_cards(self, amount: int) -> list[Card]:
409        """
410        Draw a card from this Talon. This changes the talon.
411
412        param amount: (int): The number of cards to be drawn
413        :returns: (Iterable[Card]): The cards drawn from this Talon.
414        """
415
416        assert len(self._cards) >= amount, f"There are only {len(self._cards)} on the Talon, but {amount} cards are requested"
417        draw = self._cards[:amount]
418        self._cards = self._cards[amount:]
419        return draw

Draw a card from this Talon. This changes the talon.

param amount: (int): The number of cards to be drawn :returns: (Iterable[Card]): The cards drawn from this Talon.

def trump_suit(self) -> schnapsen.deck.Suit:
421    def trump_suit(self) -> Suit:
422        """
423        Return the suit of the trump card, i.e., the bottommost card.
424        This still works, even when the Talon has become empty.
425
426        :returns: (Suit): the trump suit of the Talon
427        """
428        return self.__trump_suit

Return the suit of the trump card, i.e., the bottommost card. This still works, even when the Talon has become empty.

:returns: (Suit): the trump suit of the Talon

def trump_card(self) -> Optional[schnapsen.deck.Card]:
430    def trump_card(self) -> Optional[Card]:
431        """
432        Returns the current trump card, i.e., the bottommost card.
433        Or None in case this Talon is empty
434
435        :returns: (Card): The trump card, or None if the Talon is empty
436        """
437        if len(self._cards) > 0:
438            return self._cards[-1]
439        return None

Returns the current trump card, i.e., the bottommost card. Or None in case this Talon is empty

:returns: (Card): The trump card, or None if the Talon is empty

@dataclass(frozen=True)
class Trick(abc.ABC):
450@dataclass(frozen=True)
451class Trick(ABC):
452    """
453    A complete trick. This is, the move of the leader and if that was not an exchange, the move of the follower.
454    """
455
456    cards: Iterable[Card] = field(init=False, repr=False, hash=False)
457    """All cards used as part of this trick. This includes cards used in marriages"""
458
459    @abstractmethod
460    def is_trump_exchange(self) -> bool:
461        """
462        Returns True if this is a trump exchange
463
464        :returns: True in case this was a trump exchange
465        """
466
467    @abstractmethod
468    def as_partial(self) -> PartialTrick:
469        """
470        Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts
471
472        :returns: The first part of this trick
473        """
474
475    def __getattribute__(self, __name: str) -> Any:
476        if __name == "cards":
477            # We call the method to compute the card list
478            return object.__getattribute__(self, "_cards")()
479        return object.__getattribute__(self, __name)
480
481    @abstractmethod
482    def _cards(self) -> Iterable[Card]:
483        """
484        Get all cards used in this tick. This method should not be called directly.
485        Use the cards property instead.
486
487        :returns: (Iterable[Card]): All cards used in this trick.
488        """

A complete trick. This is, the move of the leader and if that was not an exchange, the move of the follower.

cards: Iterable[schnapsen.deck.Card]

All cards used as part of this trick. This includes cards used in marriages

@abstractmethod
def is_trump_exchange(self) -> bool:
459    @abstractmethod
460    def is_trump_exchange(self) -> bool:
461        """
462        Returns True if this is a trump exchange
463
464        :returns: True in case this was a trump exchange
465        """

Returns True if this is a trump exchange

:returns: True in case this was a trump exchange

@abstractmethod
def as_partial(self) -> PartialTrick:
467    @abstractmethod
468    def as_partial(self) -> PartialTrick:
469        """
470        Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts
471
472        :returns: The first part of this trick
473        """

Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts

:returns: The first part of this trick

@dataclass(frozen=True)
class ExchangeTrick(Trick):
491@dataclass(frozen=True)
492class ExchangeTrick(Trick):
493    """
494    A Trick in which the player does a trump exchange.
495    """
496
497    exchange: TrumpExchange
498    """A trump exchange by the leading player"""
499
500    trump_card: Card
501    """The card at the bottom of the talon"""
502
503    def is_trump_exchange(self) -> bool:
504        """Returns True if this is a trump exchange"""
505        return True
506
507    def as_partial(self) -> PartialTrick:
508        """ Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts"""
509        raise Exception("An Exchange Trick does not have a first part")
510
511    def _cards(self) -> Iterable[Card]:
512        """Get all cards used in this tick. This method should not be called directly."""
513        exchange = self.exchange.cards
514        exchange.append(self.trump_card)
515        return exchange

A Trick in which the player does a trump exchange.

ExchangeTrick( exchange: TrumpExchange, trump_card: schnapsen.deck.Card)
exchange: TrumpExchange

A trump exchange by the leading player

trump_card: schnapsen.deck.Card

The card at the bottom of the talon

def is_trump_exchange(self) -> bool:
503    def is_trump_exchange(self) -> bool:
504        """Returns True if this is a trump exchange"""
505        return True

Returns True if this is a trump exchange

def as_partial(self) -> PartialTrick:
507    def as_partial(self) -> PartialTrick:
508        """ Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts"""
509        raise Exception("An Exchange Trick does not have a first part")

Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts

Inherited Members
Trick
cards
@dataclass(frozen=True)
class PartialTrick:
518@dataclass(frozen=True)
519class PartialTrick:
520    """
521    A partial trick is the move(s) played by the leading player.
522    """
523    leader_move: Union[RegularMove, Marriage]
524    """The move played by the leader of the trick"""
525
526    def is_trump_exchange(self) -> bool:
527        """Returns false to indicate that this trick is not a trump exchange"""
528        return False
529
530    def __repr__(self) -> str:
531        return f"PartialTrick(leader_move={self.leader_move})"

A partial trick is the move(s) played by the leading player.

PartialTrick( leader_move: Union[RegularMove, Marriage])
leader_move: Union[RegularMove, Marriage]

The move played by the leader of the trick

def is_trump_exchange(self) -> bool:
526    def is_trump_exchange(self) -> bool:
527        """Returns false to indicate that this trick is not a trump exchange"""
528        return False

Returns false to indicate that this trick is not a trump exchange

@dataclass(frozen=True)
class RegularTrick(Trick, PartialTrick):
534@dataclass(frozen=True)
535class RegularTrick(Trick, PartialTrick):
536    """
537    A regular trick, with a move by the leader and a move by the follower
538    """
539    follower_move: RegularMove
540    """The move played by the follower"""
541
542    def is_trump_exchange(self) -> bool:
543        """Returns false to indicate that this trick is not a trump exchange"""
544        return False
545
546    def as_partial(self) -> PartialTrick:
547        """Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts"""
548        return PartialTrick(self.leader_move)
549
550    def _cards(self) -> Iterable[Card]:
551        """Get all cards used in this tick. This method should not be called directly."""
552        return itertools.chain(self.leader_move.cards, self.follower_move.cards)
553
554    def __repr__(self) -> str:
555        """A string representation of the Trick"""
556        return f"RegularTrick(leader_move={self.leader_move}, follower_move={self.follower_move})"

A regular trick, with a move by the leader and a move by the follower

RegularTrick( leader_move: Union[RegularMove, Marriage], follower_move: RegularMove)
follower_move: RegularMove

The move played by the follower

def is_trump_exchange(self) -> bool:
542    def is_trump_exchange(self) -> bool:
543        """Returns false to indicate that this trick is not a trump exchange"""
544        return False

Returns false to indicate that this trick is not a trump exchange

def as_partial(self) -> PartialTrick:
546    def as_partial(self) -> PartialTrick:
547        """Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts"""
548        return PartialTrick(self.leader_move)

Returns the first part of this trick. Raises an Exceptption if this is not a Trick with two parts

Inherited Members
Trick
cards
PartialTrick
leader_move
@dataclass(frozen=True)
class Score:
559@dataclass(frozen=True)
560class Score:
561    """
562    The score of one of the bots. This consists of the current points and potential pending points because of an earlier played marriage.
563    Note that the socre object is immutable and supports the `+` operator, so it can be used somewhat as a usual number.
564    """
565    direct_points: int = 0
566    """The current number of points"""
567    pending_points: int = 0
568    """Points to be applied in the future because of a past marriage"""
569
570    def __add__(self, other: Score) -> Score:
571        """
572        Adds two scores together. Direct points and pending points are added separately.
573
574        :param other: (Score): The score to be added to the current one.
575        :returns: (Score): A new score object with the points of the current score and the other combined
576        """
577        total_direct_points = self.direct_points + other.direct_points
578        total_pending_points = self.pending_points + other.pending_points
579        return Score(total_direct_points, total_pending_points)
580
581    def redeem_pending_points(self) -> Score:
582        """
583        Redeem the pending points
584
585        :returns: (Score):A new score object with the pending points added to the direct points and the pending points set to zero.
586        """
587        return Score(direct_points=self.direct_points + self.pending_points, pending_points=0)
588
589    def __repr__(self) -> str:
590        """A string representation of the Score"""
591        return f"Score(direct_points={self.direct_points}, pending_points={self.pending_points})"

The score of one of the bots. This consists of the current points and potential pending points because of an earlier played marriage. Note that the socre object is immutable and supports the + operator, so it can be used somewhat as a usual number.

Score(direct_points: int = 0, pending_points: int = 0)
direct_points: int = 0

The current number of points

pending_points: int = 0

Points to be applied in the future because of a past marriage

def redeem_pending_points(self) -> Score:
581    def redeem_pending_points(self) -> Score:
582        """
583        Redeem the pending points
584
585        :returns: (Score):A new score object with the pending points added to the direct points and the pending points set to zero.
586        """
587        return Score(direct_points=self.direct_points + self.pending_points, pending_points=0)

Redeem the pending points

:returns: (Score):A new score object with the pending points added to the direct points and the pending points set to zero.

class GamePhase(enum.Enum):
594class GamePhase(Enum):
595    """
596    An indicator about the phase of the game. This is used because in Schnapsen, the rules change when the game enters the second phase.
597    """
598    ONE = 1
599    TWO = 2

An indicator about the phase of the game. This is used because in Schnapsen, the rules change when the game enters the second phase.

ONE = <GamePhase.ONE: 1>
TWO = <GamePhase.TWO: 2>
@dataclass
class BotState:
602@dataclass
603class BotState:
604    """A bot with its implementation and current state in a game"""
605
606    implementation: Bot
607    hand: Hand
608    score: Score = field(default_factory=Score)
609    won_cards: list[Card] = field(default_factory=list)
610
611    def get_move(self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
612        """
613        Gets the next move from the bot itself, passing it the state.
614        Does a quick check to make sure that the hand has the cards which are played. More advanced checks are performed outside of this call.
615
616        :param state: (PlayerPerspective): The PlayerGameState which contains the information on the current state of the game from the perspective of this player
617        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
618        :returns: The move the played
619        """
620        move = self.implementation.get_move(perspective, leader_move=leader_move)
621        assert move is not None, f"The bot {self.implementation} returned a move which is None"
622        if not isinstance(move, Move):
623            raise AssertionError(f"The bot {self.implementation} returned an object which is not a Move, got {move}")
624        return move
625
626    def copy(self) -> BotState:
627        """
628        Makes a deep copy of the current state.
629
630        :returns: (BotState): The deep copy.
631        """
632        new_bot = BotState(
633            implementation=self.implementation,
634            hand=self.hand.copy(),
635            score=self.score,  # does not need a copy because it is not mutable
636            won_cards=list(self.won_cards),
637        )
638        return new_bot
639
640    def __repr__(self) -> str:
641        return f"BotState(implementation={self.implementation}, hand={self.hand}, "\
642               f"score={self.score}, won_cards={self.won_cards})"

A bot with its implementation and current state in a game

BotState( implementation: Bot, hand: Hand, score: Score = <factory>, won_cards: list[schnapsen.deck.Card] = <factory>)
implementation: Bot
hand: Hand
score: Score
won_cards: list[schnapsen.deck.Card]
def get_move( self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
611    def get_move(self, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
612        """
613        Gets the next move from the bot itself, passing it the state.
614        Does a quick check to make sure that the hand has the cards which are played. More advanced checks are performed outside of this call.
615
616        :param state: (PlayerPerspective): The PlayerGameState which contains the information on the current state of the game from the perspective of this player
617        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
618        :returns: The move the played
619        """
620        move = self.implementation.get_move(perspective, leader_move=leader_move)
621        assert move is not None, f"The bot {self.implementation} returned a move which is None"
622        if not isinstance(move, Move):
623            raise AssertionError(f"The bot {self.implementation} returned an object which is not a Move, got {move}")
624        return move

Gets the next move from the bot itself, passing it the state. Does a quick check to make sure that the hand has the cards which are played. More advanced checks are performed outside of this call.

Parameters
  • state: (PlayerPerspective): The PlayerGameState which contains the information on the current state of the game from the perspective of this player
  • leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader. :returns: The move the played
def copy(self) -> BotState:
626    def copy(self) -> BotState:
627        """
628        Makes a deep copy of the current state.
629
630        :returns: (BotState): The deep copy.
631        """
632        new_bot = BotState(
633            implementation=self.implementation,
634            hand=self.hand.copy(),
635            score=self.score,  # does not need a copy because it is not mutable
636            won_cards=list(self.won_cards),
637        )
638        return new_bot

Makes a deep copy of the current state.

:returns: (BotState): The deep copy.

@dataclass(frozen=True)
class Previous:
645@dataclass(frozen=True)
646class Previous:
647    """
648    Information about the previous GameState.
649    This object can be used to access the history which lead to the current GameState
650    """
651
652    state: GameState
653    """The previous state of the game. """
654    trick: Trick
655    """The trick which led to the current Gamestate from the Previous state"""
656    leader_remained_leader: bool
657    """Did the leader of remain the leader."""

Information about the previous GameState. This object can be used to access the history which lead to the current GameState

Previous( state: GameState, trick: Trick, leader_remained_leader: bool)
state: GameState

The previous state of the game.

trick: Trick

The trick which led to the current Gamestate from the Previous state

leader_remained_leader: bool

Did the leader of remain the leader.

@dataclass
class GameState:
660@dataclass
661class GameState:
662    """
663    The current state of the game, as seen by the game engine.
664    This contains all information about the positions of the cards, bots, scores, etc.
665    The bot must not get direct access to this information because it would allow it to cheat.
666    """
667    leader: BotState
668    """The current leader, i.e., the one who will play the first move in the next trick"""
669    follower: BotState
670    """The current follower, i.e., the one who will play the second move in the next trick"""
671    trump_suit: Suit = field(init=False)
672    """The trump suit in this game. This information is also in the Talon."""
673    talon: Talon
674    """The talon, containing the cards not yet in the hand of the player and the trump card at the bottom"""
675    previous: Optional[Previous]
676    """The events which led to this GameState, or None, if this is the initial GameState (or previous tricks and states are unknown)"""
677
678    def __getattribute__(self, __name: str) -> Any:
679        if __name == "trump_suit":
680            # We get it from the talon
681            return self.talon.trump_suit()
682        return object.__getattribute__(self, __name)
683
684    def copy_for_next(self) -> GameState:
685        """
686        Make a copy of the gamestate, modified such that the previous state is this state, but the previous trick is not filled yet.
687        This is used to create a GameState which will be modified to become the next gamestate.
688
689        :returns: (Gamestate): A copy of the gamestate, with the previous trick not filled yet.
690        """
691        # We intentionally do no initialize the previous information. It is not known yet
692        new_state = GameState(
693            leader=self.leader.copy(),
694            follower=self.follower.copy(),
695            talon=self.talon.copy(),
696            previous=None
697        )
698        return new_state
699
700    def copy_with_other_bots(self, new_leader: Bot, new_follower: Bot) -> GameState:
701        """
702        Make a copy of the gamestate, modified such that the bots are replaced by the provided ones.
703        This is used to continue playing an existing GameState with different bots.
704
705        :param new_leader: (Bot): The new leader
706        :param new_follower: (Bot): The new follower
707        :returns: (Gamestate): A copy of the gamestate, with the bots replaced.
708        """
709        new_state = GameState(
710            leader=self.leader.copy(),
711            follower=self.follower.copy(),
712            talon=self.talon.copy(),
713            previous=self.previous
714        )
715        new_state.leader.implementation = new_leader
716        new_state.follower.implementation = new_follower
717        return new_state
718
719    def game_phase(self) -> GamePhase:
720        """What is the current phase of the game
721
722        :returns: GamePhase.ONE or GamePahse.TWO indicating the current phase
723        """
724        if self.talon.is_empty():
725            return GamePhase.TWO
726        return GamePhase.ONE
727
728    def are_all_cards_played(self) -> bool:
729        """Returns True in case the players have played all their cards and the game is has come to an end
730
731        :returns: (bool): True if all cards have been played, False otherwise
732        """
733        return self.leader.hand.is_empty() and self.follower.hand.is_empty() and self.talon.is_empty()
734
735    def __repr__(self) -> str:
736        return f"GameState(leader={self.leader}, follower={self.follower}, "\
737               f"talon={self.talon}, previous={self.previous})"

The current state of the game, as seen by the game engine. This contains all information about the positions of the cards, bots, scores, etc. The bot must not get direct access to this information because it would allow it to cheat.

GameState( leader: BotState, follower: BotState, talon: Talon, previous: Optional[Previous])
leader: BotState

The current leader, i.e., the one who will play the first move in the next trick

follower: BotState

The current follower, i.e., the one who will play the second move in the next trick

trump_suit: schnapsen.deck.Suit

The trump suit in this game. This information is also in the Talon.

talon: Talon

The talon, containing the cards not yet in the hand of the player and the trump card at the bottom

previous: Optional[Previous]

The events which led to this GameState, or None, if this is the initial GameState (or previous tricks and states are unknown)

def copy_for_next(self) -> GameState:
684    def copy_for_next(self) -> GameState:
685        """
686        Make a copy of the gamestate, modified such that the previous state is this state, but the previous trick is not filled yet.
687        This is used to create a GameState which will be modified to become the next gamestate.
688
689        :returns: (Gamestate): A copy of the gamestate, with the previous trick not filled yet.
690        """
691        # We intentionally do no initialize the previous information. It is not known yet
692        new_state = GameState(
693            leader=self.leader.copy(),
694            follower=self.follower.copy(),
695            talon=self.talon.copy(),
696            previous=None
697        )
698        return new_state

Make a copy of the gamestate, modified such that the previous state is this state, but the previous trick is not filled yet. This is used to create a GameState which will be modified to become the next gamestate.

:returns: (Gamestate): A copy of the gamestate, with the previous trick not filled yet.

def copy_with_other_bots( self, new_leader: Bot, new_follower: Bot) -> GameState:
700    def copy_with_other_bots(self, new_leader: Bot, new_follower: Bot) -> GameState:
701        """
702        Make a copy of the gamestate, modified such that the bots are replaced by the provided ones.
703        This is used to continue playing an existing GameState with different bots.
704
705        :param new_leader: (Bot): The new leader
706        :param new_follower: (Bot): The new follower
707        :returns: (Gamestate): A copy of the gamestate, with the bots replaced.
708        """
709        new_state = GameState(
710            leader=self.leader.copy(),
711            follower=self.follower.copy(),
712            talon=self.talon.copy(),
713            previous=self.previous
714        )
715        new_state.leader.implementation = new_leader
716        new_state.follower.implementation = new_follower
717        return new_state

Make a copy of the gamestate, modified such that the bots are replaced by the provided ones. This is used to continue playing an existing GameState with different bots.

Parameters
  • new_leader: (Bot): The new leader
  • new_follower: (Bot): The new follower :returns: (Gamestate): A copy of the gamestate, with the bots replaced.
def game_phase(self) -> GamePhase:
719    def game_phase(self) -> GamePhase:
720        """What is the current phase of the game
721
722        :returns: GamePhase.ONE or GamePahse.TWO indicating the current phase
723        """
724        if self.talon.is_empty():
725            return GamePhase.TWO
726        return GamePhase.ONE

What is the current phase of the game

:returns: GamePhase.ONE or GamePahse.TWO indicating the current phase

def are_all_cards_played(self) -> bool:
728    def are_all_cards_played(self) -> bool:
729        """Returns True in case the players have played all their cards and the game is has come to an end
730
731        :returns: (bool): True if all cards have been played, False otherwise
732        """
733        return self.leader.hand.is_empty() and self.follower.hand.is_empty() and self.talon.is_empty()

Returns True in case the players have played all their cards and the game is has come to an end

:returns: (bool): True if all cards have been played, False otherwise

class PlayerPerspective(abc.ABC):
 740class PlayerPerspective(ABC):
 741    """
 742    The perspective a player has on the state of the game. This only gives access to the partially observable information.
 743    The Bot gets passed an instance of this class when it gets requested a move by the GamePlayEngine
 744
 745    This class has several convenience methods to get more information about the current state.
 746
 747    :param state: (GameState): The current state of the game
 748    :param engine: (GamePlayEngine): The engine which is used to play the game5
 749    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.5
 750    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
 751    """
 752
 753    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
 754        self.__game_state = state
 755        self.__engine = engine
 756
 757    @abstractmethod
 758    def valid_moves(self) -> list[Move]:
 759        """
 760        Get a list of all valid moves the bot can play at this point in the game.
 761
 762        Design note: this could also return an Iterable[Move], but list[Move] was chosen to make the API easier to use.
 763        """
 764
 765    def get_game_history(self) -> list[tuple[PlayerPerspective, Optional[Trick]]]:
 766        """
 767        The game history from the perspective of the player. This means all the past PlayerPerspective this bot has seen, and the Tricks played.
 768        This only provides access to cards the Bot is allowed to see.
 769
 770        :returns: (list[tuple[PlayerPerspective, Optional[Trick]]]): The PlayerPerspective and Tricks in chronological order, index 0 is the first round played. Only the last Trick will be None.
 771        The last pair will contain the current PlayerGameState.
 772        """
 773
 774        # We reconstruct the history backwards.
 775        game_state_history: list[tuple[PlayerPerspective, Optional[Trick]]] = []
 776        # We first push the current state to the end
 777        game_state_history.insert(0, (self, None))
 778
 779        current_leader = self.am_i_leader()
 780        current = self.__game_state.previous
 781
 782        while current:
 783            # If we were leader, and we remained, then we were leader before
 784            # If we were follower, and we remained, then we were follower before
 785            # If we were leader, and we did not remain, then we were follower before
 786            # If we were follower, and we did not remain, then we were leader before
 787            # This logic gets reflected by the negation of a xor
 788            current_leader = not current_leader ^ current.leader_remained_leader
 789
 790            current_player_perspective: PlayerPerspective
 791            if current_leader:
 792                current_player_perspective = LeaderPerspective(current.state, self.__engine)
 793            else:  # We are following
 794                if current.trick.is_trump_exchange():
 795                    current_player_perspective = ExchangeFollowerPerspective(current.state, self.__engine)
 796                else:
 797                    current_player_perspective = FollowerPerspective(current.state, self.__engine, current.trick.as_partial().leader_move)
 798            history_record = (current_player_perspective, current.trick)
 799            game_state_history.insert(0, history_record)
 800
 801            current = current.state.previous
 802        return game_state_history
 803
 804    @abstractmethod
 805    def get_hand(self) -> Hand:
 806        """Get the cards in the hand of the current player"""
 807
 808    @abstractmethod
 809    def get_my_score(self) -> Score:
 810        """Get the socre of the current player. The return Score object contains both the direct points and pending points from a marriage."""
 811
 812    @abstractmethod
 813    def get_opponent_score(self) -> Score:
 814        """Get the socre of the other player. The return Score object contains both the direct points and pending points from a marriage."""
 815
 816    def get_trump_suit(self) -> Suit:
 817        """Get the suit of the trump"""
 818        return self.__game_state.trump_suit
 819
 820    def get_trump_card(self) -> Optional[Card]:
 821        """Get the card which is at the bottom of the talon. Will be None if the talon is empty"""
 822        return self.__game_state.talon.trump_card()
 823
 824    def get_talon_size(self) -> int:
 825        """How many cards are still on the talon?"""
 826        return len(self.__game_state.talon)
 827
 828    def get_phase(self) -> GamePhase:
 829        """What is the pahse of the game? This returns a GamePhase object.
 830        You can check the phase by checking state.get_phase == GamePhase.ONE
 831        """
 832        return self.__game_state.game_phase()
 833
 834    @abstractmethod
 835    def get_opponent_hand_in_phase_two(self) -> Hand:
 836        """If the game is in the second phase, you can get the cards in the hand of the opponent.
 837        If this gets called, but the second pahse has not started yet, this will throw en Exception
 838        """
 839
 840    @abstractmethod
 841    def am_i_leader(self) -> bool:
 842        """Returns True if the bot is the leader of this trick, False if it is a follower."""
 843
 844    @abstractmethod
 845    def get_won_cards(self) -> CardCollection:
 846        """Get a list of all cards this Bot has won until now."""
 847
 848    @abstractmethod
 849    def get_opponent_won_cards(self) -> CardCollection:
 850        """Get the list of cards the opponent has won until now."""
 851
 852    def __get_own_bot_state(self) -> BotState:
 853        """Get the internal state object of this bot. This should not be used by a bot."""
 854        bot: BotState
 855        if self.am_i_leader():
 856            bot = self.__game_state.leader
 857        else:
 858            bot = self.__game_state.follower
 859        return bot
 860
 861    def __get_opponent_bot_state(self) -> BotState:
 862        """Get the internal state object of the other bot. This should not be used by a bot."""
 863        bot: BotState
 864        if self.am_i_leader():
 865            bot = self.__game_state.follower
 866        else:
 867            bot = self.__game_state.leader
 868        return bot
 869
 870    def seen_cards(self, leader_move: Optional[Move]) -> CardCollection:
 871        """Get a list of all cards your bot has seen until now
 872
 873        :param leader_move: (Optional[Move]):The move made by the leader of the trick. These cards have also been seen until now.
 874        :returns: (CardCollection): A list of all cards your bot has seen until now
 875        """
 876        bot = self.__get_own_bot_state()
 877
 878        seen_cards: set[Card] = set()  # We make it a set to remove duplicates
 879
 880        # in own hand
 881        seen_cards.update(bot.hand)
 882
 883        # the trump card
 884        trump = self.get_trump_card()
 885        if trump:
 886            seen_cards.add(trump)
 887
 888        # all cards which were played in Tricks (icludes marriages and Trump exchanges)
 889
 890        seen_cards.update(self.__past_tricks_cards())
 891        if leader_move is not None:
 892            seen_cards.update(leader_move.cards)
 893
 894        return OrderedCardCollection(seen_cards)
 895
 896    def __past_tricks_cards(self) -> set[Card]:
 897        """
 898        Gets the cards played in past tricks
 899
 900        :returns: (set[Card]): A set of all cards played in past tricks
 901        """
 902        past_cards: set[Card] = set()
 903        prev = self.__game_state.previous
 904        while prev:
 905            past_cards.update(prev.trick.cards)
 906            prev = prev.state.previous
 907        return past_cards
 908
 909    def get_known_cards_of_opponent_hand(self) -> CardCollection:
 910        """Get all cards which are in the opponents hand, but known to your Bot. This includes cards earlier used in marriages, or a trump exchange.
 911        All cards in the second pahse of the game.
 912
 913        :returns: (CardCollection): A list of all cards which are in the opponents hand, which are known to the bot.
 914        """
 915        opponent_hand = self.__get_opponent_bot_state().hand
 916        if self.get_phase() == GamePhase.TWO:
 917            return opponent_hand
 918        # We only disclose cards which have been part of a move, i.e., an Exchange or a Marriage
 919        past_trick_cards = self.__past_tricks_cards()
 920        return OrderedCardCollection(filter(lambda c: c in past_trick_cards, opponent_hand))
 921
 922    def get_engine(self) -> GamePlayEngine:
 923        """
 924        Get the GamePlayEngine in use for the current game.
 925        This engine can be used to retrieve all information about what kind of game we are playing,
 926        but can also be used to simulate alternative game rollouts.
 927
 928        :returns: (GamePlayEngine): The GamePlayEngine in use for the current game.
 929        """
 930        return self.__engine
 931
 932    def get_state_in_phase_two(self) -> GameState:
 933        """
 934        In phase TWO of the game, all information is known, so you can get the complete state
 935
 936        This removes the real bots from the GameState. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.
 937
 938        :retruns: (GameState): The GameState in phase two of the game - the active bots are replaced by dummy bots.
 939        """
 940
 941        if self.get_phase() == GamePhase.TWO:
 942            return self.__game_state.copy_with_other_bots(_DummyBot(), _DummyBot())
 943        raise AssertionError("You cannot get the state in phase one")
 944
 945    def make_assumption(self, leader_move: Optional[Move], rand: Random) -> GameState:
 946        """
 947        Takes the current imperfect information state and makes a random guess as to the position of the unknown cards.
 948        This also takes into account cards seen earlier during marriages played by the opponent, as well as potential trump jack exchanges
 949
 950        This removes the real bots from the GameState. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.
 951
 952        :param leader_move: (Optional[Move]): the optional already executed leader_move in the current trick. This card is guaranteed to be in the hand of the leader in the returned GameState.
 953        :param rand: (Random):the source of random numbers to do the random assignment of unknown cards
 954
 955        :returns: GameState: A perfect information state object.
 956        """
 957        opponent_hand = self.__get_opponent_bot_state().hand.copy()
 958
 959        if leader_move is not None:
 960            assert all(card in opponent_hand for card in leader_move.cards), f"The specified leader_move {leader_move} is not in the hand of the opponent {opponent_hand}"
 961
 962        full_state = self.__game_state.copy_with_other_bots(_DummyBot(), _DummyBot())
 963        if self.get_phase() == GamePhase.TWO:
 964            return full_state
 965
 966        seen_cards = self.seen_cards(leader_move)
 967        full_deck = self.__engine.deck_generator.get_initial_deck()
 968
 969        opponent_hand = self.__get_opponent_bot_state().hand.copy()
 970        unseen_opponent_hand = list(filter(lambda card: card not in seen_cards, opponent_hand))
 971
 972        talon = full_state.talon
 973        unseen_talon = list(filter(lambda card: card not in seen_cards, talon))
 974
 975        unseen_cards = list(filter(lambda card: card not in seen_cards, full_deck))
 976        if len(unseen_cards) > 1:
 977            rand.shuffle(unseen_cards)
 978
 979        assert len(unseen_talon) + len(unseen_opponent_hand) == len(unseen_cards), "Logical error. The number of unseen cards in the opponents hand and in the talon must be equal to the number of unseen cards"
 980
 981        new_talon: list[Card] = []
 982        for card in talon:
 983            if card in unseen_talon:
 984                # take one of the random cards
 985                new_talon.append(unseen_cards.pop())
 986            else:
 987                new_talon.append(card)
 988
 989        full_state.talon = Talon(new_talon)
 990
 991        new_opponent_hand = []
 992        for card in opponent_hand:
 993            if card in unseen_opponent_hand:
 994                new_opponent_hand.append(unseen_cards.pop())
 995            else:
 996                new_opponent_hand.append(card)
 997        if self.am_i_leader():
 998            full_state.follower.hand = Hand(new_opponent_hand)
 999        else:
1000            full_state.leader.hand = Hand(new_opponent_hand)
1001
1002        assert len(unseen_cards) == 0, "All cards must be consumed by either the opponent hand or talon by now"
1003
1004        return full_state

The perspective a player has on the state of the game. This only gives access to the partially observable information. The Bot gets passed an instance of this class when it gets requested a move by the GamePlayEngine

This class has several convenience methods to get more information about the current state.

Parameters
  • state: (GameState): The current state of the game
  • engine: (GamePlayEngine): The engine which is used to play the game5 :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.5 :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
@abstractmethod
def valid_moves(self) -> list[Move]:
757    @abstractmethod
758    def valid_moves(self) -> list[Move]:
759        """
760        Get a list of all valid moves the bot can play at this point in the game.
761
762        Design note: this could also return an Iterable[Move], but list[Move] was chosen to make the API easier to use.
763        """

Get a list of all valid moves the bot can play at this point in the game.

Design note: this could also return an Iterable[Move], but list[Move] was chosen to make the API easier to use.

def get_game_history( self) -> list[tuple[PlayerPerspective, typing.Optional[Trick]]]:
765    def get_game_history(self) -> list[tuple[PlayerPerspective, Optional[Trick]]]:
766        """
767        The game history from the perspective of the player. This means all the past PlayerPerspective this bot has seen, and the Tricks played.
768        This only provides access to cards the Bot is allowed to see.
769
770        :returns: (list[tuple[PlayerPerspective, Optional[Trick]]]): The PlayerPerspective and Tricks in chronological order, index 0 is the first round played. Only the last Trick will be None.
771        The last pair will contain the current PlayerGameState.
772        """
773
774        # We reconstruct the history backwards.
775        game_state_history: list[tuple[PlayerPerspective, Optional[Trick]]] = []
776        # We first push the current state to the end
777        game_state_history.insert(0, (self, None))
778
779        current_leader = self.am_i_leader()
780        current = self.__game_state.previous
781
782        while current:
783            # If we were leader, and we remained, then we were leader before
784            # If we were follower, and we remained, then we were follower before
785            # If we were leader, and we did not remain, then we were follower before
786            # If we were follower, and we did not remain, then we were leader before
787            # This logic gets reflected by the negation of a xor
788            current_leader = not current_leader ^ current.leader_remained_leader
789
790            current_player_perspective: PlayerPerspective
791            if current_leader:
792                current_player_perspective = LeaderPerspective(current.state, self.__engine)
793            else:  # We are following
794                if current.trick.is_trump_exchange():
795                    current_player_perspective = ExchangeFollowerPerspective(current.state, self.__engine)
796                else:
797                    current_player_perspective = FollowerPerspective(current.state, self.__engine, current.trick.as_partial().leader_move)
798            history_record = (current_player_perspective, current.trick)
799            game_state_history.insert(0, history_record)
800
801            current = current.state.previous
802        return game_state_history

The game history from the perspective of the player. This means all the past PlayerPerspective this bot has seen, and the Tricks played. This only provides access to cards the Bot is allowed to see.

:returns: (list[tuple[PlayerPerspective, Optional[Trick]]]): The PlayerPerspective and Tricks in chronological order, index 0 is the first round played. Only the last Trick will be None. The last pair will contain the current PlayerGameState.

@abstractmethod
def get_hand(self) -> Hand:
804    @abstractmethod
805    def get_hand(self) -> Hand:
806        """Get the cards in the hand of the current player"""

Get the cards in the hand of the current player

@abstractmethod
def get_my_score(self) -> Score:
808    @abstractmethod
809    def get_my_score(self) -> Score:
810        """Get the socre of the current player. The return Score object contains both the direct points and pending points from a marriage."""

Get the socre of the current player. The return Score object contains both the direct points and pending points from a marriage.

@abstractmethod
def get_opponent_score(self) -> Score:
812    @abstractmethod
813    def get_opponent_score(self) -> Score:
814        """Get the socre of the other player. The return Score object contains both the direct points and pending points from a marriage."""

Get the socre of the other player. The return Score object contains both the direct points and pending points from a marriage.

def get_trump_suit(self) -> schnapsen.deck.Suit:
816    def get_trump_suit(self) -> Suit:
817        """Get the suit of the trump"""
818        return self.__game_state.trump_suit

Get the suit of the trump

def get_trump_card(self) -> Optional[schnapsen.deck.Card]:
820    def get_trump_card(self) -> Optional[Card]:
821        """Get the card which is at the bottom of the talon. Will be None if the talon is empty"""
822        return self.__game_state.talon.trump_card()

Get the card which is at the bottom of the talon. Will be None if the talon is empty

def get_talon_size(self) -> int:
824    def get_talon_size(self) -> int:
825        """How many cards are still on the talon?"""
826        return len(self.__game_state.talon)

How many cards are still on the talon?

def get_phase(self) -> GamePhase:
828    def get_phase(self) -> GamePhase:
829        """What is the pahse of the game? This returns a GamePhase object.
830        You can check the phase by checking state.get_phase == GamePhase.ONE
831        """
832        return self.__game_state.game_phase()

What is the pahse of the game? This returns a GamePhase object. You can check the phase by checking state.get_phase == GamePhase.ONE

@abstractmethod
def get_opponent_hand_in_phase_two(self) -> Hand:
834    @abstractmethod
835    def get_opponent_hand_in_phase_two(self) -> Hand:
836        """If the game is in the second phase, you can get the cards in the hand of the opponent.
837        If this gets called, but the second pahse has not started yet, this will throw en Exception
838        """

If the game is in the second phase, you can get the cards in the hand of the opponent. If this gets called, but the second pahse has not started yet, this will throw en Exception

@abstractmethod
def am_i_leader(self) -> bool:
840    @abstractmethod
841    def am_i_leader(self) -> bool:
842        """Returns True if the bot is the leader of this trick, False if it is a follower."""

Returns True if the bot is the leader of this trick, False if it is a follower.

@abstractmethod
def get_won_cards(self) -> schnapsen.deck.CardCollection:
844    @abstractmethod
845    def get_won_cards(self) -> CardCollection:
846        """Get a list of all cards this Bot has won until now."""

Get a list of all cards this Bot has won until now.

@abstractmethod
def get_opponent_won_cards(self) -> schnapsen.deck.CardCollection:
848    @abstractmethod
849    def get_opponent_won_cards(self) -> CardCollection:
850        """Get the list of cards the opponent has won until now."""

Get the list of cards the opponent has won until now.

def seen_cards( self, leader_move: Optional[Move]) -> schnapsen.deck.CardCollection:
870    def seen_cards(self, leader_move: Optional[Move]) -> CardCollection:
871        """Get a list of all cards your bot has seen until now
872
873        :param leader_move: (Optional[Move]):The move made by the leader of the trick. These cards have also been seen until now.
874        :returns: (CardCollection): A list of all cards your bot has seen until now
875        """
876        bot = self.__get_own_bot_state()
877
878        seen_cards: set[Card] = set()  # We make it a set to remove duplicates
879
880        # in own hand
881        seen_cards.update(bot.hand)
882
883        # the trump card
884        trump = self.get_trump_card()
885        if trump:
886            seen_cards.add(trump)
887
888        # all cards which were played in Tricks (icludes marriages and Trump exchanges)
889
890        seen_cards.update(self.__past_tricks_cards())
891        if leader_move is not None:
892            seen_cards.update(leader_move.cards)
893
894        return OrderedCardCollection(seen_cards)

Get a list of all cards your bot has seen until now

Parameters
  • leader_move: (Optional[Move]): The move made by the leader of the trick. These cards have also been seen until now. :returns: (CardCollection): A list of all cards your bot has seen until now
def get_known_cards_of_opponent_hand(self) -> schnapsen.deck.CardCollection:
909    def get_known_cards_of_opponent_hand(self) -> CardCollection:
910        """Get all cards which are in the opponents hand, but known to your Bot. This includes cards earlier used in marriages, or a trump exchange.
911        All cards in the second pahse of the game.
912
913        :returns: (CardCollection): A list of all cards which are in the opponents hand, which are known to the bot.
914        """
915        opponent_hand = self.__get_opponent_bot_state().hand
916        if self.get_phase() == GamePhase.TWO:
917            return opponent_hand
918        # We only disclose cards which have been part of a move, i.e., an Exchange or a Marriage
919        past_trick_cards = self.__past_tricks_cards()
920        return OrderedCardCollection(filter(lambda c: c in past_trick_cards, opponent_hand))

Get all cards which are in the opponents hand, but known to your Bot. This includes cards earlier used in marriages, or a trump exchange. All cards in the second pahse of the game.

:returns: (CardCollection): A list of all cards which are in the opponents hand, which are known to the bot.

def get_engine(self) -> GamePlayEngine:
922    def get_engine(self) -> GamePlayEngine:
923        """
924        Get the GamePlayEngine in use for the current game.
925        This engine can be used to retrieve all information about what kind of game we are playing,
926        but can also be used to simulate alternative game rollouts.
927
928        :returns: (GamePlayEngine): The GamePlayEngine in use for the current game.
929        """
930        return self.__engine

Get the GamePlayEngine in use for the current game. This engine can be used to retrieve all information about what kind of game we are playing, but can also be used to simulate alternative game rollouts.

:returns: (GamePlayEngine): The GamePlayEngine in use for the current game.

def get_state_in_phase_two(self) -> GameState:
932    def get_state_in_phase_two(self) -> GameState:
933        """
934        In phase TWO of the game, all information is known, so you can get the complete state
935
936        This removes the real bots from the GameState. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.
937
938        :retruns: (GameState): The GameState in phase two of the game - the active bots are replaced by dummy bots.
939        """
940
941        if self.get_phase() == GamePhase.TWO:
942            return self.__game_state.copy_with_other_bots(_DummyBot(), _DummyBot())
943        raise AssertionError("You cannot get the state in phase one")

In phase TWO of the game, all information is known, so you can get the complete state

This removes the real bots from the GameState. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.

:retruns: (GameState): The GameState in phase two of the game - the active bots are replaced by dummy bots.

def make_assumption( self, leader_move: Optional[Move], rand: random.Random) -> GameState:
 945    def make_assumption(self, leader_move: Optional[Move], rand: Random) -> GameState:
 946        """
 947        Takes the current imperfect information state and makes a random guess as to the position of the unknown cards.
 948        This also takes into account cards seen earlier during marriages played by the opponent, as well as potential trump jack exchanges
 949
 950        This removes the real bots from the GameState. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.
 951
 952        :param leader_move: (Optional[Move]): the optional already executed leader_move in the current trick. This card is guaranteed to be in the hand of the leader in the returned GameState.
 953        :param rand: (Random):the source of random numbers to do the random assignment of unknown cards
 954
 955        :returns: GameState: A perfect information state object.
 956        """
 957        opponent_hand = self.__get_opponent_bot_state().hand.copy()
 958
 959        if leader_move is not None:
 960            assert all(card in opponent_hand for card in leader_move.cards), f"The specified leader_move {leader_move} is not in the hand of the opponent {opponent_hand}"
 961
 962        full_state = self.__game_state.copy_with_other_bots(_DummyBot(), _DummyBot())
 963        if self.get_phase() == GamePhase.TWO:
 964            return full_state
 965
 966        seen_cards = self.seen_cards(leader_move)
 967        full_deck = self.__engine.deck_generator.get_initial_deck()
 968
 969        opponent_hand = self.__get_opponent_bot_state().hand.copy()
 970        unseen_opponent_hand = list(filter(lambda card: card not in seen_cards, opponent_hand))
 971
 972        talon = full_state.talon
 973        unseen_talon = list(filter(lambda card: card not in seen_cards, talon))
 974
 975        unseen_cards = list(filter(lambda card: card not in seen_cards, full_deck))
 976        if len(unseen_cards) > 1:
 977            rand.shuffle(unseen_cards)
 978
 979        assert len(unseen_talon) + len(unseen_opponent_hand) == len(unseen_cards), "Logical error. The number of unseen cards in the opponents hand and in the talon must be equal to the number of unseen cards"
 980
 981        new_talon: list[Card] = []
 982        for card in talon:
 983            if card in unseen_talon:
 984                # take one of the random cards
 985                new_talon.append(unseen_cards.pop())
 986            else:
 987                new_talon.append(card)
 988
 989        full_state.talon = Talon(new_talon)
 990
 991        new_opponent_hand = []
 992        for card in opponent_hand:
 993            if card in unseen_opponent_hand:
 994                new_opponent_hand.append(unseen_cards.pop())
 995            else:
 996                new_opponent_hand.append(card)
 997        if self.am_i_leader():
 998            full_state.follower.hand = Hand(new_opponent_hand)
 999        else:
1000            full_state.leader.hand = Hand(new_opponent_hand)
1001
1002        assert len(unseen_cards) == 0, "All cards must be consumed by either the opponent hand or talon by now"
1003
1004        return full_state

Takes the current imperfect information state and makes a random guess as to the position of the unknown cards. This also takes into account cards seen earlier during marriages played by the opponent, as well as potential trump jack exchanges

This removes the real bots from the GameState. If you want to continue the game, provide new Bots. See copy_with_other_bots in the GameState class.

Parameters
  • leader_move: (Optional[Move]): the optional already executed leader_move in the current trick. This card is guaranteed to be in the hand of the leader in the returned GameState.
  • rand: (Random): the source of random numbers to do the random assignment of unknown cards

:returns: GameState: A perfect information state object.

class LeaderPerspective(PlayerPerspective):
1020class LeaderPerspective(PlayerPerspective):
1021    """
1022    The playerperspective of the Leader.
1023
1024    :param state: (GameState): The current state of the game
1025    :param engine: (GamePlayEngine): The engine which is used to play the game
1026    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.
1027    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
1028    """
1029
1030    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1031        super().__init__(state, engine)
1032        self.__game_state = state
1033        self.__engine = engine
1034
1035    def valid_moves(self) -> list[Move]:
1036        """
1037        Get a list of all valid moves the bot can play at this point in the game.
1038
1039        :returns: (list[Move]): A list of all valid moves the bot can play at this point in the game.
1040        """
1041        moves = self.__engine.move_validator.get_legal_leader_moves(self.__engine, self.__game_state)
1042        return list(moves)
1043
1044    def get_hand(self) -> Hand:
1045        """
1046        Get the cards in the hand of the leader
1047
1048        :returns: (Hand): The cards in the hand of the leader
1049        """
1050        return self.__game_state.leader.hand.copy()
1051
1052    def get_my_score(self) -> Score:
1053        """
1054        Get the score of the leader
1055
1056        :returns: (Score): The score of the leader
1057        """
1058        return self.__game_state.leader.score
1059
1060    def get_opponent_score(self) -> Score:
1061        """
1062        Get the score of the follower
1063        """
1064        return self.__game_state.follower.score
1065
1066    def get_opponent_hand_in_phase_two(self) -> Hand:
1067        """
1068        Get the cards in the hand of the follower. This is only allowed in the second phase of the game.
1069
1070        :returns: (Hand): The cards in the hand of the follower
1071        """
1072        assert self.get_phase() == GamePhase.TWO, "Cannot get the hand of the opponent in pahse one"
1073        return self.__game_state.follower.hand.copy()
1074
1075    def am_i_leader(self) -> bool:
1076        """
1077        Returns True because this is the leader perspective
1078        """
1079        return True
1080
1081    def get_won_cards(self) -> CardCollection:
1082        """
1083        Get a list of all tricks the leader has won until now.
1084
1085        :returns: (CardCollection): A CardCollection of all tricks the leader has won until now.
1086        """
1087        return OrderedCardCollection(self.__game_state.leader.won_cards)
1088
1089    def get_opponent_won_cards(self) -> CardCollection:
1090        """
1091        Get a list of all tricks the follower has won until now.
1092
1093        :returns: (CardCollection): A CardCollection of all tricks the follower has won until now.
1094        """
1095
1096        return OrderedCardCollection(self.__game_state.follower.won_cards)
1097
1098    def __repr__(self) -> str:
1099        return f"LeaderPerspective(state={self.__game_state}, engine={self.__engine})"

The playerperspective of the Leader.

Parameters
  • state: (GameState): The current state of the game
  • engine: (GamePlayEngine): The engine which is used to play the game :attr __game_state: (GameState): The current state of the game - initialized from the state parameter. :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
LeaderPerspective( state: GameState, engine: GamePlayEngine)
1030    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1031        super().__init__(state, engine)
1032        self.__game_state = state
1033        self.__engine = engine
def valid_moves(self) -> list[Move]:
1035    def valid_moves(self) -> list[Move]:
1036        """
1037        Get a list of all valid moves the bot can play at this point in the game.
1038
1039        :returns: (list[Move]): A list of all valid moves the bot can play at this point in the game.
1040        """
1041        moves = self.__engine.move_validator.get_legal_leader_moves(self.__engine, self.__game_state)
1042        return list(moves)

Get a list of all valid moves the bot can play at this point in the game.

:returns: (list[Move]): A list of all valid moves the bot can play at this point in the game.

def get_hand(self) -> Hand:
1044    def get_hand(self) -> Hand:
1045        """
1046        Get the cards in the hand of the leader
1047
1048        :returns: (Hand): The cards in the hand of the leader
1049        """
1050        return self.__game_state.leader.hand.copy()

Get the cards in the hand of the leader

:returns: (Hand): The cards in the hand of the leader

def get_my_score(self) -> Score:
1052    def get_my_score(self) -> Score:
1053        """
1054        Get the score of the leader
1055
1056        :returns: (Score): The score of the leader
1057        """
1058        return self.__game_state.leader.score

Get the score of the leader

:returns: (Score): The score of the leader

def get_opponent_score(self) -> Score:
1060    def get_opponent_score(self) -> Score:
1061        """
1062        Get the score of the follower
1063        """
1064        return self.__game_state.follower.score

Get the score of the follower

def get_opponent_hand_in_phase_two(self) -> Hand:
1066    def get_opponent_hand_in_phase_two(self) -> Hand:
1067        """
1068        Get the cards in the hand of the follower. This is only allowed in the second phase of the game.
1069
1070        :returns: (Hand): The cards in the hand of the follower
1071        """
1072        assert self.get_phase() == GamePhase.TWO, "Cannot get the hand of the opponent in pahse one"
1073        return self.__game_state.follower.hand.copy()

Get the cards in the hand of the follower. This is only allowed in the second phase of the game.

:returns: (Hand): The cards in the hand of the follower

def am_i_leader(self) -> bool:
1075    def am_i_leader(self) -> bool:
1076        """
1077        Returns True because this is the leader perspective
1078        """
1079        return True

Returns True because this is the leader perspective

def get_won_cards(self) -> schnapsen.deck.CardCollection:
1081    def get_won_cards(self) -> CardCollection:
1082        """
1083        Get a list of all tricks the leader has won until now.
1084
1085        :returns: (CardCollection): A CardCollection of all tricks the leader has won until now.
1086        """
1087        return OrderedCardCollection(self.__game_state.leader.won_cards)

Get a list of all tricks the leader has won until now.

:returns: (CardCollection): A CardCollection of all tricks the leader has won until now.

def get_opponent_won_cards(self) -> schnapsen.deck.CardCollection:
1089    def get_opponent_won_cards(self) -> CardCollection:
1090        """
1091        Get a list of all tricks the follower has won until now.
1092
1093        :returns: (CardCollection): A CardCollection of all tricks the follower has won until now.
1094        """
1095
1096        return OrderedCardCollection(self.__game_state.follower.won_cards)

Get a list of all tricks the follower has won until now.

:returns: (CardCollection): A CardCollection of all tricks the follower has won until now.

class FollowerPerspective(PlayerPerspective):
1102class FollowerPerspective(PlayerPerspective):
1103    """
1104    The playerperspective of the Follower.
1105
1106    :param state: (GameState): The current state of the game
1107    :param engine: (GamePlayEngine): The engine which is used to play the game
1108    :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
1109    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.
1110    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
1111    :attr __leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
1112    """
1113
1114    def __init__(self, state: GameState, engine: GamePlayEngine, leader_move: Optional[Move]) -> None:
1115        super().__init__(state, engine)
1116        self.__game_state = state
1117        self.__engine = engine
1118        self.__leader_move = leader_move
1119
1120    def valid_moves(self) -> list[Move]:
1121        """
1122        Get a list of all valid moves the bot can play at this point in the game.
1123
1124        :returns: (list[Move]): A list of all valid moves the bot can play at this point in the game.
1125        """
1126
1127        assert self.__leader_move, "There is no leader move for this follower, so no valid moves."
1128        return list(self.__engine.move_validator.get_legal_follower_moves(self.__engine, self.__game_state, self.__leader_move))
1129
1130    def get_hand(self) -> Hand:
1131        """
1132        Get the cards in the hand of the follower
1133
1134        :returns: (Hand): The cards in the hand of the follower
1135        """
1136        return self.__game_state.follower.hand.copy()
1137
1138    def get_my_score(self) -> Score:
1139        """
1140        Get the score of the follower
1141
1142        :returns: (Score): The score of the follower
1143        """
1144        return self.__game_state.follower.score
1145
1146    def get_opponent_score(self) -> Score:
1147        """
1148        Get the score of the leader
1149
1150        :returns: (Score): The score of the leader
1151        """
1152
1153        return self.__game_state.leader.score
1154
1155    def get_opponent_hand_in_phase_two(self) -> Hand:
1156        """
1157        Get the cards in the hand of the leader. This is only allowed in the second phase of the game.
1158
1159        :returns: (Hand): The cards in the hand of the leader
1160        """
1161
1162        assert self.get_phase() == GamePhase.TWO, "Cannot get the hand of the opponent in pahse one"
1163        return self.__game_state.leader.hand.copy()
1164
1165    def am_i_leader(self) -> bool:
1166        """ Returns False because this is the follower perspective"""
1167        return False
1168
1169    def get_won_cards(self) -> CardCollection:
1170        """
1171        Get a list of all tricks the follower has won until now.
1172
1173        :returns: (CardCollection): A CardCollection of all tricks the follower has won until now.
1174        """
1175
1176        return OrderedCardCollection(self.__game_state.follower.won_cards)
1177
1178    def get_opponent_won_cards(self) -> CardCollection:
1179        """
1180        Get a list of all tricks the leader has won until now.
1181
1182        :returns: (CardCollection): A CardCollection of all tricks the leader has won until now.
1183        """
1184
1185        return OrderedCardCollection(self.__game_state.leader.won_cards)
1186
1187    def __repr__(self) -> str:
1188        return f"FollowerPerspective(state={self.__game_state}, engine={self.__engine}, leader_move={self.__leader_move})"

The playerperspective of the Follower.

Parameters
  • state: (GameState): The current state of the game
  • engine: (GamePlayEngine): The engine which is used to play the game
  • leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader. :attr __game_state: (GameState): The current state of the game - initialized from the state parameter. :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter. :attr __leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
FollowerPerspective( state: GameState, engine: GamePlayEngine, leader_move: Optional[Move])
1114    def __init__(self, state: GameState, engine: GamePlayEngine, leader_move: Optional[Move]) -> None:
1115        super().__init__(state, engine)
1116        self.__game_state = state
1117        self.__engine = engine
1118        self.__leader_move = leader_move
def valid_moves(self) -> list[Move]:
1120    def valid_moves(self) -> list[Move]:
1121        """
1122        Get a list of all valid moves the bot can play at this point in the game.
1123
1124        :returns: (list[Move]): A list of all valid moves the bot can play at this point in the game.
1125        """
1126
1127        assert self.__leader_move, "There is no leader move for this follower, so no valid moves."
1128        return list(self.__engine.move_validator.get_legal_follower_moves(self.__engine, self.__game_state, self.__leader_move))

Get a list of all valid moves the bot can play at this point in the game.

:returns: (list[Move]): A list of all valid moves the bot can play at this point in the game.

def get_hand(self) -> Hand:
1130    def get_hand(self) -> Hand:
1131        """
1132        Get the cards in the hand of the follower
1133
1134        :returns: (Hand): The cards in the hand of the follower
1135        """
1136        return self.__game_state.follower.hand.copy()

Get the cards in the hand of the follower

:returns: (Hand): The cards in the hand of the follower

def get_my_score(self) -> Score:
1138    def get_my_score(self) -> Score:
1139        """
1140        Get the score of the follower
1141
1142        :returns: (Score): The score of the follower
1143        """
1144        return self.__game_state.follower.score

Get the score of the follower

:returns: (Score): The score of the follower

def get_opponent_score(self) -> Score:
1146    def get_opponent_score(self) -> Score:
1147        """
1148        Get the score of the leader
1149
1150        :returns: (Score): The score of the leader
1151        """
1152
1153        return self.__game_state.leader.score

Get the score of the leader

:returns: (Score): The score of the leader

def get_opponent_hand_in_phase_two(self) -> Hand:
1155    def get_opponent_hand_in_phase_two(self) -> Hand:
1156        """
1157        Get the cards in the hand of the leader. This is only allowed in the second phase of the game.
1158
1159        :returns: (Hand): The cards in the hand of the leader
1160        """
1161
1162        assert self.get_phase() == GamePhase.TWO, "Cannot get the hand of the opponent in pahse one"
1163        return self.__game_state.leader.hand.copy()

Get the cards in the hand of the leader. This is only allowed in the second phase of the game.

:returns: (Hand): The cards in the hand of the leader

def am_i_leader(self) -> bool:
1165    def am_i_leader(self) -> bool:
1166        """ Returns False because this is the follower perspective"""
1167        return False

Returns False because this is the follower perspective

def get_won_cards(self) -> schnapsen.deck.CardCollection:
1169    def get_won_cards(self) -> CardCollection:
1170        """
1171        Get a list of all tricks the follower has won until now.
1172
1173        :returns: (CardCollection): A CardCollection of all tricks the follower has won until now.
1174        """
1175
1176        return OrderedCardCollection(self.__game_state.follower.won_cards)

Get a list of all tricks the follower has won until now.

:returns: (CardCollection): A CardCollection of all tricks the follower has won until now.

def get_opponent_won_cards(self) -> schnapsen.deck.CardCollection:
1178    def get_opponent_won_cards(self) -> CardCollection:
1179        """
1180        Get a list of all tricks the leader has won until now.
1181
1182        :returns: (CardCollection): A CardCollection of all tricks the leader has won until now.
1183        """
1184
1185        return OrderedCardCollection(self.__game_state.leader.won_cards)

Get a list of all tricks the leader has won until now.

:returns: (CardCollection): A CardCollection of all tricks the leader has won until now.

class ExchangeFollowerPerspective(PlayerPerspective):
1191class ExchangeFollowerPerspective(PlayerPerspective):
1192    """
1193    A special PlayerGameState only used for the history of a game in which a Trump Exchange happened.
1194    This state is does not allow any moves.
1195
1196    :param state: (GameState): The current state of the game
1197    :param engine: (GamePlayEngine): The engine which is used to play the game
1198    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.
1199    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
1200
1201    """
1202
1203    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1204        self.__game_state = state
1205        super().__init__(state, engine)
1206
1207    def valid_moves(self) -> list[Move]:
1208        """
1209        Get a list of all valid moves the bot can play at this point in the game.
1210
1211        Design note: this could also return an Iterable[Move], but list[Move] was chosen to make the API easier to use.
1212
1213        :returns: (list[Move]): An empty list, because no moves are allowed in this state.
1214        """
1215        return []
1216
1217    def get_hand(self) -> Hand:
1218        """
1219        Get the cards in the hand of the follower
1220
1221        :returns: (Hand): The cards in the hand of the follower
1222        """
1223
1224        return self.__game_state.follower.hand.copy()
1225
1226    def get_my_score(self) -> Score:
1227        """
1228        Get the score of the follower
1229
1230        :returns: (Score): The score of the follower
1231        """
1232
1233        return self.__game_state.follower.score
1234
1235    def get_opponent_score(self) -> Score:
1236        """
1237        Get the score of the leader
1238
1239        :returns: (Score): The score of the leader
1240        """
1241
1242        return self.__game_state.leader.score
1243
1244    def get_trump_suit(self) -> Suit:
1245        """
1246        Get the suit of the trump
1247
1248        :returns: (Suit): The suit of the trump
1249        """
1250        return self.__game_state.trump_suit
1251
1252    def get_opponent_hand_in_phase_two(self) -> Hand:
1253        """
1254        Get the cards in the hand of the leader. This is only allowed in the second phase of the game.
1255
1256        :returns: (Hand): The cards in the hand of the leader
1257        """
1258        assert self.get_phase() == GamePhase.TWO, "Cannot get the hand of the opponent in pahse one"
1259        return self.__game_state.leader.hand.copy()
1260
1261    def get_opponent_won_cards(self) -> CardCollection:
1262        """
1263        Get a list of all tricks the leader has won until now.
1264
1265        :returns: (CardCollection): A CardCollection of all tricks the leader has won until now.
1266        """
1267        return OrderedCardCollection(self.__game_state.leader.won_cards)
1268
1269    def get_won_cards(self) -> CardCollection:
1270        """
1271        Get a list of all tricks the follower has won until now.
1272
1273        :returns: (CardCollection): A CardCollection of all tricks the follower has won until now.
1274        """
1275
1276        return OrderedCardCollection(self.__game_state.follower.won_cards)
1277
1278    def am_i_leader(self) -> bool:
1279        """ Returns False because this is the follower perspective"""
1280        return False

A special PlayerGameState only used for the history of a game in which a Trump Exchange happened. This state is does not allow any moves.

Parameters
  • state: (GameState): The current state of the game
  • engine: (GamePlayEngine): The engine which is used to play the game :attr __game_state: (GameState): The current state of the game - initialized from the state parameter. :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
ExchangeFollowerPerspective( state: GameState, engine: GamePlayEngine)
1203    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1204        self.__game_state = state
1205        super().__init__(state, engine)
def valid_moves(self) -> list[Move]:
1207    def valid_moves(self) -> list[Move]:
1208        """
1209        Get a list of all valid moves the bot can play at this point in the game.
1210
1211        Design note: this could also return an Iterable[Move], but list[Move] was chosen to make the API easier to use.
1212
1213        :returns: (list[Move]): An empty list, because no moves are allowed in this state.
1214        """
1215        return []

Get a list of all valid moves the bot can play at this point in the game.

Design note: this could also return an Iterable[Move], but list[Move] was chosen to make the API easier to use.

:returns: (list[Move]): An empty list, because no moves are allowed in this state.

def get_hand(self) -> Hand:
1217    def get_hand(self) -> Hand:
1218        """
1219        Get the cards in the hand of the follower
1220
1221        :returns: (Hand): The cards in the hand of the follower
1222        """
1223
1224        return self.__game_state.follower.hand.copy()

Get the cards in the hand of the follower

:returns: (Hand): The cards in the hand of the follower

def get_my_score(self) -> Score:
1226    def get_my_score(self) -> Score:
1227        """
1228        Get the score of the follower
1229
1230        :returns: (Score): The score of the follower
1231        """
1232
1233        return self.__game_state.follower.score

Get the score of the follower

:returns: (Score): The score of the follower

def get_opponent_score(self) -> Score:
1235    def get_opponent_score(self) -> Score:
1236        """
1237        Get the score of the leader
1238
1239        :returns: (Score): The score of the leader
1240        """
1241
1242        return self.__game_state.leader.score

Get the score of the leader

:returns: (Score): The score of the leader

def get_trump_suit(self) -> schnapsen.deck.Suit:
1244    def get_trump_suit(self) -> Suit:
1245        """
1246        Get the suit of the trump
1247
1248        :returns: (Suit): The suit of the trump
1249        """
1250        return self.__game_state.trump_suit

Get the suit of the trump

:returns: (Suit): The suit of the trump

def get_opponent_hand_in_phase_two(self) -> Hand:
1252    def get_opponent_hand_in_phase_two(self) -> Hand:
1253        """
1254        Get the cards in the hand of the leader. This is only allowed in the second phase of the game.
1255
1256        :returns: (Hand): The cards in the hand of the leader
1257        """
1258        assert self.get_phase() == GamePhase.TWO, "Cannot get the hand of the opponent in pahse one"
1259        return self.__game_state.leader.hand.copy()

Get the cards in the hand of the leader. This is only allowed in the second phase of the game.

:returns: (Hand): The cards in the hand of the leader

def get_opponent_won_cards(self) -> schnapsen.deck.CardCollection:
1261    def get_opponent_won_cards(self) -> CardCollection:
1262        """
1263        Get a list of all tricks the leader has won until now.
1264
1265        :returns: (CardCollection): A CardCollection of all tricks the leader has won until now.
1266        """
1267        return OrderedCardCollection(self.__game_state.leader.won_cards)

Get a list of all tricks the leader has won until now.

:returns: (CardCollection): A CardCollection of all tricks the leader has won until now.

def get_won_cards(self) -> schnapsen.deck.CardCollection:
1269    def get_won_cards(self) -> CardCollection:
1270        """
1271        Get a list of all tricks the follower has won until now.
1272
1273        :returns: (CardCollection): A CardCollection of all tricks the follower has won until now.
1274        """
1275
1276        return OrderedCardCollection(self.__game_state.follower.won_cards)

Get a list of all tricks the follower has won until now.

:returns: (CardCollection): A CardCollection of all tricks the follower has won until now.

def am_i_leader(self) -> bool:
1278    def am_i_leader(self) -> bool:
1279        """ Returns False because this is the follower perspective"""
1280        return False

Returns False because this is the follower perspective

class WinnerPerspective(LeaderPerspective):
1283class WinnerPerspective(LeaderPerspective):
1284    """
1285    The gamestate given to the winner of the game at the very end
1286
1287    :param state: (GameState): The current state of the game
1288    :param engine: (GamePlayEngine): The engine which is used to play the game
1289    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.
1290    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
1291    """
1292
1293    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1294        self.__game_state = state
1295        self.__engine = engine
1296        super().__init__(state, engine)
1297
1298    def valid_moves(self) -> list[Move]:
1299        """raise an Exception because the game is over"""
1300        raise Exception("Cannot request valid moves from the final PlayerGameState because the game is over")
1301
1302    def __repr__(self) -> str:
1303        return f"WinnerGameState(state={self.__game_state}, engine={self.__engine})"

The gamestate given to the winner of the game at the very end

Parameters
  • state: (GameState): The current state of the game
  • engine: (GamePlayEngine): The engine which is used to play the game :attr __game_state: (GameState): The current state of the game - initialized from the state parameter. :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
WinnerPerspective( state: GameState, engine: GamePlayEngine)
1293    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1294        self.__game_state = state
1295        self.__engine = engine
1296        super().__init__(state, engine)
def valid_moves(self) -> list[Move]:
1298    def valid_moves(self) -> list[Move]:
1299        """raise an Exception because the game is over"""
1300        raise Exception("Cannot request valid moves from the final PlayerGameState because the game is over")

raise an Exception because the game is over

class LoserPerspective(FollowerPerspective):
1306class LoserPerspective(FollowerPerspective):
1307    """
1308    The gamestate given to the loser of the game at the very end
1309
1310    :param state: (GameState): The current state of the game
1311    :param engine: (GamePlayEngine): The engine which is used to play the game
1312    :attr __game_state: (GameState): The current state of the game - initialized from the state parameter.
1313    :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
1314    """
1315
1316    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1317        self.__game_state = state
1318        self.__engine = engine
1319        super().__init__(state, engine, None)
1320
1321    def valid_moves(self) -> list[Move]:
1322        """raise an Exception because the game is over"""
1323        raise Exception("Cannot request valid moves from the final PlayerGameState because the game is over")
1324
1325    def __repr__(self) -> str:
1326        return f"LoserGameState(state={self.__game_state}, engine={self.__engine})"

The gamestate given to the loser of the game at the very end

Parameters
  • state: (GameState): The current state of the game
  • engine: (GamePlayEngine): The engine which is used to play the game :attr __game_state: (GameState): The current state of the game - initialized from the state parameter. :attr __engine: (GamePlayEngine): The engine which is used to play the game - initialized from the engine parameter.
LoserPerspective( state: GameState, engine: GamePlayEngine)
1316    def __init__(self, state: GameState, engine: GamePlayEngine) -> None:
1317        self.__game_state = state
1318        self.__engine = engine
1319        super().__init__(state, engine, None)
def valid_moves(self) -> list[Move]:
1321    def valid_moves(self) -> list[Move]:
1322        """raise an Exception because the game is over"""
1323        raise Exception("Cannot request valid moves from the final PlayerGameState because the game is over")

raise an Exception because the game is over

class DeckGenerator(abc.ABC):
1329class DeckGenerator(ABC):
1330    """
1331    A Deckgenerator specifies how what the cards for a game are.
1332    """
1333
1334    @abstractmethod
1335    def get_initial_deck(self) -> OrderedCardCollection:
1336        """
1337        Get the intial deck of cards which are used in the game.
1338        This method must always return the same set of cards in the same order.
1339        """
1340
1341    @classmethod
1342    def shuffle_deck(cls, deck: OrderedCardCollection, rng: Random) -> OrderedCardCollection:
1343        """
1344        Shuffle the given deck of cards, using the random number generator as a source of randomness.
1345
1346        :param deck: (OrderedCardCollection): The deck to shuffle.
1347        :param rng: (Random): The source of randomness.
1348        :returns: (OrderedCardCollection): The shuffled deck.
1349        """
1350        the_cards = list(deck.get_cards())
1351        rng.shuffle(the_cards)
1352        return OrderedCardCollection(the_cards)

A Deckgenerator specifies how what the cards for a game are.

@abstractmethod
def get_initial_deck(self) -> schnapsen.deck.OrderedCardCollection:
1334    @abstractmethod
1335    def get_initial_deck(self) -> OrderedCardCollection:
1336        """
1337        Get the intial deck of cards which are used in the game.
1338        This method must always return the same set of cards in the same order.
1339        """

Get the intial deck of cards which are used in the game. This method must always return the same set of cards in the same order.

@classmethod
def shuffle_deck( cls, deck: schnapsen.deck.OrderedCardCollection, rng: random.Random) -> schnapsen.deck.OrderedCardCollection:
1341    @classmethod
1342    def shuffle_deck(cls, deck: OrderedCardCollection, rng: Random) -> OrderedCardCollection:
1343        """
1344        Shuffle the given deck of cards, using the random number generator as a source of randomness.
1345
1346        :param deck: (OrderedCardCollection): The deck to shuffle.
1347        :param rng: (Random): The source of randomness.
1348        :returns: (OrderedCardCollection): The shuffled deck.
1349        """
1350        the_cards = list(deck.get_cards())
1351        rng.shuffle(the_cards)
1352        return OrderedCardCollection(the_cards)

Shuffle the given deck of cards, using the random number generator as a source of randomness.

Parameters
  • deck: (OrderedCardCollection): The deck to shuffle.
  • rng: (Random): The source of randomness. :returns: (OrderedCardCollection): The shuffled deck.
class SchnapsenDeckGenerator(DeckGenerator):
1355class SchnapsenDeckGenerator(DeckGenerator):
1356    """
1357    The deck generator for the game of Schnapsen. This generator always creates the same deck of cards, in the same order.
1358
1359    :attr deck: (list[Card]): The deck of cards generated.
1360    """
1361
1362    def __init__(self) -> None:
1363        self.deck: list[Card] = []
1364        for suit in Suit:
1365            for rank in [Rank.JACK, Rank.QUEEN, Rank.KING, Rank.TEN, Rank.ACE]:
1366                self.deck.append(Card.get_card(rank, suit))
1367
1368    def get_initial_deck(self) -> OrderedCardCollection:
1369        """
1370        Get the intial deck of cards which are used in the game.
1371
1372        :returns: (OrderedCardCollection): The deck of cards used in the game.
1373        """
1374        return OrderedCardCollection(self.deck)

The deck generator for the game of Schnapsen. This generator always creates the same deck of cards, in the same order.

:attr deck: (list[Card]): The deck of cards generated.

deck: list[schnapsen.deck.Card]
def get_initial_deck(self) -> schnapsen.deck.OrderedCardCollection:
1368    def get_initial_deck(self) -> OrderedCardCollection:
1369        """
1370        Get the intial deck of cards which are used in the game.
1371
1372        :returns: (OrderedCardCollection): The deck of cards used in the game.
1373        """
1374        return OrderedCardCollection(self.deck)

Get the intial deck of cards which are used in the game.

:returns: (OrderedCardCollection): The deck of cards used in the game.

Inherited Members
DeckGenerator
shuffle_deck
class HandGenerator(abc.ABC):
1377class HandGenerator(ABC):
1378    """
1379    The HandGenerator specifies how the intial set of cards gets divided over the two player's hands and the talon
1380    """
1381    @abstractmethod
1382    def generateHands(self, cards: OrderedCardCollection) -> tuple[Hand, Hand, Talon]:
1383        """
1384        Divide the collection of cards over the two hands and the Talon
1385
1386        :param cards: The cards to be dealt
1387        :returns: Two hands of cards and the talon. The first hand is for the first player, i.e, the one who will lead the first trick.
1388        """

The HandGenerator specifies how the intial set of cards gets divided over the two player's hands and the talon

@abstractmethod
def generateHands( self, cards: schnapsen.deck.OrderedCardCollection) -> tuple[Hand, Hand, Talon]:
1381    @abstractmethod
1382    def generateHands(self, cards: OrderedCardCollection) -> tuple[Hand, Hand, Talon]:
1383        """
1384        Divide the collection of cards over the two hands and the Talon
1385
1386        :param cards: The cards to be dealt
1387        :returns: Two hands of cards and the talon. The first hand is for the first player, i.e, the one who will lead the first trick.
1388        """

Divide the collection of cards over the two hands and the Talon

Parameters
  • cards: The cards to be dealt :returns: Two hands of cards and the talon. The first hand is for the first player, i.e, the one who will lead the first trick.
class SchnapsenHandGenerator(HandGenerator):
1391class SchnapsenHandGenerator(HandGenerator):
1392    """Class used for generating the hands for the game of Schnapsen"""
1393    @classmethod
1394    def generateHands(self, cards: OrderedCardCollection) -> tuple[Hand, Hand, Talon]:
1395        """
1396        Divide the collection of cards over the two hands and the Talon
1397
1398        :param cards: (OrderedCardCollection): The cards to be dealt
1399        :returns: (tuple[Hand, Hand, Talon]): Two hands of cards and the talon. The first hand is for the first player, i.e, the one who will lead the first trick.
1400        """
1401
1402        the_cards = list(cards.get_cards())
1403        hand1 = Hand([the_cards[i] for i in range(0, 10, 2)], max_size=5)
1404        hand2 = Hand([the_cards[i] for i in range(1, 11, 2)], max_size=5)
1405        rest = Talon(the_cards[10:])
1406        return (hand1, hand2, rest)

Class used for generating the hands for the game of Schnapsen

@classmethod
def generateHands( self, cards: schnapsen.deck.OrderedCardCollection) -> tuple[Hand, Hand, Talon]:
1393    @classmethod
1394    def generateHands(self, cards: OrderedCardCollection) -> tuple[Hand, Hand, Talon]:
1395        """
1396        Divide the collection of cards over the two hands and the Talon
1397
1398        :param cards: (OrderedCardCollection): The cards to be dealt
1399        :returns: (tuple[Hand, Hand, Talon]): Two hands of cards and the talon. The first hand is for the first player, i.e, the one who will lead the first trick.
1400        """
1401
1402        the_cards = list(cards.get_cards())
1403        hand1 = Hand([the_cards[i] for i in range(0, 10, 2)], max_size=5)
1404        hand2 = Hand([the_cards[i] for i in range(1, 11, 2)], max_size=5)
1405        rest = Talon(the_cards[10:])
1406        return (hand1, hand2, rest)

Divide the collection of cards over the two hands and the Talon

Parameters
  • cards: (OrderedCardCollection): The cards to be dealt :returns: (tuple[Hand, Hand, Talon]): Two hands of cards and the talon. The first hand is for the first player, i.e, the one who will lead the first trick.
class TrickImplementer(abc.ABC):
1409class TrickImplementer(ABC):
1410    """
1411    The TrickImplementer is a blueprint for classes that specify how tricks are palyed in the game.
1412    """
1413    @abstractmethod
1414    def play_trick(self, game_engine: GamePlayEngine, game_state: GameState) -> GameState:
1415        """
1416        Plays a single Trick the game by asking the bots in the game_state for their Moves,
1417        using the MoveRequester from the game_engine.
1418        These moves are then also validated using the MoveValidator of the game_engine.
1419        Finally, the trick is recorder in the history (previous field) of the returned GameState.
1420
1421        Note, the provided GameState does not get modified by this method.
1422
1423        :param game_engine: The engine used to preform the underlying actions of the Trick.
1424        :param game_state: The state of the game before the trick is played. Thi state will not be modified.
1425        :returns: The GameState after the trick is completed.
1426        """
1427
1428    @abstractmethod
1429    def play_trick_with_fixed_leader_move(self, game_engine: GamePlayEngine, game_state: GameState,
1430                                          leader_move: Move) -> GameState:
1431        """
1432        The same as play_trick, but also takes the leader_move to start with as an argument.
1433        """

The TrickImplementer is a blueprint for classes that specify how tricks are palyed in the game.

@abstractmethod
def play_trick( self, game_engine: GamePlayEngine, game_state: GameState) -> GameState:
1413    @abstractmethod
1414    def play_trick(self, game_engine: GamePlayEngine, game_state: GameState) -> GameState:
1415        """
1416        Plays a single Trick the game by asking the bots in the game_state for their Moves,
1417        using the MoveRequester from the game_engine.
1418        These moves are then also validated using the MoveValidator of the game_engine.
1419        Finally, the trick is recorder in the history (previous field) of the returned GameState.
1420
1421        Note, the provided GameState does not get modified by this method.
1422
1423        :param game_engine: The engine used to preform the underlying actions of the Trick.
1424        :param game_state: The state of the game before the trick is played. Thi state will not be modified.
1425        :returns: The GameState after the trick is completed.
1426        """

Plays a single Trick the game by asking the bots in the game_state for their Moves, using the MoveRequester from the game_engine. These moves are then also validated using the MoveValidator of the game_engine. Finally, the trick is recorder in the history (previous field) of the returned GameState.

Note, the provided GameState does not get modified by this method.

Parameters
  • game_engine: The engine used to preform the underlying actions of the Trick.
  • game_state: The state of the game before the trick is played. Thi state will not be modified. :returns: The GameState after the trick is completed.
@abstractmethod
def play_trick_with_fixed_leader_move( self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move) -> GameState:
1428    @abstractmethod
1429    def play_trick_with_fixed_leader_move(self, game_engine: GamePlayEngine, game_state: GameState,
1430                                          leader_move: Move) -> GameState:
1431        """
1432        The same as play_trick, but also takes the leader_move to start with as an argument.
1433        """

The same as play_trick, but also takes the leader_move to start with as an argument.

class SchnapsenTrickImplementer(TrickImplementer):
1436class SchnapsenTrickImplementer(TrickImplementer):
1437    """
1438    Child of TrickImplementer, SchnapsenTrickImplementer specifies how tricks are played in the game.
1439    """
1440
1441    def play_trick(self, game_engine: GamePlayEngine, game_state: GameState) -> GameState:
1442        # TODO: Fix the docstring
1443        """
1444        Plays a single Trick the game by asking the bots in the game_state for their Moves,
1445        first asks the leader for their move, and then depending on the resulting gamestate
1446        in self.play_trick_with_fixed_leader_move, will ask (or not ask) the follower for a move.
1447
1448        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1449        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1450        :returns: (GameState): The GameState after the trick is completed.
1451        """
1452        leader_move = self.get_leader_move(game_engine, game_state)
1453        return self.play_trick_with_fixed_leader_move(game_engine=game_engine, game_state=game_state, leader_move=leader_move)
1454
1455    def play_trick_with_fixed_leader_move(self, game_engine: GamePlayEngine, game_state: GameState,
1456                                          leader_move: Move) -> GameState:
1457        """
1458        Plays a trick with a fixed leader move, in order to determine the follower move. Potentially asks the folower for a move.
1459
1460        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1461        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1462        :param leader_move: (Move): The move made by the leader of the trick.
1463        :returns: (GameState): The GameState after the trick is completed.
1464        """
1465        if leader_move.is_trump_exchange():
1466            next_game_state = game_state.copy_for_next()
1467            exchange = cast(TrumpExchange, leader_move)
1468            old_trump_card = game_state.talon.trump_card()
1469            assert old_trump_card, "There is no card at the bottom of the talon"
1470            self.play_trump_exchange(next_game_state, exchange)
1471            # remember the previous state
1472            next_game_state.previous = Previous(game_state, ExchangeTrick(exchange, old_trump_card), True)
1473            # The whole trick ends here.
1474            return next_game_state
1475
1476        # We have a PartialTrick, ask the follower for its move
1477        leader_move = cast(Union[Marriage, RegularMove], leader_move)
1478        follower_move = self.get_follower_move(game_engine, game_state, leader_move)
1479
1480        trick = RegularTrick(leader_move=leader_move, follower_move=follower_move)
1481        return self._apply_regular_trick(game_engine=game_engine, game_state=game_state, trick=trick)
1482
1483    def _apply_regular_trick(self, game_engine: GamePlayEngine, game_state: GameState, trick: RegularTrick) -> GameState:
1484        """
1485        Applies the given regular trick to the given game state, returning the resulting game state.
1486
1487        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1488        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1489        :param trick: (RegularTrick): The trick to be applied to the game state.
1490        :returns: (GameState): The GameState after the trick is completed.
1491        """
1492
1493        # apply the trick to the next_game_state
1494        # The next game state will be modified during this trick. We start from the previous state
1495        next_game_state = game_state.copy_for_next()
1496
1497        if trick.leader_move.is_marriage():
1498            marriage_move: Marriage = cast(Marriage, trick.leader_move)
1499            self._play_marriage(game_engine, next_game_state, marriage_move=marriage_move)
1500            regular_leader_move: RegularMove = cast(Marriage, trick.leader_move).underlying_regular_move()
1501        else:
1502            regular_leader_move = cast(RegularMove, trick.leader_move)
1503
1504        # # apply changes in the hand and talon
1505        next_game_state.leader.hand.remove(regular_leader_move.card)
1506        next_game_state.follower.hand.remove(trick.follower_move.card)
1507
1508        # We set the leader for the next state based on what the scorer decides
1509        next_game_state.leader, next_game_state.follower, leader_remained_leader = game_engine.trick_scorer.score(trick, next_game_state.leader, next_game_state.follower, next_game_state.trump_suit)
1510
1511        # important: the winner takes the first card of the talon, the loser the second one.
1512        # this also ensures that the loser of the last trick of the first phase gets the face up trump
1513        if not next_game_state.talon.is_empty():
1514            drawn = next_game_state.talon.draw_cards(2)
1515            next_game_state.leader.hand.add(drawn[0])
1516            next_game_state.follower.hand.add(drawn[1])
1517
1518        next_game_state.previous = Previous(game_state, trick=trick, leader_remained_leader=leader_remained_leader)
1519
1520        return next_game_state
1521
1522    def get_leader_move(self, game_engine: GamePlayEngine, game_state: GameState) -> Move:
1523        """
1524        Get the move of the leader of the trick.
1525
1526        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1527        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1528        :returns: (Move): The move of the leader of the trick.
1529        """
1530
1531        # ask first players move trough the requester
1532        leader_game_state = LeaderPerspective(game_state, game_engine)
1533        leader_move = game_engine.move_requester.get_move(game_state.leader, leader_game_state, None)
1534        if not game_engine.move_validator.is_legal_leader_move(game_engine, game_state, leader_move):
1535            raise Exception(f"Leader {game_state.leader.implementation} played an illegal move")
1536
1537        return leader_move
1538
1539    def play_trump_exchange(self, game_state: GameState, trump_exchange: TrumpExchange) -> None:
1540        """
1541        Apply a trump exchange to the given game state. This method modifies the game state.
1542
1543        :param game_state: (GameState): The state of the game before the trump exchange is played. This state will be modified.
1544        :param trump_exchange: (TrumpExchange): The trump exchange to be applied to the game state.
1545        """
1546        assert trump_exchange.jack.suit is game_state.trump_suit, \
1547            f"A trump exchange can only be done with a Jack of the same suit as the current trump. Got a {trump_exchange.jack} while the  Trump card is a {game_state.trump_suit}"
1548        # apply the changes in the gamestate
1549        game_state.leader.hand.remove(trump_exchange.jack)
1550        old_trump = game_state.talon.trump_exchange(trump_exchange.jack)
1551        game_state.leader.hand.add(old_trump)
1552        # We notify the both bots that an exchange happened
1553        game_state.leader.implementation.notify_trump_exchange(trump_exchange)
1554        game_state.follower.implementation.notify_trump_exchange(trump_exchange)
1555
1556    def _play_marriage(self, game_engine: GamePlayEngine, game_state: GameState, marriage_move: Marriage) -> None:
1557        """
1558        Apply a marriage to the given game state. This method modifies the game state.
1559
1560        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1561        :param game_state: (GameState): The state of the game before the marriage is played. This state will be modified.
1562        :param marriage_move: (Marriage): The marriage to be applied to the game state.
1563        """
1564
1565        score = game_engine.trick_scorer.marriage(marriage_move, game_state)
1566        game_state.leader.score += score
1567
1568    def get_follower_move(self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move) -> RegularMove:
1569        """
1570        Get the move of the follower of the trick.
1571
1572        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1573        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1574        :param leader_move: (Move): The move made by the leader of the trick.
1575        :returns: (RegularMove): The move of the follower of the trick.
1576        """
1577
1578        follower_game_state = FollowerPerspective(game_state, game_engine, leader_move)
1579
1580        follower_move = game_engine.move_requester.get_move(game_state.follower, follower_game_state, leader_move)
1581        if not game_engine.move_validator.is_legal_follower_move(game_engine, game_state, leader_move, follower_move):
1582            raise Exception(f"Follower {game_state.follower.implementation} played an illegal move")
1583        return cast(RegularMove, follower_move)

Child of TrickImplementer, SchnapsenTrickImplementer specifies how tricks are played in the game.

def play_trick( self, game_engine: GamePlayEngine, game_state: GameState) -> GameState:
1441    def play_trick(self, game_engine: GamePlayEngine, game_state: GameState) -> GameState:
1442        # TODO: Fix the docstring
1443        """
1444        Plays a single Trick the game by asking the bots in the game_state for their Moves,
1445        first asks the leader for their move, and then depending on the resulting gamestate
1446        in self.play_trick_with_fixed_leader_move, will ask (or not ask) the follower for a move.
1447
1448        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1449        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1450        :returns: (GameState): The GameState after the trick is completed.
1451        """
1452        leader_move = self.get_leader_move(game_engine, game_state)
1453        return self.play_trick_with_fixed_leader_move(game_engine=game_engine, game_state=game_state, leader_move=leader_move)

Plays a single Trick the game by asking the bots in the game_state for their Moves, first asks the leader for their move, and then depending on the resulting gamestate in self.play_trick_with_fixed_leader_move, will ask (or not ask) the follower for a move.

Parameters
  • game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
  • game_state: (GameState): The state of the game before the trick is played. This state will not be modified. :returns: (GameState): The GameState after the trick is completed.
def play_trick_with_fixed_leader_move( self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move) -> GameState:
1455    def play_trick_with_fixed_leader_move(self, game_engine: GamePlayEngine, game_state: GameState,
1456                                          leader_move: Move) -> GameState:
1457        """
1458        Plays a trick with a fixed leader move, in order to determine the follower move. Potentially asks the folower for a move.
1459
1460        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1461        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1462        :param leader_move: (Move): The move made by the leader of the trick.
1463        :returns: (GameState): The GameState after the trick is completed.
1464        """
1465        if leader_move.is_trump_exchange():
1466            next_game_state = game_state.copy_for_next()
1467            exchange = cast(TrumpExchange, leader_move)
1468            old_trump_card = game_state.talon.trump_card()
1469            assert old_trump_card, "There is no card at the bottom of the talon"
1470            self.play_trump_exchange(next_game_state, exchange)
1471            # remember the previous state
1472            next_game_state.previous = Previous(game_state, ExchangeTrick(exchange, old_trump_card), True)
1473            # The whole trick ends here.
1474            return next_game_state
1475
1476        # We have a PartialTrick, ask the follower for its move
1477        leader_move = cast(Union[Marriage, RegularMove], leader_move)
1478        follower_move = self.get_follower_move(game_engine, game_state, leader_move)
1479
1480        trick = RegularTrick(leader_move=leader_move, follower_move=follower_move)
1481        return self._apply_regular_trick(game_engine=game_engine, game_state=game_state, trick=trick)

Plays a trick with a fixed leader move, in order to determine the follower move. Potentially asks the folower for a move.

Parameters
  • game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
  • game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
  • leader_move: (Move): The move made by the leader of the trick. :returns: (GameState): The GameState after the trick is completed.
def get_leader_move( self, game_engine: GamePlayEngine, game_state: GameState) -> Move:
1522    def get_leader_move(self, game_engine: GamePlayEngine, game_state: GameState) -> Move:
1523        """
1524        Get the move of the leader of the trick.
1525
1526        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1527        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1528        :returns: (Move): The move of the leader of the trick.
1529        """
1530
1531        # ask first players move trough the requester
1532        leader_game_state = LeaderPerspective(game_state, game_engine)
1533        leader_move = game_engine.move_requester.get_move(game_state.leader, leader_game_state, None)
1534        if not game_engine.move_validator.is_legal_leader_move(game_engine, game_state, leader_move):
1535            raise Exception(f"Leader {game_state.leader.implementation} played an illegal move")
1536
1537        return leader_move

Get the move of the leader of the trick.

Parameters
  • game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
  • game_state: (GameState): The state of the game before the trick is played. This state will not be modified. :returns: (Move): The move of the leader of the trick.
def play_trump_exchange( self, game_state: GameState, trump_exchange: TrumpExchange) -> None:
1539    def play_trump_exchange(self, game_state: GameState, trump_exchange: TrumpExchange) -> None:
1540        """
1541        Apply a trump exchange to the given game state. This method modifies the game state.
1542
1543        :param game_state: (GameState): The state of the game before the trump exchange is played. This state will be modified.
1544        :param trump_exchange: (TrumpExchange): The trump exchange to be applied to the game state.
1545        """
1546        assert trump_exchange.jack.suit is game_state.trump_suit, \
1547            f"A trump exchange can only be done with a Jack of the same suit as the current trump. Got a {trump_exchange.jack} while the  Trump card is a {game_state.trump_suit}"
1548        # apply the changes in the gamestate
1549        game_state.leader.hand.remove(trump_exchange.jack)
1550        old_trump = game_state.talon.trump_exchange(trump_exchange.jack)
1551        game_state.leader.hand.add(old_trump)
1552        # We notify the both bots that an exchange happened
1553        game_state.leader.implementation.notify_trump_exchange(trump_exchange)
1554        game_state.follower.implementation.notify_trump_exchange(trump_exchange)

Apply a trump exchange to the given game state. This method modifies the game state.

Parameters
  • game_state: (GameState): The state of the game before the trump exchange is played. This state will be modified.
  • trump_exchange: (TrumpExchange): The trump exchange to be applied to the game state.
def get_follower_move( self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move) -> RegularMove:
1568    def get_follower_move(self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move) -> RegularMove:
1569        """
1570        Get the move of the follower of the trick.
1571
1572        :param game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
1573        :param game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
1574        :param leader_move: (Move): The move made by the leader of the trick.
1575        :returns: (RegularMove): The move of the follower of the trick.
1576        """
1577
1578        follower_game_state = FollowerPerspective(game_state, game_engine, leader_move)
1579
1580        follower_move = game_engine.move_requester.get_move(game_state.follower, follower_game_state, leader_move)
1581        if not game_engine.move_validator.is_legal_follower_move(game_engine, game_state, leader_move, follower_move):
1582            raise Exception(f"Follower {game_state.follower.implementation} played an illegal move")
1583        return cast(RegularMove, follower_move)

Get the move of the follower of the trick.

Parameters
  • game_engine: (GamePlayEngine): The engine used to preform the underlying actions of the Trick.
  • game_state: (GameState): The state of the game before the trick is played. This state will not be modified.
  • leader_move: (Move): The move made by the leader of the trick. :returns: (RegularMove): The move of the follower of the trick.
class MoveRequester:
1586class MoveRequester:
1587    """
1588    An moveRequester captures the logic of requesting a move from a bot.
1589    This logic also determines what happens in case the bot is to slow, throws an exception during operation, etc
1590    """
1591
1592    @abstractmethod
1593    def get_move(self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1594        """
1595        Get a move from the bot, potentially applying timeout logic.
1596
1597        """

An moveRequester captures the logic of requesting a move from a bot. This logic also determines what happens in case the bot is to slow, throws an exception during operation, etc

@abstractmethod
def get_move( self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1592    @abstractmethod
1593    def get_move(self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1594        """
1595        Get a move from the bot, potentially applying timeout logic.
1596
1597        """

Get a move from the bot, potentially applying timeout logic.

class SimpleMoveRequester(MoveRequester):
1600class SimpleMoveRequester(MoveRequester):
1601    """The SimplemoveRquester just asks the move, and does not time out"""
1602
1603    def get_move(self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1604        """
1605        Get a move from the bot
1606
1607        :param bot: (BotState): The bot to request the move from
1608        :param perspective: (PlayerPerspective): The perspective of the bot
1609        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
1610        """
1611        return bot.get_move(perspective, leader_move=leader_move)

The SimplemoveRquester just asks the move, and does not time out

def get_move( self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1603    def get_move(self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1604        """
1605        Get a move from the bot
1606
1607        :param bot: (BotState): The bot to request the move from
1608        :param perspective: (PlayerPerspective): The perspective of the bot
1609        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
1610        """
1611        return bot.get_move(perspective, leader_move=leader_move)

Get a move from the bot

Parameters
  • bot: (BotState): The bot to request the move from
  • perspective: (PlayerPerspective): The perspective of the bot
  • leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
class SilencingMoveRequester(MoveRequester):
1626class SilencingMoveRequester(MoveRequester):
1627    """
1628    This MoveRequester just asks the move, but before doing so it routes stdout to a dummy file
1629
1630    :param requester: (MoveRequester): The MoveRequester to use to request the move.
1631    """
1632
1633    def __init__(self, requester: MoveRequester) -> None:
1634        self.requester = requester
1635
1636    @contextlib.contextmanager
1637    @staticmethod
1638    def __nostdout() -> Generator[None, Any, None]:
1639        """
1640        A context manager that silences stdout
1641
1642        :returns: (Generator[None, Any, None]): A context manager that silences stdout
1643        """
1644
1645        save_stdout = sys.stdout
1646        sys.stdout = _DummyFile()
1647        yield
1648        sys.stdout = save_stdout
1649
1650    def get_move(self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1651        """
1652        Get a move from the bot, potentially applying timeout logic.
1653
1654        :param bot: (BotState): The bot to request the move from
1655        :param perspective: (PlayerPerspective): The perspective of the bot
1656        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
1657        :returns: (Move): The move returned by the bot.
1658        """
1659
1660        with SilencingMoveRequester.__nostdout():
1661            return self.requester.get_move(bot, perspective, leader_move)

This MoveRequester just asks the move, but before doing so it routes stdout to a dummy file

Parameters
  • requester: (MoveRequester): The MoveRequester to use to request the move.
SilencingMoveRequester(requester: MoveRequester)
1633    def __init__(self, requester: MoveRequester) -> None:
1634        self.requester = requester
requester
def get_move( self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1650    def get_move(self, bot: BotState, perspective: PlayerPerspective, leader_move: Optional[Move]) -> Move:
1651        """
1652        Get a move from the bot, potentially applying timeout logic.
1653
1654        :param bot: (BotState): The bot to request the move from
1655        :param perspective: (PlayerPerspective): The perspective of the bot
1656        :param leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader.
1657        :returns: (Move): The move returned by the bot.
1658        """
1659
1660        with SilencingMoveRequester.__nostdout():
1661            return self.requester.get_move(bot, perspective, leader_move)

Get a move from the bot, potentially applying timeout logic.

Parameters
  • bot: (BotState): The bot to request the move from
  • perspective: (PlayerPerspective): The perspective of the bot
  • leader_move: (Optional[Move]): The move made by the leader of the trick. This is None if the bot is the leader. :returns: (Move): The move returned by the bot.
class MoveValidator(abc.ABC):
1664class MoveValidator(ABC):
1665    """
1666    An object of this class can be used to check whether a move is valid.
1667    """
1668    @abstractmethod
1669    def get_legal_leader_moves(self, game_engine: GamePlayEngine, game_state: GameState) -> Iterable[Move]:
1670        """
1671        Get all legal moves for the current leader of the game.
1672
1673        :param game_engine: The engine which is playing the game
1674        :param game_state: The current state of the game
1675
1676        :returns: An iterable containing the current legal moves.
1677        """
1678
1679    def is_legal_leader_move(self, game_engine: GamePlayEngine, game_state: GameState, move: Move) -> bool:
1680        """
1681        Whether the provided move is legal for the leader to play.
1682        The basic implementation checks whether the move is in the legal leader moves.
1683        Derived classes might implement this more performantly.
1684        """
1685        assert move, 'The move played by the leader cannot be None'
1686        return move in self.get_legal_leader_moves(game_engine, game_state)
1687
1688    @abstractmethod
1689    def get_legal_follower_moves(self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move) -> Iterable[Move]:
1690        """
1691        Get all legal moves for the current follower of the game.
1692
1693        :param game_engine: The engine which is playing the game
1694        :param game_state: The current state of the game
1695        :param leader_move: The move played by the leader of the trick.
1696
1697        :returns: An iterable containing the current legal moves.
1698        """
1699
1700    def is_legal_follower_move(self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move, move: Move) -> bool:
1701        """
1702        Whether the provided move is legal for the follower to play.
1703        The basic implementation checks whether the move is in the legal fllower moves.
1704        Derived classes might implement this more performantly.
1705        """
1706        assert move, 'The move played by the follower cannot be None'
1707        assert leader_move, 'The move played by the leader cannot be None'
1708        return move in self.get_legal_follower_moves(game_engine=game_engine, game_state=game_state, leader_move=leader_move)

An object of this class can be used to check whether a move is valid.

class SchnapsenMoveValidator(MoveValidator):
1711class SchnapsenMoveValidator(MoveValidator):
1712    """
1713    The move validator for the game of Schnapsen.
1714    """
1715
1716    def get_legal_leader_moves(self, game_engine: GamePlayEngine, game_state: GameState) -> Iterable[Move]:
1717        """
1718        Get all legal moves for the current leader of the game.
1719
1720        :param game_engine: The engine which is playing the game
1721        :param game_state: The current state of the game
1722
1723        :returns: An iterable containing the current legal moves.
1724        """
1725        # all cards in the hand can be played
1726        cards_in_hand = game_state.leader.hand
1727        valid_moves: list[Move] = [RegularMove(card) for card in cards_in_hand]
1728        # trump exchanges
1729        if not game_state.talon.is_empty():
1730            trump_jack = Card.get_card(Rank.JACK, game_state.trump_suit)
1731            if trump_jack in cards_in_hand:
1732                valid_moves.append(TrumpExchange(trump_jack))
1733        # mariages
1734        for card in cards_in_hand.filter_rank(Rank.QUEEN):
1735            king_card = Card.get_card(Rank.KING, card.suit)
1736            if king_card in cards_in_hand:
1737                valid_moves.append(Marriage(card, king_card))
1738        return valid_moves
1739
1740    def is_legal_leader_move(self, game_engine: GamePlayEngine, game_state: GameState, move: Move) -> bool:
1741        """
1742        Whether the provided move is legal for the leader to play.
1743
1744        :param game_engine: (GamePlayEngine): The engine which is playing the game
1745        :param game_state: (GameState): The current state of the game
1746        :param move: (Move): The move to check
1747
1748        :returns: (bool): Whether the move is legal
1749        """
1750        cards_in_hand = game_state.leader.hand
1751        if move.is_marriage():
1752            marriage_move = cast(Marriage, move)
1753            # we do not have to check whether they are the same suit because of the implementation of Marriage
1754            return marriage_move.queen_card in cards_in_hand and marriage_move.king_card in cards_in_hand
1755        if move.is_trump_exchange():
1756            if game_state.talon.is_empty():
1757                return False
1758            trump_move: TrumpExchange = cast(TrumpExchange, move)
1759            return trump_move.jack in cards_in_hand
1760        # it has to be a regular move
1761        regular_move = cast(RegularMove, move)
1762        return regular_move.card in cards_in_hand
1763
1764    def get_legal_follower_moves(self, game_engine: GamePlayEngine, game_state: GameState, leader_move: Move) -> Iterable[Move]:
1765        """
1766        Get all legal moves for the current follower of the game.
1767
1768        :param game_engine: (GamePlayEngine): The engine which is playing the game
1769        :param game_state: (GameState): The current state of the game
1770        :param leader_move: (Move): The move played by the leader of the trick.
1771
1772        :returns: (Iterable[Move]): An iterable containing the current legal moves.
1773        """
1774
1775        hand = game_state.follower.hand
1776        if leader_move.is_marriage():
1777            leader_card = cast(Marriage, leader_move).queen_card
1778        else:
1779            leader_card = cast(RegularMove, leader_move).card
1780        if game_state.game_phase() is GamePhase.ONE:
1781            # no need to follow, any card in the hand is a legal move
1782            return RegularMove.from_cards(hand.get_cards())
1783        # information from https://www.pagat.com/marriage/schnaps.html
1784        # ## original formulation ##
1785        # if your opponent leads a non-trump:
1786        #     you must play a higher card of the same suit if you can;
1787        #     failing this you must play a lower card of the same suit;
1788        #     if you have no card of the suit that was led you must play a trump;
1789        #     if you have no trumps either you may play anything.
1790        # If your opponent leads a trump:
1791        #     you must play a higher trump if possible;
1792        #     if you have no higher trump you must play a lower trump;
1793        #     if you have no trumps at all you may play anything.
1794        # ## implemented version, realizing that the rules for trump are overlapping with the normal case ##
1795        # you must play a higher card of the same suit if you can
1796        # failing this, you must play a lower card of the same suit;
1797        # --new--> failing this, if the opponen did not play a trump, you must play a trump
1798        # failing this, you can play anything
1799        leader_card_score = game_engine.trick_scorer.rank_to_points(leader_card.rank)
1800        # you must play a higher card of the same suit if you can;
1801        same_suit_cards = hand.filter_suit(leader_card.suit)
1802        if same_suit_cards:
1803            higher_same_suit, lower_same_suit = [], []
1804            for card in same_suit_cards:
1805                # TODO this is slightly ambigousm should this be >= ??
1806                higher_same_suit.append(card) if game_engine.trick_scorer.rank_to_points(card.rank) > leader_card_score else lower_same_suit.append(card)
1807            if higher_same_suit:
1808                return RegularMove.from_cards(higher_same_suit)
1809        # failing this, you must play a lower card of the same suit;
1810            elif lower_same_suit:
1811                return RegularMove.from_cards(lower_same_suit)
1812            raise AssertionError("Somethign is wrong in the logic here. There should be cards, but they are neither placed in the low, nor higher list")
1813        # failing this, if the opponen did not play a trump, you must play a trump
1814        trump_cards = hand.filter_suit(game_state.trump_suit)
1815        if leader_card.suit != game_state.trump_suit and trump_cards:
1816            return RegularMove.from_cards(trump_cards)
1817        # failing this, you can play anything
1818        return RegularMove.from_cards(hand.get_cards())

The move validator for the game of Schnapsen.

class TrickScorer(abc.ABC):
1821class TrickScorer(ABC):
1822    @abstractmethod
1823    def score(self, trick: RegularTrick, leader: BotState, follower: BotState, trump: Suit) -> tuple[BotState, BotState, bool]:
1824        """
1825        Score the trick for the given leader and follower. The returned bots are the same bots provided (not copies) and have the score of the trick applied.
1826        They are returned in order (new_leader, new_follower). If appropriate, also pending points have been applied.
1827        The boolean is True if the leading bot remained the same, i.e., the past leader remains the leader
1828        """
1829
1830    @abstractmethod
1831    def declare_winner(self, game_state: GameState) -> Optional[tuple[BotState, int]]:
1832        """return a bot and the number of points if there is a winner of this game already
1833
1834        :param game_state: The current state of the game
1835        :returns: The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
1836
1837        """
1838
1839    @abstractmethod
1840    def rank_to_points(self, rank: Rank) -> int:
1841        """Get the point value for a given rank
1842
1843        :param rank: the rank of a card for which you want to obtain the points
1844        :returns: The score for that card
1845        """
1846
1847    @abstractmethod
1848    def marriage(self, move: Marriage, gamestate: GameState) -> Score:
1849        """Get the score which the player obtains for the given marriage
1850
1851        :param move: The marriage for which to get the score
1852        :param gamestate: the current state of the game, usually used to get the trump suit
1853        :returns: The score for this marriage
1854        """

Helper class that provides a standard way to create an ABC using inheritance.

@abstractmethod
def score( self, trick: RegularTrick, leader: BotState, follower: BotState, trump: schnapsen.deck.Suit) -> tuple[BotState, BotState, bool]:
1822    @abstractmethod
1823    def score(self, trick: RegularTrick, leader: BotState, follower: BotState, trump: Suit) -> tuple[BotState, BotState, bool]:
1824        """
1825        Score the trick for the given leader and follower. The returned bots are the same bots provided (not copies) and have the score of the trick applied.
1826        They are returned in order (new_leader, new_follower). If appropriate, also pending points have been applied.
1827        The boolean is True if the leading bot remained the same, i.e., the past leader remains the leader
1828        """

Score the trick for the given leader and follower. The returned bots are the same bots provided (not copies) and have the score of the trick applied. They are returned in order (new_leader, new_follower). If appropriate, also pending points have been applied. The boolean is True if the leading bot remained the same, i.e., the past leader remains the leader

@abstractmethod
def declare_winner( self, game_state: GameState) -> Optional[tuple[BotState, int]]:
1830    @abstractmethod
1831    def declare_winner(self, game_state: GameState) -> Optional[tuple[BotState, int]]:
1832        """return a bot and the number of points if there is a winner of this game already
1833
1834        :param game_state: The current state of the game
1835        :returns: The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
1836
1837        """

return a bot and the number of points if there is a winner of this game already

Parameters
  • game_state: The current state of the game :returns: The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
@abstractmethod
def rank_to_points(self, rank: schnapsen.deck.Rank) -> int:
1839    @abstractmethod
1840    def rank_to_points(self, rank: Rank) -> int:
1841        """Get the point value for a given rank
1842
1843        :param rank: the rank of a card for which you want to obtain the points
1844        :returns: The score for that card
1845        """

Get the point value for a given rank

Parameters
  • rank: the rank of a card for which you want to obtain the points :returns: The score for that card
@abstractmethod
def marriage( self, move: Marriage, gamestate: GameState) -> Score:
1847    @abstractmethod
1848    def marriage(self, move: Marriage, gamestate: GameState) -> Score:
1849        """Get the score which the player obtains for the given marriage
1850
1851        :param move: The marriage for which to get the score
1852        :param gamestate: the current state of the game, usually used to get the trump suit
1853        :returns: The score for this marriage
1854        """

Get the score which the player obtains for the given marriage

Parameters
  • move: The marriage for which to get the score
  • gamestate: the current state of the game, usually used to get the trump suit :returns: The score for this marriage
class SchnapsenTrickScorer(TrickScorer):
1857class SchnapsenTrickScorer(TrickScorer):
1858    """
1859    A TrickScorer that scores ac cording to the Schnapsen rules
1860    """
1861
1862    SCORES = {
1863        Rank.ACE: 11,
1864        Rank.TEN: 10,
1865        Rank.KING: 4,
1866        Rank.QUEEN: 3,
1867        Rank.JACK: 2,
1868    }
1869
1870    def rank_to_points(self, rank: Rank) -> int:
1871        """
1872        Convert a rank to the number of points it is worth.
1873
1874        :param rank: The rank to convert.
1875        :returns: The number of points the rank is worth.
1876        """
1877        return SchnapsenTrickScorer.SCORES[rank]
1878
1879    def marriage(self, move: Marriage, gamestate: GameState) -> Score:
1880        """
1881        Get the score which the player obtains for the given marriage
1882
1883        :param move: The marriage for which to get the score
1884        :param gamestate: the current state of the game, usually used to get the trump suit
1885        :returns: The score for this marriage
1886        """
1887
1888        if move.suit is gamestate.trump_suit:
1889            # royal marriage
1890            return Score(pending_points=40)
1891        # any other marriage
1892        return Score(pending_points=20)
1893
1894    def score(self, trick: RegularTrick, leader: BotState, follower: BotState, trump: Suit) -> tuple[BotState, BotState, bool]:
1895        """
1896        Score the trick for the given leader and follower. The returned bots are the same bots provided (not copies) and have the score of the trick applied.
1897
1898        :param trick: The trick to score
1899        :param leader: The botstate of the leader
1900        :param follower: The botstate of the follower
1901        :param trump: The trump suit
1902        :returns: The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
1903        """
1904
1905        if trick.leader_move.is_marriage():
1906            regular_leader_move: RegularMove = cast(Marriage, trick.leader_move).underlying_regular_move()
1907        else:
1908            regular_leader_move = cast(RegularMove, trick.leader_move)
1909
1910        leader_card = regular_leader_move.card
1911        follower_card = trick.follower_move.card
1912        assert leader_card != follower_card, f"The leader card {leader_card} and follower_card {follower_card} cannot be the same."
1913        leader_card_points = self.rank_to_points(leader_card.rank)
1914        follower_card_points = self.rank_to_points(follower_card.rank)
1915
1916        if leader_card.suit is follower_card.suit:
1917            # same suit, either trump or not
1918            if leader_card_points > follower_card_points:
1919                leader_wins = True
1920            else:
1921                leader_wins = False
1922        elif leader_card.suit is trump:
1923            # the follower suit cannot be trumps as per the previous condition
1924            leader_wins = True
1925        elif follower_card.suit is trump:
1926            # the leader suit cannot be trumps because of the previous conditions
1927            leader_wins = False
1928        else:
1929            # the follower did not follow the suit of the leader and did not play trumps, hence the leader wins
1930            leader_wins = True
1931        winner, loser = (leader, follower) if leader_wins else (follower, leader)
1932        # record the win
1933        winner.won_cards.extend([leader_card, follower_card])
1934        # apply the points
1935        points_gained = leader_card_points + follower_card_points
1936        winner.score += Score(direct_points=points_gained)
1937        # add winner's total of direct and pending points as their new direct points
1938        winner.score = winner.score.redeem_pending_points()
1939        return winner, loser, leader_wins
1940
1941    def declare_winner(self, game_state: GameState) -> Optional[tuple[BotState, int]]:
1942        """
1943        Declaring a winner uses the logic from https://web.archive.org/web/20230303074822/https://www.pagat.com/marriage/schnaps.html#out , but simplified, because we do not have closing of the Talon and no need to guess the scores.
1944        The following text adapted accordingly from that website.
1945
1946        If a player has 66 or more points, she scores points toward game as follows:
1947
1948            * one game point, if the opponent has made at least 33 points;
1949            * two game points, if the opponent has made fewer than 33 points, but has won at least one trick (opponent is said to be Schneider);
1950            * three game points, if the opponent has won no tricks (opponent is said to be Schwarz).
1951
1952        If play continued to the very last trick with the talon exhausted, the player who takes the last trick wins the hand, scoring one game point, irrespective of the number of card points the players have taken.
1953
1954        :param game_state: (GameState): The current gamestate
1955        :returns: (Optional[tuple[BotState, int]]): The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
1956        """
1957        if game_state.leader.score.direct_points >= 66:
1958            follower_score = game_state.follower.score.direct_points
1959            if follower_score == 0:
1960                return game_state.leader, 3
1961            elif follower_score >= 33:
1962                return game_state.leader, 1
1963            else:
1964                # second case in explaination above, 0 < score < 33
1965                assert follower_score < 66, "Found a follower score of more than 66, while the leader also had more than 66. This must never happen."
1966                return game_state.leader, 2
1967        elif game_state.follower.score.direct_points >= 66:
1968            raise AssertionError("Would declare the follower winner, but this should never happen in the current implementation")
1969        elif game_state.are_all_cards_played():
1970            return game_state.leader, 1
1971        else:
1972            return None

A TrickScorer that scores ac cording to the Schnapsen rules

SCORES = {<Rank.ACE: 1>: 11, <Rank.TEN: 10>: 10, <Rank.KING: 13>: 4, <Rank.QUEEN: 12>: 3, <Rank.JACK: 11>: 2}
def rank_to_points(self, rank: schnapsen.deck.Rank) -> int:
1870    def rank_to_points(self, rank: Rank) -> int:
1871        """
1872        Convert a rank to the number of points it is worth.
1873
1874        :param rank: The rank to convert.
1875        :returns: The number of points the rank is worth.
1876        """
1877        return SchnapsenTrickScorer.SCORES[rank]

Convert a rank to the number of points it is worth.

Parameters
  • rank: The rank to convert. :returns: The number of points the rank is worth.
def marriage( self, move: Marriage, gamestate: GameState) -> Score:
1879    def marriage(self, move: Marriage, gamestate: GameState) -> Score:
1880        """
1881        Get the score which the player obtains for the given marriage
1882
1883        :param move: The marriage for which to get the score
1884        :param gamestate: the current state of the game, usually used to get the trump suit
1885        :returns: The score for this marriage
1886        """
1887
1888        if move.suit is gamestate.trump_suit:
1889            # royal marriage
1890            return Score(pending_points=40)
1891        # any other marriage
1892        return Score(pending_points=20)

Get the score which the player obtains for the given marriage

Parameters
  • move: The marriage for which to get the score
  • gamestate: the current state of the game, usually used to get the trump suit :returns: The score for this marriage
def score( self, trick: RegularTrick, leader: BotState, follower: BotState, trump: schnapsen.deck.Suit) -> tuple[BotState, BotState, bool]:
1894    def score(self, trick: RegularTrick, leader: BotState, follower: BotState, trump: Suit) -> tuple[BotState, BotState, bool]:
1895        """
1896        Score the trick for the given leader and follower. The returned bots are the same bots provided (not copies) and have the score of the trick applied.
1897
1898        :param trick: The trick to score
1899        :param leader: The botstate of the leader
1900        :param follower: The botstate of the follower
1901        :param trump: The trump suit
1902        :returns: The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
1903        """
1904
1905        if trick.leader_move.is_marriage():
1906            regular_leader_move: RegularMove = cast(Marriage, trick.leader_move).underlying_regular_move()
1907        else:
1908            regular_leader_move = cast(RegularMove, trick.leader_move)
1909
1910        leader_card = regular_leader_move.card
1911        follower_card = trick.follower_move.card
1912        assert leader_card != follower_card, f"The leader card {leader_card} and follower_card {follower_card} cannot be the same."
1913        leader_card_points = self.rank_to_points(leader_card.rank)
1914        follower_card_points = self.rank_to_points(follower_card.rank)
1915
1916        if leader_card.suit is follower_card.suit:
1917            # same suit, either trump or not
1918            if leader_card_points > follower_card_points:
1919                leader_wins = True
1920            else:
1921                leader_wins = False
1922        elif leader_card.suit is trump:
1923            # the follower suit cannot be trumps as per the previous condition
1924            leader_wins = True
1925        elif follower_card.suit is trump:
1926            # the leader suit cannot be trumps because of the previous conditions
1927            leader_wins = False
1928        else:
1929            # the follower did not follow the suit of the leader and did not play trumps, hence the leader wins
1930            leader_wins = True
1931        winner, loser = (leader, follower) if leader_wins else (follower, leader)
1932        # record the win
1933        winner.won_cards.extend([leader_card, follower_card])
1934        # apply the points
1935        points_gained = leader_card_points + follower_card_points
1936        winner.score += Score(direct_points=points_gained)
1937        # add winner's total of direct and pending points as their new direct points
1938        winner.score = winner.score.redeem_pending_points()
1939        return winner, loser, leader_wins

Score the trick for the given leader and follower. The returned bots are the same bots provided (not copies) and have the score of the trick applied.

Parameters
  • trick: The trick to score
  • leader: The botstate of the leader
  • follower: The botstate of the follower
  • trump: The trump suit :returns: The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
def declare_winner( self, game_state: GameState) -> Optional[tuple[BotState, int]]:
1941    def declare_winner(self, game_state: GameState) -> Optional[tuple[BotState, int]]:
1942        """
1943        Declaring a winner uses the logic from https://web.archive.org/web/20230303074822/https://www.pagat.com/marriage/schnaps.html#out , but simplified, because we do not have closing of the Talon and no need to guess the scores.
1944        The following text adapted accordingly from that website.
1945
1946        If a player has 66 or more points, she scores points toward game as follows:
1947
1948            * one game point, if the opponent has made at least 33 points;
1949            * two game points, if the opponent has made fewer than 33 points, but has won at least one trick (opponent is said to be Schneider);
1950            * three game points, if the opponent has won no tricks (opponent is said to be Schwarz).
1951
1952        If play continued to the very last trick with the talon exhausted, the player who takes the last trick wins the hand, scoring one game point, irrespective of the number of card points the players have taken.
1953
1954        :param game_state: (GameState): The current gamestate
1955        :returns: (Optional[tuple[BotState, int]]): The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
1956        """
1957        if game_state.leader.score.direct_points >= 66:
1958            follower_score = game_state.follower.score.direct_points
1959            if follower_score == 0:
1960                return game_state.leader, 3
1961            elif follower_score >= 33:
1962                return game_state.leader, 1
1963            else:
1964                # second case in explaination above, 0 < score < 33
1965                assert follower_score < 66, "Found a follower score of more than 66, while the leader also had more than 66. This must never happen."
1966                return game_state.leader, 2
1967        elif game_state.follower.score.direct_points >= 66:
1968            raise AssertionError("Would declare the follower winner, but this should never happen in the current implementation")
1969        elif game_state.are_all_cards_played():
1970            return game_state.leader, 1
1971        else:
1972            return None

Declaring a winner uses the logic from https://web.archive.org/web/20230303074822/https://www.pagat.com/marriage/schnaps.html#out , but simplified, because we do not have closing of the Talon and no need to guess the scores. The following text adapted accordingly from that website.

If a player has 66 or more points, she scores points toward game as follows:

* one game point, if the opponent has made at least 33 points;
* two game points, if the opponent has made fewer than 33 points, but has won at least one trick (opponent is said to be Schneider);
* three game points, if the opponent has won no tricks (opponent is said to be Schwarz).

If play continued to the very last trick with the talon exhausted, the player who takes the last trick wins the hand, scoring one game point, irrespective of the number of card points the players have taken.

Parameters
  • game_state: (GameState): The current gamestate :returns: (Optional[tuple[BotState, int]]): The botstate of the winner and the number of game points, in case there is a winner already. Otherwise None.
@dataclass
class GamePlayEngine:
1975@dataclass
1976class GamePlayEngine:
1977    """
1978    The GamePlayengine combines the different aspects of the game into an engine that can execute games.
1979    """
1980    deck_generator: DeckGenerator
1981    hand_generator: HandGenerator
1982    trick_implementer: TrickImplementer
1983    move_requester: MoveRequester
1984    move_validator: MoveValidator
1985    trick_scorer: TrickScorer
1986
1987    def play_game(self, bot1: Bot, bot2: Bot, rng: Random) -> tuple[Bot, int, Score]:
1988        """
1989        Play a game between bot1 and bot2, using the rng to create the game.
1990
1991        :param bot1: The first bot playing the game. This bot will be the leader for the first trick.
1992        :param bot2: The second bot playing the game. This bot will be the follower for the first trick.
1993        :param rng: The random number generator used to shuffle the deck.
1994
1995        :returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.
1996        """
1997        cards = self.deck_generator.get_initial_deck()
1998        shuffled = self.deck_generator.shuffle_deck(cards, rng)
1999        hand1, hand2, talon = self.hand_generator.generateHands(shuffled)
2000
2001        leader_state = BotState(implementation=bot1, hand=hand1)
2002        follower_state = BotState(implementation=bot2, hand=hand2)
2003
2004        game_state = GameState(
2005            leader=leader_state,
2006            follower=follower_state,
2007            talon=talon,
2008            previous=None
2009        )
2010        winner, points, score = self.play_game_from_state(game_state=game_state, leader_move=None)
2011        return winner, points, score
2012
2013    def get_random_phase_two_state(self, rng: Random) -> GameState:
2014        """
2015        Get a random GameState in the second phase of the game.
2016
2017        :param rng: The random number generator used to shuffle the deck.
2018
2019        :returns: A GameState in the second phase of the game.
2020        """
2021
2022        class RandBot(Bot):
2023            def __init__(self, rand: Random, name: Optional[str] = None) -> None:
2024                super().__init__(name)
2025                self.rng = rand
2026
2027            def get_move(
2028                self,
2029                perspective: PlayerPerspective,
2030                leader_move: Optional[Move],
2031            ) -> Move:
2032                moves: list[Move] = perspective.valid_moves()
2033                move = self.rng.choice(moves)
2034                return move
2035
2036        while True:
2037            cards = self.deck_generator.get_initial_deck()
2038            shuffled = self.deck_generator.shuffle_deck(cards, rng)
2039            hand1, hand2, talon = self.hand_generator.generateHands(shuffled)
2040            leader_state = BotState(implementation=RandBot(rand=rng), hand=hand1)
2041            follower_state = BotState(implementation=RandBot(rand=rng), hand=hand2)
2042            game_state = GameState(
2043                leader=leader_state,
2044                follower=follower_state,
2045                talon=talon,
2046                previous=None
2047            )
2048            second_phase_state, _ = self.play_at_most_n_tricks(game_state, RandBot(rand=rng), RandBot(rand=rng), 5)
2049            winner = self.trick_scorer.declare_winner(second_phase_state)
2050            if winner:
2051                continue
2052            if second_phase_state.game_phase() == GamePhase.TWO:
2053                return second_phase_state
2054
2055    def play_game_from_state_with_new_bots(self, game_state: GameState, new_leader: Bot, new_follower: Bot, leader_move: Optional[Move]) -> tuple[Bot, int, Score]:
2056        """
2057        Continue a game  which might have started before with other bots, with new bots.
2058        The new bots are new_leader and new_follower.
2059        The leader move is an optional paramter which can be provided to force this first move from the leader.
2060
2061        :param game_state: The state of the game to start from
2062        :param new_leader: The bot which will take the leader role in the game.
2063        :param new_follower: The bot which will take the follower in the game.
2064        :param leader_move: if provided, the leader will be forced to play this move as its first move.
2065
2066        :returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.
2067        """
2068
2069        game_state_copy = game_state.copy_with_other_bots(new_leader=new_leader, new_follower=new_follower)
2070        return self.play_game_from_state(game_state_copy, leader_move=leader_move)
2071
2072    def play_game_from_state(self, game_state: GameState, leader_move: Optional[Move]) -> tuple[Bot, int, Score]:
2073        """
2074        Continue a game  which might have been started before.
2075        The leader move is an optional paramter which can be provided to force this first move from the leader.
2076
2077        :param game_state: The state of the game to start from
2078        :param leader_move: if provided, the leader will be forced to play this move as its first move.
2079
2080        :returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.
2081        """
2082        winner: Optional[BotState] = None
2083        points: int = -1
2084        while not winner:
2085            if leader_move is not None:
2086                # we continues from a game where the leading bot already did a move, we immitate that
2087                game_state = self.trick_implementer.play_trick_with_fixed_leader_move(game_engine=self, game_state=game_state, leader_move=leader_move)
2088                leader_move = None
2089            else:
2090                game_state = self.trick_implementer.play_trick(self, game_state)
2091            winner, points = self.trick_scorer.declare_winner(game_state) or (None, -1)
2092
2093        winner_state = WinnerPerspective(game_state, self)
2094        winner.implementation.notify_game_end(won=True, perspective=winner_state)
2095
2096        loser_state = LoserPerspective(game_state, self)
2097        game_state.follower.implementation.notify_game_end(False, perspective=loser_state)
2098
2099        return winner.implementation, points, winner.score
2100
2101    def play_one_trick(self, game_state: GameState, new_leader: Bot, new_follower: Bot) -> GameState:
2102        """
2103        Plays one tricks (including the one started by the leader, if provided) on a game which might have started before.
2104        The new bots are new_leader and new_follower.
2105
2106        This method does not make changes to the provided game_state.
2107
2108        :param game_state: The state of the game to start from
2109        :param new_leader: The bot which will take the leader role in the game.
2110        :param new_follower: The bot which will take the follower in the game.
2111
2112        :returns: The GameState reached and the number of steps actually taken.
2113        """
2114        state, rounds = self.play_at_most_n_tricks(game_state, new_leader, new_follower, 1)
2115        assert rounds == 1, f"We called play_at_most_n_tricks with rounds=1, but it returned not excactly 1 round, got {rounds} rounds."
2116        return state
2117
2118    def play_at_most_n_tricks(self, game_state: GameState, new_leader: Bot, new_follower: Bot, n: int) -> tuple[GameState, int]:
2119        """
2120        Plays up to n tricks (including the one started by the leader, if provided) on a game which might have started before.
2121        The number of tricks will be smaller than n in case the game ends before n tricks are played.
2122        The new bots are new_leader and new_follower.
2123
2124        This method does not make changes to the provided game_state.
2125
2126        :param game_state: The state of the game to start from
2127        :param new_leader: The bot which will take the leader role in the game.
2128        :param new_follower: The bot which will take the follower in the game.
2129        :param n: the maximum number of tricks to play
2130
2131        :returns: The GameState reached and the number of steps actually taken.
2132        """
2133        assert n >= 0, "Cannot play less than 0 rounds"
2134        game_state_copy = game_state.copy_with_other_bots(new_leader=new_leader, new_follower=new_follower)
2135
2136        winner: Optional[BotState] = None
2137        rounds_played = 0
2138        while not winner:
2139            if rounds_played == n:
2140                break
2141            game_state_copy = self.trick_implementer.play_trick(self, game_state_copy)
2142            winner, _ = self.trick_scorer.declare_winner(game_state_copy) or (None, -1)
2143            rounds_played += 1
2144        if winner:
2145            winner_state = WinnerPerspective(game_state_copy, self)
2146            winner.implementation.notify_game_end(won=True, perspective=winner_state)
2147
2148            loser_state = LoserPerspective(game_state_copy, self)
2149            game_state_copy.follower.implementation.notify_game_end(False, perspective=loser_state)
2150
2151        return game_state_copy, rounds_played
2152
2153    def __repr__(self) -> str:
2154        return f"GamePlayEngine(deck_generator={self.deck_generator}, "\
2155               f"hand_generator={self.hand_generator}, "\
2156               f"trick_implementer={self.trick_implementer}, "\
2157               f"move_requester={self.move_requester}, "\
2158               f"move_validator={self.move_validator}, "\
2159               f"trick_scorer={self.trick_scorer})"

The GamePlayengine combines the different aspects of the game into an engine that can execute games.

GamePlayEngine( deck_generator: DeckGenerator, hand_generator: HandGenerator, trick_implementer: TrickImplementer, move_requester: MoveRequester, move_validator: MoveValidator, trick_scorer: TrickScorer)
deck_generator: DeckGenerator
hand_generator: HandGenerator
trick_implementer: TrickImplementer
move_requester: MoveRequester
move_validator: MoveValidator
trick_scorer: TrickScorer
def play_game( self, bot1: Bot, bot2: Bot, rng: random.Random) -> tuple[Bot, int, Score]:
1987    def play_game(self, bot1: Bot, bot2: Bot, rng: Random) -> tuple[Bot, int, Score]:
1988        """
1989        Play a game between bot1 and bot2, using the rng to create the game.
1990
1991        :param bot1: The first bot playing the game. This bot will be the leader for the first trick.
1992        :param bot2: The second bot playing the game. This bot will be the follower for the first trick.
1993        :param rng: The random number generator used to shuffle the deck.
1994
1995        :returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.
1996        """
1997        cards = self.deck_generator.get_initial_deck()
1998        shuffled = self.deck_generator.shuffle_deck(cards, rng)
1999        hand1, hand2, talon = self.hand_generator.generateHands(shuffled)
2000
2001        leader_state = BotState(implementation=bot1, hand=hand1)
2002        follower_state = BotState(implementation=bot2, hand=hand2)
2003
2004        game_state = GameState(
2005            leader=leader_state,
2006            follower=follower_state,
2007            talon=talon,
2008            previous=None
2009        )
2010        winner, points, score = self.play_game_from_state(game_state=game_state, leader_move=None)
2011        return winner, points, score

Play a game between bot1 and bot2, using the rng to create the game.

Parameters
  • bot1: The first bot playing the game. This bot will be the leader for the first trick.
  • bot2: The second bot playing the game. This bot will be the follower for the first trick.
  • rng: The random number generator used to shuffle the deck.

:returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.

def get_random_phase_two_state(self, rng: random.Random) -> GameState:
2013    def get_random_phase_two_state(self, rng: Random) -> GameState:
2014        """
2015        Get a random GameState in the second phase of the game.
2016
2017        :param rng: The random number generator used to shuffle the deck.
2018
2019        :returns: A GameState in the second phase of the game.
2020        """
2021
2022        class RandBot(Bot):
2023            def __init__(self, rand: Random, name: Optional[str] = None) -> None:
2024                super().__init__(name)
2025                self.rng = rand
2026
2027            def get_move(
2028                self,
2029                perspective: PlayerPerspective,
2030                leader_move: Optional[Move],
2031            ) -> Move:
2032                moves: list[Move] = perspective.valid_moves()
2033                move = self.rng.choice(moves)
2034                return move
2035
2036        while True:
2037            cards = self.deck_generator.get_initial_deck()
2038            shuffled = self.deck_generator.shuffle_deck(cards, rng)
2039            hand1, hand2, talon = self.hand_generator.generateHands(shuffled)
2040            leader_state = BotState(implementation=RandBot(rand=rng), hand=hand1)
2041            follower_state = BotState(implementation=RandBot(rand=rng), hand=hand2)
2042            game_state = GameState(
2043                leader=leader_state,
2044                follower=follower_state,
2045                talon=talon,
2046                previous=None
2047            )
2048            second_phase_state, _ = self.play_at_most_n_tricks(game_state, RandBot(rand=rng), RandBot(rand=rng), 5)
2049            winner = self.trick_scorer.declare_winner(second_phase_state)
2050            if winner:
2051                continue
2052            if second_phase_state.game_phase() == GamePhase.TWO:
2053                return second_phase_state

Get a random GameState in the second phase of the game.

Parameters
  • rng: The random number generator used to shuffle the deck.

:returns: A GameState in the second phase of the game.

def play_game_from_state_with_new_bots( self, game_state: GameState, new_leader: Bot, new_follower: Bot, leader_move: Optional[Move]) -> tuple[Bot, int, Score]:
2055    def play_game_from_state_with_new_bots(self, game_state: GameState, new_leader: Bot, new_follower: Bot, leader_move: Optional[Move]) -> tuple[Bot, int, Score]:
2056        """
2057        Continue a game  which might have started before with other bots, with new bots.
2058        The new bots are new_leader and new_follower.
2059        The leader move is an optional paramter which can be provided to force this first move from the leader.
2060
2061        :param game_state: The state of the game to start from
2062        :param new_leader: The bot which will take the leader role in the game.
2063        :param new_follower: The bot which will take the follower in the game.
2064        :param leader_move: if provided, the leader will be forced to play this move as its first move.
2065
2066        :returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.
2067        """
2068
2069        game_state_copy = game_state.copy_with_other_bots(new_leader=new_leader, new_follower=new_follower)
2070        return self.play_game_from_state(game_state_copy, leader_move=leader_move)

Continue a game which might have started before with other bots, with new bots. The new bots are new_leader and new_follower. The leader move is an optional paramter which can be provided to force this first move from the leader.

Parameters
  • game_state: The state of the game to start from
  • new_leader: The bot which will take the leader role in the game.
  • new_follower: The bot which will take the follower in the game.
  • leader_move: if provided, the leader will be forced to play this move as its first move.

:returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.

def play_game_from_state( self, game_state: GameState, leader_move: Optional[Move]) -> tuple[Bot, int, Score]:
2072    def play_game_from_state(self, game_state: GameState, leader_move: Optional[Move]) -> tuple[Bot, int, Score]:
2073        """
2074        Continue a game  which might have been started before.
2075        The leader move is an optional paramter which can be provided to force this first move from the leader.
2076
2077        :param game_state: The state of the game to start from
2078        :param leader_move: if provided, the leader will be forced to play this move as its first move.
2079
2080        :returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.
2081        """
2082        winner: Optional[BotState] = None
2083        points: int = -1
2084        while not winner:
2085            if leader_move is not None:
2086                # we continues from a game where the leading bot already did a move, we immitate that
2087                game_state = self.trick_implementer.play_trick_with_fixed_leader_move(game_engine=self, game_state=game_state, leader_move=leader_move)
2088                leader_move = None
2089            else:
2090                game_state = self.trick_implementer.play_trick(self, game_state)
2091            winner, points = self.trick_scorer.declare_winner(game_state) or (None, -1)
2092
2093        winner_state = WinnerPerspective(game_state, self)
2094        winner.implementation.notify_game_end(won=True, perspective=winner_state)
2095
2096        loser_state = LoserPerspective(game_state, self)
2097        game_state.follower.implementation.notify_game_end(False, perspective=loser_state)
2098
2099        return winner.implementation, points, winner.score

Continue a game which might have been started before. The leader move is an optional paramter which can be provided to force this first move from the leader.

Parameters
  • game_state: The state of the game to start from
  • leader_move: if provided, the leader will be forced to play this move as its first move.

:returns: A tuple with the bot which won the game, the number of points obtained from this game and the score attained.

def play_one_trick( self, game_state: GameState, new_leader: Bot, new_follower: Bot) -> GameState:
2101    def play_one_trick(self, game_state: GameState, new_leader: Bot, new_follower: Bot) -> GameState:
2102        """
2103        Plays one tricks (including the one started by the leader, if provided) on a game which might have started before.
2104        The new bots are new_leader and new_follower.
2105
2106        This method does not make changes to the provided game_state.
2107
2108        :param game_state: The state of the game to start from
2109        :param new_leader: The bot which will take the leader role in the game.
2110        :param new_follower: The bot which will take the follower in the game.
2111
2112        :returns: The GameState reached and the number of steps actually taken.
2113        """
2114        state, rounds = self.play_at_most_n_tricks(game_state, new_leader, new_follower, 1)
2115        assert rounds == 1, f"We called play_at_most_n_tricks with rounds=1, but it returned not excactly 1 round, got {rounds} rounds."
2116        return state

Plays one tricks (including the one started by the leader, if provided) on a game which might have started before. The new bots are new_leader and new_follower.

This method does not make changes to the provided game_state.

Parameters
  • game_state: The state of the game to start from
  • new_leader: The bot which will take the leader role in the game.
  • new_follower: The bot which will take the follower in the game.

:returns: The GameState reached and the number of steps actually taken.

def play_at_most_n_tricks( self, game_state: GameState, new_leader: Bot, new_follower: Bot, n: int) -> tuple[GameState, int]:
2118    def play_at_most_n_tricks(self, game_state: GameState, new_leader: Bot, new_follower: Bot, n: int) -> tuple[GameState, int]:
2119        """
2120        Plays up to n tricks (including the one started by the leader, if provided) on a game which might have started before.
2121        The number of tricks will be smaller than n in case the game ends before n tricks are played.
2122        The new bots are new_leader and new_follower.
2123
2124        This method does not make changes to the provided game_state.
2125
2126        :param game_state: The state of the game to start from
2127        :param new_leader: The bot which will take the leader role in the game.
2128        :param new_follower: The bot which will take the follower in the game.
2129        :param n: the maximum number of tricks to play
2130
2131        :returns: The GameState reached and the number of steps actually taken.
2132        """
2133        assert n >= 0, "Cannot play less than 0 rounds"
2134        game_state_copy = game_state.copy_with_other_bots(new_leader=new_leader, new_follower=new_follower)
2135
2136        winner: Optional[BotState] = None
2137        rounds_played = 0
2138        while not winner:
2139            if rounds_played == n:
2140                break
2141            game_state_copy = self.trick_implementer.play_trick(self, game_state_copy)
2142            winner, _ = self.trick_scorer.declare_winner(game_state_copy) or (None, -1)
2143            rounds_played += 1
2144        if winner:
2145            winner_state = WinnerPerspective(game_state_copy, self)
2146            winner.implementation.notify_game_end(won=True, perspective=winner_state)
2147
2148            loser_state = LoserPerspective(game_state_copy, self)
2149            game_state_copy.follower.implementation.notify_game_end(False, perspective=loser_state)
2150
2151        return game_state_copy, rounds_played

Plays up to n tricks (including the one started by the leader, if provided) on a game which might have started before. The number of tricks will be smaller than n in case the game ends before n tricks are played. The new bots are new_leader and new_follower.

This method does not make changes to the provided game_state.

Parameters
  • game_state: The state of the game to start from
  • new_leader: The bot which will take the leader role in the game.
  • new_follower: The bot which will take the follower in the game.
  • n: the maximum number of tricks to play

:returns: The GameState reached and the number of steps actually taken.

class SchnapsenGamePlayEngine(GamePlayEngine):
2162class SchnapsenGamePlayEngine(GamePlayEngine):
2163    """
2164    A GamePlayEngine configured according to the rules of Schnapsen
2165    """
2166
2167    def __init__(self) -> None:
2168        super().__init__(
2169            deck_generator=SchnapsenDeckGenerator(),
2170            hand_generator=SchnapsenHandGenerator(),
2171            trick_implementer=SchnapsenTrickImplementer(),
2172            move_requester=SimpleMoveRequester(),
2173            move_validator=SchnapsenMoveValidator(),
2174            trick_scorer=SchnapsenTrickScorer()
2175        )
2176
2177    def __repr__(self) -> str:
2178        return super().__repr__()

A GamePlayEngine configured according to the rules of Schnapsen