Module mk2lib.mk2deck

Market implementation and card decks definitions.

Classes

class Market (game: MachiKoroGame, use_promo: bool = True)
Expand source code
class Market:
    """
    Implementation of 5-5-5 market of Machi Koro 2.
    """

    def __init__(self, game: MachiKoroGame, use_promo: bool = True):
        """
        Create a new Market object, clone and shuffle decks and make an initial deal.
        """
        self.est_low = cast(list[Establishment], Market._make_card_deck(DECK_1_6))
        self.est_high = cast(list[Establishment], Market._make_card_deck(DECK_7_12))
        self.landmarks = cast(list[Landmark], Market._make_card_deck(DECK_LANDMARKS))
        if not use_promo:
            self.landmarks = list(filter(lambda c: not c.is_promo, self.landmarks))
        self.dealt_low: dict[str, Establishment] = {}
        self.dealt_high: dict[str, Establishment] = {}
        self.dealt_landmarks: dict[str, Landmark] = {}
        self.game = game
        initial_deal = self.deal_to_market()
        self.game.emit_event(DealtCardsToMarket(initial_deal, initial=True))

    @staticmethod
    def _make_card_deck(cards: list[Establishment] | list[Landmark]) -> list[Card]:
        """
        Clone and shuffle card deck.

        :param cards: Source card deck.
        :return: Cloned and shuffled card deck with un-stacked cards.
        """
        deck = []
        for card in cards:
            for _ in range(card.quantity):
                deck.append(replace(card, quantity=1))
        shuffle(deck)
        return cast(list[Card], deck)

    def deal_to_market(self) -> list[Card]:
        """
        Deal cards, until there's 5 of each type.

        Duplicate cards are stacked.

        :return: List of cards that were dealt to market.
        """
        dealt = []
        for deck, _market in (
            (self.est_low, self.dealt_low),
            (self.est_high, self.dealt_high),
            (self.landmarks, self.dealt_landmarks),
        ):
            market = cast(dict[str, Card], _market)
            while deck and len(market) < 5:
                card = deck.pop()
                dealt.append(card)
                if card.name in market:
                    market[card.name].quantity += 1
                else:
                    market[card.name] = card
        return cast(list[Card], dealt)

    def can_build(self, player: Player) -> list[Card]:
        """
        Check is player can build something.

        Checks that market has any cards up for building and that player
        can afford at least any one card.

        :param player: Player, whose build phase it is.
        """
        affordable_cards = []
        for market in (self.dealt_low, self.dealt_high, self.dealt_landmarks):
            for card in market.values():
                if player.can_afford(card.get_real_price(self.game, player)):
                    affordable_cards.append(card)
        return cast(list[Card], affordable_cards)

    def build_card(self, player: Player, card_name: str) -> Card | None:
        """
        Build the specified card.

        Checks whether card is dealt to market, plus if player can afford and is
        allowed to build it by rules.

        If yes - gives card to player and emits CardBuilt event. If for any reason
        building is illegal - either CardUnavailable or NotEnoughMoney event is
        emitted.

        :param player: Player who builds the card.
        :param card_name: Name of the card that player wants to build.
        :return: Card instance, if built successfully. None otherwise.
        """
        for market in (self.dealt_low, self.dealt_high, self.dealt_landmarks):
            if card_name in market:
                card = replace(market[card_name], quantity=1)
                real_price = card.get_real_price(self.game, player)
                if player.can_afford(real_price) and real_price is not None:
                    player.spend_coins(real_price)
                    if market[card_name].quantity > 1:
                        market[card_name].quantity -= 1
                    else:
                        market.pop(card_name)
                    player.add_card(card)
                    self.game.emit_event(
                        CardBuilt(
                            buyer=player,
                            card=card,
                            price_paid=real_price,
                        )
                    )
                    dealt = self.deal_to_market()
                    if dealt:
                        self.game.emit_event(DealtCardsToMarket(dealt))
                    return card
                if real_price is None:
                    self.game.emit_event(
                        CardUnavailable(
                            buyer=player,
                            card_name=card_name,
                            prohibited=True,
                        )
                    )
                else:
                    self.game.emit_event(
                        NotEnoughMoney(
                            buyer=player,
                            card_name=card_name,
                            card_price=real_price,
                        )
                    )
                return None
        self.game.emit_event(CardUnavailable(buyer=player, card_name=card_name))
        return None

    def serialize(self) -> dict:
        """
        Prepare JSON serializable version of this Market instance.

        Preserves card state and order.

        :return: JSON-friendly dict that has enough state to reconstruct Market object.
        """
        return {
            "est_low": [card.name for card in self.est_low],
            "est_high": [card.name for card in self.est_high],
            "landmarks": [card.name for card in self.landmarks],
            "dealt_low": {name: card.quantity for name, card in self.dealt_low.items()},
            "dealt_high": {
                name: card.quantity for name, card in self.dealt_high.items()
            },
            "dealt_landmarks": {
                name: card.quantity for name, card in self.dealt_landmarks.items()
            },
        }

    @classmethod
    def deserialize(cls, game: MachiKoroGame, data: dict) -> Market:
        """
        Reconstruct Market from saved dict, produced by .serialize()

        :return: Loaded Market from saved data.
        """
        market = cls.__new__(cls)  # bypass __init__, we restore manually
        market.game = game

        # reconstruct decks in order
        def rebuild_deck(names):
            return [replace(ALL_CARDS[name], quantity=1) for name in names]

        market.est_low = rebuild_deck(data["est_low"])
        market.est_high = rebuild_deck(data["est_high"])
        market.landmarks = rebuild_deck(data["landmarks"])

        # reconstruct dealt dicts (quantities matter!)
        def rebuild_dealt(d):
            return {
                name: replace(ALL_CARDS[name], quantity=qty) for name, qty in d.items()
            }

        market.dealt_low = rebuild_dealt(data["dealt_low"])
        market.dealt_high = rebuild_dealt(data["dealt_high"])
        market.dealt_landmarks = rebuild_dealt(data["dealt_landmarks"])

        return market

Implementation of 5-5-5 market of Machi Koro 2.

Create a new Market object, clone and shuffle decks and make an initial deal.

Static methods

def deserialize(game: MachiKoroGame, data: dict)

Reconstruct Market from saved dict, produced by .serialize()

:return: Loaded Market from saved data.

Methods

def build_card(self, player: Player, card_name: str) ‑> Card | None
Expand source code
def build_card(self, player: Player, card_name: str) -> Card | None:
    """
    Build the specified card.

    Checks whether card is dealt to market, plus if player can afford and is
    allowed to build it by rules.

    If yes - gives card to player and emits CardBuilt event. If for any reason
    building is illegal - either CardUnavailable or NotEnoughMoney event is
    emitted.

    :param player: Player who builds the card.
    :param card_name: Name of the card that player wants to build.
    :return: Card instance, if built successfully. None otherwise.
    """
    for market in (self.dealt_low, self.dealt_high, self.dealt_landmarks):
        if card_name in market:
            card = replace(market[card_name], quantity=1)
            real_price = card.get_real_price(self.game, player)
            if player.can_afford(real_price) and real_price is not None:
                player.spend_coins(real_price)
                if market[card_name].quantity > 1:
                    market[card_name].quantity -= 1
                else:
                    market.pop(card_name)
                player.add_card(card)
                self.game.emit_event(
                    CardBuilt(
                        buyer=player,
                        card=card,
                        price_paid=real_price,
                    )
                )
                dealt = self.deal_to_market()
                if dealt:
                    self.game.emit_event(DealtCardsToMarket(dealt))
                return card
            if real_price is None:
                self.game.emit_event(
                    CardUnavailable(
                        buyer=player,
                        card_name=card_name,
                        prohibited=True,
                    )
                )
            else:
                self.game.emit_event(
                    NotEnoughMoney(
                        buyer=player,
                        card_name=card_name,
                        card_price=real_price,
                    )
                )
            return None
    self.game.emit_event(CardUnavailable(buyer=player, card_name=card_name))
    return None

Build the specified card.

Checks whether card is dealt to market, plus if player can afford and is allowed to build it by rules.

If yes - gives card to player and emits CardBuilt event. If for any reason building is illegal - either CardUnavailable or NotEnoughMoney event is emitted.

:param player: Player who builds the card. :param card_name: Name of the card that player wants to build. :return: Card instance, if built successfully. None otherwise.

def can_build(self, player: Player) ‑> list[Card]
Expand source code
def can_build(self, player: Player) -> list[Card]:
    """
    Check is player can build something.

    Checks that market has any cards up for building and that player
    can afford at least any one card.

    :param player: Player, whose build phase it is.
    """
    affordable_cards = []
    for market in (self.dealt_low, self.dealt_high, self.dealt_landmarks):
        for card in market.values():
            if player.can_afford(card.get_real_price(self.game, player)):
                affordable_cards.append(card)
    return cast(list[Card], affordable_cards)

Check is player can build something.

Checks that market has any cards up for building and that player can afford at least any one card.

:param player: Player, whose build phase it is.

def deal_to_market(self) ‑> list[Card]
Expand source code
def deal_to_market(self) -> list[Card]:
    """
    Deal cards, until there's 5 of each type.

    Duplicate cards are stacked.

    :return: List of cards that were dealt to market.
    """
    dealt = []
    for deck, _market in (
        (self.est_low, self.dealt_low),
        (self.est_high, self.dealt_high),
        (self.landmarks, self.dealt_landmarks),
    ):
        market = cast(dict[str, Card], _market)
        while deck and len(market) < 5:
            card = deck.pop()
            dealt.append(card)
            if card.name in market:
                market[card.name].quantity += 1
            else:
                market[card.name] = card
    return cast(list[Card], dealt)

Deal cards, until there's 5 of each type.

Duplicate cards are stacked.

:return: List of cards that were dealt to market.

def serialize(self) ‑> dict
Expand source code
def serialize(self) -> dict:
    """
    Prepare JSON serializable version of this Market instance.

    Preserves card state and order.

    :return: JSON-friendly dict that has enough state to reconstruct Market object.
    """
    return {
        "est_low": [card.name for card in self.est_low],
        "est_high": [card.name for card in self.est_high],
        "landmarks": [card.name for card in self.landmarks],
        "dealt_low": {name: card.quantity for name, card in self.dealt_low.items()},
        "dealt_high": {
            name: card.quantity for name, card in self.dealt_high.items()
        },
        "dealt_landmarks": {
            name: card.quantity for name, card in self.dealt_landmarks.items()
        },
    }

Prepare JSON serializable version of this Market instance.

Preserves card state and order.

:return: JSON-friendly dict that has enough state to reconstruct Market object.