Generating Playing Cards In JavaScript

JavaScript Functions For Card Games

In this blog post, we will delve into the development of a Solitaire game using JavaScript. Specifically, we will explore important functionalities such as shuffling the deck, dealing out the cards, and setting up the game state. Along the way, we will encounter challenges and discover the significance of associating the array and DOM cards. So, join me and ChatGPT as we unravel the intricacies of building a Solitaire game, from shuffling the deck to creating the deck, clearing the game state, and dealing the cards, setting the stage for an engaging Solitaire experience.

Shuffling the Deck

At the core of our Solitaire game lies the "shuffleDeck" function, which implements the Fisher-Yates algorithm. This algorithm ensures that the deck is thoroughly randomized, providing an element of unpredictability to the game. Let's take a closer look at how it works:

function 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]];
    }
}

Why the Fisher-Yates Algorithm?

The Fisher-Yates algorithm is an excellent choice for shuffling the deck because it guarantees unbiased randomness. By swapping cards in a backward loop, every card has an equal chance of occupying any position in the deck. This randomness ensures a fair and engaging gameplay experience for Solitaire enthusiasts.

The Importance of Associating the Array and DOM Cards

During the development process, we encountered a notable challenge that revolved around associating the array of cards with their corresponding DOM elements. This association is crucial for updating the game's visual representation and maintaining synchronization between the game state and the user interface.

To overcome this challenge, we employed a clever solution. By associating each card object with its corresponding DOM element, we created a seamless connection between the array of cards and the representation of those cards in the Document Object Model (DOM). This association allowed us to manipulate both the card objects and their visual representation effortlessly.

Dealing Cards in Solitaire

Before we dive into the implementation details of the "dealCards" function, let's understand how cards are dealt in Solitaire. Solitaire typically involves seven tableau piles, with each pile initially receiving a different number of face-down cards. The first pile contains one card, the second has two cards, the third has three, and so on. The top card of each tableau pile is face-up, while the rest remain face-down.

Additionally, Solitaire features a stock pile where the remaining cards are placed face-down. The waste pile, initially empty, holds the cards that have been drawn from the stock pile but are not yet placed in the tableau or foundation piles. Lastly, there are foundation piles where the goal is to build up each suit in ascending order, starting with the Ace.

Understanding the "dealCards" Function

Now, let's explore the "dealCards" function, which handles the distribution of cards from the deck to the tableau piles in our Solitaire game. This function takes an array of "cardElements" as input, where each element represents a card's DOM element associated with its corresponding card object.

  1. It iterates through the "cardElements" array, representing each card's DOM element and associated card object.
  2. It adds the card to the current tableau pile while simultaneously removing it from the stock pile.
  3. The function updates the visual representation of the card by appending its DOM element to the corresponding tableau pile in the DOM.
  4. The function also handles the distinction between starting tableau piles (where the top card is face-up) and the rest (where cards remain face-down).
  5. The iteration continues until 28 cards are dealt, distributing them evenly among the seven tableau piles.
let cardsDealt = 0;
let tableauIndex = 0;
let startingPile = 1;

In the "dealCards" function, we begin by initializing three variables:

  • cardsDealt: This variable keeps track of the number of cards that have been dealt so far.
  • tableauIndex: This variable represents the index of the current tableau pile.
  • startingPile: This variable indicates the index of the starting tableau pile.

Next, we enter a while loop that continues until 28 cards have been dealt. Since there are seven tableau piles with four cards each, a total of 28 cards need to be distributed.

while (cardsDealt < 28) {

Inside the loop, we perform three important actions:

const { card, cardElement } = cardElements[cardsDealt];    
tableau[tableauIndex].push(card);
stock.splice(stock.indexOf(card), 1);

First, we use destructuring to extract the card and cardElement from the cardElements array. The cardElements array contains elements representing the DOM element and associated card object for each card in the deck. By accessing cardElements[cardsDealt], we retrieve the specific card element and card object corresponding to the current iteration of the loop.

Next, we add the card object to the tableau pile. The tableau array is a collection of seven sub-arrays, where each sub-array represents a tableau pile. We push the card object into the sub-array that corresponds to the current tableauIndex. This action effectively adds the card to the appropriate tableau pile.

Finally, we remove the dealt card from the stock pile. The stock array represents the remaining cards in the stock pile. To remove the card object from the stock array, we use the splice method. By finding the index of the card object in the stock array (stock.indexOf(card)), we can specify that we want to remove 1 element from that index. This action simulates the process of dealing a card from the stock pile, as the dealt card is no longer part of the remaining cards.

By performing these steps in the loop, the "dealCards" function deals cards to the tableau piles one by one, adding them to the appropriate sub-array in the tableau array and removing them from the stock array.

const tableauPile = document.getElementById(`tableau-${tableauIndex + 1}`); 
    
if (tableauIndex < startingPile) {
    cardElement.classList.add('flipped');
}
    
tableauPile.appendChild(cardElement);

We then retrieve the DOM element of the current tableau pile using its ID. The ID is dynamically generated by concatenating the string "tableau-" with the tableauIndex + 1. Since arrays are zero-indexed, we add 1 to tableauIndex to match the corresponding tableau pile numbering in the DOM.

If the current tableauIndex is less than the startingPile, we add the 'flipped' class to the cardElement. This class ensures that cards in the starting tableau piles are displayed face-up. The cardElement represents the visual representation of the card in the DOM.

Finally, we append the cardElement to the corresponding tableau pile in the DOM using the appendChild method.

cardsDealt++;
tableauIndex++;
    
if (tableauIndex === tableau.length) {
    tableauIndex = startingPile;
    startingPile++;
}

You put all these pieces together and you get:

function dealCards(cardElements) {
    let cardsDealt = 0;
    let tableauIndex = 0;
    let startingPile = 1;
    while (cardsDealt < 28) {
        const { card, cardElement } = cardElements[cardsDealt];
        tableau[tableauIndex].push(card);
        stock.splice(stock.indexOf(card), 1);
        const tableauPile = document.getElementById(`tableau-${tableauIndex + 1}`);
        if (tableauIndex < startingPile) {
            cardElement.classList.add('flipped');
        }
        tableauPile.appendChild(cardElement);
        cardsDealt++;
        tableauIndex++;
        if (tableauIndex === tableau.length) {
            tableauIndex = startingPile;
            startingPile++;
        }
    }
}

Starting the Game

The newGame function serves the purpose of initializing a new game of Solitaire. It performs several essential tasks to set up the game state, including clearing arrays and variables, creating and shuffling the deck, creating DOM elements for the cards, dealing the cards to the tableau piles, and starting the game loop. By calling this function, we ensure that all necessary steps are taken to begin a fresh game.

Clearing Arrays/Variables

In the newGame function, the arrays and variables related to the game state are cleared. This step is crucial because it resets the previous game's state and prepares the game for a new session. The clearing process involves the following:

The newGame function serves the purpose of initializing a new game of Solitaire. It performs several essential tasks to set up the game state, including clearing arrays and variables, creating and shuffling the deck, creating DOM elements for the cards, dealing the cards to the tableau piles, and starting the game loop. By calling this function, we ensure that all necessary steps are taken to begin a fresh game.

for (let i = 0; i < tableau.length; i++) {
    tableau[i].length = 0;
}

for (let i = 0; i < foundation.length; i++) {
    foundation[i].length = 0;
}

stock.length = 0;
waste.length = 0;

selectedCard = null;
selectedPileType = null;
selectedPileIndex = null;

const tableauPiles = document.getElementsByClassName('pile');
for (const pile of tableauPiles) {
    pile.innerHTML = '';
}

Creating Cards in the Arrays

Following the clearing step, the newGame function creates a new deck of cards in the deck array. The process involves iterating over the suits array and ranks array to generate card objects. Each card object represents a single card and contains properties for suit, rank, and value.

const deck = [];
for (const suit of suits) {
  for (let i = 0; i < ranks.length; i++) {
    const card = {
        suit,
        rank: ranks[i],
        value: values[i],
    };
    deck.push(card);
  }
}

Shuffling the Deck and Adding to Stock

After creating the deck, it is shuffled using the shuffleDeck function. Shuffling the deck randomizes the order of the cards, ensuring a fair distribution for the new game.

shuffleDeck(deck);

Once the deck is shuffled, it is added to the stock pile. This is done by using the spread operator (...) to spread the elements of the deck array into the stock array. As a result, the stock pile contains all the cards from the shuffled deck.

stock.push(...deck);

Creating Cards in the DOM

After shuffling the deck, the newGame function proceeds to create DOM elements for each card in the deck. This involves iterating over the deck array and dynamically creating a DOM element for each card. We make sure to add the card's rank and suit as text within the element and add CSS classes as attributes.

const cardElements = [];
for (const card of deck) {
  const { suit, rank } = card;
  const cardElement = document.createElement('div');
  
  cardElement.classList.add('card', suit);
  
  cardElement.innerHTML = `${rank} `;
  if (suit === 'hearts') {
      cardElement.innerHTML += '&#9829;';
  } else if (suit === 'spades') {
      cardElement.innerHTML += '&#9824;';
  } else if (suit === 'clubs') {
      cardElement.innerHTML += '&#9827;';
  } else if (suit === 'diamonds') {
      cardElement.innerHTML += '&#9830;';
  }

  //...
}

To establish a connection between the card objects in the deck array and their corresponding DOM elements, the newGame function associates each card object with its DOM element. This is achieved by setting properties on both the card object and the DOM element.

stockPile.appendChild(cardElement);

card.cardElement = cardElement;
cardElement.card = card;

cardElements.push({ card, cardElement });

Calling dealCards and gameLoop Functions

With the deck created, shuffled, and associated with DOM elements, the newGame function proceeds to call two additional functions: dealCards(cardElements) and gameLoop().

dealCards(cardElements); 
gameLoop();

The dealCards function distributes the cards from the stock pile to the tableau piles, as explained earlier. The gameLoop function handles user interactions, updates the game state, and executes game logic, allowing the player to interact with the Solitaire game. Calling gameLoop initiates the gameplay for the newly set-up game.

Putting It All Together

function newGame() {
    for (let i = 0; i < tableau.length; i++) {
        tableau[i].length = 0;
    }
    for (let i = 0; i < foundation.length; i++) {
        foundation[i].length = 0;
    }
    stock.length = 0;
    waste.length = 0;
    selectedCard = null;
    selectedPileType = null;
    selectedPileIndex = null;
    const tableauPiles = document.getElementsByClassName('pile');
    for (const pile of tableauPiles) {
        pile.innerHTML = '';
    }
    const deck = [];  
    for (const suit of suits) {
        for (let i = 0; i < ranks.length; i++) {
            const card = {
                suit,
                rank: ranks[i],
                value: values[i],
            };
            deck.push(card);
        }
    } 
    shuffleDeck(deck);
    stock.push(...deck);
    const cardElements = [];
    for (const card of deck) {
        const { suit, rank } = card;
        const cardElement = document.createElement('div');
        cardElement.classList.add('card', suit);
        cardElement.innerHTML = `${rank} `;
        if (suit === 'hearts') {
            cardElement.innerHTML += '&#9829;';
        } else if (suit === 'spades') {
            cardElement.innerHTML += '&#9824;';
        } else if (suit === 'clubs') {
            cardElement.innerHTML += '&#9827;';
        } else if (suit === 'diamonds') {
            cardElement.innerHTML += '&#9830;';
        }
        stockPile.appendChild(cardElement);
        card.cardElement = cardElement;
        cardElement.card = card;
        cardElements.push({ card, cardElement });
    }
    dealCards(cardElements);
    gameLoop();
}

Conclusion

Setting up a Solitaire game involves several crucial steps. We explored the Fisher-Yates algorithm for shuffling the deck, ensuring unbiased randomness and a fair gameplay experience. Additionally, we discussed the importance of associating the array of cards with their corresponding DOM elements to maintain synchronization between the game state and the user interface.

We then dived into the "dealCards" function, which handles the distribution of cards from the deck to the tableau piles, following the rules of Solitaire. By iterating through the card elements and associating them with their respective card objects, we seamlessly connected the game logic with its visual representation.

Finally, we examined the "newGame" function, which initializes a fresh game of Solitaire. It clears the game state, creates and shuffles the deck, creates DOM elements for the cards, deals the cards to the tableau piles, and starts the game loop. By calling this function, we ensure that all necessary steps are taken to begin an engaging game of Solitaire.

Now that you understand the intricacies of setting up the game, you're ready to embark on your Solitaire development journey. Enjoy creating your own Solitaire game using JavaScript and have fun exploring further enhancements and features!