Exploring algorithms in Picture Poker

because I love the math and I love the game and I graduated forever ago and I have to stay sharp.


Note: This article was written with lots of love by a human being, without the use of generative AI. Also I don't condone gambling.

Intro

Hi, I’m a Zoomer, so I grew up playing Mario games on the Nintendo DS. I'm 24 now and yes I still own a DS and I still play on it from time to time. The other day, I was playing Picture Poker and if you’ve never played it, you should. It features a simple deck of cards and an equally simple scoring system, so like any nerd I decided I would try to work out the math behind the game.


The game (you just lost)

Picture Poker is a minigame included in two hit DS games, New Super Mario Bros., and Super Mario 64 DS. The minigame is played with a modified deck of cards, presumably for simplicity and because children are incapable of counting to thirteen. Someone also decided that those same children should have no problem collecting 100 coins in Rainbow Road or finding all of those white glowing rabbits for the 150th star. Those stars were a challenge, even for 24-year-old me.

But anyways, the deck has only 6 unique faces instead of the usual thirteen, and the cards also don't have any suits. from highest to lowest rank, the cards in the deck have the following faces: Star, Mario, Luigi, Flower, Mushroom, and Cloud. I'll represent these with emojis because it's 2025: ⭐🔴🟢🌻🍄☁️.

Luigi holds a magical deck of cards where these are all equally likely to be drawn from the deck at any point in time. In other words, we can assume the deck is infinite, or that each draw is an independent event, very much opposed to the behavior of a finite deck of cards.

You also have coins you can use to bet. It costs you one coin to play.

The game begins with Luigi the dealer dealing a hand of 5 cards to you and another 5 for himself. You each have a moment to look at your hands. You may also increase your bet at this time. You can choose a nomber of cards in your hand to discard, after which the dealer will replenish your hand. That means you may choose keep all of your cards or discard all of them, or otherwise decide strategically. Luigi will do the same for his hand. Then, both players reveal their hands. The winner is determined by comparing the combinations of one player's hand to the other.

Combination notation

Hands are compared by the combination they compose. We will be using a slightly different notation than of that in the game. We'll use the variables AA and BB to each represent a face, with the condition ABA \neq B. It also helps to sort the cards by frequency descending, meaning that the a three-of-a-kind would appear before a pair, and a pair would appear before a single card.

Let's first understand the combination notation:

Now that we undersand combinations, here are the winning combinations from best to worst:

Winning combinations

  1. Flush (x16): A5A^5
    • Five cards of the same face, five of a kind.
  2. Four of a kind (x8): A4A^4
    • Four cards of the same face and a card of a different face.
  3. Full house (x6): A3B2A^3 B^2
    • Three of a kind and one pair
  4. Three of a kind (x4): A3A^3
    • Three of a kind and two other cards
  5. Double pair (x3): A2B2A^2 B^2
    • Two pairs and another card
  6. Single pair (x2): A2A^2
    • One pair and three other cards

Everything else is considered Junk and not a winner (x0): \empty


What are the odds?!

Okay, let's finally crunch some numbers! Let's answer a simple question about the game's odds:

What's the probability I get a Flush? (before discarding)

Recall that a flush consists of a hand of five cards of a kind. Let's compose our hand one card at a time.

The odds of this hand being composed by each of the draws can be found by multiplying the probabilities together:

66×16×16×16×16=665=164=11296 \dfrac{6}{6} \times \dfrac{1}{6} \times \dfrac{1}{6} \times \dfrac{1}{6} \times \dfrac{1}{6} = \dfrac{6}{6^5} = \dfrac{1}{6^4} = \dfrac{1}{1296}

This is equal to a 0.07716049382%0.07\overline{716049382}% chance, identical to rolling five dice and them all landing on 6. Incredible luck indeed. For comparison, the odds of encountering a Shiny Pokémon is around 14096\dfrac{1}{4096}, and in Minecraft, the odds of a given sheep being colored pink is about 1610\dfrac{1}{610}.

What's the best discarding strategy?

idk tbh. Left as an excersize to the reader?


The deterministic finite automaton

I drew a DFA that receives 5 sorted cards as an input, and ACCEPTs the string if it matches one of the winning combinations.

Here's a cleaner version of the DFA I drew on a whiteboard.

s₀ A A^2 A^3 A^4 A^5 A^4 A^3B A^3B^2 A^3 A^2B^2 A^2 A^2B :( START A A A A A B B B * B B * everything else

General case for solving a DFA with input length nO(n)n \Rarr \mathcal{O}(n)

In this game, Luigi always hands the player 5 cards, so this should run in constant time. But this DFA only works if the input is sorted! Let's come up with an algorithm to do this.


How to sort a hand of cards

How can we sort a hand of cards? Let's compose a function that sorts a hand. ABCABAABBC \text{ABCAB} \rarr \text{AABBC} . Keep in mind that we must sort the hand by the frequency of the cards, and ties should be broken by lexicographical order. Let's also leverage the fact that we have a tiny alphabet and a fixed hand size.

Recall our given alphabet:

Σ={ABCDEF} \Sigma = {ABCDEF}

# Made with love <3

def freq_sort(s Σn\in \Sigma^n): Σn\Sigma^n
    freqs = An empty vector of length Σ|\Sigma|
    buckets = An array of length s+1|s| + 1, all values initialized to the empty string.

    # First step: count occurences
    for each character of s:
        freqs[character]++

    # Second step: After gathering frequencies, sort them into buckets
    for c of Σ\Sigma: # Loop over all of the symbols in the alphabet, in lexicographical order
        count = freqs[c]
        if 0 < count:
            str = Concatenate buckets[count] and c
            buckets[count] = str

    # Last step: Loop over the buckets in reverse order to sort the hand
    out = the empty string
    for bucket, index of buckets (in reverse):
        # The index indicates the frequency :)
        out += bucket * index
    
    return out