Card Tricks

Playing card

Number of cards drawn: 0

Number of cards remaining:

About this page and its code

It all started as an idea for practicing jQuery with images of playing cards. I wanted to make the cards appear and change using jQuery. When I first tried it, I didn't understand jQuery well enough, and I gave up. Now I know jQuery a little more, and it works the way I envisioned.

Step 1 was finding the card images. They come from the Ornamental Guyenne deck by mariotomo at Open Clip Art.

Step 2 was handling the cards. At first I thought I wanted an array. Then I thought I should use a multidimensional array (suits plus card values). Finally I decided a JSON file would work best, giving me flexibility to make other card applications later on. I made the file quickly by creating an Excel spreadsheet and then dumping it into Mr. Data Converter.

Step 3 was to lay out the DIVs so I could have two cards side by side, with the deck face-down on the right and the turned cards face-up on the left. Like all things with floats, there were some frustrations. The worst was when I removed the card image element on the left and the DIV collapsed. I finally realized I could fix it by adding height in CSS.

The first jQuery thing was to have a card appear on the left when I clicked the deck on the right. I just used a hard-coded path at first. (At that point, I had the image tag in the HTML. Later I removed that element and wrote it in with jQuery.)

$('#rightcard img').click(function() {
    var card = "cards/d01.png";
    $('#leftcard img').attr('src', card);
}

Once that was working, I added the random number script. I knew I would need to destroy the deck one card at a time as I moved the cards from the deck to the face-up stack on the left. It's necessary so that cards don't repeat. That's why I copied all the card objects from cards (the JSON file) into a new array, newDeck.

for (var i=0; i < cards.length; i++) {
    newDeck[i] = cards[i];
}

What I needed to do was remove the card object from newDeck after I had randomly drawn it. Probably the first time I've used splice(). It lets us delete an item from an array using its index (represented here by the variable num). (Note: splice() is plain JavaScript, not jQuery.)

var num = Math.floor(Math.random() * (newDeck.length - 1) );
var playerCard = newDeck[num];
newDeck.splice(num, 1);

What I didn't realize, though, was that I would need to shuffle the deck if I wanted to replay, or go through the deck again. After a few tests, I saw too much repetition in the order of the cards as they were drawn (because random numbers in JavaScript are not really random). I felt sure that someone with more skill than I possess would have solved this already, and I was right. I found this great explainer about the Fisher–Yates Shuffle, and it gave me an efficient, fast function for shuffling my array each time.

Little by little, I added things to the script, like the text on each side that shows how many cards have been drawn and how many cards remain in the array. Each time I added something or took something away, of course, I broke something. One thing at a time: Break it, fix it.

Finally, I broke out chunks of the script into their own functions. I did this mostly while I was making the process repeatable, which required me to make the left side (temporarily) clickable and to reshuffle the deck. That led me to wrap the initial instructions in a new setup() function so I could run it again for the replay in the deckIsFinished() function.

But isn’t this stupid? It’s not even a game

Fair enough. It's not a game at all. But now that I've learned how to handle the complete deck by showing every card exactly once, never repeating any, in a random order that pretty neatly simulates a real deck of cards, I can move on to something more competitive.

And then I realized I should not draw a random card

What was I thinking? Obviously, I wasn't. When we take a card from the top of the deck, we are not taking a random card; we are taking whichever one is on top. This depends on a legitimate shuffle, which I had in fact accomplished (see above).

So where I used to have this:

$('#rightcard img').click(function() {
	var num = Math.floor(Math.random() * (newDeck.length - 1) );
	var playerCard = newDeck[num];
	var card = 'cards/' + playerCard.suit + playerCard.number + '.png';
	...

I now have this:

$('#rightcard img').click(function() {
	var playerCard = newDeck.pop();
	var card = 'cards/' + playerCard.suit + playerCard.number + '.png';
	...

I also deleted the splice() instruction, because with pop(), it's no longer needed. The pop() method automatically deletes the last item in the array. Usually we capture it with a variable as we pop it, as I have done with playerCard here.

You might argue that pop() is suspect, because by taking the card from the end of the array, I'm taking from the bottom of the deck. The alternative, however, is inferior. Although shift() takes the first item out of the array, it is less efficient than pop() because taking the first item means the entire array must be re-indexed, meaning index item 2 becomes 1, and 3 becomes 2, and so on. I learned a lot about JavaScript arrays by reading this: Let’s get those Javascript Arrays to work fast.

Summary

My first instinct for handling a deck of cards (draw a random card) turned out to be wrong, but by following that instinct, I learned about splice() to remove any specific item from inside an array. If I ever need to pluck a random item from an array and delete it, now I know how.

Testing my app led me to search for a shuffle algorithm. Finding one, and adding it to my code, made the order of cards in my array truly random, which allowed me to treat it like a real deck, properly shuffled, and simply draw cards in order from it.

I considered using shift() to draw cards from the top of the deck (the start of the array), but a quick search revealed that pop() was a much superior solution, and it won't change the fairness of a game to draw cards from the bottom of the deck (the end of the array) instead of from the top — even though that's not how we would do it with a physical deck.

Text, code and design by Mindy McAdams

Fork it at GitHub