5
\$\begingroup\$

I have completed my initial version of BlackJack with Javascript and some JQuery. Any thoughts of this version? I can't seem still to implement timer successfully as I have commented out. After implement the timer (delay of 2 seconds for each card dealt, the hit and stand button breaks).

I updated my code to fixes some issues and good for testing now. Feel free to check it out from my github, https://github.com/ngaisteve1/BlackJackJS for further review.

// Variable/Object declaration and initialization - Start
const isDebug = false;
//const DELAY = 2000;
var gameOver = false;
const deck = {
    cards: []
}

var tempCard;
const player = {
    cards: [],
    handValue: 0,
    isWinner: false,
    canHit: true,
    hasAce: false
}

const dealer = {
    cards: [],
    handValue: 0,
    isWinner: false,
    canHit: true,
    hasAce: false
}

var result = document.getElementById("gameResult");

const cardSuit = ["hearts", "diams", "clubs", "spades"];
const cardFace = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"];

$(".checkmarkDealer").hide();
$(".checkmarkPlayer").hide();
$("#handValueDealer").hide();
//Variable/Object declaration and initialization - End


// var x = document.getElementById("myAudio"); 

// function playAudio() { 
//   x.play(); 
// } 

if (!isDebug) {
    document.getElementById("btnDevelopment").style.display = "none";
    document.getElementById("deck").style.display = "none";
    document.getElementById("oneDeck").style.display = "none";
    document.getElementById("playerCards").style.display = "none";
    document.getElementById("dealerCards").style.display = "none";
    //document.getElementById("result").style.display = "none";
} else {
    document.getElementById("btnDevelopment").style.display = "block";
    document.getElementById("deck").style.display = "block";
    document.getElementById("oneDeck").style.display = "block";
    document.getElementById("playerCards").style.display = "block";
    document.getElementById("dealerCards").style.display = "block";
    //document.getElementById("result").style.display = "block";
}

const showGameButtons = (cardDealt) => {
    if (cardDealt) {
        $("#btnDeal").hide();
        $("#btnHit").show();
        $("#btnStand").show();

        //document.getElementById("btnDeal").disabled = true;
        //document.getElementById("btnHit").disabled = false;
        //document.getElementById("btnStand").disabled = false;
    } else {
        $("#btnDeal").show();
        $("#btnHit").hide();
        $("#btnStand").hide();

        //document.getElementById("btnDeal").disabled = false;
        //document.getElementById("btnHit").disabled = true;
        //document.getElementById("btnStand").disabled = true;
    }

    if (player.isWinner === true) {
        document.getElementById("containerDealer").classList.remove("winner");
        document.getElementById("containerPlayer").classList.add("winner");

        $("#handValueDealer").show();
        $(".checkmarkPlayer").show();
        $(".checkmarkDealer").hide();
    } else if (dealer.isWinner === true) {
        document.getElementById("containerPlayer").classList.remove("winner");
        document.getElementById("containerDealer").classList.add("winner");

        $("#handValueDealer").show();
        $(".checkmarkPlayer").hide();
        $(".checkmarkDealer").show();
    } else {



    }
}
showGameButtons(false);


// In JavaScript, functions are objects.
// You can work with functions as if they were objects.
function card(suit, face) {
    this.suit = suit;
    this.face = face;

    switch (face) {
        case "A":
            this.faceValue = 11;
            break;
        case "J":
        case "Q":
        case "K":
            this.faceValue = 10;
            break;
        default:
            this.faceValue = parseInt(face);
            break;
    }
};



const createDeck = () => {
    deck.cards = [];
    deck.cards.length = 0;
    cardSuit.forEach(function (suit) {
        cardFace.forEach(function (face) {
            deck.cards.push(new card(suit, face));
        });
    });
}

const shuffleDeck = () => {
    // Fisher–Yates shuffle algorithm
    let temp, i, rnd;
    for (i = 0; i < deck.cards.length; i++) {
        rnd = Math.floor(Math.random() * deck.cards.length);
        temp = deck.cards[i];
        deck.cards[i] = deck.cards[rnd];
        deck.cards[rnd] = temp;
    }
}

const newDeck = () => {
    createDeck();
    shuffleDeck();
    document.getElementById("oneDeck").innerHTML = "";

    player.cards = [];
    player.handValue = 0;

    dealer.cards = [];
    dealer.handValue = 0;

    var myNode = document.getElementById("cardContainerPlayer");
    var fc = myNode.firstChild.firstChild;

    while (fc) {
        myNode.removeChild(fc);
        fc = myNode.firstChild;
    }

    var myNodeDealer = document.getElementById("cardContainerDealer");
    var fcDealer = myNodeDealer.firstChild.firstChild;

    while (fcDealer) {
        myNodeDealer.removeChild(fcDealer);
        fcDealer = myNodeDealer.firstChild;
    }

    document.getElementById("playerCards").innerHTML = "";
    document.getElementById("dealerCards").innerHTML = "";

    document.getElementById("oneDeck").innerHTML = JSON.stringify(deck);
}

const burnOneCard = () => {
    // Remove the top deck to burn
    deck.cards.splice(0, 1);
}

const showDeck = () => {
    document.getElementById("oneDeck").innerHTML = JSON.stringify(deck);
}

const dealOneCardToPlayer = (x, isHit) => {
    // return new Promise(function (resolve) {
    //     setTimeout(function () {
    // Take a card from the top deck to be assigned to tempcard.
    tempCard = deck.cards.splice(0, 1);

    //console.log(tempCard[0].face);
    //console.log(tempCard[0].faceValue);

    player.cards.push(tempCard);

    //console.log(player.handValue);

    if(tempCard[0].face === "A"){
        player.hasAce = true;
    }

    player.handValue = countHandValue(tempCard[0], player, isHit);
    document.getElementById("handValuePlayer").innerHTML = player.handValue;

    // if (player.cards.length === 5) {
    //     player.canHit = false;
    // }

    // conditional (ternary) operator
    player.canHit = player.cards.length === 5 ? false : true

    if (player.canHit) {
        $("#btnHit").show();
    } else {
        $("#btnHit").hide();
    }

    //player.cards.push(new card("Spades","A"));
    //player.cards.push(new card("Spades","10"));
    document.getElementById("playerCards").innerHTML = JSON.stringify(player);


    makeCardPlayer(tempCard[0]);
    //         resolve();
    //     }, DELAY);
    // });

}

const dealOneCardToDealer = (holeCard, isHit) => {
    // return new Promise(function (resolve) {
    //     setTimeout(function () {
    // Take a card from the top deck to be assigned to tempcard.
    tempCard = deck.cards.splice(0, 1);

    //console.log(tempCard[0].face);
    //console.log(dealer.handValue);

    if(tempCard[0].face === "A"){
        dealer.hasAce = true;
    }

    dealer.handValue = countHandValue(tempCard[0], dealer, isHit);
    document.getElementById("handValueDealer").innerHTML = dealer.handValue;
    

    dealer.cards.push(tempCard);

    //dealer.handValue = countHandValue(tempCard[0]);    

    // conditional (ternary) operator
    dealer.canHit = dealer.cards.length === 5 ? false : true
    
    if (dealer.canHit) {
        $("#btnHit").show();
    } else {
        $("#btnHit").hide();
    }

    document.getElementById("dealerCards").innerHTML = JSON.stringify(dealer);


    makeCardDealer(tempCard[0], holeCard);
    //         resolve();
    //     }, DELAY);
    // });        
}

const countAllHandValue = (cardsOnHand) => {
    //console.log(hasAceInHand(cardsOnHand));
    let sum = 0;
    for (let key in cardsOnHand) {
        let arr = cardsOnHand[key];
        for (let i = 0; i < arr.length; i++) {
            let obj = arr[i];
            for (let prop in obj) {
                if (prop === "faceValue") {              
                    sum = sum + obj[prop];              
                }
            }
        }
    }
    return sum;
}

const countHandValue = (onecard, person, isHit) => {
    if(isHit){        
        // Only can cover one Ace for this solution. More than one Ace will be a bug.
        if (person.handValue > 10 && person.hasAce === true) {
            person.cards.forEach(card => {
                console.log(card[0]);
                if (card[0].face === 'A') card[0].faceValue = 1;
                return card[0];
            });
            person.handValue = countAllHandValue(person.cards);
            // loop through all the Ace and transform all Ace's face value from 11 to 1
            // Recalculate all the cards on hand again.          
        } else {
            person.handValue = person.handValue + onecard.faceValue;            
        }
    } else {
        person.handValue = person.handValue + onecard.faceValue;
    }
    

    //console.log(person.handValue);
    //console.log(onecard);

    return person.handValue;
}

const showHandValue = () => {
    document.getElementById("playerCardsHandValue").innerHTML = player.handValue;
    document.getElementById("dealerCardsHandValue").innerHTML = dealer.handValue;
}

const getDeckCardCount = () => {
    document.getElementById("deckCardCount").innerHTML = deck.cards.length;
}

const checkGameOver = () => {
    if (gameOver) {
        $(".holeCard > :nth-child(1)").show();
        $(".holeCard > :nth-child(2)").show();

        $(".holeCard").removeClass("holeCard");
        $("#handValueDealer").show();

        showGameButtons(false);
    }
}

const checkEndGame1 = () => {
    gameOver = true;
    if (player.handValue === 21 && dealer.handValue !== 21) {
        result.innerHTML = "BlackJack! Player won.";
        player.isWinner = true;
    } else if (player.handValue !== 21 && dealer.handValue === 21) {
        result.innerHTML = "BlackJack! Dealer won.";
        dealer.isWinner = true;
    } else if (player.handValue === 21 && dealer.handValue === 21) {
        result.innerHTML = "Push.";
    } else {
        gameOver = false;
    }
}

const checkEndGame2 = () => {
    if (player.cards.length <= 5 && player.handValue > 21) {
        result.innerHTML = "Bust! Dealer won.";
        dealer.isWinner = true;
        gameOver = true;
    }
}

const checkEndGame3 = () => {

    if (player.cards.length <= 5 && dealer.cards.length <= 5) {
        // Check bust
        if (player.handValue <= 21 && dealer.handValue > 21) {
            result.innerHTML = "Bust! Player won.";
            player.isWinner = true;
        } else if (player.handValue === 21 && dealer.handValue !== 21) {
            result.innerHTML = "BlackJack! Player won.";
            player.isWinner = true;
        } else if (player.handValue !== 21 && dealer.handValue === 21) {
            result.innerHTML = "BlackJack! Dealer won.";
            dealer.isWinner = true;

        } else if (player.handValue === dealer.handValue) {
            result.innerHTML = "Push.";
        } else if (player.handValue > dealer.handValue) {
            result.innerHTML = "Player won.";
            player.isWinner = true;
        } else if (player.handValue < dealer.handValue) {
            result.innerHTML = "Dealer won.";
            dealer.isWinner = true;
        } else {
            result.innerHTML = "Error";
        }
    } else {
        result.innerHTML = "Error";
    }
    gameOver = true;
}

// This function use JQuery lib
function makeCardPlayer(_card) {
    // .card is created in the template card css class
    var card = $(".card.templatePlayer").clone();

    card.removeClass("templatePlayer");

    // .cardFace is created in the template card css class
    // It will search for this css class and add the content aka innerHTML
    card.find(".playerCardFace").html(_card.face);

    // .suit is created in the template card css class
    // It will search for this css class and add the content aka innerHTML
    card.find(".playerCardSuit").html("&" + _card.suit + ";");
    // &spades; -> ♠, &clubs; -> ♣, &hearts; -> ♥, &diams; -> ♦
    // more char, https://www.w3schools.com/charsets/ref_utf_symbols.asp

    // hearts and diamonds are red color. otherwise, default black color.
    if (_card.suit === "hearts" || _card.suit === "diams") {
        card.addClass("red");
    }

    // option: replace previous card with new card (show one card all the time)
    $("#cardContainerPlayer").append(card);
}

// This function use JQuery lib
function makeCardDealer(_card, _holeCard) {
    // .card is created in the template card css class
    var card = $(".card.templateDealer").clone();

    card.removeClass("templateDealer");

    // .cardFace is created in the template card css class
    // It will search for this css class and add the content aka innerHTML
    card.find(".dealerCardFace").html(_card.face);

    // .suit is created in the template card css class
    // It will search for this css class and add the content aka innerHTML
    card.find(".dealerCardSuit").html("&" + _card.suit + ";");
    // &spades; -> ♠, &clubs; -> ♣, &hearts; -> ♥, &diams; -> ♦
    // more char, https://www.w3schools.com/charsets/ref_utf_symbols.asp

    // hearts and diamonds are red color. otherwise, default black color.
    if (_card.suit === "hearts" || _card.suit === "diams") {
        card.addClass("red");
    }

    if (_holeCard) {
        card.addClass("holeCard");
    }

    // option: replace previous card with new card (show one card all the time)
    $("#cardContainerDealer").append(card);

    $(".holeCard > :nth-child(1)").hide();
    $(".holeCard > :nth-child(2)").hide();

}

const deal = () => {
    newDeck();

    // Option: to burn first card before deal a card
    // to the first player
    burnOneCard;

    // dealOneCardToPlayer()
    //     .then(dealOneCardToDealer)
    //     .then(dealOneCardToPlayer)
    //     .then(dealOneCardToDealer(true));

    dealOneCardToPlayer("",false);
    dealOneCardToDealer(false,false);
    dealOneCardToPlayer("",false);

    // true for hole card
    dealOneCardToDealer(true,false);



    showGameButtons(true);
    checkEndGame1();
    checkGameOver();
}

const hit = () => {
    dealOneCardToPlayer("", true);    
    checkEndGame2();
    checkGameOver();
}

const stand = () => {
    // Recalculate dealer's hand value
    //dealer.handValue = countAllHandValue(dealer.cards);

    // Simple AI to automate dealer's decision to hit or stand
    if (dealer.handValue >= 17) {
        checkEndGame3();
    } else {
        // Hit until dealer's hand value is more than 16
        while (dealer.handValue < 17) {
            dealOneCardToDealer(false, true);
            checkEndGame3();
        }
    }
    checkGameOver();
}
body{
    font-size: 2em;
}

h3, h5 {
    text-align: center;
}

h5{
    margin-top:-40px;
}

/*debugging purpose*/
div#oneDeck {
    border: 1px solid green;
    margin: 10px;
    padding: 10px;
}

/*debugging purpose*/
div#playerCards {
    border: 1px solid blue;
    margin: 10px;
    padding: 10px;
}

/*debugging purpose*/
div#dealerCards {
    border: 1px solid red;
    margin: 10px;
    padding: 10px;
}

#mainContainer {
    max-width: 600px;
    margin: 0 auto;
}

fieldset {
    margin-top: 30px;
    border: 1px solid #999;
    border-radius: 8px;
    box-shadow: 0 0 10px #999;
}

legend {
    background: #fff;
}

#cardContainerPlayer {
    display: flex;
    flex-wrap: wrap;
}

.card {
    display: inline-block;
    vertical-align: top; /*float: left;*/
    text-align: center;
    margin: 5px;
    padding: 10px;
    width: 70px;
    height: 100px;
    font-size: 26px;
    background-color: black;
    border: solid 1px black;
    color: white;
    border-radius: 10px;
}

.holeCard {
    /*visibility: hidden;*/
    border: solid 1px black;
    background: repeating-linear-gradient( 45deg, #606dbc, #606dbc 10px, #465298 10px, #465298 20px );
}

.red {
    background-color: red;
    border: solid 1px #8C001A;
}

.templatePlayer, .templateDealer {
    display: none;
}

#btnGame {
    margin: 10px;
}

.winner {
    border: solid 5px #7ac142;
}

.btnGame {
    background-color: dodgerblue; /* Green */
    border: none;
    color: white;
    padding: 15px 32px;        
    /*border-radius:10px;*/
    text-align: center;
    text-decoration: none;
    display: inline-block;
    font-size: 16px;
    cursor: pointer;
    -webkit-transition-duration: 0.4s; /* Safari */
    transition-duration: 0.4s;
    box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);
}

#btnHit {
    margin-right: 20px;
}

.flex-container {
    padding: 0;
    margin: 0;
    display: flex;
    justify-content: space-between;
    max-width: 100%;
    overflow: auto;
    /*border: 1px solid red*/
}
<link href="check.css" rel="stylesheet" />
<link href="BlackJack.css" rel="stylesheet" />
<h3>Simple Javascript BlackJack Game</h3>
<h5>developed by Steve Ngai</h5>
<div id="mainContainer">
    <div id="btnDevelopment">
        <input type='button' value='Create new Deck' onclick='newDeck();' />
        <input type='button' value='Burn a card' onclick='burnOneCard();' />
        <input type='button' value='Refresh Deck' onclick='showDeck();' />
        <input type='button' value='Deal a card to Player' onclick='dealOneCardToPlayer();' />
        <input type='button' value='Deal a card to Dealer' onclick='dealOneCardToDealer();' />
        <input type='button' value='Show hand value' onclick='showHandValue();' />
        <input type='button' value='Check end game' onclick='checkEndGame();' />
        <input type='button' value='Refresh deck remaining cards count' onclick='getDeckCardCount();' />
    </div>

    <fieldset id="deck">
        <legend>Remaining cards in the Deck: <span id="deckCardCount"></span></legend>
        <div id="oneDeck"></div>
    </fieldset>

    <fieldset id="containerDealer">
        <legend>Dealer (Hand Value: <span id="handValueDealer"></span>)</legend>
        <div style="width:30px">
            <svg class="checkmarkDealer" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
                <circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none" />
                <path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8" />
            </svg>
        </div>
        <div id="dealerCards"></div>
        <div id="cardContainerDealer">
            <div class="card templateDealer">
                <span class="dealerCardFace"></span>
                <span class="dealerCardSuit"></span>
            </div>
        </div>
        <div id="dealerCardsHandValue"></div>
    </fieldset>

    <div id="btnGame">
        <div class="flex-container">
            <div class="btn">
                <input type='button' class="btnGame" id="btnDeal" value='Deal' onclick='deal();' />
            </div>
            <div class="btn">
                <input type='button' class="btnGame" id="btnHit" value='Hit' onclick='hit();' />            
                <input type='button' class="btnGame" id="btnStand" value='Stand' onclick='stand();' />
            </div>            
        </div>
    </div>

    <fieldset id="containerPlayer">
        <legend>Player (Hand Value: <span id="handValuePlayer"></span>)</legend>
        <div style="width:30px">
            <svg class="checkmarkPlayer" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
                <circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none" />
                <path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8" />
            </svg>
        </div>
        <div id="playerCards"></div>
        <div id="cardContainerPlayer">
            <div class="card templatePlayer">
                <span class="playerCardFace"></span>
                <span class="playerCardSuit"></span>
            </div>
        </div>
        <div id="playerCardsHandValue"></div>
    </fieldset>

    <fieldset id="result">
        <legend>Game Result</legend>
        <div id="gameResult"></div>
    </fieldset>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="BlackJack.js"></script>

\$\endgroup\$
  • \$\begingroup\$ Just wondering have you thought of using classes or separate files to clean up the logic and context so it's not just 1 big file but multiple small ones that easily follow the flow of the game? \$\endgroup\$ – Andrei May 3 at 14:05
  • \$\begingroup\$ In my C# BlackJack version, I split into several classes but in Javascript looks like very thing in the the method right? \$\endgroup\$ – Steve Ngai May 4 at 4:17
  • 1
    \$\begingroup\$ Note that 21 and BlackJack are not the same. BlackJack has priority over 21. BlackJack is 21 with 2 cards. \$\endgroup\$ – dfhwze Jun 29 at 15:09
  • 1
    \$\begingroup\$ If you want to add real cards, see card-ts.github.io/playingcardts \$\endgroup\$ – Danny '365CSI' Engelman Aug 18 at 7:45
3
\$\begingroup\$

This looks like it showcases your skills well, and the comments will help anyone looking back to know that you're a programmer who is interested in doing their research and knowing things.

I tested this in Chrome on my local computer.

Your question included a mention about your timeout function not working. You'll need to make a stackoverflow question for that.

Overall:

This was very straight-forward to setup and the game works great! It is a little confusing visually that the cards aren't cleaned up after every game. Since the deck is reshuffled after each play.

If you were to make a more advanced version, I'd suggest creating a way for hard-core blackjack players to count cards. Maybe look into the casino rules and see how often they shuffle cards back into the deck, or how many decks they play with. You may be able to create a more interesting game if the player can use the same strategies on your web-page game as they can in a casino!

Code:

Your HTML:

  • Place all your .css and .js references in a head div. This is done so that when other developers come along they can see very quickly where all your styling and scripts are coming from.
<head>
   <link href="check.css" rel="stylesheet">
   <link href="BlackJack.css" rel="stylesheet">
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
   <script src="BlackJack.js"></script>
</head>

Your CSS:

  • Careful with pixel counts for view-sizes in css. Pixel counts are fine for borders, but for viewports and playing surfaces they can cause your code to become outdated, or make your webpage hard to view on a mobile device. Try to limit the amount of times you define a style element in terms of a large pixel count; and instead use percentages. Think about this when you're developing: "I want my playing surface to take up 70% of the screen, that's 600px right now, but on a 4k display that would be much less; and on some phone displays it would be over 100% of the phone screen."

Your JavaScript:

  • Your logic in your javascript functions for *dealOneCardToPlayer * and *dealOneCardToDealer * is largely the same. This is not efficient and can be confusing for other developers, or you in the future. You need to identify what the difference in variables are in these functions and combine the functions to operate the same way on different data. You can pass in elements, objects, and dictionaries in javascript, so these don't need to be separate. The functions for makeCardPlayer and makeCardDealer could have the same thing happen to them, but it looks like it will require more values and more if/else checks, unless you break it up into smaller functions.
\$\endgroup\$
  • \$\begingroup\$ Thanks for the comment. Yeah, I wanted to reduce repeat code for that 2 methods. Just that, i am still contemplating on it because dealer has a hole card. About the css, I didn't realize that it is not mobile friendly. Ok, will check on it as well. \$\endgroup\$ – Steve Ngai May 4 at 1:49
1
\$\begingroup\$

I found the solution already to the timer issue.

const deal = () => {
    init(); // Can be improved by continue to use the remaining card in the deck

    newDeck();

    // Option: to burn first card before deal a card
    // to the first player
    burnOneCard;

    setTimeout(function () {
        dealOneCardToPlayer("", false);
    }, 500);

    setTimeout(function () {
        dealOneCardToDealer(false, false);
    }, 1000);

    setTimeout(function () {
        dealOneCardToPlayer("", false);
    }, 1500);

    // true for hole card
    setTimeout(function () {
        dealOneCardToDealer(true, false);
    }, 2000);

    showGameButtons(true);
    checkEndGame1();
    checkGameOver();

    getDeckCardCount();

}

\$\endgroup\$
  • 2
    \$\begingroup\$ Note that self-answers (like any other answer) are expected to review the code, not just present an alternative implementation without any reasoning. \$\endgroup\$ – Toby Speight Jul 1 at 8:25
  • 1
    \$\begingroup\$ This kind of thing would be for stack overflow. This is code review. \$\endgroup\$ – Kellen Mills Jul 2 at 22:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.