├── src ├── styles.css ├── images │ ├── Pickachu.png │ ├── Pidgetto.png │ ├── Squirtle.png │ ├── pokeball.png │ ├── Bulbasaur.png │ ├── ButterFree.png │ └── Charmander.png ├── index.js ├── card.scss ├── card.js ├── app.scss └── App.js ├── README.md ├── .codesandbox └── workspace.json ├── package.json └── public └── index.html /src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /src/images/Pickachu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/Pickachu.png -------------------------------------------------------------------------------- /src/images/Pidgetto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/Pidgetto.png -------------------------------------------------------------------------------- /src/images/Squirtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/Squirtle.png -------------------------------------------------------------------------------- /src/images/pokeball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/pokeball.png -------------------------------------------------------------------------------- /src/images/Bulbasaur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/Bulbasaur.png -------------------------------------------------------------------------------- /src/images/ButterFree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/ButterFree.png -------------------------------------------------------------------------------- /src/images/Charmander.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayankshubham/memory-card-game/HEAD/src/images/Charmander.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memory-card-game 2 | 3 | Check out the detailed article on [creating a memory card game on Medium](https://medium.com/codex/building-a-card-memory-game-in-react-e6400b226b8f) 4 | 5 | Check the [codesandbox link for the demo](https://codesandbox.io/s/clever-smoke-77b6s?from-embed) 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import App from "./App"; 5 | 6 | const rootElement = document.getElementById("root"); 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | rootElement 12 | ); 13 | -------------------------------------------------------------------------------- /.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /src/card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | width: 100%; 3 | height: 100%; 4 | border-radius: 4px; 5 | box-shadow: 2px 2px 4px 4px #DEDEDE; 6 | transition: 0.3s; 7 | transform-style: preserve-3d; 8 | position: relative; 9 | cursor: pointer; 10 | 11 | img { 12 | width: 95%; 13 | height: 95%; 14 | } 15 | 16 | .card-face { 17 | backface-visibility: hidden; 18 | position: absolute; 19 | width: 100%; 20 | height: 100%; 21 | &.card-back-face { 22 | transform: rotateY(180deg); 23 | } 24 | } 25 | 26 | &.is-flipped { 27 | transform: rotateY(180deg); 28 | } 29 | 30 | &.is-inactive { 31 | // visibility: hidden; 32 | opacity: 0; 33 | } 34 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "version": "1.0.0", 4 | "description": "React example starter project", 5 | "keywords": [ 6 | "react", 7 | "starter" 8 | ], 9 | "main": "src/index.js", 10 | "dependencies": { 11 | "@material-ui/core": "4.11.3", 12 | "classnames": "2.3.0", 13 | "react": "17.0.2", 14 | "react-dom": "17.0.2", 15 | "react-scripts": "4.0.0" 16 | }, 17 | "devDependencies": { 18 | "@babel/runtime": "7.13.8", 19 | "typescript": "4.1.3" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test --env=jsdom", 25 | "eject": "react-scripts eject" 26 | }, 27 | "browserslist": [ 28 | ">0.2%", 29 | "not dead", 30 | "not ie <= 11", 31 | "not op_mini all" 32 | ] 33 | } -------------------------------------------------------------------------------- /src/card.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classnames from "classnames"; 3 | import pokeball from "./images/pokeball.png"; 4 | import "./card.scss"; 5 | 6 | const Card = ({ onClick, card, index, isInactive, isFlipped, isDisabled }) => { 7 | const handleClick = () => { 8 | !isFlipped && !isDisabled && onClick(index); 9 | }; 10 | 11 | return ( 12 |
19 |
20 | pokeball 21 |
22 |
23 | pokeball 24 |
25 |
26 | ); 27 | }; 28 | 29 | export default Card; 30 | -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- 1 | .bold { 2 | font-weight: 600; 3 | text-transform: uppercase; 4 | } 5 | .App { 6 | position: absolute; 7 | width:100%; 8 | height: 100%; 9 | 10 | header { 11 | position: relative; 12 | width: 100%; 13 | text-align: center; 14 | margin-bottom: 8px; 15 | 16 | > div { 17 | font-size: 15px; 18 | width: 324px; 19 | text-align: center; 20 | margin: 0 auto; 21 | } 22 | } 23 | 24 | footer { 25 | width: 360px; 26 | position: relative; 27 | margin: 0 auto; 28 | padding: 10px 4px; 29 | margin-top: 10px; 30 | 31 | .score { 32 | justify-content: center; 33 | display: flex; 34 | 35 | div { 36 | padding: 8px 37 | } 38 | } 39 | 40 | .restart { 41 | display: flex; 42 | justify-content: center 43 | } 44 | } 45 | 46 | .container { 47 | border: 1px solid #DEDEDE; 48 | padding: 12px; 49 | box-shadow: 0 0 4px 4px #DEDEDE; 50 | display: grid; 51 | grid-template-columns: repeat(4, 1fr); 52 | grid-template-rows: repeat(3, 1fr); 53 | justify-items: center; 54 | align-items: stretch; 55 | gap: 1rem; 56 | margin: 0 auto; 57 | width: 360px; 58 | height: 300px; 59 | perspective: 100%; 60 | max-width: 720px; 61 | } 62 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from "react"; 2 | import { 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogContentText, 7 | Button, 8 | DialogTitle 9 | } from "@material-ui/core"; 10 | import Card from "./card"; 11 | import "./app.scss"; 12 | 13 | const uniqueElementsArray = [ 14 | { 15 | type: "Pikachu", 16 | image: require(`./images/Pickachu.png`) 17 | }, 18 | { 19 | type: "ButterFree", 20 | image: require(`./images/ButterFree.png`) 21 | }, 22 | { 23 | type: "Charmander", 24 | image: require(`./images/Charmander.png`) 25 | }, 26 | { 27 | type: "Squirtle", 28 | image: require(`./images/Squirtle.png`) 29 | }, 30 | { 31 | type: "Pidgetto", 32 | image: require(`./images/Pidgetto.png`) 33 | }, 34 | { 35 | type: "Bulbasaur", 36 | image: require(`./images/Bulbasaur.png`) 37 | } 38 | ]; 39 | 40 | function shuffleCards(array) { 41 | const length = array.length; 42 | for (let i = length; i > 0; i--) { 43 | const randomIndex = Math.floor(Math.random() * i); 44 | const currentIndex = i - 1; 45 | const temp = array[currentIndex]; 46 | array[currentIndex] = array[randomIndex]; 47 | array[randomIndex] = temp; 48 | } 49 | return array; 50 | } 51 | export default function App() { 52 | const [cards, setCards] = useState( 53 | shuffleCards.bind(null, uniqueElementsArray.concat(uniqueElementsArray)) 54 | ); 55 | const [openCards, setOpenCards] = useState([]); 56 | const [clearedCards, setClearedCards] = useState({}); 57 | const [shouldDisableAllCards, setShouldDisableAllCards] = useState(false); 58 | const [moves, setMoves] = useState(0); 59 | const [showModal, setShowModal] = useState(false); 60 | const [bestScore, setBestScore] = useState( 61 | JSON.parse(localStorage.getItem("bestScore")) || Number.POSITIVE_INFINITY 62 | ); 63 | const timeout = useRef(null); 64 | 65 | const disable = () => { 66 | setShouldDisableAllCards(true); 67 | }; 68 | const enable = () => { 69 | setShouldDisableAllCards(false); 70 | }; 71 | 72 | const checkCompletion = () => { 73 | if (Object.keys(clearedCards).length === uniqueElementsArray.length) { 74 | setShowModal(true); 75 | const highScore = Math.min(moves, bestScore); 76 | setBestScore(highScore); 77 | localStorage.setItem("bestScore", highScore); 78 | } 79 | }; 80 | const evaluate = () => { 81 | const [first, second] = openCards; 82 | enable(); 83 | if (cards[first].type === cards[second].type) { 84 | setClearedCards((prev) => ({ ...prev, [cards[first].type]: true })); 85 | setOpenCards([]); 86 | return; 87 | } 88 | // This is to flip the cards back after 500ms duration 89 | timeout.current = setTimeout(() => { 90 | setOpenCards([]); 91 | }, 500); 92 | }; 93 | const handleCardClick = (index) => { 94 | if (openCards.length === 1) { 95 | setOpenCards((prev) => [...prev, index]); 96 | setMoves((moves) => moves + 1); 97 | disable(); 98 | } else { 99 | clearTimeout(timeout.current); 100 | setOpenCards([index]); 101 | } 102 | }; 103 | 104 | useEffect(() => { 105 | let timeout = null; 106 | if (openCards.length === 2) { 107 | timeout = setTimeout(evaluate, 300); 108 | } 109 | return () => { 110 | clearTimeout(timeout); 111 | }; 112 | }, [openCards]); 113 | 114 | useEffect(() => { 115 | checkCompletion(); 116 | }, [clearedCards]); 117 | const checkIsFlipped = (index) => { 118 | return openCards.includes(index); 119 | }; 120 | 121 | const checkIsInactive = (card) => { 122 | return Boolean(clearedCards[card.type]); 123 | }; 124 | 125 | const handleRestart = () => { 126 | setClearedCards({}); 127 | setOpenCards([]); 128 | setShowModal(false); 129 | setMoves(0); 130 | setShouldDisableAllCards(false); 131 | // set a shuffled deck of cards 132 | setCards(shuffleCards(uniqueElementsArray.concat(uniqueElementsArray))); 133 | }; 134 | 135 | return ( 136 |
137 |
138 |

Play the Flip card game

139 |
140 | Select two cards with same content consequtively to make them vanish 141 |
142 |
143 |
144 | {cards.map((card, index) => { 145 | return ( 146 | 155 | ); 156 | })} 157 |
158 | 175 | 182 | 183 | Hurray!!! You completed the challenge 184 | 185 | 186 | 187 | You completed the game in {moves} moves. Your best score is{" "} 188 | {bestScore} moves. 189 | 190 | 191 | 192 | 195 | 196 | 197 |
198 | ); 199 | } 200 | --------------------------------------------------------------------------------