Some time ago, I ran across a pretty fascinating service, the Deck of Cards API. This API handles everything imaginable related to working with decks of cards. It handles creating a shuffled set of cards (containing one or more decks), dealing out a card (or cards), and even reshuffling.
Even better, it includes card images you can use if you don't want to find your own:
It's an incredibly feature-filled API, and best of all, it's completely free. No need for even a key. I've known about this API for a while and have contemplated building a card game with it, but realized that games can quickly go from simple to fairly complex.
In fact, my friends strongly urged me not to spend time on this, and honestly, they were probably right, but I've got a long history of building code demos that don't make sense. ;)
For my demo, I went with the following rules:
Initially, the player and computer both have an array representing their hands.
playerCards:[],
pcCards:[],
The deal
method handles setting up the hands for both players:
async deal() {
// first to player, then PC, then player, then PC
this.playerCards.push(await this.drawCard());
// for the dealer, the first card is turned over
let newcard = await this.drawCard();
newcard.showback = true;
this.pcCards.push(newcard);
this.playerCards.push(await this.drawCard());
this.pcCards.push(await this.drawCard());
},
Two things to point out. First, I deal to the player, then the PC (or dealer, name-wise I kinda go back and forth), and then back again. I also modify the card result object to have showback
set such that I can render the back of the card for the dealer.
Here's how that's done in HTML:
<div id="pcArea" class="cardArea">
<h3>Dealer</h3>
<template x-for="card in pcCards">
<!-- todo: don't like the logic in template -->
<img :src="card.showback?BACK_CARD:card.image" :title="card.showback?'':card.title">
</template>
</div>
<div id="playerArea" class="cardArea">
<h3>Player</h3>
<template x-for="card in playerCards">
<img :src="card.image" :title="card.title">
</template>
</div>
BACK_CARD
is simply a constant:
const BACK_CARD = "https://deckofcardsapi.com/static/img/back.png";
So, at this point, I could hit the app and get a Blackjack hand:
At the bottom, I used a div to display the current status:
My logic was like so:
Let's focus on the player first. To hit, we simply add a card:
async hitMe() {
this.hitMeDisabled = true;
this.playerCards.push(await this.drawCard());
let count = this.getCount(this.playerCards);
if(count.lowCount >= 22) {
this.playerTurn = false;
this.playerBusted = true;
}
this.hitMeDisabled = false;
},
Bust checking was a bit complex. I built a function to get the 'count' for the hand, but in Blackjack, Aces can be 1 or 11.
I figured out (and hope I'm right), that you can never have two 'high' aces, so my function returns a lowCount
and highCount
value where for the high version, if an Ace exists, it's counted as 11, but only one. Here's that logic:
getCount(hand) {
/*
For a hand, I return 2 values, a low value, where aces are considered 1s, and a high value, where aces are 11. Note that this fails to properly handle a case where I have 3 aces
and could have a mix... although thinking about it, you can only have ONE ace at 11, so
maybe the logic is: low == all aces at 1. high = ONE ace at 11. fixed!
*/
let result = {};
// first we will do low, all 1s
let lowCount = 0;
for(card of hand) {
if(card.value === 'JACK' || card.value === 'KING' || card.value === 'QUEEN') lowCount+=10;
else if(card.value === 'ACE') lowCount += 1;
else lowCount += Number(card.value);
//console.log(card);
}
//console.log('lowCount', lowCount);
let highCount = 0;
let oneAce = false;
for(card of hand) {
if(card.value === 'JACK' || card.value === 'KING' || card.value === 'QUEEN') highCount+=10;
else if(card.value === 'ACE') {
if(oneAce) highCount += 1;
else {
highCount += 10;
oneAce = true;
}
}
else highCount += Number(card.value);
}
//console.log('highCount', highCount);
return { lowCount, highCount };
},
If the player busts, we end the game and let the user start over. If they stand, it's time for the dealer to take over. That logic was simple - hit while below 17 and either bust or stand.
In order to make it a bit more exciting, I used a variable and async function, delay
, to slow the dealer's actions so you can see them play out in (kinda) real-time. Here's the dealer's logic:
async startDealer() {
/*
Idea is - I take a card everytime I'm < 17. so i check my hand,
and do it, see if im going to stay or hit. if hit, i do a delay though
so the game isn't instant.
*/
// really first, initial text
this.pcText = 'The dealer begins their turn...';
await delay(DEALER_PAUSE);
// first, a pause while we talk
this.pcText = 'Let me show my hand...';
await delay(DEALER_PAUSE);
// reveal my second card
this.pcCards[0].showback = false;
// what does the player have, we need the best under 22
let playerCount = this.getCount(this.playerCards);
let playerScore = playerCount.lowCount;
if(playerCount.highCount < 22) playerScore = playerCount.highCount;
//console.log('dealer needs to beat', playerScore);
// ok, now we're going to loop until i bust/win
let dealerLoop = true;
while(dealerLoop) {
let count = this.getCount(this.pcCards);
/*
We are NOT doing 'soft 17', so 1 ace always count as 11
*/
if(count.highCount <= 16) {
this.pcText = 'Dealer draws a card...';
await delay(DEALER_PAUSE);
this.pcCards.push(await this.drawCard());
} else if(count.highCount <= 21) {
this.pcText = 'Dealer stays...';
await delay(DEALER_PAUSE);
dealerLoop = false;
this.pcTurn = false;
if(count.highCount >= playerScore) this.pcWon = true;
else this.playerWon = true;
} else {
dealerLoop = false;
this.pcTurn = false;
this.pcBusted = true;
}
}
}
FYI, pcText
is used in the white status area as a way of setting game messages.
And basically - that's it. If you want to play it yourself, check out the CodePen below, and feel free to fork it and add improvements: