Building a Blackjack Game with JavaScript

We've covered a lot in our journey through JavaScript together, and now it's time to put everything we've learned into practice with a fun final project: building a simple Blackjack game using the Deck of Cards API.

We'll create this game using what we've learned about JavaScript, including loops, control flows, asynchronous code, destructuring, modules, and working with APIs.

Ready? Let's dive in!

What is Blackjack?

Before we start coding, let's quickly review how Blackjack works so we're all on the same page. Blackjack, also known as 21, is a popular card game whose goal is to beat the dealer by having a hand value closest to 21 without going over.

Basic Rules:

  1. Card Values:

    • Number cards (2-10) are worth their face value.
    • Face cards (Jack, Queen, King) are worth 10 points.
    • Aces can be worth 1 or 11 points, depending on what benefits the hand more.
  2. Game Play:

    • Both the player and the dealer start with two cards.
    • The player’s cards are both visible, but only one of the dealer’s cards is visible.
    • The player can choose to "hit" (take another card) or "stand" (keep their current hand).
    • If the player’s hand value exceeds 21, they "bust" and lose the game.
    • Once the player stands, the dealer reveals their hidden card and must hit until their hand value is at least 17.
    • The winner is determined based on whose hand value is closest to 21 without exceeding it. If the dealer busts, the player wins.

Alright, now that we know the rules, let's set up our project!

Setting Up the Project

First, let's set up our project folder. We'll keep things organized by splitting our code into multiple files.

Here's the basic structure:

blackjack/
  ├── index.html
  ├── styles.css
  ├── script.js
  ├── deck.js
  ├── game.js

HTML

Let's start with our HTML file, index.html. This will include the basic structure of our game.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Blackjack Game</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <h1>Blackjack Game</h1>
  <div id="game">
    <div id="player-hand" class="hand">
      <h2>Player's Hand</h2>
      <div id="player-cards" class="cards"></div>
      <p id="player-score"></p>
    </div>
    <div id="dealer-hand" class="hand">
      <h2>Dealer's Hand</h2>
      <div id="dealer-cards" class="cards"></div>
      <p id="dealer-score"></p>
    </div>
    <div id="controls">
      <button id="hit-button">Hit</button>
      <button id="stand-button">Stand</button>
      <button id="new-game-button">New Game</button>
    </div>
    <p id="result"></p>
  </div>
  <script type="module" src="script.js"></script>
</body>
</html>

CSS

Now, let's add some basic styles in styles.css to make our game look more modern and responsive.

body {
  font-family: Arial, sans-serif;
  text-align: center;
  background-color: #2e7d32;
  color: white;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
}

h1 {
  margin-top: 20px;
}

#game {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: 600px;
}

.hand {
  margin: 20px;
  border: 2px solid white;
  padding: 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: 300px;
}

.cards {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

.cards img {
  margin: 5px;
}

#controls {
  position: fixed;
  bottom: 20px;
  width: 100%;
  display: flex;
  justify-content: center;
  gap: 10px;
}

button {
  margin: 10px;
  padding: 10px 20px;
  font-size: 16px;
  cursor: pointer;
  border: none;
  border-radius: 5px;
  background-color: #388e3c;
  color: white;
  transition: background-color 0.3s;
}

button:hover {
  background-color: #1b5e20;
}

button:disabled {
  background-color: #9e9e9e;
}

#result {
  font-size: 20px;
  margin-top: 20px;
}

## JavaScript Time!

In the following few sections, we will break apart different functions and tie them together to build up our game.

Deck Module

Let's create the deck.js file, which will handle interactions with the Deck of Cards API.

// deck.js

const apiUrl = 'https://deckofcardsapi.com/api/deck';

export async function createDeck() {
  const response = await fetch(`${apiUrl}/new/shuffle/?deck_count=1`);
  const data = await response.json();
  return data.deck_id;
}

export async function drawCards(deckId, count) {
  const response = await fetch(`${apiUrl}/${deckId}/draw/?count=${count}`);
  const data = await response.json();
  return data.cards;
}
  • createDeck function: This function makes an API call to create a new shuffled deck and returns the deck ID.
  • drawCards function: This function takes a deck ID and a count of cards to draw, makes an API call to draw that number of cards from the deck, and returns the drawn cards.

Game Logic

Next, we'll create the game.js file, which will contain the game logic for our Blackjack game.

// game.js

import { createDeck, drawCards } from './deck.js';

let deckId;
let playerHand = [];
let dealerHand = [];
let playerScore = 0;
let dealerScore = 0;
let gameOver = false;

export async function startNewGame() {
  deckId = await createDeck();
  playerHand = await drawCards(deckId, 2);
  dealerHand = await drawCards(deckId, 2);
  gameOver = false;
  updateScores();
  updateUI();
  document.getElementById('result').innerText = '';
}
  • Variables:

    • deckId: Stores the ID of the current deck.
    • playerHand and dealerHand: Arrays storing the player's and dealer's cards.
    • playerScore and dealerScore: Variables to keep track of the scores.
    • gameOver: A flag to check if the game is over.
  • startNewGame function: Initializes a new game by creating a deck and drawing two cards each for the player and dealer. It resets the game state and updates the UI.

export async function hit() {
  if (!gameOver) {
    const newCard = await drawCards(deckId, 1);
    playerHand.push(newCard[0]);
    updateScores();
    updateUI();
    checkForBust();
  }
}
  • hit function: Draws a new card for the player, updates the score and UI, and checks if the player has busted.
export async function stand() {
  if (!gameOver) {
    gameOver = true;
    while (dealerScore < 17) {
      const newCard = await drawCards(deckId, 1);
      dealerHand.push(newCard[0]);
      updateScores();
      updateUI();
      if (dealerScore >= 17) {
        checkForWinner();
      }
    }
  }
}
  • stand function: Simulates the dealer's turn, drawing cards until the dealer's score is at least 17, and then determines the winner.
function updateScores() {
  playerScore = calculateScore(playerHand);
  dealerScore = calculateScore(dealerHand);
  document.getElementById('player-score').innerText = `Score: ${playerScore}`;
  document.getElementById('dealer-score').innerText = `Score: ${dealerScore}`;
}

function calculateScore(hand) {
  let score = 0;
  let hasAce = false;
  hand.forEach(card => {
    if (card.value === 'ACE') {
      hasAce = true;
      score += 11;
    } else if (['KING', 'QUEEN', 'JACK'].includes(card.value)) {
      score += 10;
    } else {
      score += parseInt(card.value);
    }
  });
  if (score > 21 && hasAce) {
    score -= 10;
  }
  return score;
}
  • updateScores function: Calculates and updates the scores of the player and dealer.
  • calculateScore function: Calculates the score for a given hand, considering Aces as either 1 or 11.
function updateUI() {
  const playerCardsDiv = document.getElementById('player-cards');
  const dealerCardsDiv = document.getElementById('dealer-cards');
  playerCardsDiv.innerHTML = '';
  dealerCardsDiv.innerHTML = '';

  playerHand.forEach(card => {
    const img = document.createElement('img');
    img.src = card.image;
    playerCardsDiv.appendChild(img);
  });

  dealerHand.forEach(card => {
    const img = document.createElement('img');
    img.src = card.image;
    dealerCardsDiv.appendChild(img);
  });
}
  • updateUI function: Updates the UI to reflect the current state of the player's and dealer's hands.
function checkForBust() {
  if (playerScore > 21) {
    document.getElementById('result').innerText = 'You bust! Dealer wins!';
    gameOver = true;
  }
}

function checkForWinner() {
  if (dealerScore > 21) {
    document.getElementById('result').innerText = 'Dealer busts! You win!';
  } else if (dealerScore >= playerScore) {
    document.getElementById('result').innerText = 'Dealer wins!';
  } else {
    document.getElementById('result').innerText = 'You win!';
  }
}
  • checkForBust function: Checks if the player has busted (score > 21) and updates the result if they have.
  • checkForWinner function: Determines the winner based on the final scores and updates the result.

Main Script

Finally, let's create the main script, script.js, to tie everything together.

// script.js

import { startNewGame, hit, stand } from './game.js';

document.getElementById('new-game-button').addEventListener('click', startNewGame);
document.getElementById('hit-button').addEventListener('click', hit);
document.getElementById('stand-button').addEventListener('click', stand);

startNewGame();
  • This script imports the functions from game.js and binds the UI buttons to these functions.
  • When the user clicks the "New Game", "Hit", or "Stand" buttons, the corresponding function is called to handle the action.
  • The game starts automatically when the page loads by calling startNewGame().

Running the Game

As we covered in the modules section, you'll need a local server to run code with modules.

One of the easiest ways to do this is using the Live Server extension in Visual Studio Code (VS Code). Here's how to set it up:

  1. Install Live Server Extension:

    • Open VS Code.
    • Go to the Extensions view by clicking the Extensions icon in the Activity Bar on the side of the window.
    • Search for "Live Server" and install the extension by Ritwick Dey.
  2. Running the Server:

    • Open your project folder in VS Code.
    • Right-click on your index.html file (or whichever file is your main HTML file) in the Explorer view.
    • Select "Open with Live Server".

This will start a local server and open your project in a new browser tab. The URL will typically be something like http://127.0.0.1:5500/index.html. Now, your modules should work perfectly!

Complete Files

If you followed along you should have a working project that looks like this.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Blackjack Game</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <h1>Blackjack Game</h1>
  <div id="game">
    <div id="player-hand" class="hand">
      <h2>Player's Hand</h2>
      <div id="player-cards" class="cards"></div>
      <p id="player-score"></p>
    </div>
    <div id="dealer-hand" class="hand">
      <h2>Dealer's Hand</h2>
      <div id="dealer-cards" class="cards"></div>
      <p id="dealer-score"></p>
    </div>
    <div id="controls">
      <button id="hit-button">Hit</button>
      <button id="stand-button">Stand</button>
      <button id="new-game-button">New Game</button>
    </div>
    <p id="result"></p>
  </div>
  <script type="module" src="script.js"></script>
</body>
</html>

styles.css

body {
  font-family: Arial, sans-serif;
  text-align: center;
  background-color: #2e7d32;
  color: white;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
}

h1 {
  margin-top: 20px;
}

#game {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: 600px;
}

.hand {
  margin: 20px;
  border: 2px solid white;
  padding: 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: 300px;
}

.cards {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

.cards img {
  margin: 5px;
}

#controls {
  position: fixed;
  bottom: 20px;
  width: 100%;
  display: flex;
  justify-content: center;
  gap: 10px;
}

button {
  margin: 10px;
  padding: 10px 20px;
  font-size: 16px;
  cursor: pointer;
  border: none;
  border-radius: 5px;
  background-color: #388e3c;
  color: white;
  transition: background-color 0.3s;
}

button:hover {
  background-color: #1b5e20;
}

button:disabled {
  background-color: #9e9e9e;
}

#result {
  font-size: 20px;
  margin-top: 20px;
}

deck.js

const apiUrl = 'https://deckofcardsapi.com/api/deck';

export async function createDeck() {
  const response = await fetch(`${apiUrl}/new/shuffle/?deck_count=1`);
  const data = await response.json();
  return data.deck_id;
}

export async function drawCards(deckId, count) {
  const response = await fetch(`${apiUrl}/${deckId}/draw/?count=${count}`);
  const data = await response.json();
  return data.cards;
}

game.js

import { createDeck, drawCards } from './deck.js';

let deckId;
let playerHand = [];
let dealerHand = [];
let playerScore = 0;
let dealerScore = 0;
let gameOver = false;

export async function startNewGame() {
  deckId = await createDeck();
  playerHand = await drawCards(deckId, 2);
  dealerHand = await drawCards(deckId, 2);
  gameOver = false;
  updateScores();
  updateUI();
  document.getElementById('result').innerText = '';
}

export async function hit() {
  if (!gameOver) {
    const newCard = await drawCards(deckId, 1);
    playerHand.push(newCard[0]);
    updateScores();
    updateUI();
    checkForBust();
  }
}

export async function stand() {
  if (!gameOver) {
    gameOver = true;
    while (dealerScore < 17) {
      const newCard = await drawCards(deckId, 1);
      dealerHand.push(newCard[0]);
      updateScores();
      updateUI();
      if (dealerScore >= 17) {
        checkForWinner();
      }
    }
  }
}

function updateScores() {
  playerScore = calculateScore(playerHand);
  dealerScore = calculateScore(dealerHand);
  document.getElementById('player-score').innerText = `Score: ${playerScore}`;
  document.getElementById('dealer-score').innerText = `Score: ${dealerScore}`;
}

function calculateScore(hand) {
  let score = 0;
  let hasAce = false;
  hand.forEach(card => {
    if (card.value === 'ACE') {
      hasAce = true;
      score += 11;
    } else if (['KING', 'QUEEN', 'JACK'].includes(card.value)) {
      score += 10;
    } else {
      score += parseInt(card.value);
    }
  });
  if (score > 21 && hasAce) {
    score -= 10;
  }
  return score;
}

function updateUI() {
  const playerCardsDiv = document.getElementById('player-cards');
  const dealerCardsDiv = document.getElementById('dealer-cards');
  playerCardsDiv.innerHTML = '';
  dealerCardsDiv.innerHTML = '';

  playerHand.forEach(card => {
    const img = document.createElement('img');
    img.src = card.image;
    playerCardsDiv.appendChild(img);
  });

  dealerHand.forEach(card => {
    const img = document.createElement('img');
    img.src = card.image;
    dealerCardsDiv.appendChild(img);
  });
}

function checkForBust() {
  if (playerScore > 21) {
    document.getElementById('result').innerText = 'You bust! Dealer wins!';
    gameOver = true;
  }
}

function checkForWinner() {
  if (dealerScore > 21) {
    document.getElementById('result').innerText = 'Dealer busts! You win!';
  } else if (dealerScore >= playerScore) {
    document.getElementById('result').innerText = 'Dealer wins!';
  } else {
    document.getElementById('result').innerText = 'You

 win!';
  }
}

script.js

import { startNewGame, hit, stand } from './game.js';

document.getElementById('new-game-button').addEventListener('click', startNewGame);
document.getElementById('hit-button').addEventListener('click', hit);
document.getElementById('stand-button').addEventListener('click', stand);

startNewGame();

Congrats! You've built a pretty complex Blackjack game using JavaScript. This project is a great way to practice your skills and see how different concepts come together in a real-world application.

Feel free to expand on this project by adding more features, such as betting or whatever else you think could add flavor. The best way to learn is by building real projects without tutorials and knowing what you need as you go.

Thanks for sticking with me through this JavaScript journey. Keep coding, and have fun!

JavaScriptBeginner
Avatar for Niall Maher

Written by Niall Maher

Founder of Codú - The web developer community! I've worked in nearly every corner of technology businesses: Lead Developer, Software Architect, Product Manager, CTO, and now happily a Founder.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.