A Solitaire Investigation

At a recent family gathering, my father showed me a solitaire game that his father used to play. Its rules are simple:

  • The goal is to discard all cards.
  • Play one card at a time from the top of the deck, forming a single row of cards. Always play the next card to the right of the one preceding it.
  • You can remove three cards at a time in any combination from the ends of the row (3 from one side or 2 from one and one from the other).
  • The removed cards must add up to either 10, 20, or 30, with face cards counting as 10 and aces as 1.
  • The final discard may be four cards, which are guaranteed to add up to a multiple of 10.

I wondered whether or not there could be a strategy to this game, or if the outcome is strictly determined by the lay of the cards after dealing them. That is, are there some unwinnable shuffles? And can one's strategy affect the odds of winning given a certain shuffle?

Probability of Winning

A first question might be to ask what the probability of winning is given a simple greedy strategy of always removing cards when possible. Note that after each deal, there's the potential that multiple different removals might be possible.

def remove_cards(in_play):
    """Remove cards from an ordered list of denominations
       currently in play, and return the remaining cards
       after one removal.
    """
    remaining = in_play[:]

    # Cannot remove if length is less than three.
    if len(in_play) >= 3:

        # Three from the end.
        if sum(in_play[-3:]) % 10 == 0:
            remaining = in_play[:-3]

        # 2 from the end; 1 from the start.
        elif (in_play[0] + sum(in_play[-2:])) % 10 == 0:
            remaining = in_play[1:-2]

        # 1 from the end; 2 from the start.
        elif (sum(in_play[:2]) + in_play[-1]) % 10 == 0:
            remaining = in_play[2:-1]

        # All three from the start.
        elif sum(in_play[:3]) % 10 == 0:
            remaining = in_play[3:]

    return remaining

This will handle a single removal from the row of in-play cards, using a simple algorithm that arbitrarily favors removing the most recently played cards first. Now wrap this function in a game simulator that removes cards after each deal until the result does not change.

from random import shuffle

def simulate_game():
    """Simulate a new solitaire game."""
    # Four suits with numbers 1 through 10, plus face cards.
    cards = (range(1, 11) + [10] * 3) * 4
    shuffle(cards)

    in_play = []
    for card in cards:
        # Add a new card.
        in_play.append(card)

        while True:
            # Remove cards until it is impossible.
            previous = in_play
            in_play = remove_cards(in_play)

            if previous == in_play:
                break

    return in_play

If any cards remain in play after this game, we have lost. This makes it straightforward to find the approximate success probability.

def find_success_prob(n=100):
    wins = 0.0
    for _ in range(n):
        remaining = simulate_game()

        # A victory if we have either one or four cards adding
        # to a multiple of ten.
        if len(remaining) <= 4 and sum(remaining) % 10 == 0:
            wins += 1

    return wins / n

A simple greedy strategy that prioritizes removing the most recenty played cards yields somewhere around a 22% success rate (21.8605% with a million simulations). However, this does not answer the question of whether choosing a different strategy might make one more or less likely to succeed.

A Different Strategy?

In fact, there is some reason to believe that the opposite strategy might be more effective — removing cards from the beginning of the row FIRST. After all, if a row starts with [9, 9, 10, ...], then these cards are in a sense "stranded" until either a 10 - ace combination or a two arrives up. This is a fixed, known requirement, whereas it seems intuitively that there might be more leeway at the end of the list for new possibilities to open up.

To test this, I reversed the prioritization order in the discard_cards function and repeated the one million simulations. To make a long story short, reversing the strategy and favoring removing things from the left side of the row yields a 21.8360% success probability calculated with a million simulations.

Using R's prop.test function to perform a chi-squared test reveals the difference to be statistically nonsignificant, p = .676. And of course, just looking at the two values makes it clear that the difference would be practically insignificant in any case — both values are essentially 22%.

The Solitaire Gamble

At least in this version of solitaire, it appears to be a simple gamble whether or not a shuffle will reveal a working combination. Of course, with imperfect shuffling and repeated plays, it will become more and more likely that clusters of cards summing to 10 or multiples thereof will reappear, making the 22% chance of success pessimistic.

But regardless of whether or not strategy makes any difference, solitaire that depends solely on the shuffle apparently provides enough reward to sustain excitement and suspense. Of course, my father's father never had access to the Internet, either.