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__()
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.
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.
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.
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.
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.
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
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.
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
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.
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
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
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.
Is this Move a regular move (not a mariage or trump exchange)
:returns: a bool indicating whether this is a regular move
Inherited Members
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.
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.
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.
Inherited Members
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.
The suit of this marriage, gets derived from the suit of the queen and king.
Is this Move a marriage?
:returns: a bool indicating whether this move is a marriage
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.
Inherited Members
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.
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.
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
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
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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
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
Inherited Members
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.
All cards used as part of this trick. This includes cards used in marriages
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
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
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.
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
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
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.
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
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
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
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.
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.
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.
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
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
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.
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
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.
The current follower, i.e., the one who will play the second move in the next trick
The talon, containing the cards not yet in the hand of the player and the trump card at the bottom
The events which led to this GameState, or None, if this is the initial GameState (or previous tricks and states are unknown)
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.
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.
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
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
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.
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.
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.
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
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.
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.
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
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
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?
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
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
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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
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
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
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
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
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.
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.
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.
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.
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
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
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
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
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
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.
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.
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.
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.
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
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
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
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
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
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.
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.
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.
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
Inherited Members
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.
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
Inherited Members
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.
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.
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.
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.
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
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
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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
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.
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.
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.
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.
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 """
Get all legal moves for the current leader of the game.
Parameters
- game_engine: The engine which is playing the game
- game_state: The current state of the game
:returns: An iterable containing the current legal moves.
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)
Whether the provided move is legal for the leader to play. The basic implementation checks whether the move is in the legal leader moves. Derived classes might implement this more performantly.
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 """
Get all legal moves for the current follower of the game.
Parameters
- game_engine: The engine which is playing the game
- game_state: The current state of the game
- leader_move: The move played by the leader of the trick.
:returns: An iterable containing the current legal moves.
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)
Whether the provided move is legal for the follower to play. The basic implementation checks whether the move is in the legal fllower moves. Derived classes might implement this more performantly.
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.
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
Get all legal moves for the current leader of the game.
Parameters
- game_engine: The engine which is playing the game
- game_state: The current state of the game
:returns: An iterable containing the current legal moves.
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
Whether the provided move is legal for the leader to play.
Parameters
- game_engine: (GamePlayEngine): The engine which is playing the game
- game_state: (GameState): The current state of the game
- move: (Move): The move to check
:returns: (bool): Whether the move is legal
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())
Get all legal moves for the current follower of the game.
Parameters
- game_engine: (GamePlayEngine): The engine which is playing the game
- game_state: (GameState): The current state of the game
- leader_move: (Move): The move played by the leader of the trick.
:returns: (Iterable[Move]): An iterable containing the current legal moves.
Inherited Members
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.
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
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.
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
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
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
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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