Bootcamp
Search…
1.3: High Card DOM

Introduction

Many of us will have seen High Card from Coding Basics. For a refresher on High Card, see the Coding Basics page here. In this module we will implement High Card from scratch, adding DOM elements to make the game more visual and interactive.

Basic High Card

The following is a wireframe of our High Card UI. Game info will be in a big box on top, and there will be 2 buttons below for player 1 and 2 to draw cards respectively.
The following is an adaptation of our code from Coding Basics, where we add UI elements with DOM manipulation and event listeners to trigger game logic and UI updates based on user actions. The logic of this first version of High Card is the same as the original, non-DOM game.
1
// Get a random index ranging from 0 (inclusive) to max (exclusive).
2
const getRandomIndex = (max) => Math.floor(Math.random() * max);
3
​
4
// Shuffle an array of cards
5
const shuffleCards = (cards) => {
6
// Loop over the card deck array once
7
for (let currentIndex = 0; currentIndex < cards.length; currentIndex += 1) {
8
// Select a random index in the deck
9
const randomIndex = getRandomIndex(cards.length);
10
// Select the card that corresponds to randomIndex
11
const randomCard = cards[randomIndex];
12
// Select the card that corresponds to currentIndex
13
const currentCard = cards[currentIndex];
14
// Swap positions of randomCard and currentCard in the deck
15
cards[currentIndex] = randomCard;
16
cards[randomIndex] = currentCard;
17
}
18
// Return the shuffled deck
19
return cards;
20
};
21
​
22
const makeDeck = () => {
23
// Initialise an empty deck array
24
const newDeck = [];
25
// Initialise an array of the 4 suits in our deck. We will loop over this array.
26
const suits = ["hearts", "diamonds", "clubs", "spades"];
27
​
28
// Loop over the suits array
29
for (let suitIndex = 0; suitIndex < suits.length; suitIndex += 1) {
30
// Store the current suit in a variable
31
const currentSuit = suits[suitIndex];
32
​
33
// Loop from 1 to 13 to create all cards for a given suit
34
// Notice rankCounter starts at 1 and not 0, and ends at 13 and not 12.
35
// This is an example of a loop without an array.
36
for (let rankCounter = 1; rankCounter <= 13; rankCounter += 1) {
37
// By default, the card name is the same as rankCounter
38
let cardName = `${rankCounter}`;
39
​
40
// If rank is 1, 11, 12, or 13, set cardName to the ace or face card's name
41
if (cardName === "1") {
42
cardName = "ace";
43
} else if (cardName === "11") {
44
cardName = "jack";
45
} else if (cardName === "12") {
46
cardName = "queen";
47
} else if (cardName === "13") {
48
cardName = "king";
49
}
50
​
51
// Create a new card with the current name, suit, and rank
52
const card = {
53
name: cardName,
54
suit: currentSuit,
55
rank: rankCounter,
56
};
57
​
58
// Add the new card to the deck
59
newDeck.push(card);
60
}
61
}
62
​
63
// Return the completed card deck
64
return newDeck;
65
};
66
​
67
const deck = shuffleCards(makeDeck());
68
​
69
// Player 1 starts first
70
let playersTurn = 1;
71
​
72
// Use let for player1Card object because player1Card will be reassigned
73
let player1Card;
74
​
75
// create two buttons
76
const player1Button = document.createElement("button");
77
player1Button.innerText = "Player 1 Draw";
78
document.body.appendChild(player1Button);
79
​
80
const player2Button = document.createElement("button");
81
player2Button.innerText = "Player 2 Draw";
82
document.body.appendChild(player2Button);
83
​
84
// Create game info div as global value
85
// fill game info div with starting instructions
86
const gameInfo = document.createElement("div");
87
gameInfo.innerText = "Its player 1 turn. Click to draw a card!";
88
document.body.appendChild(gameInfo);
89
​
90
// Create a helper function for output to abstract complexity
91
// of DOM manipulation away from game logic
92
const output = (message) => {
93
gameInfo.innerText = message;
94
};
95
​
96
// Add an event listener on player 1's button to draw card and switch
97
// to player 2's turn
98
player1Button.addEventListener("click", () => {
99
if (playersTurn === 1) {
100
player1Card = deck.pop();
101
playersTurn = 2;
102
}
103
});
104
​
105
// Add event listener on player 2's button to draw card and determine winner
106
// Switch back to player 1's turn to repeat game
107
player2Button.addEventListener("click", () => {
108
if (playersTurn === 2) {
109
const player2Card = deck.pop();
110
playersTurn = 1;
111
​
112
if (player1Card.rank > player2Card.rank) {
113
output("player 1 wins");
114
} else if (player1Card.rank < player2Card.rank) {
115
output("player 2 wins");
116
} else {
117
output("tie");
118
}
119
}
120
});
Copied!

Refactoring

The above is a lot of code, so we can organise it into sections (soon we'll learn how to split up sections into multiple files) within our JS file.

Global Setup

Global variables that store game-wide data or DOM elements
1
const deck = shuffleCards(makeDeck());
2
​
3
let playersTurn = 1; // matches with starting instructions
4
let player1Card;
5
​
6
const player1Button = document.createElement("button");
7
​
8
const player2Button = document.createElement("button");
9
​
10
const gameInfo = document.createElement("div");
Copied!

Helper Functions

Helper functions for game logic
1
// Get a random index ranging from 0 (inclusive) to max (exclusive).
2
const getRandomIndex = (max) => Math.floor(Math.random() * max);
3
​
4
// Shuffle an array of cards
5
const shuffleCards = (cards) => {
6
// Loop over the card deck array once
7
for (let currentIndex = 0; currentIndex < cards.length; currentIndex += 1) {
8
// Select a random index in the deck
9
const randomIndex = getRandomIndex(cards.length);
10
// Select the card that corresponds to randomIndex
11
const randomCard = cards[randomIndex];
12
// Select the card that corresponds to currentIndex
13
const currentCard = cards[currentIndex];
14
// Swap positions of randomCard and currentCard in the deck
15
cards[currentIndex] = randomCard;
16
cards[randomIndex] = currentCard;
17
}
18
// Return the shuffled deck
19
return cards;
20
};
21
​
22
const makeDeck = () => {
23
// Initialise an empty deck array
24
const newDeck = [];
25
// Initialise an array of the 4 suits in our deck. We will loop over this array.
26
const suits = ["hearts", "diamonds", "clubs", "spades"];
27
​
28
// Loop over the suits array
29
for (let suitIndex = 0; suitIndex < suits.length; suitIndex += 1) {
30
// Store the current suit in a variable
31
const currentSuit = suits[suitIndex];
32
​
33
// Loop from 1 to 13 to create all cards for a given suit
34
// Notice rankCounter starts at 1 and not 0, and ends at 13 and not 12.
35
// This is an example of a loop without an array.
36
for (let rankCounter = 1; rankCounter <= 13; rankCounter += 1) {
37
// By default, the card name is the same as rankCounter
38
let cardName = `${rankCounter}`;
39
​
40
// If rank is 1, 11, 12, or 13, set cardName to the ace or face card's name
41
if (cardName === "1") {
42
cardName = "ace";
43
} else if (cardName === "11") {
44
cardName = "jack";
45
} else if (cardName === "12") {
46
cardName = "queen";
47
} else if (cardName === "13") {
48
cardName = "king";
49
}
50
​
51
// Create a new card with the current name, suit, and rank
52
const card = {
53
name: cardName,
54
suit: currentSuit,
55
rank: rankCounter,
56
};
57
​
58
// Add the new card to the deck
59
newDeck.push(card);
60
}
61
}
62
​
63
// Return the completed card deck
64
return newDeck;
65
};
66
​
67
// Create a helper function for output to abstract complexity
68
// of DOM manipulation away from game logic
69
const output = (message) => {
70
gameInfo.innerText = message;
71
};
Copied!

Player Action Callbacks

Callbacks that get triggered when player 1 and 2 click on their respective buttons.
1
const player1Click = () => {
2
if (playersTurn === 1) {
3
player1Card = deck.pop();
4
playersTurn = 2;
5
}
6
};
7
​
8
const player2Click = () => {
9
if (playersTurn === 2) {
10
const player2Card = deck.pop();
11
playersTurn = 1;
12
​
13
if (player1Card.rank > player2Card.rank) {
14
output("player 1 wins");
15
} else if (player1Card.rank < player2Card.rank) {
16
output("player 2 wins");
17
} else {
18
output("tie");
19
}
20
}
21
};
Copied!

Game Initialisation

We can now centralise our game initialisation into a single function called initGame.
1
const initGame = () => {
2
// initialize button functionality
3
player1Button.innerText = "Player 1 Draw";
4
document.body.appendChild(player1Button);
5
​
6
player2Button.innerText = "Player 2 Draw";
7
document.body.appendChild(player2Button);
8
​
9
player1Button.addEventListener("click", player1Click);
10
player2Button.addEventListener("click", player2Click);
11
​
12
// fill game info div with starting instructions
13
gameInfo.innerText = "Its player 1 turn. Click to draw a card!";
14
document.body.appendChild(gameInfo);
15
};
Copied!

High Card with More Visual Output

Now let's display more complex HTML elements in our Game Info box instead of just text. This will require more advanced DOM manipulation because it involves nested elements, e.g. cards within the game info box, various properties within each card.

DOM Manipulation Strategies

When creating visual DOM element output with JS we'll use the following strategies to help us.
  1. 1.
    Hard-code HTML first to determine what structure we need, before coding JS for those elements
  2. 2.
    Apply CSS styles with CSS classes defined in a separate CSS file, and avoid applying CSS styles to DOM elements inline. This helps us keep our CSS organised and manageable.
  3. 3.
    Try to keep game logic as simple as possible by creating helper functions for creation or manipulation of each DOM element. This allows us to separate more "operational code" (creating elements, adding elements to the DOM, adding CSS classes to elements) from the higher-level logic (e.g. createGameInfoBox, createPlayerButton, createCard, revealCard). Try to avoid mixing DOM manipulation code with higher-level logic.

Create Sample Element with HTML

Before writing DOM manipulation code in JS, we can create a sample element with HTML to verify what we want our JS code to achieve. The following might be the HTML we want for a single card.
1
<div class="card">
2
<div class="name red">3</div>
3
<div class="suit">♥️</div>
4
</div>
Copied!
The CSS for the card elements might be the following.
1
.card {
2
margin: 10px;
3
padding: 10px;
4
background-color: grey;
5
width: 50px;
6
text-align: center;
7
border-radius: 8px;
8
display: inline-block;
9
}
10
​
11
.suit {
12
margin: 5px;
13
font-size: 20px;
14
}
15
​
16
.name {
17
margin: 5px;
18
font-size: 24px;
19
font-weight: bold;
20
font-family: sans-serif;
21
}
22
​
23
.red {
24
color: red;
25
}
Copied!
Using the above HTML and CSS we can verify what we hope to achieve with our JS DOM manipulation by rendering a hard-coded element in our browser.
We might also want to wrap our cards in a container in which our JS can add these cards programatically. The resulting HTML might look like the following.
1
<div class="card-container">
2
<div class="card">
3
<div class="name red">3</div>
4
<div class="suit">♥️</div>
5
</div>
6
<div class="card">
7
<div class="name red">4</div>
8
<div class="suit">♥️</div>
9
</div>
10
</div>
Copied!

Translate HTML to JS DOM Manipulation

The JS to build the card element might look like the following. Note that we are still using hard-coded values, and we will parameterise those values below.
1
const suit = document.createElement("div");
2
suit.classList.add("suit");
3
suit.innerText = "♥️";
4
​
5
const name = document.createElement("div");
6
name.classList.add("name", "red");
7
name.innerText = "3";
8
​
9
const card = document.createElement("div");
10
card.classList.add("card");
11
​
12
card.appendChild(name);
13
card.appendChild(suit);
Copied!
We will want to abstract this into a function to re-use for multiple cards, such as the following.
1
const makeCard = () => {
2
const suit = document.createElement("div");
3
suit.classList.add("suit");
4
suit.innerText = "♥️";
5
​
6
const name = document.createElement("div");
7
name.classList.add("name", "red");
8
name.innerText = "3";
9
​
10
const card = document.createElement("div");
11
card.classList.add("card");
12
​
13
card.appendChild(name);
14
card.appendChild(suit);
15
};
Copied!

Parameterise Card Attributes

We will now update makeCard to accept a parameter containing all the relevant attributes for a single card. We will include visual attributes in addition to the attributes we used for cards in Coding Basics (name, suit, rank).
One of the new visual attributes is colour, representing the colour of the card's suit. To get the correct colour for each card, we could determine it based on the card's suit. For this example, we will encode the colour directly into the card metadata object along with name, suit, rank, and other metadata.
We will store metadata needed to display our card in a cardInfo object, including suitSymbol, displayName, and colour. suitSymbol is the symbol of the card's suit. displayName is the short-form display name of the card's name. colour is the colour of the card's suit. We use displayName because our element has no room for full names of face cards like "queen".
1
const cardInfo = {
2
suitSymbol: "♦️",
3
suit: "diamond",
4
name: "queen",
5
displayName: "Q",
6
colour: "red",
7
rank: 12,
8
};
Copied!
Now we can pass and use the cardInfo attributes in our createCard function.
1
const createCard = (cardInfo) => {
2
const suit = document.createElement("div");
3
suit.classList.add("suit");
4
suit.innerText = cardInfo.suitSymbol;
5
​
6
const name = document.createElement("div");
7
name.classList.add("name", cardInfo.colour);
8
name.innerText = "3";
9
​
10
const card = document.createElement("div");
11
card.classList.add("card");
12
​
13
card.appendChild(name);
14
card.appendChild(suit);
15
​
16
return card;
17
};
Copied!

Update Deck Creation to Include Visual Card Attributes

How would we alter createDeck to include the new visual attributes for each card's metadata?

Integrate Card Element Generation with Game Logic

Add the cardContainer DOM element as a global variable.
1
let cardContainer;
Copied!
Initialise cardContainer as a div with CSS class card-container, and add it to the document body. Add this logic to the initGame function.
1
cardContainer = document.createElement("div");
2
cardContainer.classList.add("card-container");
3
document.body.appendChild(cardContainer);
Copied!
Update the player 1 and 2 button callback functions to create and render the relevant elements in cardContainer.
1
const player1Click = () => {
2
if (playersTurn === 1) {
3
// Pop player 1's card metadata from the deck
4
player1Card = deck.pop();
5
​
6
// Create card element from card metadata
7
const cardElement = createCard(player1Card);
8
// Empty cardContainer in case this is not the 1st round of gameplay
9
cardContainer.innerHTML = "";
10
// Append the card element to the card container
11
cardContainer.appendChild(cardElement);
12
​
13
// Switch to player 2's turn
14
playersTurn = 2;
15
}
16
};
17
​
18
const player2Click = () => {
19
if (playersTurn === 2) {
20
// Pop player 2's card metadata from the deck
21
const player2Card = deck.pop();
22
​
23
// Create card element from card metadata
24
const cardElement = createCard(player2Card);
25
// Append card element to card container
26
cardContainer.appendChild(cardElement);
27
​
28
// Switch to player 1's turn
29
playersTurn = 1;
30
​
31
// Determine and output winner
32
if (player1Card.rank > player2Card.rank) {
33
output("player 1 wins");
34
} else if (player1Card.rank < player2Card.rank) {
35
output("player 2 wins");
36
} else {
37
output("tie");
38
}
39
}
40
};
Copied!

Exercise

Fork and clone the Coding Bootcamp High Card repo and use code from High Card above to construct a working High Card game.