This is a classic memory card game that involves a deck of 52 cards. The application combines state management, the MVC (Model-View-Controller) architecture, CSS animation, JavaScript interaction with CSS, and DOM manipulations.
The code defines three main components: the view, the model, the controller. The utility is an additional component for generating an array of random numbers that does not fit into the idea of any MVC components.
**// rendering the game elements and updating the DOM
const view = {
...
};
// maintaining the game state
const model = {
...
};
// handling user input and updating the game state accordingly
const controller = {
...
};**
The view contains a number of functions that generate HTML elements based on the game state, such as getCardElement
, which generates a div
element representing a card, and getCardContent
, which generates the content for the front face of a card. The displayCards
function is used to display a set of cards, while the flipCards
function is used to flip one or more cards over. Other functions are used to update the score and the number of tries, and to display an animation when the player makes an incorrect match.
displayCards(indexes) {
const rootElement = document.querySelector("#cards");
rootElement.innerHTML = indexes
.map((index) => this.getCardElement(index))
.join("");
},
- The
map
method iterates over each index in theindexes
array and calls thegetCardElement
method with the current index. - The
getCardElement
method returns a string representation of each card element based on the provided index. - The resulting array of card element strings is joined together using the
join
method with an empty string as the separator.
flipCards(...cards) {
cards.map((card) => {
if (card.classList.contains("back")) {
card.classList.remove("back");
card.innerHTML = this.getCardContent(Number(card.dataset.index));
return;
}
card.classList.add("back");
card.innerHTML = null;
});
},
- If the
card
element has the class "back" (checked usingclassList.contains("back")
), it removes the "back" class from thecard
element usingclassList.remove("back")
. - It sets the inner HTML of the
card
element to the result of callingthis.getCardContent
with thedataset.index
attribute of thecard
converted to a number. - If the
card
element does not have the class "back". It adds the "back" class to thecard
element usingclassList.add("back")
. - It sets the inner HTML of the
card
element tonull
.
const model = {
revealedCards: [],
isRevealedCardsMatched() {
return (
this.revealedCards[0].dataset.index % 13 ===
this.revealedCards[1].dataset.index % 13
);
},
score: 0,
triedTimes: 0,
};
The model contains an array revealedCards
that holds the cards that have been flipped over, and functions to check whether the cards match and update the score and number of tries.
const controller = {
// update state
currentState: GAME_STATE.FirstCardAwaits,
// this function generates a random array of numbers representing the cards
generateCards() {
...
},
// this function is called when a card is clicked.
// it first checks if the card is not already flipped by
// verifying if it does not have the CSS class "back".
dispatchCardAction(card) {
...
// cards await flipping & matching numbers
switch (this.currentState) {
// flip 1st card
case GAME_STATE.FirstCardAwaits:
...
// flip 2nd card
case GAME_STATE.SecondCardAwaits:
...
// if card's numbers matched +10 point
// keep matched cards revealed
// return state to **FirstCardAwaits**
if (model.isRevealedCardsMatched()) {
...
// game ends
if (model.score === 260) {
...
}
// card's number not matching
// flip to card's back
// return state to **FirstCardAwaits**
} else {
...
}
}
}
...
},
// flip reavealed cards' to **back** with CSS
// clear the **revealCards** array
// return state to **FirstCardAwaits**
resetCards() {
...
},
};
The controller defines a currentState
variable that keeps track of the current game state, and a dispatchCardAction
function that handles card clicks. When a card is clicked, the dispatchCardAction
function flips the card over and updates the game state. If the cards match, the score is updated and the cards are removed from the board. If the cards do not match, an animation is displayed, and the cards are flipped back over.
generateCards() {
view.displayCards(utility.getRandomNumberArray(52));
},
The generateCards()
: This function generates a random array of numbers representing the cards and calls the displayCards()
function of the view
object to render the cards on the screen.
dispatchCardAction(card) {
if (!card.classList.contains("back")) {
return;
}
switch (this.currentState) {
case GAME_STATE.FirstCardAwaits:
view.flipCards(card);
model.revealedCards.push(card);
this.currentState = GAME_STATE.SecondCardAwaits;
break;
case GAME_STATE.SecondCardAwaits:
view.renderTriedTimes(++model.triedTimes);
view.flipCards(card);
model.revealedCards.push(card);
// if them paired
if (model.isRevealedCardsMatched()) {
// paired success
view.renderScore((model.score += 10));
this.currentState = GAME_STATE.CardsMatched;
view.pairCards(...model.revealedCards);
model.revealedCards = [];
if (model.score === 260) {
console.log("showGameFinished");
this.currentState = GAME_STATE.GameFinished;
view.showGameFinished(); // game ends
return;
}
this.currentState = GAME_STATE.FirstCardAwaits;
} else {
// pair failed
this.currentState = GAME_STATE.CardsMatchFailed;
view.appendWrongAnimation(...model.revealedCards);
setTimeout(() => {
view.flipCards(...model.revealedCards);
model.revealedCards = [];
this.currentState = GAME_STATE.FirstCardAwaits;
}, 1000);
}
// see if pairing success
break;
}
console.log("this.currentState", this.currentState);
console.log(
"revealedCards",
model.revealedCards.map((card) => card.dataset.index)
);
},
- If the current state is
GAME_STATE.FirstCardAwaits
, the clicked card is flipped, added to therevealedCards
array in themodel
object, and thecurrentState
is updated toGAME_STATE.SecondCardAwaits
. - If the current state is
GAME_STATE.SecondCardAwaits
, the number of tried times is rendered on the view by incrementing thetriedTimes
property in themodel
object, the clicked card is flipped, added to therevealedCards
array, and the function checks if the revealed cards match using theisRevealedCardsMatched()
function in themodel
object.- If the revealed cards match, the score is updated by adding 10 points, the
currentState
is set toGAME_STATE.CardsMatched
, the matched cards are visually paired on the view by calling thepairCards()
function, therevealedCards
array is cleared, and if the score reaches 260 (indicating all cards are matched), the game is finished, and theshowGameFinished()
function is called on the view. - If the revealed cards do not match, the
currentState
is set toGAME_STATE.CardsMatchFailed
, an animation indicating a wrong match is appended to the revealed cards on the view by calling theappendWrongAnimation()
function, and after a delay of 1 second, the revealed cards are flipped back, therevealedCards
array is cleared, and thecurrentState
is set toGAME_STATE.FirstCardAwaits
.
- If the revealed cards match, the score is updated by adding 10 points, the
resetCards() {
view.flipCards(...model.revealedCards);
model.revealedCards = [];
controller.currentState = GAME_STATE.FirstCardAwaits;
},
resetCards()
called when the game needs to be reset. It flips back all the cards in the revealedCards
array on the view, clears the revealedCards
array in the model
object, and sets the currentState
to GAME_STATE.FirstCardAwaits
.
Generates an array of random numbers. It uses the Fisher-Yates algorithm to shuffle the numbers in the array randomly.
**getRandomNumberArray(count) {
const number = Array.from(Array(count).keys());
for (let index = number.length - 1; index > 0; index--) {
let randomIndex = Math.floor(Math.random() * (index + 1));
[number[index], number[randomIndex]] = [
number[randomIndex],
number[index],
];
}
return number;
},**
The **getRandomNumberArray**
method creates an array of consecutive integers from 0 to count-1
using the **Array.from()**
method and the **keys()**
method of the **Array**
object.
It then shuffles the array using the Fisher-Yates shuffle algorithm, which swaps each element in the array with a randomly chosen element from the remaining unshuffled elements. Finally, it returns the shuffled array.
The code also defines two constants: GAME_STATE
, which is an object that maps game states to string values, and Symbols
, which is an array of image URLs representing the card symbols.
This paragraph will highlight personally intriguing syntaxes or solutions
image source: docs.flutter.dev
MVC (Model-View-Controller) is a pattern in software design commonly used to implement user interfaces, data, and controlling logic. It emphasizes a separation between the software's business logic and display. This "separation of concerns" provides for a better division of labor and improved maintenance.
- Model: Manages data and business logic.
- View: Handles layout and display.
- Controller: Routes commands to the model and view parts.
-MDN
const GAME_STATE = {
FirstCardAwaits: "FirstCardAwaits",
SecondCardAwaits: "SecondCardAwaits",
CardsMatchFailed: "CardsMatchFailed",
CardsMatched: "CardsMatched",
GameFinished: "GameFinished",
};
This object represents different states of a game. Each state is represented by a key-value pair, where the key is a descriptive name for the state, and the value is a string that uniquely identifies the state.
switch (expression) {
case value1:
statements
case value2:
statements
// …
case valueN:
statements
default:
statements
}
The
switch
statement evaluates an expression, matching the expression's value against a series ofcase
clauses, and executes statements after the firstcase
clause with a matching value, until abreak
statement is encountered. Thedefault
clause of aswitch
statement will be jumped to if nocase
matches the expression's value. -MDN
*// Array.from(arrayLike, mapFn)*
*console.log(Array.from([1, 2, 3], x => x + x));
// Expected output: Array [2, 4, 6]*
The
Array.from()
static method creates a new, shallow-copiedArray
instance from an iterable or array-like object. -MDN
const array1 = ['a', 'b', 'c'];
const iterator = array1.keys();
for (const key of iterator) {
console.log(key);
}
// Expected output: 0
// Expected output: 1
// Expected output: 2
The
keys()
method returns a new array iterator object that contains the keys for each index in the array.