Skip to content

Programming Solitaire Games

ichabod801 edited this page Apr 28, 2020 · 5 revisions

The Basics

To start off with our exploration of solitaire games in the t_games system, let's look at a simple, trimmed down version of the FreeCell game. This is just the minimum need to play the standard FreeCell game that you tend to find on people's phones:

class SimpleCell(solitaire.Solitaire):
    """
    A simple version of FreeCell. (Solitaire)

    Overridden Methods:
    set_checkers
    set_options
    """

    categories = ['Card Games', 'Solitaire Games', 'Open Games']
    credits = CREDITS
    name = 'Simple Cell'
    rules = RULES

    def set_checkers(self):
        """Set up the game specific rules. (None)"""
        super(FreeCell, self).set_checkers()
        # Set the game specific rules checkers.
        self.build_checkers = [solitaire.build_one]
        self.lane_checkers = [solitaire.lane_one]
        self.pair_checkers = [solitaire.pair_down, solitaire.pair_alt_color]
        self.sort_checkers = [solitaire.sort_ace, solitaire.sort_up]
        # Set the dealers
        self.dealers = [solitaire.deal_all]

    def set_options(self):
        """Set the game options. (None)"""
        self.options = {'num-cells': 4, 'num-tableau': 8}

That's a lot simpler than most t_games you are going to see, even considering that it's not taking into account any optional variants the player might want to play. That's because of the large similarities across solitaire games. That allows most of the code to be in the Solitaire class.

Rule Checkers

First we have the set_checker method. That sets the lists of functions that validate moves based on the rules for the specific solitaire game being played, known as the Rule Checkers. First is build_checkers, which checks stacks of cards (including single cards) moving moving on(to) the tableau. Note that this checks the whole stack, not that each card in the stack is correctly on top of the previous card. So it just checks build_one, which is the rule that you can only make a move if you could make it one card at a time, using the free cells and empty lanes. The build_checkers attribute is often empty, and is empty in Klondike, for example.

Next is lane_checkers, which is similar to build_checkers, but it checks moving stacks of cards into empty lanes. Here, again, it checks that the move could be done one card at a time. In Klondike it would check that the base card of the stack being moved is a king.

Next is pair_checkers. This is what checks that each card in a stack is correctly built on the previous card. It has two rule functions in it, one to check that the cards are played in descending rank order, and one to check that each card is the opposite color of the previous card.

The pair_checker functions are used in conjunction with the build_checker and lane_checker functions. Say you want to move a card on the tableau. That card has other cards played on top of it. The pair_checker functions are used to make sure the cards in that stack have all been played correctly. In this case, that they are in descending rank and alternating color. If that is not the case, any attempt to build or lane that card will fail.

This brings up another point about the use of rule checkers. There are rules for each type of move in solitaire that are considered to be universal across all solitaire games. For example, when building on the tableau, you must be building onto the top card of a tableau pile, the card at the base of the stack being moved must be face up and not yet sorted, and all the cards in the stack have to have been played correctly according to the pair checkers.

The final checker we have is sort_checkers, which determine which cards can be sorted to the foundations. Here the rules are that aces may be sorted to empty foundations, and the next highest rank may be sorted to non-empty foundations.

These are not the only lists of rule checking functions that are used in the parent Solitaire class. The other ones are:

  • free_checkers: Checks cards you are trying to put in free cells.
  • match_checkers: Checks cards you are trying to pair with each other.

You will note that super() was used to call the parent class's set_checker() method, which sets the defaults for all of the rule checker lists. All of them default to empty except match_checkers, which defaults to [match_none], (matching cards is not allowed).

The set_checkers method also sets the dealers attribute. These functions deal the initial layout of the cards. For FreeCell ([deal_all]), it simply deals all the cards out onto the tableau. Klondike, on the other hand, uses [deal_klondike, deal_stock_all]. These deal the standard Klondike layout of seven piles in increasing size, and then deals the rest of the cards into the stock.

As of version 50.x of t_games, there are 76 rule checker functions defined in t_games/card_games/solitaire_games/rule_checkers.py. You can make a wide variety of solitaire games with just the rule checker functions defined there and the solitaire options described in the next section. If you want to go beyond that you will need to write your own rule checkers, and maybe override methods of the Solitaire class. For information on that, check out the More details section, below.

Solitaire Options

Solitaire games have a dictionary of options specific to solitaire games. This is the options attribute. You can set some of the options yourself, as done here in the set_options method. The rest of the options have defaults, which are set by the parent Solitaire class in the solitaire_opitions method (which you should probably not mess with).

For FreeCell, we only need to change two of the standard solitaire options: num-cells (the number of free cells available) and num-tableau (the number of tableau piles). The full list of standard solitaire options is:

  • deck-spec: The deck specifications. This is a list or tuple of the parameters passed to the TrackingDeck class using *args (besides the game class itself). (Defaults to [])
  • max-passes: The number of times you can go through the stock, -1 for no limit. (Defaults to -1)
  • num-tableau: The number of tableau piles. (Defaults to 7)
  • num-foundations: The number of foundation piles. (Defaults to 4)
  • num-reserve: The number of reserve piles. (Defaults to 0)
  • num-cells: The number of free cells. (Defaults to 0)
  • turn-count: The number of cards turned over from the stock each time. (Defaults to 3)
  • wrap-ranks: A flag for kings being considered one rank below aces. (Defaults to False)

More Details

This section has more details on the classes used in solitaire games, to help with programming more complicated or esoteric solitaire games.

TrackingCard

The card class used in the Solitaire class are a sub-class of the standard cards.Card class called TrackingCard. TrackingCard instances keep track of where they are in the game (using the game_location attribute) and in the deck (using the deck_location attribute). Now, all the locations a TrackingCard would be in are lists of TrackingCards. So these are just references to those lists. You can indeed change the state of the deck and/or the game through the card.

Note that if the card is not in the game, it's game_location and deck_location should be pointing to the same space.

TrackingDeck

A TrackingDeck is a deck of TrackingCards. It has three list attributes that are used for values of TrackingCard.deck_location: cards (the cards still in the deck), in_play (the cards being used in the game), and discards (the cards neither in the deck nor the game).

TrackingDeck also has a card_map attribute. This is a dictionary of all the cards, with the two-letter abbreviation of the card (like JH for Jack of Hearts) as the key. This allows the game to find where a specific card is easily. It gets the card abbreviation from the user. It sends that to the find method of the TrackingDeck, which uses card_map to return the actual TrackingCard object. Then the game can use the game_location attribute of the TrackingDeck to get the list that the card is in. This is much easier than checking every list of cards in the game to find the card.

Solitaire

The Solitaire class is a sub-class of Game which provides the base functionality for the single-deck solitaire games.

Piles

The Solitaire class has several attributes that are lists of TrackingCards, which can be referenced by the game_location attribute of a TrackingCard:

  • cells: The free cells. (list of TrackingCard)
  • foundations: The piles that cards are sorted to, and the filling of which wins the game. (list of list of TrackingCard)
  • reserve: Piles where building is not allowed. (list of list of TrackingCard)
  • stock: A pile of face down cards that can be turned over. (list of TrackingCard)
  • tableau: The main playing area, where building is done. (list of list of TrackingCard)
  • waste: A pile of face up, playable cards turned over from the stock. (list of TrackingCard)

Typically a card needs to be at the top of the pile (pile[-1]) to be playable. Each one of these has a _text function (cell_text, foundation_text, and so on) that converts the pile or list of piles into a string. The str method of Solitaire combines all these to make the text of the layout as it is shown to the user. These are the methods you would override to display the game in a non-standard way (see bisley_game.py or pyramid_game.py for examples).

Moves

The Solitaire class provides several commands for the typical moves of a solitaire game. These moves are:

  • build: Put a card or stack of cards onto another card on the tableau.
  • free: Put a card in one of the free cells.
  • lane: Put a card or stack of cards into an empty pile on the tableau.
  • match: Pair two cards together and sort them (for games like Pyramid and Monte Carlo).
  • sort: Move a card to the correct foundation. (There is also an auto command to sort multiple cards.)
  • turn: Turn cards from the stock over into the waste.

These can be overridden for specific games. For example, Monte Carlo and Spider override do_turn so that the turn command puts cards on the tableau piles rather than in the waste. However, you generally want to achieve game specific rules with rule checker functions, as described above.

All of these commands work pretty much the same. They all have a do_ method, obviously. So the build move uses the do_build method. That method first checks the arguments to the build command, to make sure they are cards and the right number of cards. Then it finds the cards that are passed as arguments. In the case of build, and other commands that can move stacks of cards, the do_build method also finds the stack of cards played on top of the card designated to move. Each move also has a _check method. So do_build sends the cards that it found to the build_check method. The build_check method checks the cards for compliance with any universal rules. For build this means the destination card must be on top of a tableau pile, and the moving card must be face up, not in the foundations, and at the base of a legal stack (as determined by the super_stack method). If all of those conditions are met, do_build passes the cards it found to each of the functions in build_checker. If all of those functions return True, the cards are sent to the transfer method.

The transfer method is what actually moves the cards around in a solitaire game. None of the move functions like do_free or do_sort actually move cards. They all call transfer to move the cards. The transfer method makes sure that the tracking information in the TrackingCards is updated. It also handles things like turning moved cards face up (if the face_up parameter is set to True), and turning revealed cards on the tableau face up (as in Klondike).

If the track parameter is True, the transfer method also keeps track of which cards were moved where (in the moves attribute). This allows for undoing moves. One thing to note about undoing moves is that some moves which seem like a single move to the user are done with multiple calls to transfer, and thus multiple moves appended to the move attribute. So that these sub-moves can all be undone as one move, there is an undo_ndx parameter. If the undo_ndx of a stored move is greater than 0 (evaluates to True), then the next stored move is also undone. Typically undo_ndx is how many moves need to be undone, but it really just needs to evaluate to True to get the next move undone. Also note that if track is True and undo_ndx is not (it's 0), then that is counted as a move in terms of the score given at the end of the game. Sometimes it is easier to do things for a solitaire game without using transfer, but if you don't use it you are going to mess up undoing moves and the scoring.

There is also the guess and guess_two methods. If a card is entered without a move, Solitaire tries to guess what move the user wanted to make. This is handled through Solitaire.default, which calls guess if it detects a card being input, and guess_two if it detects two cards being input. The guess method tries the following moves in order until it detects a legal moves: sort, build, match, free, and lane. The guess_two method checks build then match.

MultiTrackingDeck

MultiTrackingDeck is a deck of TrackingCards where you can have multiple copies of the same card (like two Queen of Hearts). Since their can be multiple cards with the same suit and rank, the card_map attribute uses lists of TrackingCards for values. Likewise, the find method of MultiTrackingDeck returns a list of cards matching the provided string.

MultiSolitaire

MultiSolitaire is a sub-class of Solitaire. It handles getting lists of cards from a MultiTrackingDeck instead of getting single cards from a TrackingDeck. When a move is made, it takes all of the possible cards that the user could be referencing and tries to make the move with each possibility. It keeps track of all the legal moves it finds. It makes one of them, and stores the rest in the alt_moves attribute. The player can then use the 'alternate' command to make one of the other moves, assuming the computer picked the wrong one to make. MultiSolitaire also allows for Location Specifiers, so that you specify which of the multiple possibilities you want to do ahead of time, rather than cycling through all of the alternate moves.

Clone this wiki locally