de Marcin Moskala

Am folosit programarea pentru a-mi da seama cum funcționează cu adevărat numărarea cardurilor

Am folosit programarea pentru a mi da seama cum functioneaza cu

Când eram mai tânăr, îmi plăcea filmul 21. Poveste grozavă, abilități de actorie și, evident, acest vis interior de a câștiga uriaș și de a învinge cazinoul. Nu am învățat niciodată să număr cărți și niciodată nu am jucat de fapt Blackjack. Însă am vrut întotdeauna să verific dacă acest număr de carduri a fost un lucru real sau dacă doar momeala unui cazinou a pătruns pe internet datorită banilor mari și viselor mari.

Astăzi sunt programator. Deoarece am avut ceva timp suplimentar între pregătirile atelierului și dezvoltarea proiectului, am decis să dezvăluie în cele din urmă adevărul. Așa că am scris un program minim care simulează jocul cu numărarea cărților.

Cum am făcut-o și care au fost rezultatele? Sa vedem.

1611646208 839 Am folosit programarea pentru a mi da seama cum functioneaza cu

Model

Aceasta ar trebui să fie o implementare minimă. Atât de minim încât nici nu am introdus conceptul de card. Cardurile sunt reprezentate de numărul de puncte pe care le evaluează. De exemplu, un As este 11 sau 1.

Pachetul este o listă de numere întregi și îl putem genera așa cum se arată mai jos. Citiți-l ca „patru 10, număr de la 2 la 9 și singur 11, totul de 4 ori”:

fun generateDeck(): List<Int> = (List(4) { 10 } + (2..9) + 11) * 4

Definim următoarea funcție care ne permite să înmulțim conținutul List:

private operator fun <E> List<E>.times(num: Int) = (1..num).flatMap { this }

Pachetul dealerului nu este altceva decât 6 punți amestecate – în majoritatea cazinourilor:

fun generateDealerDeck() = (generateDeck() * 6).shuffled()

Numărarea cardurilor

Diferite tehnici de numărare a cărților sugerează moduri diferite de numărare a cărților. O vom folosi pe cea mai populară, care evaluează o carte ca 1 când este mai mică decât 7, -1 pentru zeci și ași și 0 în caz contrar.

Acesta este Kotlin punerea în aplicare a acestor reguli:

fun cardValue(card: Int) = when (card) {
    in 2..6 -> 1
    10, 11 -> -1
    else -> 0
}

Trebuie să numărăm toate cărțile folosite. În majoritatea cazinourilor, putem vedea toate cărțile care au fost folosite.

În implementarea noastră, ne va fi mai ușor să numărăm puncte din cărțile rămase în pachet și să scădem acest număr din 0. Deci implementarea poate fi 0 — this.sumBy { card -> cardValue(card)} care este un echivalent of -this.sumBy { cardValue(it)}} or -sumBy(::cardValue). Aceasta este suma punctelor pentru toate cărțile folosite.

Ceea ce ne interesează este așa-numitul „Adevărat număr”, care este numărul de puncte numărate împărțit la numărul de punți rămase. În mod normal, jucătorul trebuie să estimeze acest număr.

În implementarea noastră, putem folosi un număr mult mai precis și să calculăm trueCount pe aici:

fun List<Int>.trueCount(): Int = -sumBy(::cardValue) * 52 / size

Strategia de pariere

Jucătorul trebuie întotdeauna să decidă înainte de joc câți bani pariază. Bazat pe Acest articol, Am decis să folosesc regula în care jucătorul își calculează unitatea de pariere – care este egală cu 1/1000 din banii lor rămași. Apoi calculează pariul ca unitate de pariere de ori numărul real minus 1. De asemenea, am aflat că pariul trebuie să fie între 25 și 1000.

Iată funcția:

fun getBetSize(trueCount: Int, bankroll: Double): Double {
    val bettingUnit = bankroll / 1000
    return (bettingUnit * (trueCount - 1)).coerceIn(25.0, 1000.0)
}

Ce sa fac in continuare?

Există o decizie finală pentru jucătorul nostru. În fiecare joc, jucătorul trebuie să facă unele acțiuni. Pentru a lua decizii, jucătorul trebuie să decidă pe baza informațiilor despre mâna sa și a cărții vizibile a dealerului.

Trebuie să reprezentăm mâinile jucătorului și dealerului cumva. Din punct de vedere matematic, mâna nu este altceva decât o listă de cărți. Din punctul de vedere al jucătorului, acesta este reprezentat de puncte, numărul de ași neutilizați dacă poate fi împărțit și dacă este un blackjack. Din punct de vedere al optimizării, prefer să calculez toate aceste proprietăți o dată și să refolosesc valorile, deoarece acestea sunt verificate iar și iar.

Așa că am reprezentat mâna astfel:

class Hand private constructor(val cards: List<Int>) {
    val points = cards.sum()
    val unusedAces = cards.count { it == 11 }
    val canSplit = cards.size == 2 && cards[0] == cards[1]
    val blackjack get() = cards.size == 2 && points == 21
}

Ași

Există un defect în această funcție: ce se întâmplă dacă trecem de 21 și tot avem un as neutilizat? Trebuie să schimbăm Asul de la 11 la 1 atâta timp cât este posibil. Dar unde ar trebui făcut acest lucru? S-ar putea face în constructor, dar ar fi foarte înșelător dacă cineva ar pune mâna de pe cărțile 11 și 11 pentru a avea cărțile 11 și 1.

Acest comportament trebuie făcut în metoda din fabrică. După o analiză, așa am implementat-o ​​(există și un operator plus implementat):

class Hand private constructor(val cards: List<Int>) {
    val points = cards.sum()
    val unusedAces = cards.count { it == 11 }
    val canSplit = cards.size == 2 && cards[0] == cards[1]
    val blackjack get() = cards.size == 2 && points == 21

    operator fun plus(card: Int) = Hand.fromCards(cards + card)

    companion object {
        fun fromCards(cards: List<Int>): Hand {
            var hand = Hand(cards)
            while (hand.unusedAces >= 1 && hand.points > 21) {
                hand = Hand(hand.cards - 11 + 1)
            }
            return hand
        }
    }
}

Posibile decizii sunt reprezentate ca o enumerare (enum):

enum class Decision { STAND, DOUBLE, HIT, SPLIT, SURRENDER }

Este timpul să implementați funcția de decizie a jucătorului. Există numeroase strategii pentru asta.

Am decis să folosesc Aceasta:

Am folosit programarea pentru a mi da seama cum functioneaza cu

L-am implementat folosind următoarea funcție. Am presupus că plierea nu este permisă de cazinou:

fun decide(hand: Hand, casinoCard: Int, firstTurn: Boolean): Decision = when {
    firstTurn && hand.canSplit && hand.cards[0] == 11 -> SPLIT
    firstTurn && hand.canSplit && hand.cards[0] == 9 && casinoCard !in listOf(7, 10, 11) -> SPLIT
    firstTurn && hand.canSplit && hand.cards[0] == 8 -> SPLIT
    firstTurn && hand.canSplit && hand.cards[0] == 7 && casinoCard <= 7 -> SPLIT
    firstTurn && hand.canSplit && hand.cards[0] == 6 && casinoCard <= 6 -> SPLIT
    firstTurn && hand.canSplit && hand.cards[0] == 4 && casinoCard in 5..6 -> SPLIT
    firstTurn && hand.canSplit && hand.cards[0] in 2..3 && casinoCard <= 7 -> SPLIT
    hand.unusedAces >= 1 && hand.points >= 19 -> STAND
    hand.unusedAces >= 1 && hand.points == 18 && casinoCard < 9 -> STAND
    hand.points > 16 -> STAND
    hand.points > 12 && casinoCard < 4 -> STAND
    hand.points > 11 && casinoCard in 4..6 -> STAND
    hand.unusedAces >= 1 && casinoCard in 2..6 && hand.points >= 18 -> if (firstTurn) DOUBLE else STAND
    hand.unusedAces >= 1 && casinoCard == 3 && hand.points >= 17 -> if (firstTurn) DOUBLE else HIT
    hand.unusedAces >= 1 && casinoCard == 4 && hand.points >= 15 -> if (firstTurn) DOUBLE else HIT
    hand.unusedAces >= 1 && casinoCard in 5..6 -> if (firstTurn) DOUBLE else HIT
    hand.points == 11 -> if (firstTurn) DOUBLE else HIT
    hand.points == 10 && casinoCard < 10 -> if (firstTurn) DOUBLE else HIT
    hand.points == 9 && casinoCard in 3..6 -> if (firstTurn) DOUBLE else HIT
    else -> HIT
}
1611646209 632 Am folosit programarea pentru a mi da seama cum functioneaza cu

Să ne jucăm!

Tot ce avem nevoie acum este o simulare a jocului. Ce se întâmplă într-un joc? În primul rând, cărțile sunt luate și amestecate.

Să le reprezentăm ca o listă modificabilă:

val cards = generateDealerDeck().toMutableList()

Noi vom avea nevoie pop funcții pentru acesta:

fun <T> MutableList<T>.pop(): T = removeAt(lastIndex)
fun <T> MutableList<T>.pop(num: Int): List<T> = (1..num).map { pop() }

De asemenea, trebuie să știm câți bani avem:

var bankroll = initialMoney

Apoi jucăm iterativ până când … până când? Conform acest forum, este în mod normal până când se utilizează 75% din cărți. Apoi cărțile sunt amestecate, așa că practic începem de la început.

Deci îl putem implementa astfel:

val shufflePoint = cards.size * 0.25
while (cards.size > shufflePoint) {

Jocul începe. Cazinoul ia o singură carte:

val casinoCard = cards.pop()

Alți jucători iau și cărți. Acestea sunt cărți arse, dar le vom arde mai târziu pentru a permite jucătorului să le includă acum în timpul calculului punctelor (arderea lor ar oferi jucătorului informații care nu sunt chiar accesibile în acest moment).

Luăm și un card și luăm decizii. Problema este că începem ca un singur jucător, dar putem împărți cărțile și să participăm ca 2 jucători.

Prin urmare, este mai bine să reprezentați jocul ca un proces recursiv:

fun playFrom(playerHand: Hand, bet: Double, firstTurn: Boolean): List<Pair<Double, Hand>> =
        when (decide(playerHand, casinoCard, firstTurn)) {
            STAND -> listOf(bet to playerHand)
            DOUBLE -> playFrom(playerHand + cards.pop(), bet * 2, false)
            HIT -> playFrom(playerHand + cards.pop(), bet, false)
            SPLIT -> playerHand.cards.flatMap {
                val newCards = listOf(it, cards.pop())
                val newHand = Hand.fromCards(newCards)
                playFrom(newHand, bet, false)
            }
            SURRENDER -> emptyList()
        }

Dacă nu ne împărțim, valoarea returnată este întotdeauna un singur pariu și o mână finală.

Dacă ne împărțim, lista celor două pariuri și mâini va fi returnată. Dacă îndoim, atunci se returnează o listă goală.

Acesta este modul în care ar trebui să începem această funcție:

val betsAndHands = playFrom(
        playerHand = Hand.fromCards(cards.pop(2)),
        bet = getBetSize(cards.trueCount(), bankroll),
        firstTurn = true
)

După aceea, dealerul de cazinou trebuie să-și joace jocul. Este mult mai simplu, deoarece primesc o carte nouă doar atunci când au mai puțin de 17 puncte. Altfel ține.

var casinoHand = Hand.fromCards(listOf(casinoCard, cards.pop()))
while (casinoHand.points < 17) {
    casinoHand += cards.pop()
}

Atunci trebuie să ne comparăm rezultatele.

Trebuie să o facem separat pentru fiecare mână:

for ((bet, playerHand) in betsAndHands) {
    when {
        playerHand.blackjack -> bankroll += bet * if (casinoHand.blackjack) 1.0 else 1.5
        playerHand.points > 21 -> bankroll -= bet
        casinoHand.points > 21 -> bankroll += bet
        casinoHand.points > playerHand.points -> bankroll -= bet
        casinoHand.points < playerHand.points -> bankroll += bet
        else -> bankroll -= bet
    }
}

În cele din urmă putem arde câteva cărți folosite de alți jucători. Să presupunem că jucăm cu alte două persoane și ele folosesc în medie câte 3 cărți:

cards.pop(6)

Asta e! În acest fel, simularea va juca pachetul întregului dealer și apoi se va opri.

În acest moment, putem verifica dacă avem mai mulți bani sau mai puțin decât înainte:

val differenceInBankroll = bankroll - initialMoney
return differenceInBankroll

Simularea este foarte rapidă. Puteți face mii de simulări în câteva secunde. În acest fel puteți calcula cu ușurință rezultatul mediu:

(1..10000).map { simulate() }.average().let(::print)

Începeți cu acest algoritm și distrați-vă. Aici puteți juca cu codul online:

Blackjack
Kotlin chiar în browser.try.kotlinlang.org

Rezultate

Din păcate, jucătorul meu simulat încă pierde bani. Mult mai puțin decât un jucător standard, dar această numărare nu a ajutat suficient. Poate mi-a fost dor de ceva. Aceasta nu este disciplina mea.

Corectează-mă dacă mă înșel;) Deocamdată, această întreagă numărare a cardurilor arată ca o înșelătorie imensă. Poate că acest site web prezintă doar un algoritm prost. Deși acesta este cel mai popular algoritm pe care l-am găsit!

Aceste rezultate ar putea explica de ce, deși există tehnici cunoscute de numărare a cardurilor de ani de zile – și toate aceste filme au fost produse (cum ar fi 21) – cazinourile din întreaga lume oferă încă Blackjack atât de fericit.

Cred că știu (poate este chiar dovedit matematic) că singura modalitate de a câștiga cu un cazinou este să nu joci deloc. Ca în aproape orice alt joc de pericol.

1611646209 611 Am folosit programarea pentru a mi da seama cum functioneaza cu

Despre autor

Marcin Moskała (@marcinmoskala) este trainer și consultant, concentrându-se în prezent pe oferirea Kotlin în ateliere Android și avansate Kotlin (formular de contact pentru a aplica pentru echipa ta). El este, de asemenea, un vorbitor, autorul articole și o carte despre dezvoltarea Android în Kotlin.