├── 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 |
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 |
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 |
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()
--------------------------------------------------------------------------------