├── memory-game-javascript.png ├── assets ├── FredokaOne-Regular.ttf ├── styles.css └── game.js ├── README.md └── index.html /memory-game-javascript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/memory-game/HEAD/memory-game-javascript.png -------------------------------------------------------------------------------- /assets/FredokaOne-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankozik/memory-game/HEAD/assets/FredokaOne-Regular.ttf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Memory game created in JavaScript 3 |

4 |

You can read the written tutorial about the implementation on webtips.dev 🧠

5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 🧠 Memory Game in JavaScript 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |
0 moves
18 |
time: 0 sec
19 |
20 |
21 |
22 |
23 |
You won!
24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /assets/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Fredoka; 3 | src: url(./FredokaOne-Regular.ttf); 4 | } 5 | 6 | html { 7 | width: 100%; 8 | height: 100%; 9 | background: linear-gradient(325deg, #6f00fc 0%,#fc7900 50%,#fcc700 100%); 10 | font-family: Fredoka; 11 | } 12 | 13 | .game { 14 | position: absolute; 15 | top: 50%; 16 | left: 50%; 17 | transform: translate(-50%, -50%); 18 | } 19 | 20 | .controls { 21 | display: flex; 22 | gap: 20px; 23 | margin-bottom: 20px; 24 | } 25 | 26 | button { 27 | background: #282A3A; 28 | color: #FFF; 29 | border-radius: 5px; 30 | padding: 10px 20px; 31 | border: 0; 32 | cursor: pointer; 33 | font-family: Fredoka; 34 | font-size: 18pt; 35 | } 36 | 37 | .disabled { 38 | color: #757575; 39 | } 40 | 41 | .stats { 42 | color: #FFF; 43 | font-size: 14pt; 44 | } 45 | 46 | .board-container { 47 | position: relative; 48 | } 49 | 50 | .board, 51 | .win { 52 | border-radius: 5px; 53 | box-shadow: 0 25px 50px rgb(33 33 33 / 25%); 54 | background: linear-gradient(135deg, #6f00fc 0%,#fc7900 50%,#fcc700 100%); 55 | transition: transform .6s cubic-bezier(0.4, 0.0, 0.2, 1); 56 | backface-visibility: hidden; 57 | } 58 | 59 | .board { 60 | padding: 20px; 61 | display: grid; 62 | grid-template-columns: repeat(4, auto); 63 | grid-gap: 20px; 64 | } 65 | 66 | .board-container.flipped .board { 67 | transform: rotateY(180deg) rotateZ(50deg); 68 | } 69 | 70 | .board-container.flipped .win { 71 | transform: rotateY(0) rotateZ(0); 72 | } 73 | 74 | .card { 75 | position: relative; 76 | width: 100px; 77 | height: 100px; 78 | cursor: pointer; 79 | } 80 | 81 | .card-front, 82 | .card-back { 83 | position: absolute; 84 | border-radius: 5px; 85 | width: 100%; 86 | height: 100%; 87 | background: #282A3A; 88 | transition: transform .6s cubic-bezier(0.4, 0.0, 0.2, 1); 89 | backface-visibility: hidden; 90 | } 91 | 92 | .card-back { 93 | transform: rotateY(180deg) rotateZ(50deg); 94 | font-size: 28pt; 95 | user-select: none; 96 | text-align: center; 97 | line-height: 100px; 98 | background: #FDF8E6; 99 | } 100 | 101 | .card.flipped .card-front { 102 | transform: rotateY(180deg) rotateZ(50deg); 103 | } 104 | 105 | .card.flipped .card-back { 106 | transform: rotateY(0) rotateZ(0); 107 | } 108 | 109 | .win { 110 | position: absolute; 111 | top: 0; 112 | left: 0; 113 | width: 100%; 114 | height: 100%; 115 | text-align: center; 116 | background: #FDF8E6; 117 | transform: rotateY(180deg) rotateZ(50deg); 118 | } 119 | 120 | .win-text { 121 | position: absolute; 122 | top: 50%; 123 | left: 50%; 124 | transform: translate(-50%, -50%); 125 | font-size: 21pt; 126 | color: #282A3A; 127 | } 128 | 129 | .highlight { 130 | color: #6f00fc; 131 | } -------------------------------------------------------------------------------- /assets/game.js: -------------------------------------------------------------------------------- 1 | const selectors = { 2 | boardContainer: document.querySelector('.board-container'), 3 | board: document.querySelector('.board'), 4 | moves: document.querySelector('.moves'), 5 | timer: document.querySelector('.timer'), 6 | start: document.querySelector('button'), 7 | win: document.querySelector('.win') 8 | } 9 | 10 | const state = { 11 | gameStarted: false, 12 | flippedCards: 0, 13 | totalFlips: 0, 14 | totalTime: 0, 15 | loop: null 16 | } 17 | 18 | const shuffle = array => { 19 | const clonedArray = [...array] 20 | 21 | for (let index = clonedArray.length - 1; index > 0; index--) { 22 | const randomIndex = Math.floor(Math.random() * (index + 1)) 23 | const original = clonedArray[index] 24 | 25 | clonedArray[index] = clonedArray[randomIndex] 26 | clonedArray[randomIndex] = original 27 | } 28 | 29 | return clonedArray 30 | } 31 | 32 | const pickRandom = (array, items) => { 33 | const clonedArray = [...array] 34 | const randomPicks = [] 35 | 36 | for (let index = 0; index < items; index++) { 37 | const randomIndex = Math.floor(Math.random() * clonedArray.length) 38 | 39 | randomPicks.push(clonedArray[randomIndex]) 40 | clonedArray.splice(randomIndex, 1) 41 | } 42 | 43 | return randomPicks 44 | } 45 | 46 | const generateGame = () => { 47 | const dimensions = selectors.board.getAttribute('data-dimension') 48 | 49 | if (dimensions % 2 !== 0) { 50 | throw new Error("The dimension of the board must be an even number.") 51 | } 52 | 53 | const emojis = ['🥔', '🍒', '🥑', '🌽', '🥕', '🍇', '🍉', '🍌', '🥭', '🍍'] 54 | const picks = pickRandom(emojis, (dimensions * dimensions) / 2) 55 | const items = shuffle([...picks, ...picks]) 56 | const cards = ` 57 |
58 | ${items.map(item => ` 59 |
60 |
61 |
${item}
62 |
63 | `).join('')} 64 |
65 | ` 66 | 67 | const parser = new DOMParser().parseFromString(cards, 'text/html') 68 | 69 | selectors.board.replaceWith(parser.querySelector('.board')) 70 | } 71 | 72 | const startGame = () => { 73 | state.gameStarted = true 74 | selectors.start.classList.add('disabled') 75 | 76 | state.loop = setInterval(() => { 77 | state.totalTime++ 78 | 79 | selectors.moves.innerText = `${state.totalFlips} moves` 80 | selectors.timer.innerText = `time: ${state.totalTime} sec` 81 | }, 1000) 82 | } 83 | 84 | const flipBackCards = () => { 85 | document.querySelectorAll('.card:not(.matched)').forEach(card => { 86 | card.classList.remove('flipped') 87 | }) 88 | 89 | state.flippedCards = 0 90 | } 91 | 92 | const flipCard = card => { 93 | state.flippedCards++ 94 | state.totalFlips++ 95 | 96 | if (!state.gameStarted) { 97 | startGame() 98 | } 99 | 100 | if (state.flippedCards <= 2) { 101 | card.classList.add('flipped') 102 | } 103 | 104 | if (state.flippedCards === 2) { 105 | const flippedCards = document.querySelectorAll('.flipped:not(.matched)') 106 | 107 | if (flippedCards[0].innerText === flippedCards[1].innerText) { 108 | flippedCards[0].classList.add('matched') 109 | flippedCards[1].classList.add('matched') 110 | } 111 | 112 | setTimeout(() => { 113 | flipBackCards() 114 | }, 1000) 115 | } 116 | 117 | // If there are no more cards that we can flip, we won the game 118 | if (!document.querySelectorAll('.card:not(.flipped)').length) { 119 | setTimeout(() => { 120 | selectors.boardContainer.classList.add('flipped') 121 | selectors.win.innerHTML = ` 122 | 123 | You won!
124 | with ${state.totalFlips} moves
125 | under ${state.totalTime} seconds 126 |
127 | ` 128 | 129 | clearInterval(state.loop) 130 | }, 1000) 131 | } 132 | } 133 | 134 | const attachEventListeners = () => { 135 | document.addEventListener('click', event => { 136 | const eventTarget = event.target 137 | const eventParent = eventTarget.parentElement 138 | 139 | if (eventTarget.className.includes('card') && !eventParent.className.includes('flipped')) { 140 | flipCard(eventParent) 141 | } else if (eventTarget.nodeName === 'BUTTON' && !eventTarget.className.includes('disabled')) { 142 | startGame() 143 | } 144 | }) 145 | } 146 | 147 | generateGame() 148 | attachEventListeners() --------------------------------------------------------------------------------