6
\$\begingroup\$

What would be the best way to organize BlackJack in JavaScript and maybe start with blank slate?

Specific areas:

  • Updating UI
  • Incorporating the dealer hand into the Hand class to reduce the repetitiveness
  • Possibly reducing the nested IFs
  • Incorporating the Split

I wasn't worried about the betting amounts/payouts/chip counts for this prototype, but of course will add that.

Code:

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>BlackJack Early Prototype</title>
    <style media="screen">
      .flex-row{
        display: flex;
        flex-flow: row wrap;
        justify-content: space-around;
      }
    </style>
  </head>
  <body>
    <button id='deal'>deal</button>
    <div class="flex-row">
      <div id="dealer">
      </div>
      <div id="hand1">

      </div>
      <div id="hand2">

      </div>
      <div id="hand3">

      </div>
    </div>


  <script type="text/javascript">
    const suits = [
      {
        digit: 'H',
        word: 'hearts'
      },
      {
        digit: 'C',
        word: 'clubs'
      },
      {
        digit: 'D',
        word: 'diamonds'
      },
      {
        digit: 'S',
        word: 'spades'
      }
    ]
    const cardsWithoutSuits = [
      {
        numeric: 11,
        word: 'ace',
        digit: 'A'
      },
      {
        numeric: 2,
        word: 'two',
      },
      {
        numeric: 3,
        word: 'three',
      },
      {
        numeric: 4,
        word: 'four',
      },
      {
        numeric: 5,
        word: 'five',
      },
      {
        numeric: 6,
        word: 'six',
      },
      {
        numeric: 7,
        word: 'seven',
      },
      {
        numeric: 8,
        word: 'eight',
      },
      {
        numeric: 9,
        word: 'nine',
      },
      {
        numeric: 10,
        word: 'ten',
      },
      {
        numeric: 10,
        word: 'jack',
        digit: 'J'
      },
      {
        numeric: 10,
        word: 'queen',
        digit: 'Q'
      },
      {
        numeric: 10,
        word: 'king',
        digit: 'K'
      }
    ]

    class Hand{
      constructor(bet){
        // assigning the bet value
        this.bet = bet;
        // default values, no cards dealt yet
        this.cards = [];
        this.value = 0;
        this.blackjack = false;
        this.soft = false;
        this.bust = false;
        this.winner = false;
        this.aceQuantity = 0;
        this.canHit = false;
        this.canStay = false;
        this.canSplit = false;
        this.canDouble = false;
        this.finished = false;
        this.textResult = '';
        this.payout = 0;
      };
      evaluate(){
        this.aceQuantity = this.cards.filter(x => x.word === 'ace').length;
        this.value = this.cards.filter(x => x.word !== 'ace').reduce((total, x) => +total + x.numeric, 0);
        this.soft = false;
        for (var i = 0; i < this.aceQuantity; i++) {
          if (this.value + 11 > 21) this.value += 1;
          else {
            this.value +=11;
            if (this.value !== 21) this.soft = true;
          }
        }

        if (this.cards.length === 2) {
          this.canDouble = true;
          this.canSplit = this.cards[0].word === this.cards[1].word;
        } else {
          this.canSplit = false;
          this.canDouble = false;
        }

        if (this.value > 21){
          this.bust = true;
          this.finished = true;
          this.canHit = false;
          this.canStay = false;
        }

        if(this.value === 21){
          this.finished = true;
          this.canHit = false;
          if (this.cards.length === 2) {
            this.blackjack = true;
          }
        }

        if (this.bust) {
          this.textResult = `Busted!`;
          this.payout = 0;
        } else if (this.blackjack) {
          this.textResult = `BlackJack!`;
        } else if (game.dealerFinished) {
          if (this.finished) {
            if (game.dealerValue > 21) {
                            this.textResult = `Winner!`;
            } else if (this.value > game.dealerValue) {
              this.textResult = `Winner!`;
            } else if (this.value === game.dealerValue) {
              this.textResult = `Push`;
            } else if (this.value < game.dealerValue) {
              this.textResult = `Loser`;
            }
          }
        } else {
          this.textResult = `Standing on ${this.value}`;
        }
      }
    }

    function createDeck(decks = 1){
      let deck = [];
      for (let i = 0; i < decks; i++) {
        suits.forEach( x => {
          cardsWithoutSuits.forEach( y => {
            deck.push({
              numeric: y.numeric,
              word: y.word,
              suit: x.word,
              phrase: `${y.word} of ${x.word}`,
              abbr: `${y.hasOwnProperty('digit') ? y.digit : y.numeric}${x.digit}`
            })
          })
        })
      }
      return deck;
    }

    function shuffle(array){
      let array2 = [];
      while (array.length){
        let index = Math.floor(Math.random() * array.length);
        let card = array.splice(index, 1);
        array2.push(card[0]);
      }
      return array2;
    }

    let game = {
      state: 'start',
      deck: [],
      dealerCards: [],
      dealerFinished: false,
      dealerValue: 0,
      hands: [],
      shuffle: function(){
        this.deck = shuffle(createDeck(4));
      },
      deal: function(){
        this.hands.forEach(x => {
          x.cards.push(this.deck.shift());
          x.evaluate();
        });
        this.dealerCards.push(this.deck.shift());
        this.hands.forEach(x => {
          x.cards.push(this.deck.shift());
          x.evaluate();
        });
        this.dealerCards.push(this.deck.shift());
      },
      tempCreateTestHands: function(){
        this.hands.push(new Hand(5));
        this.hands.push(new Hand(10));
        this.hands.push(new Hand(25));
      },
      dealCard: function(hand){
        if (hand === -1) {
          this.dealerCards.push(this.deck.shift());
          updateUI();
        } else {
          this.hands[hand].cards.push(this.deck.shift());
          this.hands[hand].evaluate();
        }
      },
      tempStart: function(){
        game.dealerFinished = false;
        game.hands = [];
        game.dealerCards = [];
        if (game.deck.length < 30) game.shuffle();
        game.tempCreateTestHands();
        game.deal();
        updateUI();
      },
      dealerTurn: function(){
        let aceQuantity = this.dealerCards.filter(x => x.word === 'ace').length;
        let value = this.dealerCards.filter(x => x.word !== 'ace').reduce((total, x) => +total + x.numeric, 0);
        for (var i = 0; i < aceQuantity; i++) {
          if (value + 11 > 21) value += 1;
          else {
            value +=11;
          }
        }
        this.dealerValue = value;
        if (value < 17) {
          this.dealCard(-1);
        } else {
          this.dealerFinished = true;
          game.hands.forEach(x => x.evaluate());
          updateUI();
        }
      }

    }


game.tempStart();




function updateUI(){
  const playersUI = [document.getElementById('hand1'), document.getElementById('hand2'), document.getElementById('hand3')];
  const dealer = document.getElementById('dealer');

  const handsRemaining = game.hands.filter(x => !x.finished).length;
  if (handsRemaining) dealer.innerHTML = `<h2>Dealer</h2><p>Card Hidden</p><p>${game.dealerCards[1].phrase}</p><p>Showing ${game.dealerCards[1].numeric}</p>`;
  else dealer.innerHTML = `<h2>Dealer</h2>` + game.dealerCards.map(x => `<p>${x.phrase}</p>`).join('') + `<p>Total: ${game.dealerValue}</p>`;

  if (!handsRemaining && !game.dealerFinished) {
    game.dealerTurn();
  }

  for (var i = 0; i < game.hands.length; i++) {
    let buttons = '';
    if (game.hands[i].finished){
      buttons += `<div>`;
      if (game.hands[i].busted) buttons +=`BUSTED`;
      else {
        buttons += game.hands[i].textResult;
      }

      buttons += `</div>`
    } else {


      buttons += `<div>
        <button class='hit' onclick="buttonHandler(${i}, 'hit')">HIT</button>
        <button class='stay' onclick="buttonHandler(${i}, 'stay')">STAY</button>
      `;
      if (game.hands[i].canDouble) buttons +=`<button class='double' onclick="buttonHandler(${i}, 'double')">DOUBLE</button>`;
      if (game.hands[i].canSplit) buttons +=`<button class='split' onclick="buttonHandler(${i}, 'split')">SPLIT</button>`;
      buttons += `</div>`;
    }
    playersUI[i].innerHTML = `<h2>Hand ${i + 1}</h2>` + game.hands[i].cards.map(x => `<p>${x.phrase}</p>`).join('') + `<p>Total: ${game.hands[i].soft ? 'Soft' : ''} ${game.hands[i].value}</p>${buttons}`;
  }
}




function buttonHandler(playerHand, action){
  switch (action) {
    case 'stay':
      game.hands[playerHand].finished = true;
      break;
    case 'hit':
      game.dealCard(playerHand);
      break;
    case 'double':
      game.dealCard(playerHand);
      game.hands[playerHand].finished = true;
      game.hands[playerHand].bet = game.hands[playerHand].bet * 2;
      break;
    case 'split':
      console.log(`split function not setup yet...`);
      break;
    default:
      console.log(`error, cannot find ${action} in the switch statement.`);
  }

  updateUI();
}


document.getElementById("deal").onclick = function() {
  // alert("hello");
  game.tempStart();
 }
  </script>
  </body>
</html>
\$\endgroup\$
3
\$\begingroup\$

You can see my suggested improvements here. As the previous guy, I'm also biased to a more functional style. This is what I've done:

  • As previous answer pointed out the code is poorly formatted. I ran Prettier with standard configuration

  • I removed some unnecessary comments. It's obvious that this.bet = bet is assigning the bet value.

constructor(bet) {
    // assigning the bet value
    this.bet = bet;
    // default values, no cards dealt yet
    this.cards = [];
    ...
}
  • There's a *= operator you can use like foo *= 2 instead of foo = 2 * foo
  • If you use template literals, then there's no point in also using the + operator for concatenation (note that template literals allow multi line).
  • This is a bit opinionated, but I think you always should use braces for if-statements
  • Use some if you want to know if there's any elements that fulfills the condition (a boolean) and filter().length if you want to know how many they are (a number). In this case we want the boolean that says if any elements exist and we don't care about how many the elements are outside of that. Using the right one improves readability.
const handsRemaining = game.hands.filter(x => !x.finished).length;

to

const isHandsRemaining = game.hands.some(x => !x.finished);
  • You can turn some of the if-else-blocks with string concatenation to template literals with ternaries for better readability.
buttons += `<div>`;
if (busted) {
  buttons += `BUSTED`;
} else {
  buttons += textResult;
}
buttons += `</div>`;

to

buttons += `<div>${busted ? 'BUSTED' : textResult}</div>`;
  • Use destructuring over dot notation where applicable (somewhat opionated)
  • Dictate which players exist in one place (I just added a const players, but this would typically come from the server) instead of having it spread out in the markup and various places in the code
  • Favor Array.map over Array.forEach.
  • Favor const over let.

Also shuffle should be named after the shuffling algorithm you use or have a comment that explains what algorithm it is.

There's more improvements to be done, but at least this is a start :)

Edit: typos

\$\endgroup\$
1
\$\begingroup\$

NB: Note that I'm biased towards a more functional style

Good:

  • List of suits and cards with plain old data
  • The update function is declarative. Very good
  • Event handlers mostly delegate to other functions (but you can do even more)

Bad:

  • Check your code formatting
  • Storing phrases and other ui-related things directly in the state is not good. It adds noise, and it should be done in the ui logic
  • Game result should probably be some kind of enum. The ui should decide how to display the enum
  • There's too much state in the hand class. You can get rid of almost the entire class, and replace with util functions operating on plain old data. These util functions can operate on any hand, including dealer's
  • The game should probe hands for information when it calculates who's the winner, not the other way around. It seems weird that the hand tells the game it has won. If you also make a separate function out of this you can use early returns to avoid nested if's
  • Update ui should take required state as an argument. Not rely on globals, and preferrably relying on some sort of state object rather than the entire game object
  • A typical game loop consists of an update function: 1. reading events 2. updating the game state 3. updating the ui. I suggest you also do this, instead of updating it ad-hoc from button handlers. By doing this you won't need to remember to update the ui from so many places.

In general, use more pure functions when you can to avoid keeping track of all this state. Store the state in plain old objects instead. Only store the state you need, the ui should compute its result from it.

Some inspiration:

state = intitialState

updateGame(event){
    state = handleEvent(state, event)
    state = updateState(state)
    updateUi(state)
}

onButton(event){
    updateGame(event)
}
\$\endgroup\$

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.