✨ A minimalist and nostalgic card-matching game featuring some of my favourite emojis 👾 ✨
Breaking down the process (with pictures!)
- From an array of
emojis
, created and styled 12 starter cards. - Added click handler function calls to each card.
- For every 2 clicks, 1 turn is taken (2 clicks will eventually represent either matched or mismatched cards). The click handler updates state values (
move
andturns
) depending on how many times the user has clicked cards. - Turns are displayed via state logic beneath the cards.
- Added a
shuffle
function for randomization of card placement using the Fisher-Yates Sorting Algorithm. This algorithm is often used to shuffle an array of items, as many of the typical methods associated with arrays (like mapping) follow an algorithm in sequence. - Appended
cards
array with two matchingemojis
per loop (two at a time) in order to focus on the game's matching logic; emoji pairs to be randomly appended at a later point.
On each click, the entire game would reshuffle:
This is a state issue. Because React is designed to re-render components on any change of state and the app's state changes with each card
click (clicks counted to display turns
taken), the turns
counter logic was the culprit. The call to the shuffle
function was originally happening within the component whose state changes on click. This means that the shuffle
function was being called repeatedly with every click/state change and the cards were re-rendering & shuffling.
While there are many potential solutions to this sort of re-rendering issue (React.memo, React.useRef), sometimes the most straightforward solution is to move logic out of the component. This is also key in moving towards improved separation of concerns.
Here you can see that the shuffle function still randomizes the placement of the cards when the application is refreshed and the cards stay in place when the aforementioned state change takes place (on each click of a card):
Following this bug fix, I made another call to the shuffle
function in order to randomize the emojis
array. This ensures that when they are appended to the cards
array, we'll have a different assortment of emojis
for each game:
- Added state management for
selections
(the emojis selected on each turn will populate theselections
array for comparison and clear out every 2 clicks). - Wrote
checkMatch
function to compare card selections and check for a match. As I'm working incrementally, currently this function prints to console either "It's a match! 🥳" or "Sorry, no match. 😢" - Updated click handler function to use
setSelections
when a card is clicked, updating theselections
array with the emoji that has just been clicked. - Added call to
useEffect
to monitor changes inselections
state. This ensures that when a comparison between two selected cards is made, both emojis are present in theselections
array (accounting for React's asynchronous state updates).
Today my focus was to begin addressing the issue of double clicks, meaning: two clicks should not be counted as a match if the user is double clicking the same card. While technically they will have clicked the same emoji twice, their selections should come from two different cards– as in a traditional matching/memory game.
I've opted to compare not only emojis
but ids
as well. As each card is assigned a key
during rendering, I can easily take the cards' key
values and assign them to an id
variable in an object representing a first
or second
selection. By comparing ids
, we can account for the double click issue.
I began by typing out what I wanted the object to look like. I will often write out the general structure of an object this way as it helps me to visualize the data as a starting point:
I then refactored the object to reduce the number of lines for better readability:
And then, using this object structure, updated the state initialization and corresponding code accordingly:
-
Completed yesterday's refactor by passing both
id
andemoji
values via the click handler for each card. -
Set up
handleClick
function to receive both of these values. -
Tested to ensure that the values were printing to the console as expected (
first
andsecond
objects should be updated withid
andemoji
values;id
values should be different;selections.first
andselections.second
should reset tonull
values following each turn taken): -
Adjusted
handleClick
conditionals to make the code more human-readable. For example: instead of writingif (move < 2)
, I wroteif (move === 1)
. Instead of writingsetMove((move) => move + 1)
, I wrotesetMove(2)
. -
Updated
checkMatch
function to first check theids
of selected emojis. If theids
forfirst
andsecond
selections
are the same: a message prints to indicate that the same card has been selected twice, theselections
state is reset tonull
values, and function exection ends. User will then be able to take another turn.
- Added
cards
to state in order to leverage state for managing their visibility:With that in place, cards/emojis can be displayed dynamically based on a conditional which checks if theconst [cards, setCards] = useState( shuffledEmojis.map((emoji, index) => ({ id: index, emoji: emoji, matched: false }))
matched
property istrue
orfalse
. (If true, the matched cards/emojis should no longer be visible). - Began tracking the number of
matches
in both the state of the application and the UI itself so that users can see the number of turns they've taken vs. number of matches made. The state of the match counter is updated via thecheckMatch
function. - Since we now have the
cards
in state (re: point 1), I modified thecheckMatch
function to update theirmatched
property. For eachcard
that has been matched,matched
will now be set totrue
. Because the app is set up to render cards based on this value, cards/emojis will now disappear when matched.
This (point 3) was done via the prev
(previous) React pattern. The TLDR: useState
set
functions (as established at state initialization, ie: const [cards, setCards] = useState( [...]
) will have access to the previous state via the set
function.
What this means is that we can copy the entirety of the previous state (cards
) when we call setCards
to update the state (typically using the spread operator: ...cards
). We can then add to/update the previous state as needed. You can read more about this in the React documentation here. Further discussion about this topic can be found on Stack Overflow.
Changes to the checkMatch
function:
The app in its current state, with emojis disappearing on match and both the turn & match counters functioning:
3:36: The main container simply holds all of the elements and determines the size of the card. .thecard holds the two front and back elements and also controls the mouse hover action with the hover pseudoclass. The front element is the front-facing element which shows us the default face. The back element is the reverse side of the card, which shows when a user hovers over the card. If you take away the preserve-3d value, all you get is a reversal of the front side of the card (the back doesn't show). It's easier to visualize this with text in the front of the card.
Sandwich analogy: without preserve-3d, we only flip the top slice. With preserve-3d, we flip the entire sandwich. (What a great visual!!!)
4:48: the backface-visibility: hidden; property (applied to "thefront" and "theback" both) controls the reverse side of just that div. Keeping either visible will conflict with the 3D layers and cause a flicker in the animation.
CSS Commit Nov 18: Add notes re: grid-template-columns: repeat(4, 7rem);
Also re: .maincontainer { position: static; }
https://www.w3schools.com/css/css_positioning.asp
Notes to be added:
- Confetti
- Testing refresher
- Deployment challenges
Building a reset function to be triggered after click to reset game (initially: after confetti):
- Reset state variables
- Shuffle deck to reinitialize emojis in the deck
- Reset counters: turns, matches, selections.
- Bundle all of these things into a single function.
- Fixed an issue where a second array of 12 cards was being rendered (initial array was not cleared/reset). The fix: give the cards their own dedicated state, reset with
shuffleCards
function at end of game as needed. - Fixed an issue where, after the above fix, confetti was being triggered on an endless loop. The fix:
useEffect
now checksif ((matches * 2) === cards.length)
.useEffect
essentially allows us to React to changes in the state of the game. In the code block,useEffect
is "watching" formatches
andcards
, and whenever either chamges, React checks the condition specified.
The syntax, per React documentation, is: useEffect(setup, dependencies?)
. The docs describe the hook as "[synchronizing] a component with an external system" (i.e. a non-React component).
React docs
Before this fix, confetti was being triggered repeatedly because its logic didn't depend on the game's state. The logic was not tied to or dependent upon state updates. Prior to giving cards
their own dedicated state, the confetti was triggered as expected. My suspicion is that the confetti logic started malfunctioning because the cards
state was not fully cleared/reset within the React lifecycle (which can behave in unexpected ways, to us humans). The condition for triggering confetti ((matches * 2) === cards.length
) depends on an up-to-date state of the game, and without running useEffect
, React does not know when to check the conditional again.
Again: useEffect
watches for changes in specific variables (dependencies, per the syntax/docs), and whenever those variables change, it runs the effect (in this case, checking a condition and executing handleCelebrate
if the condition is met).
- Played with CSS dark mode colours; just some minor edits as I attended the Girl Geek X Elevate Conference and met with my QueerTech mentor. I highly recommend both organizations! Both organizations' conferences have had a fantastic lineup of speakers & topics and have been invaluable to me as I learn more about the industry.
The cards have been slightly off center, I think due to the grid styling (notice how they're shifted slightly to the left relative to the cards
div):
Colour-blocking sections of the app has been really helpful for visualizing containing elements and styling. Added margin-left
rule for the cards
container in order to center the cards collection with the header h1
and counters:
I have also adjusted the width of the counters so that they are of equal width and spacing and further explored the "dark mode" colour theme. This is the app in its current state: