CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
1CC1000 - Information Systems and Programming - Lab: object-oriented programming

1. Presentation

Wish Solitaire is a simple solo card game that requires a 32-card deck to play.

To begin, shuffle the cards and deal the entire deck into 8 piles of 4 cards each. Arrange the piles in a column.

Flip over the top card of each pile so they are face-up. Then, remove any pairs of cards that have the same rank, regardless of suit — for example, two 10s, two Kings, etc.

Once you remove the top card of a pile, flip the next card in the pile so it becomes face-up.

The goal of the game is to remove all the piles by matching pairs of cards.

We will use classes and objects in Python to implement this game.

2. Card

 

2.1. Suit of a card

There are only four card suits: spades, hearts, diamonds, and clubs. This kind of data (finite set of named values) is called an enumeration, and programming languages generally provide specific support for these types.

In Python, you need to define a class that inherits from the Enum class (see the documentation).

from enum import Enum

class Suit(Enum):
    SPADE = 1
    HEART = 2
    DIAMOND = 3
    CLUB = 4

By convention, the names of the members in the set are upper-cased.

As you can see, it is mandatory to assign a value (not necessarily an integer) to the members of the enumeration. If you want this value to be a string (the characters “♠”, “♥”, “♦”, and “♣” exist in Unicode), you can use the variant StrEnum, which results, when using the functional form, in:

from enum import StrEnum

Suit = StrEnum('Suit', [('SPADE', '♠'), ('HEART', '♥'), ('DIAMOND', '♦'), ('CLUB', '♣')])

Test this type by trying out some instructions. For example, you can print a suit value, or compare two suit values:

print(Suit.SPADE)
print(Suit.SPADE == Suit.HEART)

2.2. Definition of a card

We will use a Card class to represent a playing card. An object of this class will have three attributes: _suit (of type Suit), _order (an integer between 1 and 13), and _hidden (a boolean). The attributes _suit and _order uniquely define the card, they are not modifiable after initialization, but will be accessible as a property (decorator @property on the accessor). The _hidden attribute indicates whether the card is face-up (False) or face-down (True). Initially, the card will be visible; two methods, reveal(self) and hide(self) will update its value.

Define this Card class with its constructor and specified methods.

2.3. Representation of a card as string

Add an __str__(self) method to convert a card into a string. This method is implicitly called by str(). If the card is hidden, it will be represented as [██]. If the card is face-up, the representation should be something like [9♠], [K♥], [A♦], or [10♣], depending on the rank and suit.

Create and display some cards to check your code.

Cards are also available as Unicode characters (Unicode block U+1F0A0 à U+1F0FF): 🂩 🂾 🃁 🃚, but when they are displayed in a terminal with the standard size (🂩 🂾 🃁 🃚), they can be hard to recognize.

2.4. Comparison of cards

Create two identical cards and compare them using ==. Add a __eq__(self, other) method so that the result is the one expected.

Even though only the value of the cards is taken into account in this game, the comparison method will consider the color (but not whether it is visible) so that this Card class can be used in other games.

3. Deck

We need a class representing a deck of 32 cards (from 7 to Ace). This class will have a single attribute: a list of Card objects.

Define a Deck class with a constructor that initializes the deck by creating all 32 cards, then shuffling them.

You can use Python’s random.shuffle() function to shuffle the elements in a mutable sequence (such as a list).

Add an __str__(self) method so that a deck can be displayed.

Add a pop(self) method which remove and return the top card from the deck (None if the deck is empty).

4. Stack

We need a class representing a stack of cards. This class will have a single attribute: a list of Card objects.

Define a Stack class with a constructor that initializes the stack by drawing four cards from the deck received as argument. Only the top card is revealed.

Add an __str__(self) method so that a stack can be displayed.

Add a __len__(self) method which returns the number of cards in the stack.

Add a pop(self) method which remove and return the top card from the stack (None if the stack is empty). The next card, if available, is revealed.

5. Game

The Game class maintains the game state using 8 stacks. Define this class with a constructor and a display method that follows the example below:

0: [██][A♥]
1: [██][██][██][Q♠]
2: [██][██][██][K♥]
3: [██][██][A♦]
4:
5: [██][██][██][Q♥]
6: [██][██][A♣]
7: [██][██][10♣]

The is_clearable(self, i, j) method checks if the top cards of stacks i and j have the same value. The method returns False if at least one of the stacks is empty or if the top cards have not the same value.

The clear(self, i, j) method removes the top cards from stacks i and j if the cards have the same value (checked using is_clearable(self, i, j)). It return True if the cards have been removed, False otherwise.

The is_over(self) method returns False if there are at least two stacks with clearable top cards, and True otherwise. This method checks all possible pairs of stacks.

The is_cleared(self) method returns True if all stacks have been cleared, meaning they contain no cards.

6. Main

If you correctly implements the methods, the following main function should allow a human player to play Wish Solitaire.

def main():
    g = Game()
    while not g.is_over():
        print(g)
        try:
            i, j = map(int, input().split())
            if 0 <= i < 8 and  0 <= j < 8:
                if not g.clear(i, j):
                    print(f"Cannot removed cards from stacks {i} and {j}")
            else:
                print("Invalid stack number")
        except ValueError:
            print("Input two indexes")
    print(g)
    if g.is_cleared():
        print("You win!")
    else:
        print("You lose!")

main()