Gra w karty

WPROWADZENIE

Referencja: Allen B. Downey, Jeffrey Elkner and Chris Meyers, How to Think Like a Computer Scientist [różne wersje dostępne w sieci].

Gra Old Maid (odmiana gry Czarny Piotruś) jest przykładem zastosowania dziedziczenia. Celem gry jest pozbycie się wszystkich kart z ręki. Karty odkładamy w parach, a parę tworzą dwie czarne (Trefl, Pik) lub dwie czerwone (Karo, Kier) karty o tym samym numerze. Gracze w koło ciągną od siebie karty i odkładają pary aż zostanie jeden gracz z jedną kartą bez pary (na początku gry usuwa się z talii królową trefl). W poniższym programie komputer symuluje grę wszystkich graczy.


# Implementacja w Pythonie 2.7 (__cmp__).

class Card:
    """Klasa reprezentująca karty do gry."""
    # Te listy poniżej są widoczne w metodach tej klasy.
    # Numery i kolory kart są zakodowane (mapowane) w dwóch listach.
    suit_list = ["trefl", "karo", "kier", "pik"]
    rank_list = ["puste","As","2","3","4","5","6","7","8","9","10","Walet","Dama","Krol"]

    def __init__(self, suit=0, rank=0):
        """Konstruktor karty."""
        self.suit = suit
        self.rank = rank

    def __str__(self):
        """Zwróć postać napisową karty."""
        # siódemka kier ma postać "7 Kier"
        return "{} {}".format(Card.rank_list[self.rank], Card.suit_list[self.suit])

    def __repr__(self):
        """Zwróć reprezentację napisową."""
        # siódemka kier ma postać "Card(2, 7)"
        return "Card({}, {})".format(self.suit, self.rank)

    def __cmp__(self, other):
        """Porównaj karty."""
        # Sprawdzamy kolory kart.
        if self.suit > other.suit:
            return 1
        if self.suit < other.suit:
            return -1
        # Kolory te same, sprawdzamy numery.
        if self.rank > other.rank:
            return 1
        if self.rank < other.rank:
            return -1
        return 0      # remis
        # Krótka wersja (Py2).
        #return cmp((self.suit, self.rank), (other.suit, other.rank))

class Deck:
    """Klasa reprezentująca talię."""

    def __init__(self):
        """Stwórz całą talię kart."""
        self.cards = []
        for suit in range(4):
            for rank in range(1, 14):
                self.cards.append(Card(suit, rank))

    def __str__(self):
        """Zwróć postać napisową talii kart."""
        s = ""
        for i in range(len(self.cards)):
            s = s + " " * i + str(self.cards[i]) + "\n"
        return s

    def shuffle(self):
        """Tasuj talię."""
        import random
        n_cards = len(self.cards)
        for i in range(n_cards):
            j = random.randrange(i, n_cards)
            self.cards[i], self.cards[j] = self.cards[j], self.cards[i]

    def remove_card(self, card):
        """Unicestwianie karty."""
        if card in self.cards:
            self.cards.remove(card)
            return True
        else:
            return False

    def pop_card(self):
        """Rozdanie (wydanie na zewnatrz) jednej karty."""
        return self.cards.pop()   # pop() usuwa ostatni element z listy

    def is_empty(self):
        """Test czy talia jest pusta."""
        return self.cards == []

    def deal(self, hands, n_cards=999):
        """Rozdaj karty do rąk."""
        # Domyślnie rozdaje wszystkie karty do dostępnych rąk.
        # hands to lista lub tuple rąk.
        n_hands = len(hands)
        for i in range(n_cards):
            if self.is_empty():
                break
            card = self.pop_card()    # weź kartę
            hand = hands[i % n_hands] # do której reki
            hand.add_card(card)       # dodaj kartę

class Hand(Deck):   # DZIEDZICZENIE z klasy Deck
    """Klasa reprezentująca rękę gracza."""
    # Ręka to odmiana talii, każda metoda talii działa także dla ręki.

    def __init__(self, name=""):
        """Konstruktor ręki."""
        self.cards = []
        self.name = name   # nazwa ręki, domyślnie pusta

    def __str__(self):
        """Zwróć postać napisową ręki."""
        # To nadpisze metodę __str__ odziedziczoną z Deck.
        s = "Ręka " + self.name
        if self.is_empty(): 
            s = s + " jest pusta\n"
        else:
            s = s + " zawiera\n"
        # Korzystamy z przestrzeni nazw Deck.
        return s + Deck.__str__(self)

    def add_card(self, card):
        """Dodanie karty do ręki."""
        self.cards.append(card)

class CardGame:
    """Ogólna klasa dla gier karcianych."""
    # W tej klasie chcemy mieć wspólne cechy różnych gier.

    def __init__(self):
        """Konstruktor gry karcianej."""
        # Mamy tu nie tylko inicjalizację atrybutu, ale konkretne obliczenia.
        # Ogólnie gra ma dwa atrybuty: deck, hands.
        self.deck = Deck()
        self.deck.shuffle()   # tasowanie talii
        self.hands = []

    def print_hands(self):
        """Wypisz ręce graczy."""
        for hand in self.hands:
            print(hand)

class OldMaidHand(Hand):   # tworzymy rękę do naszej gry

    # Metoda __init__ jest dziedziczona z klasy Hand.
    def remove_matches(self):
        count = 0
        # Kopiujemy karty, bo Python może zgłupieć, gdy pętla jest po liście,
        # która się zmienia (usuwamy karty).
        original_cards = self.cards[:]
        for card in original_cards:
            match = Card(3 - card.suit, card.rank)
            if match in self.cards:
                self.cards.remove(card)
                self.cards.remove(match)
                print("Ręka {}: {} tworzy parę z {}".format(self.name, card, match))
                count += 1
        return count

class OldMaidGame(CardGame):   # tworzymy naszą grę

    def play(self, names):
        # Usuwamy królową trefl.
        self.deck.remove_card(Card(0, 12))
        # Tworzymy rekę każdego gracza jako atrybuty gry.
        for name in names:
            self.hands.append(OldMaidHand(name))
        # Rozdajemy karty do rąk.
        self.deck.deal(self.hands)
        print("---- Rozdano karty")
        self.print_hands()
        # Usuwamy początkowe pary.
        matches = self.remove_all_matches()
        print("---- Pary usunięte, początek gry")
        self.print_hands()
        # Gramy dopóki nie będzie sparowanych 50 kart.
        turn = 0
        n_hands = len(self.hands)
        while matches < 25:         # 25 par to 50 kart
            matches += self.play_one_turn(turn)
            turn = (turn + 1) % n_hands
        print("---- Koniec gry")
        self.print_hands()

    def remove_all_matches(self):
        count = 0
        for hand in self.hands:
            count += hand.remove_matches()
        return count

    def play_one_turn(self, i):
        if self.hands[i].is_empty():
            return 0
        neighbor = self.find_neighbor(i)
        picked_card = self.hands[neighbor].pop_card()
        self.hands[i].add_card(picked_card)
        print("Ręka {} pobiera {}".format(self.hands[i].name, picked_card))
        count = self.hands[i].remove_matches()
        self.hands[i].shuffle()
        return count

    def find_neighbor(self, i):
        n_hands = len(self.hands)
        for next in range(1, n_hands):
            neighbor = (i + next) % n_hands
            if not self.hands[neighbor].is_empty():
                return neighbor

game = OldMaidGame()
game.play(["Adam", "Bogdan", "Cezary"])