Using HTML, CSS and JavaScript, I built a two player memory game. The game rules follow:
To establish a structure for the game, I started with a standard HTML setup. I included a scoreboard and a gameboard.
<div class="card" data-icon="cat">
<i class="fas fa-cat fa-5x card-front"></i>
<img class="card-back" src="img/question.png" alt="question mark">
</div>
<div class="card" data-icon="cat">
<i class="fas fa-cat fa-5x card-front"></i>
<img class="card-back" src="img/question.png" alt="question mark">
</div>
I created a grid of divs which serve as the cards, a pair of them is shown in the code above. Each card has an image on the back, which is the side you see at the game start. The other side of each card has an icon. There are currently 16 cards total, and 8 unique icons for 8 matches. That's about it for the HTML!
This game serves up a handful of challenges, and here is how I tackled some of those.
To begin the game, the cards must be shuffled. Below is my
shuffleCards
function.
function shuffleCards() {
cards.forEach(card => {
let randomPos = Math.floor(Math.random() * 16);
card.style.order = randomPos;
});
}
To shuffle the cards, I created a function that generates a random number between
1-16. First, it goes through each card in the
cards
array
using
forEach,
and assigns it a random number. The random
number is generated by first taking
Math.random
and multiplying
it by our number of cards (16) to get a random number between 1 and 16. Then we use
Math.floor
to round this result down to an integer, which
then gets applied to the
order
style on the card divs to change their displayed
order on screen. This is called upon the start of a new game.
The next task is to make the cards display properly when clicked, and also to assign a "first" or "second" status to them to allow the match and win checks to take place.
function flipCard() {
if (lockBoard) return;
if (this === firstCard) return;
this.classList.add('flip');
if (!secondFlip) {
secondFlip = true;
firstCard = this;
} else {
secondCard = this;
checkForMatch();
checkForWin();
}
}
In the
flipCard
function shown above, there are several things
happening. First, we make sure that the cards aren't clickable when they're flipping back
over. Secondly, we make sure that if the player clicks the same card a second time within
a turn, nothing happens. When a card is clicked, it is has a
flip
class added to it. Each card div has both an image (shared with all other card backs), and
an icon. You can see both in the HTML snippet included above. The icon is not visible upon
loading the game, but when the
flip
class is added, it will add
a rotation animation, and display the icon on the underside of the card. If the card is
the first one clicked in a turn, it is assigned a
firstCard
class,
otherwise, we know it's the
secondCard
. Once a second card is
clicked,
flipCard
calls
checkForMatch
(to check for a match) and
checkForWin
(to check for a winner).
The next task is to see if the cards match.
function checkForMatch() {
let isMatch = firstCard.dataset.icon === secondCard.dataset.icon;
if (isMatch) {
updateMatchColor();
disableCards();
updateScore();
setTimeout(() => {
updateScoreboard();
}, 500);
} else {
unflipCards();
setTimeout(() => {
nextPlayer();
}, 1500);
}
}
When the
flip
class is added to two cards,
checkForMatch
is the function that tests to see if the
dataset
on each matches the other. If it does, then the paired match stays flipped and turns the
color of the player to whom it is awarded. Those cards then get disabled from further game
play, and the score is updated. If the cards are not a match, the
flip
class is removed from them, and they are turned back over to remain in play. The first
setTimeout
here allows the scoreboard to be updated before
highlighting the next player. The second timeout is to allow the cards to finish flipping
back over when it's not a match before changing players.
When a match is not made, then it's the other player's turn.
<div id="scoreboard" class="row player-panel">
<div class="col player-0-panel active">
<div class="player-name" id="player-0">BLUE PLAYER</div>
<div class="player-score" id="score-0">0</div>
</div>
<div class="col">
<button id="new-game">NEW GAME</button>
</div>
<div class="col player-1-panel">
<div class="player-name" id="player-1">RED PLAYER</div>
<div class="player-score" id="score-1">0</div>
</div>
</div>
In my HTML file, I included a div for each player. One of the player divs has a class
of
active
added by default.
function nextPlayer() {
if (activePlayer === 0) {
activePlayer = 1;
document.querySelector('.player-0-panel').classList.toggle('active');
document.querySelector('.player-1-panel').classList.toggle('active');
} else {
activePlayer = 0;
document.querySelector('.player-0-panel').classList.toggle('active');
document.querySelector('.player-1-panel').classList.toggle('active');
}
}
If the player has made a match, they get another turn. This is done by leaving them in
active status with the
active
class. If they do not get a
match, then the
nextPlayer
function is called, and the
active
class toggles to the other player. The active player is highlighted on
the scoreboard at top of the game.
When the cards have all been flipped over, a winner must be declared.
function checkForWin() {
if (scores[0] + scores[1] == 8) {
// The Timeout function here is to allow the cards to finish
// flipping and updating color before showing the winner.
setTimeout(() => {
updateWinner();
}, 1000);
}
}
function updateWinner() {
if (scores[0] > scores[1]) {
document.getElementById('scoreboard').style.display = 'none';
document.getElementById('winner-alert').style.display = 'block';
document.getElementById('winner-alert').style.color = '#044497';
document.getElementById('winner-alert').textContent = 'BLUE WINS!';
} else if (scores[0] < scores[1]) {
document.getElementById('scoreboard').style.display = 'none';
document.getElementById('winner-alert').style.display = 'block';
document.getElementById('winner-alert').style.color = 'red';
document.getElementById('winner-alert').textContent = 'RED WINS!';
} else {
document.getElementById('scoreboard').style.display = 'none';
document.getElementById('winner-alert').style.display = 'block';
document.getElementById('winner-alert').textContent = 'TIE GAME, TRY AGAIN!';
}
setTimeout(() => {
resetGame();
}, 1500);
}
Each time there is a match, the
checkForWin
function is called.
After all 8 pairs of cards have been matched, then a winner is calculated. Whichever player
has a greater number of matches wins. The timeout shown in
checkForWin
allows the last card to finish flipping over and being assigned its color before declaring
the winner. When the
updateWinner
function runs, it changes the
style and text content of the winning player's scoreboard to show who won, or that it's a
tie. The timeout there is to allow this message to display briefly before resetting the
board for a new game.
The players need an ability to start a new game, or to have one load automatically when their current game is complete.
function resetGame() {
secondFlip = false;
lockBoard = false;
firstCard = null;
secondCard = null;
scores = [0,0];
activePlayer = 0;
cards.forEach(card => card.addEventListener('click', flipCard));
cards.forEach(card => card.classList.remove('flip'));
cards.forEach(card => card.classList.remove('blue'));
cards.forEach(card => card.classList.remove('red'));
lockBoard = true;
setTimeout(() => {
shuffleCards();
lockBoard = false;
}, 1000);
document.getElementById('score-0').textContent = '0';
document.getElementById('score-1').textContent = '0';
document.getElementById('scoreboard').style.display = '';
document.getElementById('winner-alert').style.display = 'none';
}
When a player has won, or the "New Game" button has been clicked, the gameboard is reset. The scores are reset, and the added classes are removed from all cards. The timeout here allows the cards to finish flipping over before shuffling begins.
In order to make the game play more smoothly, I did have to add a timer to a lot of the functions included, as you can see above. I felt this was one of the fun challenges. Adding these in definitely improved the user experience of the game. Many were issues that I never anticpated having until I ran into them during game play. Some of the timers added are:
This was a fun exercise for me. I ran into some roadblocks that were a little tricky, but it gave me a good opportunity to learn some new tricks.
Please note, this game is best viewed in a desktop setting.