This guide will walk you through building a simple Blackjack game, focusing on creating the deck, shuffling it, dealing hands, and calculating scores.
Understanding the Game
Before diving into coding, let's briefly go over the game's basic rules:
- Objective: Beat the house by having a hand with a higher score than the house's hand without exceeding 21 points.
- Deck: The game uses a standard 52-card deck with four suits: clubs, diamonds, hearts, and spades.
- Card Values: Cards 2 through 10 are worth their face value, face cards (Jack, Queen, King) are worth 10, and Aces can be worth 1 or 11.
Step 1: Creating the Deck
The first step is to generate a standard 52-card deck where each card is represented as a string combining its value and suit (e.g., "AS" for Ace of Spades).
Implementation Details:
- Variables for Suits and Values: Arrays are used to store the four suits ('C', 'D', 'H', 'S') and the thirteen values ('A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K').
- Nested Loops: A loop for suits is nested inside a loop for values. Ensuring every combination of value and suit is created.
- Deck Array: Each combination of value and suit is concatenated into a string (e.g., value + suit) and pushed into the deck array.
const createDeck = () => {
let deck = []
const suits = ['S', 'H', 'C', 'D']
const values = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K'];
for (let suit of suits) {
for (let value of values) {
deck.push(value + suit);
}
}
return deck;
}
This loop constructs a deck where each card is uniquely represented, totalling 52 cards.
Step 2: Shuffling the Deck
Shuffling the deck is crucial for ensuring the game is fair and unpredictable.
Implementation Details:
- Fisher-Yates Shuffle Algorithm: A time-tested algorithm for efficiently shuffling an array.
- Random Selection and Swapping: For each card in the deck, another card is randomly selected and swapped with it. This process iterates backwards from the last card to the first, ensuring every card has an equal chance to end up in any position.
const shuffleDeck = (deck) => {
for (let i = deck.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[deck[i], deck[j]] = [deck[j], deck[i]];
}
return deck;
}
This shuffling algorithm effectively randomises the deck, a crucial aspect of the game's setup.
Step 3: Dealing a Hand of Cards
Creating a function to deal a hand to a player involves selecting the top N cards from the deck.
Implementation Details:
- Splice Method: Used to remove a specified number of cards from the beginning of the deck array. This method alters the original deck and returns the removed elements as a new array (the hand).
const dealHand = (deck, num) => deck.splice(0, num);
Here, `n` is the number of cards to deal. This simulates the action of dealing cards from the top of the deck and ensures the deck size is reduced by the number of cards dealt.
Step 4: Computing the Points
Calculating the score of a hand is slightly complex due to the dual value of Aces.
Implementation Details:
- Point Calculation: Numeric cards contribute their face value, face cards contribute 10, and Aces can be either 1 or 11.
- Handling Aces: Since Aces can have two values, their initial value is 11. If the total exceeds 21, the value of Aces is adjusted to 1 by subtracting 10 points for each Ace until the total is under 21 or all Aces have been adjusted.
const computePoints = (hand) => {
let points = 0;
let acesCount = 0;
for (let card of hand) {
let value = card.substring(0, card.length - 1);
if (value === 'A') {
acesCount++;
points += 11;
} else if (['J', 'Q', 'K', 'T'].includes(value)) {
points += 10;
} else {
points += parseInt(value, 10);
}
}
while (points > 21 && acesCount > 0) {
points -= 10;
acesCount--;
}
return points;
}
This approach ensures that the hand's point total is calculated accurately, adhering to Blackjack's rules and strategies for Aces.
Testing Your Functions
To ensure your game works correctly, write simple tests for each function. Testing can be done using console-based checks or by integrating a framework like Jest for more comprehensive testing.
Testing without a framework leans on the fundamental concept of assertions: checking if the code behaves as expected. We'll outline tests for each function, focusing on key behaviours and edge cases.
Preparing for Manual Testing
Before we begin, it's helpful to establish a simple structure for our tests. We'll use a basic test function that accepts a description of the test and a callback function containing our test logic. If the callback throws an error, the test fails; otherwise, it passes.
const test = (description, fn) => {
try {
fn();
console.log('[PASS] ' + description);
} catch (error) {
console.error('[FAIL] ' + description + '\n' + error.message);
}
}
Testing the Deck Creation
Verify that the createDeck function generates a 52-card deck with the correct composition.
test('createDeck creates a 52 card deck with unique cards', () => {
const deck = createDeck();
const uniqueCards = new Set(deck);
if (deck.length !== 52) throw new Error('Deck should contain 52 cards.');
if (uniqueCards.size !== 52) throw new Error('Deck should have 52 unique cards.');
});
Testing the Deck Shuffle
Ensure that the shuffleDeck function modifies the deck order. Since randomness is involved, testing for a specific outcome is challenging. Instead, we can check that the shuffled deck differs from the original order.
test('shuffleDeck modifies the deck order', () => {
const deck = createDeck();
const originalDeckString = JSON.stringify(deck);
shuffleDeck(deck);
const shuffledDeckString = JSON.stringify(deck);
if (originalDeckString === shuffledDeckString) throw new Error('Deck should be shuffled and not in original order.');
});
Testing the Hand Dealing
Confirm that dealHand correctly deals the specified number of cards and removes them from the deck.
test('dealHand deals the correct number of cards and reduces deck size', () => {
const deck = createDeck();
const handSize = 5;
const hand = dealHand(deck, handSize);
if (hand.length !== handSize) throw new Error(`Hand should contain ${handSize} cards.`);
if (deck.length !== 52 - handSize) throw new Error(`Deck should have ${handSize} fewer cards.`);
});
Testing the Points Computation
Check that computePoints accurately calculates the points for various hands, correctly handling Aces as both 1 and 11.
test('computePoints calculates correct values, including handling of Aces', () => {
const cases = [
{ hand: ['AC', '2S'], expected: 13 },
{ hand: ['5D', 'TH'], expected: 15 },
{ hand: ['AH', 'AS'], expected: 12 },
{ hand: ['AC', '7S', '9C'], expected: 17 },
{ hand: ['AC', 'KH'], expected: 21 },
{ hand: ['9H', '7D', '5S'], expected: 21 },
];
cases.forEach(({ hand, expected }) => {
const result = computePoints(hand);
if (result !== expected) throw new Error(`Expected ${expected} points, got ${result} for hand [${hand.join(', ')}].`);
});
});
Running the Tests
With all tests defined, you can call them in your script to run them. This basic testing approach provides immediate feedback and can catch many logical errors and edge cases. Remember, while this method is helpful for small projects or learning purposes, for larger projects or more complex applications, integrating a testing framework like Jest might be more efficient and provide additional features like mock functions, more comprehensive assertions, and asynchronous test handling.
Conclusion
Building a simple Blackjack game in JavaScript enhances your understanding of basic programming concepts and provides insight into handling logic that involves randomness, conditional statements, and array manipulation. By creating a deck, shuffling it, dealing cards, and calculating scores, you learn to implement core game mechanics applicable to a wide range of other games and applications.
Key Takeaways
- Modular Function Design: By breaking down the game into distinct functions (creating the deck, shuffling, dealing, and scoring), we make our code more readable, maintainable, and testable. Each function performs a specific task and can updated, tested, and debugged independently.
- Algorithm Implementation: The Fisher-Yates shuffle algorithm demonstrates how a simple yet effective method can solve the problem of randomising an array. Understanding such algorithms is crucial for developing efficient code.
- Handling Edge Cases: Calculating scores in Blackjack, especially the dual nature of Aces, highlights the importance of considering and correctly handling edge cases in programming. This thought process is invaluable for developing robust applications.
- Manual Testing Without Frameworks: While testing frameworks like Jest provide powerful tools for automated testing, learning to write and execute simple tests manually is a great starting point. It teaches the testing fundamentals, including writing test cases, asserting outcomes, and debugging failures.
Moving Forward
After completing a basic version of Blackjack, consider enhancing the game with additional features:
- Graphical User Interface (GUI): Integrate HTML and CSS to move your game from the console to the web, making it interactive and visually appealing.
- Game Logic Enhancements: Add features like splitting hands, doubling down, or insurance bets to make the game more complex and engaging.
- Multiplayer Support: Extend the game to support multiple players by taking turns on the same device or implementing a simple server to handle players over the network.
- Use of Frameworks for Testing and Development: Explore testing frameworks like Jest for more sophisticated testing, including mocks and spies, and consider frameworks like React or Vue.js for building the game's UI.
Creating a simple Blackjack game is just the beginning. The skills you've honed here lay the foundation for more complex projects. Programming is about continuous learning and problem-solving. With each new project, you'll encounter different challenges and learn new ways to overcome them, continually growing as a developer.