Dyota's blog

PowerShell: Push Your Luck (Blackjack variant)

Gambling

I wanted to see what the chances are on landing the prize on this Magic card. This is basically a Blackjack type of minigame: you draw cards in sequence, and add up the numbers that you draw. The limit is 7; if you hit exactly 7, you get a special effect in the game, and if you go over 7, you bust and get nothing.

I had already done a playing card deck in PowerShell previously, and I lifted most of the code there to simulate the card-dealing aspects of it. I simplified it a lot because I only care about one attribute here, and that is just "value" (i.e. I don't need suits or special pips in this case).

The probabilities here are based on a real 60-card deck that I have, with the actual mana values of the cards.

There are two parts to this exercise: the first is simulating the actual draws, and the second is analysing the results. I do the first part in PowerShell, and I do the second in Power Query, in Excel. I use PowerShell because I had already set up all of the classes for a deck of cards, and it works better when generating lots of iterations using random numbers. Once I actually have the results, Power Query is much better suited to handle the numbers in the form of lists and tables.

Results

These are the results. It shows the percentage change of hitting the outcome at card number X. Blackjack means hitting exactly 7; bust means going over 7; and live means remaining under 7.

Each table represents the value of the first card that you draw. I set it up this way to aid in decision-making - you might want to play differently depending on what number you draw out first.

As an example of how to read the table: if you start with a 3, you have a 0% chance of hitting blackjack with the first card, but you have a 32% chance of hitting blackjack on the second card. However, on that second card, you have a 18% chance of going bust.

Starting card value 2

Outcome Card 1 Card 2 Card 3 Card 4
Blackjack 0% 18% 17% 0%
Bust 0% 0% 77% 100%
Live 100% 82% 6% 0%

Starting card value 3

Outcome Card 1 Card 2 Card 3 Card 4
Blackjack 0% 32% 14%
Bust 0% 18% 86%
Live 100% 50% 0%

Starting card value 4

Outcome Card 1 Card 2 Card 3 Card 4
Blackjack 0% 26% 0%
Bust 0% 47% 100%
Live 100% 27% 0%

Starting card value 5

Outcome Card 1 Card 2 Card 3 Card 4
Blackjack 0% 27%
Bust 0% 73%
Live 100% 0%

Simulation

Full code

using namespace System.Collections.Generic; # for List

# define classes
class Card {
    [int16] $value

    Card([int16] $value) {
        $this.value = $value
    }
}

class Stack {
    [List[Card]] $cardList

    Stack() { [List[Card]] $this.cardList = @() }

    [int] Count() { return $this.cardList.Count }

    [Card] Deal($i) {
        $dealtCard = $this.cardList[$i]
        $this.cardList.RemoveAt($i)
        return $dealtCard
    }

    [void] Add([Card]$card) { $this.cardList.Add($card) }

    [int] Sum() {
        $sum = 0
        foreach ($card in $this.cardList) { $sum += $card.value }
        return $sum
    }
}


class Deck : Stack {
    Deck() {
        $startingDeck = @(
            @{ value = 2; number = 9 },
            @{ value = 3; number = 9 },
            @{ value = 4; number = 11 },
            @{ value = 5; number = 7 }
        )

        $startingDeck.ForEach({
                $thisValue = $_.value
                $thisNumber = $_.number

                for ($i = 0; $i -lt $thisNumber; $i++) {
                    $this.Add([Card]::new($thisValue))
                }
            })
    }

    [Card] Deal() {
        $deckCount = $this.Count()

        $randomNumber = if ($deckCount -gt 1) {
            Get-Random ($deckCount - 1)
        }
        else {
            0
        }

        $dealCard = $this.Deal($randomNumber)

        return $dealCard
    }
}

# define simulation functions

function pushYourLuck ([Deck] $deck) {
    $revealedCard = $deck.Deal()
    $revealPile.Add($revealedCard)
}

function simulate {
    
    $deck = [Deck]::new()
    $revealPile = [Stack]::new()

    while ($revealPile.Sum() -lt 7) {
        pushYourLuck($deck)
    }
    # Write-Host Total is $revealPile.Sum(), result is $result, $revealPile.Count() cards revealed.

    return [Stack] $revealPile
}

# set up simulation

$lives = 50000

[List[Stack]] $results = @()

[List[List[int]]] $resultsJson = @()

# run simulation

for ($l = $lives; $l -gt 0; $l --) {
    $revealPile = simulate
    $results.Add($revealPile)
}

# make results into in a list of lists of just numbers

$results | ForEach-Object {
    # each object here is an array of Cards
    [List[int]] $numbers = @()
    $_.cardList | ForEach-Object {
        # each object here is a Card
        $numbers.Add($_.value)
    }
    $resultsJson.Add($numbers)
}

# save results

$resultsJson | ConvertTo-Json | Out-File "$root\gamble.json"

Analysis

Full code

// results
let
    Source = Json.Document(File.Contents(root & "\gamble.json")),
    #"Converted to Table" = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
    Column1 = #"Converted to Table"[Column1]
in
    Column1

// analysis

let
    subset = results,

    maxNumberOfCards = List.Max(List.Transform(
        results, 
        each List.Count(_)
    )),
    
    limit = 7, 

    cardSequence = {1..maxNumberOfCards}, 

    makeCountRecord = (countingFunction as function) as record=> 
        let 
            values = List.Transform(
                cardSequence,
                countingFunction
            ),
            fieldNames = List.Transform(
                cardSequence,
                each "Card " & Text.From(_)
            )
        in Record.FromList(values, fieldNames),
    
    // counting functions
    count = (subset as list, cardNumber as number) as number => 
        List.Count(List.Select(subset, each List.Count(_) >= cardNumber)),
    
    blackjack = (subset as list, cardNumber as number) => 
        List.Count(
            List.Select(
                subset, 
                each List.Count(_) >= cardNumber
                and List.Sum(List.FirstN(_, cardNumber)) = limit )
        ) / count(subset, cardNumber),
    
    bust = (subset as list, cardNumber as number) =>
        List.Count(
            List.Select(
                subset, 
                each List.Count(_) >= cardNumber
                and List.Sum(List.FirstN(_, cardNumber)) > limit )
        ) / count(subset, cardNumber),
    
    live = (subset as list, cardNumber as number) =>
        List.Count(
            List.Select(
                subset, 
                each List.Count(_) >= cardNumber
                and List.Sum(List.FirstN(_, cardNumber)) < limit )
        ) / count(subset, cardNumber),
    
    // make "columns"
    makeResultsRecord = (subset as list, startingValue as number) as record => [
        startingValue = [Card 1 = startingValue],
        // count = makeCountRecord( each count(subset, _) ), 
        blackjack = makeCountRecord( each blackjack(subset, _) ),
        bust = makeCountRecord( each bust(subset, _) ),
        live = makeCountRecord( each live(subset, _) )
    ], 

    // these are the card values in the deck
    cardValues = {2, 3, 4, 5},

    subsets = List.Transform(
        cardValues, 
        each 
            let 
                thisCardValue = _,
                subset = List.Select(
                    results, 
                    each _{0} = thisCardValue
                )
            in makeResultsRecord(subset, thisCardValue)
    ),

    expandRecord = (subset as record) as table => 
        let
            convertToTable = Record.ToTable(subset),
            columnNames = Record.FieldNames(convertToTable[Value]{1}),
            expanded = Table.ExpandRecordColumn(convertToTable, "Value", columnNames, columnNames)
        in
            expanded,

    combined = Table.Combine(
        List.Transform(
            subsets,
            each expandRecord(_)
        )
    ),
    #"Changed Type" = Table.TransformColumnTypes(combined,{{"Card 2", Percentage.Type}, {"Card 3", Percentage.Type}, {"Card 4", Percentage.Type}})

in
    #"Changed Type"

#montecarlo #powerquery #powershell