JavaScript BlackJack App

En bild på färdig BlackJack JavaScript App

I denna artikel tänkte jag skapa en JavaScript BlackJack app.

Till att börja med behöver jag bestämma mig för dom BlackJack regler (finns massa varianter) som jag väljer att använda. För att inte göra det för krångligt ska jag hålla mig till en så enkel variant som möjligt. Målet med BlackJack går som bekant ut på att få bättre kort än givaren och komma så nära 21 som möjligt utan gå över (bli tjock).

  1. Spelaren börjar med en pott på 10 000:-
  2. Betting gränsen sätts till mellan 5 – 50:-
  3. Givaren blandar 6 lekar (6 * 52 = 312 kort)
    1. Ett stopp-kort sätts mellan 60 – 75 från sista kortet vilket betyder omblandning av lekar.
    2. Ett ess är vanligtvis lika med 1 eller 11 poäng, medan Knektar, Drottningar och Kungar räknas som 10 poäng och kort märkta 2 – 10 värderas enligt siffran på kortet.
  4. Givaren delar ut ett kort öppet till spelaren och sig själv och ett andra kort öppet till spelaren och ett hålkort med baksidan upp till sig själv.
    1. Om spelaren har 10, J, Q eller K och ett Ess, är det en sk. naturlig BlackJack. Om givaren inte har naturlig BlackJack, betalar han genast ut vinst till spelaren 3 : 2.
    2. Om givaren har en naturlig BlackJack medans spelaren inte har det förlorar spelaren på en gång.
  5. Nu kollar givaren om spelaren vill fortsätta ta kort ”hit” eller stanna dvs. är nöjd.
  6. Spelaren stannar eller ”tjock” / ”busted”
  7. Dealern öppnar sitt hålkort och drar kort tills han har 17 eller mer var efter han måste stanna.
    1. Om dealern inte är över 21 dvs. busted jämför man vem som kom närmast 21
  8. Extra – Lägg till Möjlighet att Splitta par, Dubbla och ”Försäkra sig”.

Nu har jag satt reglerna, så nu gäller det att översätta detta till JavaScript logik. Jag gör detta steg för steg.

Filer

Du kan ladda ner filerna här nedanför.

Dom filer jag behöver är:

  • index.html
  • css/styles.css
  • img/ (innehållande sprite PNG för kortlek)
  • js/blackjack.js

Steg för steg..

  1. Skapa 6 kortlekar js/blackjack.js
    1. En Array för färger / sviter, en för värden och en för dom 6 lekarna.
      1. Använd dom två första för att skapa leken/lekarna.
  2. Blanda lekarna.
    1. Loopa igenom lekarna och blanda dom.
  3. Skapa Objekt för spelare och givare:
    1. Spelar-objektet ska ha namn/värde par för, insats, vinster, kort i handen, antal ess och summan av kortens värde.
    2. Givar-objektet ska ha det samma förutom bet och vinster.
  4. Skapa HTML dokument med layout:
    1. Input element för användarnamn.
    2. Knappar för att starta spelet, betta, dra kort och stanna.
  5. Välj button Element
  6. Lyssna efter ”klick”
    1. Lyssna efter klick på knappen id=username
    2. Lyssna efter klick på Bet knapp
      1. Uppdatera player.bet med x och player.pot med -x
    3. Lyssna efter klick DEAL knapp
      1. Starta spel genom att dela ut två kort
        1. Kontrollera om spelare eller givare har 21
      2. Uppdatera player.sum och dealer.sum med summan av korten i handen
    4. Lyssna efter klick HIT knapp
      1. Dela ut ett kort till spelare
      2. Uppdatera player.sum
    5. Lyssna efter klick STAND knapp
      1. Givaren tar kort tills han har 17+
      2. Kontrollera om spelare eller givare har vunnit
        1. Givare vinner, player.losses + 1
        2. Spelare vinner, player.wins + bet * 2

Skapa 6 Kortlekar

Jag börjar med att skapa dom 6 kortlekarna, och för att loopa genom alla värden i en Array kommer jag att använda Array.forEach() i funktionen createDecks()

js/blackjack.js

// En konstant Array med med 4 färger / sviter
const suits = ['heart', 'spade', 'diamond', 'club'];

// ..och en Array med 13 möjliga värden
const values = ['a', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'j', 'q', 'k'];

// ..och till sist en Array för lekarna
const decks = [];

// ..och så en funktion som skapar n antal kortlekar i decks[] Array
function createDecks(n) {
    // for loop med n som står för antal lekar
    for (var i = 0; i < nr; i++) {
        // jag använder mig av Array.forEach för varje värde i suits..
        suits.forEach(function (suit) {
            // ..och för varje värde i values
            values.forEach(function (value) {
                decks.push(suit + '_' + value);
            });
        });
    }
}

// Skapa 6 lekar
createDecks(6);

Blanda lekarna

För att blanda lekarna skapar jag ytterligare en funktion, shuffle():

js/blackjack.js

// Blanda decks Array
function shuffleDecks() {
    for (let i = decks.length; i > 0; i--) {
        // Hämta ett slumpmässigt index => decks.length till 0
        let j = Math.floor(Math.random() * i);
        
        // lagra decks[i] temporärt
        let temp = decks[i];
        
        // byt plats med decks[i] och decks[j]
        decks[i] = decks[j];
        decks[j] = temp;
    }
    // Sätt STOPP kort mellan 60 - 75 kort från sista kortet
    decks[decks.length - Math.floor(Math.random() * (74 - 59) + 59)] = "STOP";
}

// Blanda lekarna
shuffle();

Mer om JavaScript slumpmässigt nummer på w3schools.com.

Objekt för spelare & givare

const player = {
    username: '',
    wins: 0,
    nrWins: 0,
    losses: 0,
    bet: 0,
    pot: 10000,
    hand: [],
    aces: 0,
    sum: 0
};

const dealer = {
    hand: [],
    aces: 0,
    sum: 0
};

const game = {
    init: false
};

index.html

Eftersom jag valde att använda Bootstrap, så blir stylingen av layouten att bli mycket enklare.

<!doctype html>
<html lang="en">

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">


    <title>JS BlackJack Game</title>
    <!-- favicon -->
    <link rel="icon" href="favicon.ico">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <!-- Min egen stilmall -->
    <link rel="stylesheet" href="css/styles.css?version=1.1.4">
</head>

<body>

    <div class="container">
        <header class="row">
            <!-- col-6 tar upp halva bredden -->
            <div class="col-6">
                <input class="form-control" type="text" name="username" id="username" placeholder="Username" style="width: 100%;" />
            </div>
            <div class="col-6">
                <button type="button" id="start" class="btn btn-outline-primary" style="width: 100%;">START</button>
            </div>
        </header>
        <hr />
        <div class="row">
            <div class="col-6">
                <div id="player-card">
                    <div class="card border border-info">
                        <div class="bg-info card-header text-center" id="player">
                            <h4 class="text-white font-weight-light">Player Cards</h4>
                        </div>
                        <div id="player-cards" class="card-body"></div>
                        <div class="bg-info card-footer" id="player-footer">
                            <div class="text-white" id="sum-player">SUM PLAYER: 0</div>
                        </div>
                    </div>
                </div>

                <hr />

                <div id="dealer-card">
                    <div class="card border border-secondary">
                        <div class="bg-secondary card-header text-center" id="dealer">
                            <h4 class="text-white font-weight-light">Dealer Cards</h4>
                        </div>
                        <div id="dealer-cards" class="card-body"></div>
                        <div class="bg-secondary card-footer" id="dealer-footer">
                            <div class="text-white" id="sum-dealer">SUM DEALER: 0</div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="col-6">
                <div class="btn-group-vertical" style="width: 100%;">
                    <button type="button" class="btn btn-outline-primary btn-lg" id="bet">BET</button>
                    <button type="button" class="btn btn-outline-secondary btn-lg" id="deal" disabled>DEAL</button>
                    <button type="button" class="btn btn-outline-info btn-lg" id="hit" disabled>HIT</button>
                    <button type="button" class="btn btn-outline-info btn-lg" id="stand" disabled>STAND</button>
                    <button type="button" class="btn btn-outline-warning btn-lg" id="reset" disabled>RESET</button>
                </div>

                <hr style="margin-top: 3rem; margin-bottom: 1.8rem;" />

                <div class="card" style="width: 100%;">
                    <ul class="list-group list-group-flush">
                        <li class="list-group-item">MAX BET IS 250$</li>
                        <div class="progress">
                            <div class="progress-bar" role="progressbar" style="width: 0%;" aria-valuemin="0" aria-valuemax="100"></div>
                        </div>
                        <li class="list-group-item" id="bet-output">BET: 0</li>
                        <div class="progress">
                            <div id="bets-bar" class="progress-bar bg-primary" role="progressbar" style="width: 0%;" aria-valuemin="0" aria-valuemax="100"></div>
                        </div>
                        <li class="list-group-item" id="wins-output">WINS: 0</li>
                        <div class="progress">
                            <div id="wins-bar" class="progress-bar bg-warning" role="progressbar" style="width: 0%;" aria-valuemin="0" aria-valuemax="100"></div>
                        </div>
                        <li class="list-group-item" id="losses-output">LOSSES: 0</li>
                        <div class="progress">
                            <div id="losses-bar" class="progress-bar bg-danger" role="progressbar" style="width: 0%;" aria-valuemin="0" aria-valuemax="100"></div>
                        </div>
                        <li class="list-group-item" id="pot-output">POT: 0</li>
                        <div class="progress">
                            <div id="pot-bar" class="progress-bar bg-info" role="progressbar" style="width: 100%;" aria-valuemin="0" aria-valuemax="100"></div>
                        </div>
                    </ul>
                </div>
            </div>
        </div>

       <!-- Denna del ska jag kommentera av när jag lagt till
            möjligheten att splitta kort
        <div id="player-card-split" class="col-sm-6 hidden">
            <div class="card">
                <div class="card-header text-center">
                    <h4 class="font-weight-bold">Player Cards</h4>
                </div>
                <div id="player-cards-split" class="card-body"></div>
                <div class="card-footer"></div>
            </div>
        </div> -->

        <hr />
        <!-- Denna knapp är till för att lösa ut vinst -->
        <div class="row">
            <div class="col-12">
                <button type="button" class="btn btn-secondary btn-lg" id="cahs-in" style="width: 100%;" disabled>CASH IN WINS</button>
            </div>
        </div>
    </div>

    <!-- Optional JavaScript -->
    <script src="js/blackjack.js?version=1.0.3"></script>

</body>

</html>

Egen Stilmall

body {
    background: #f8f9fa;
}

.hidden {
    display: none;
}

#username {
    margin-bottom: 18px;
    margin-top: 18px;
}

#start {
    margin-top: 18px;
    padding: 7px;
}

#start:disabled {
    color: #fff;
    background-color: #007bff !important;
}

#start:hover {
    color: inherit;
    background-color: inherit;
    cursor: default;
}
.btn-group-vertical button {
    margin-bottom: .8rem;
}

button {
    border-radius: 4px !important;
}

button:disabled {
    cursor: default;
}

#player-cards,
#dealer-cards {
    padding-bottom: 18px;
    border: 1px solid #666;
    margin: 1em;
    border-radius: 4px;
    display: flex;
    flex-flow: row;
    height: 180px;
}
#player-card {
    margin-bottom: 26px;
}
#dealer-card {
    margin-top: 26px;
}

ul li {
    text-shadow: .5px .5px 0px #000;
    font-weight: bold;
}

#wins-output {
    color: #d39e00;
}

#losses-output {
    color: orangered;
}

#bet-output {
    color: #29b529;
}

#pot-output {
    color: #0062cc;
}

#blackjack {
    -webkit-transition: width 1s ease-in-out, left 1.5s ease-in-out;
    transition: width 1s ease-in-out, left 1.5s ease-in-out;
}

CSS för kort

För att visa en specifik kort har jag valt att använda mig av CSS klasser så att dom överensstämmer med kortleks-Arrayn som jag tidigare skapade. Korten finns på fyra PNG filer i en så kallad CSS-sprite format.

/*** Kort ***/
.cards {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    margin: 0;
    padding: 0;
    width: 101px;
    height: auto;
    background-repeat: no-repeat;
    display: flex;
    flex-flow: row;
}

.back {
    background-image: url("../img/card_back.png");
    background-position: 0 0;
}

/*** Hjärter ***/
.heart_a {
    background-image: url("../img/heart.png");
    background-position: 0 0;
}

.heart_2 {
    background-image: url("../img/heart.png");
    background-position: 0 -141px;
}

.heart_3 {
    background-image: url("../img/heart.png");
    background-position: 0 -281px;
}

.heart_4 {
    background-image: url("../img/heart.png");
    background-position: 0 -422px;
}

.heart_5 {
    background-image: url("../img/heart.png");
    background-position: 0 -562px;
}

.heart_6 {
    background-image: url("../img/heart.png");
    background-position: 0 -703.2px;
}

.heart_7 {
    background-image: url("../img/heart.png");
    background-position: 0 -844px;
}

.heart_8 {
    background-image: url("../img/heart.png");
    background-position: 0 -984px;
}

.heart_9 {
    background-image: url("../img/heart.png");
    background-position: 0 -1124px;
}

.heart_10 {
    background-image: url("../img/heart.png");
    background-position: 0 -1266px;
}

.heart_j {
    background-image: url("../img/heart.png");
    background-position: 0 -1406px;
}

.heart_q {
    background-image: url("../img/heart.png");
    background-position: 0 -1546px;
}

.heart_k {
    background-image: url("../img/heart.png");
    background-position: 0 -1686px;
}

/*
* Kopiera från ovan och ersätt .heart_
*  med .spade_ .diamond_ och .club_ 
*/

Välj HTML Element

Okej, nu lägger jag till funktion för att lyssna efter klick. Jag använder mig av document.querySelector() för att välja det första HTML Element-objekt som matchar den angivna uppsättning CSS-väljare, dvs. jag kan välja ett element med hjälp av id, klass, attribut, eller en blandning av dessa. Nu har jag satt id på alla knappar så det blir ett med hjälp av hashtag #.

// Knappar för att styra spelet
const bet = document.querySelector('#bet');
const deal = document.querySelector('#deal');
const hit = document.querySelector('#hit');
const stand = document.querySelector('#stand');
const start = document.querySelector('#start');
const reset = document.querySelector('#reset');
const username = document.querySelector('#username');

// Utaskrifts-element
const betOutput = document.querySelector('#bet-output');
const winsOutput = document.querySelector('#wins-output');
const lossesOutput = document.querySelector('#losses-output');
const potOutput = document.querySelector('#pot-output');

// Dessa används för att visualisera & förlust + vinst
const playerCard = document.querySelector('#player-card div');
const dealerCard = document.querySelector('#dealer-card div');
const playerHint = document.querySelector('#player');
const dealerHint = document.querySelector('#dealer');
const playerFooter = document.querySelector('#player-footer');
const dealerFooter = document.querySelector('#dealer-footer');

// ..dessa för själva korten
const playerCards = document.querySelector('#player-cards');
const dealerCards = document.querySelector('#dealer-cards');

// Den sammanlagda summan av korten visas i dessa
const sumCardsPlayer = document.querySelector('#sum-player');
const sumCardsDealer = document.querySelector('#sum-dealer');

// Visuell representation av vinster, förluster & potten
const potBar = document.querySelector('#pot-bar');
const betsBar = document.querySelector('#bets-bar');
const lossesBar = document.querySelector('#losses-bar');
const winsBar = document.querySelector('#wins-bar');

Lyssna efter klick

För att lyssna efter event på button Element använder jag addEventListener()

KLick på START

För att läsa in från input fältet lyssnar jag efter klick på START knapp. Sedan använder jag Node.textContent (som anger eller returnerar textinnehållet i Noden och alla dess ättlingar) och uppdaterar player.username med det.

start.addEventListener('click', function (e) {
    let userName = username.value;
    if (username != "") {
        this.textContent = 'GAME ON!';
        
        // input fältet är inte tomt, spara användarnamn
        player.username = userName;
        // initiera pot med 10 000
        player.pot = 10000;
        potOutput.textContent = 'POT: ' + player.pot;

        // Ta eventuellt bort error
        username.classList.remove('error');

        // Gör input fältet ej väljbart
        username.setAttribute('disabled', true);

        this.removeEventListener('click', this);
        this.classList.remove('btn-outline-primary');
        this.classList.add('btn-primary');
        this.disabled = true;
        
        // gör klart för att lyssna efter klick på bet knappen
        bet.addEventListener('click', upadeBet);
        bet.classList.remove('btn-outline-primary');
        bet.classList.add('btn-primary');
    } else {
        username.classList.add('text-warning');
        username.setAttribute('placeholder', 'Användarnamn tack!');
    }
});

Klick på BET knapp

Ovan började JS lyssna efter klick på BET knappen, och anropar funktionen upadeBet() om det händer.

function upadeBet() {
    player.bet += 10;
    player.pot -= 10;
    betOutput.textContent = 'BET: ' + player.bet;
    potOutput.textContent = 'POT: ' + player.pot;

    // Uppdatera visuellt att potten blir mindre & bet blir större
    updateBetsBar();
    updatePotBar();
    if (deal.disabled = true) {
        // Börja lyssna efter klick på DEAL knapp
        deal.disabled = false;
        // ändra utseende så att man ser att knappen är aktiv
        deal.classList.remove('btn-outline-secondary');
        deal.classList.add('btn-secondary');
        // anropa dealFirst om knappen klickas
        deal.addEventListener('click', dealFirst);
    }
}

KLick på DEAL

function dealFirst() {
    // sluta lyssna efter klick på BET & DEAL
    deal.removeEventListener('click', dealFirst);
    bet.removeEventListener('click', upadeBet);

    // visualisera att knapparna är inaktiva
    bet.disabled = true;
    bet.classList.remove('btn-primary');
    bet.classList.add('btn-outline-primary');
    deal.disabled = true;
    deal.classList.remove('btn-secondary');
    deal.classList.add('btn-outline-secondary');

    // Visualisera att HIT & STAND knapparna är aktiva
    hit.disabled = false;
    hit.classList.remove('btn-outline-info');
    hit.classList.add('btn-info');
    stand.disabled = false;
    stand.classList.remove('btn-outline-info');
    stand.classList.add('btn-info');

    // Dela ut dom första två korten
    dealCard('player');
    dealCard('dealer');
    dealCard('player');
    dealCard('dealer');

    // Visa summan av korten
    sumCardsPlayer.textContent = 'PLAYER SUM: ' + player.sum;
    sumCardsDealer.textContent = 'DEALER SUM: ' + getValue(dealer.hand[1]) + ' & A HOLE CARD';

    // Kolla om givare eller spelare har vunnit på en gång
    if (dealer.sum == 21) {
        dealerWin(dealer.sum);
    } else if (player.sum == 21) {
        playerWin(player.sum);
    } else if (player.sum == 21 && dealer.sum == 21) {
        // eftersom jag inte lagt till möjligheten att försäkra sig
        // blir det oavgjort om båda får BlackJack
        draw();
    }
}

Klick på HIT

hit.addEventListener('click', function (e) {
    dealCard('player');
    // Om spelarens summa är under eller lika med 21 fortsätter spelet
    if (player.sum <= 21) {
        // Om spelaren har 21 avbryts hans spel
        if (player.sum == 21) {
            // ..och givaren drar kort tills han får 21 eller går över
            while (dealer.sum < 21) {
                dealCard('dealer');
            } // ..om givaren får 21 blir det oavgjort
            if (dealer.sum == 21) {
                draw();
            } else {
                // annars vinner spelaren med 21
                playerWin();
            }
        } else {
            // annars uppdateras summan av korten
            sumCardsPlayer.textContent = 'PLAYER SUM: ' + player.sum;
        }
    } else {
        // spelaren har gått över 21 och förlorar
        dealerWin(dealer.sum);
    }
});

Klick på STAND

stand.addEventListener('click', function (e) {
    // Visa summan av korten
    sumCardsPlayer.textContent = 'PLAYER SUM: ' + player.sum;
    sumCardsDealer.textContent = 'DEALER SUM: ' + dealer.sum;

    // Lägg till disabled på knapparna deal hit och stand
    deal.disabled = true;
    deal.classList.remove('btn-secondary');
    deal.classList.add('btn-outline-secondary');
    hit.disabled = true;
    hit.classList.remove('btn-info');
    hit.classList.add('btn-outline-info');
    stand.disabled = true;
    stand.classList.remove('btn-info');
    stand.classList.add('btn-outline-info');

    // Visualisera att ett nytt giv kan börjas med RESET knapp
    reset.disabled = false;
    reset.classList.remove('btn-outline-warning');
    reset.classList.add('btn-warning');

    // När spelaren stannat drar givaren kort tills han har 17+
    while (dealer.sum < 17) {
        dealCard('dealer');
    }
    // Om dealer.sum är mindre än player.sum
    if (dealer.sum < player.sum) {
        playerWin(player.sum);
    } else if (dealer.sum > 21) {
        playerWin(player.sum);
    } else {
        dealerWin(dealer.sum);
    }
});

Klick på RESET

reset.addEventListener('click', function (e) {
    resetPlay();
});

function resetPlay() {
    // Ta bort visualisering av vinst & förlust
    playerFooter.classList.remove('bg-danger', 'bg-success', 'bg-success');
    playerHint.classList.remove('bg-danger', 'bg-success', 'bg-success');
    playerCard.classList.remove('border-danger', 'border-success');

    playerFooter.classList.add('bg-info');
    playerHint.classList.add('bg-info');

    dealerFooter.classList.remove('bg-danger', 'bg-success', 'bg-success');
    dealerCard.classList.remove('border-danger', 'border-success');
    dealerHint.classList.remove('bg-danger', 'bg-success', 'bg-success');
    
    // Nolla värdena för spelare och givare inför nästa giv
    player.aces = 0;
    player.bet = 0;
    player.hand = [];
    player.sum = 0;
    dealer.aces = 0;
    dealer.bet = 0;
    dealer.hand = [];
    dealer.sum = 0;

    playerCards.textContent = '';
    dealerCards.textContent = '';

    sumCardsPlayer.textContent = 'PLAYER SUM:';
    sumCardsDealer.textContent = 'DEALER SUM:';

    // Lägg till disabled på RESET
    reset.disabled = true;
    reset.classList.remove('btn-warning');
    reset.classList.add('btn-outline-warning');

    // Ta bort disabled på BET
    bet.disabled = false;
    bet.classList.remove('btn-outline-primary');
    bet.classList.add('btn-primary');
    bet.addEventListener('click', upadeBet);
}

Så där, det var i stort sett allt. Kom gärna med kommentarer.

JavaScript BlackJack App

Lämna ett svar

E-postadressen publiceras inte. Obligatoriska fält är märkta *

Rulla till toppen