← Volver a código

Memory

Explora el código (HTML / CSS / JS) del juego

HTML

  1. <!doctype html>
  2. <html lang="es">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Memory</title>
  7. <link rel="stylesheet" href="css/memory.css">
  8. </head>
  9. <body>
  10. <!-- Estructura del juego (canvas, HUD, botones…) -->
  11. <script src="js/memory.js"></script>
  12. </body>
  13. </html>

CSS

  1. body {
  2. display: flex;
  3. justify-content: center;
  4. align-items: center;
  5. height: 100vh;
  6. background-color: #50a9d8 !important;
  7. font-family: Arial, sans-serif;
  8. margin: 0;
  9. flex-direction: column;
  10. position: relative;
  11. overflow: hidden;
  12. }
  13. .memory-game {
  14. width: 90vw;
  15. height: 90vw;
  16. max-width: 640px;
  17. max-height: 640px;
  18. display: flex;
  19. flex-wrap: wrap;
  20. perspective: 1000px;
  21. }
  22. .memory-card {
  23. width: calc(25% - 10px);
  24. height: calc(25% - 10px);
  25. margin: 5px;
  26. position: relative;
  27. transform: scale(1);
  28. transform-style: preserve-3d;
  29. transition: transform 0.5s;
  30. cursor: pointer;
  31. }
  32. .memory-card:active {
  33. transform: scale(0.97);
  34. transition: transform 0.2s;
  35. }
  36. .memory-card.flip {
  37. transform: rotateY(180deg);
  38. }
  39. .memory-card img {
  40. width: 100%;
  41. height: 100%;
  42. border-radius: 5px;
  43. position: absolute;
  44. backface-visibility: hidden;
  45. }
  46. .front-face {
  47. transform: rotateY(180deg);
  48. }
  49. .game-controls {
  50. position: absolute;
  51. top: 50%;
  52. left: 50%;
  53. transform: translate(-50%, -50%);
  54. }
  55. #start-button {
  56. display: none;
  57. width: 100px;
  58. height: 100px;
  59. border-radius: 50%;
  60. background-color: #28a745;
  61. color: white;
  62. border: none;
  63. font-size: 1.5em;
  64. cursor: pointer;
  65. justify-content: center;
  66. align-items: center;
  67. box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  68. transition:
  69. background-color 0.3s,
  70. transform 0.3s;
  71. }
  72. #start-button:hover {
  73. background-color: #218838;
  74. transform: scale(1.1);
  75. }
  76. /* ================================
  77. CONTROLES DE MÚSICA
  78. ================================ */
  79. .music-controls {
  80. position: fixed;
  81. top: 20px;
  82. right: 20px;
  83. z-index: 1000;
  84. display: flex;
  85. align-items: center;
  86. gap: 8px;
  87. padding: 8px 12px;
  88. border-radius: 30px;
  89. background: rgba(0, 0, 0, 0.45);
  90. border: 1px solid rgba(255, 255, 255, 0.25);
  91. box-shadow: 0 5px 20px rgba(0, 0, 0, 0.25);
  92. backdrop-filter: blur(8px);
  93. -webkit-backdrop-filter: blur(8px);
  94. }
  95. .music-button {
  96. width: 36px;
  97. height: 36px;
  98. display: flex;
  99. justify-content: center;
  100. align-items: center;
  101. padding: 0;
  102. border: none;
  103. border-radius: 50%;
  104. background: rgba(255, 255, 255, 0.16);
  105. color: #ffffff;
  106. font-size: 14px;
  107. cursor: pointer;
  108. transition:
  109. background-color 0.2s,
  110. transform 0.2s;
  111. }
  112. .music-button:hover {
  113. background: rgba(255, 255, 255, 0.28);
  114. transform: scale(1.08);
  115. }
  116. .music-button:active {
  117. transform: scale(0.94);
  118. }
  119. .music-button-main {
  120. background: rgba(255, 255, 255, 0.25);
  121. }
  122. .music-button.is-muted {
  123. background: rgba(220, 53, 69, 0.8);
  124. }
  125. #volume-level {
  126. min-width: 38px;
  127. color: #ffffff;
  128. font-size: 13px;
  129. font-weight: 700;
  130. text-align: center;
  131. }
  132. /* ================================
  133. TABLET
  134. ================================ */
  135. @media (max-width: 768px) {
  136. .memory-card {
  137. width: calc(25% - 8px);
  138. height: calc(25% - 8px);
  139. margin: 4px;
  140. }
  141. #start-button {
  142. width: 80px;
  143. height: 80px;
  144. font-size: 1.2em;
  145. }
  146. .music-controls {
  147. top: 12px;
  148. right: 12px;
  149. padding: 6px 9px;
  150. gap: 6px;
  151. }
  152. .music-button {
  153. width: 34px;
  154. height: 34px;
  155. font-size: 13px;
  156. }
  157. }
  158. /* ================================
  159. MÓVIL
  160. ================================ */
  161. @media (max-width: 480px) {
  162. .memory-card {
  163. width: calc(25% - 6px);
  164. height: calc(25% - 6px);
  165. margin: 3px;
  166. }
  167. #start-button {
  168. width: 70px;
  169. height: 70px;
  170. font-size: 1em;
  171. }
  172. .music-controls {
  173. top: 10px;
  174. right: 10px;
  175. }
  176. .music-button {
  177. width: 32px;
  178. height: 32px;
  179. }
  180. #volume-level {
  181. min-width: 34px;
  182. font-size: 12px;
  183. }
  184. }
  185. .arcade-action-link {
  186. display: inline-flex;
  187. align-items: center;
  188. justify-content: center;
  189. color: #ffffff;
  190. text-decoration: none;
  191. }
  192. .arcade-action-link:hover {
  193. color: #ffffff;
  194. text-decoration: none;
  195. }

JS

  1. const IMG_BASE = "/sources/img/memory/";
  2. const T = (window.APP_LOCALE === 'en')
  3. ? {
  4. cardAlt: 'Memory Card',
  5. card: 'Card',
  6. muteMusic: 'Mute music',
  7. enableMusic: 'Enable music'
  8. }
  9. : {
  10. cardAlt: 'Carta de Memory',
  11. card: 'Carta',
  12. muteMusic: 'Silenciar música',
  13. enableMusic: 'Activar música'
  14. };
  15. const cardsData = [
  16. {
  17. id: 1,
  18. imgSrc: IMG_BASE + 'bluy1.png'
  19. },
  20. {
  21. id: 2,
  22. imgSrc: IMG_BASE + 'bluy2.png'
  23. },
  24. {
  25. id: 3,
  26. imgSrc: IMG_BASE + 'bluy3.png'
  27. },
  28. {
  29. id: 4,
  30. imgSrc: IMG_BASE + 'bluy4.png'
  31. },
  32. {
  33. id: 5,
  34. imgSrc: IMG_BASE + 'bluy5.png'
  35. },
  36. {
  37. id: 6,
  38. imgSrc: IMG_BASE + 'bluy6.png'
  39. },
  40. {
  41. id: 7,
  42. imgSrc: IMG_BASE + 'bluy7.png'
  43. },
  44. {
  45. id: 8,
  46. imgSrc: IMG_BASE + 'bluy8.png'
  47. }
  48. ];
  49. const memoryGame = document.querySelector('.memory-game');
  50. const startButton = document.getElementById('start-button');
  51. const backgroundMusic = document.getElementById('background-music');
  52. const volumeDownButton = document.getElementById('volume-down');
  53. const volumeUpButton = document.getElementById('volume-up');
  54. const muteButton = document.getElementById('mute-button');
  55. const muteIcon = document.getElementById('mute-icon');
  56. const volumeLevel = document.getElementById('volume-level');
  57. let hasFlippedCard = false;
  58. let lockBoard = false;
  59. let firstCard = null;
  60. let secondCard = null;
  61. let matchesFound = 0;
  62. let musicStarted = false;
  63. /* ================================
  64. MÚSICA
  65. ================================ */
  66. const savedVolume = localStorage.getItem('memoryMusicVolume');
  67. const savedMuted = localStorage.getItem('memoryMusicMuted');
  68. backgroundMusic.volume = savedVolume !== null
  69. ? Number(savedVolume)
  70. : 0.4;
  71. backgroundMusic.muted = savedMuted === 'true';
  72. function clampVolume(value) {
  73. return Math.min(
  74. 1,
  75. Math.max(0, value)
  76. );
  77. }
  78. function updateVolumeInterface() {
  79. const percentage = Math.round(
  80. backgroundMusic.volume * 100
  81. );
  82. volumeLevel.textContent = `${percentage}%`;
  83. if (
  84. backgroundMusic.muted ||
  85. backgroundMusic.volume === 0
  86. ) {
  87. muteIcon.className = 'fa-solid fa-volume-xmark';
  88. muteButton.classList.add('is-muted');
  89. muteButton.title = T.enableMusic;
  90. muteButton.setAttribute(
  91. 'aria-label',
  92. T.enableMusic
  93. );
  94. return;
  95. }
  96. muteButton.classList.remove('is-muted');
  97. muteButton.title = T.muteMusic;
  98. muteButton.setAttribute(
  99. 'aria-label',
  100. T.muteMusic
  101. );
  102. if (backgroundMusic.volume <= 0.35) {
  103. muteIcon.className = 'fa-solid fa-volume-low';
  104. } else {
  105. muteIcon.className = 'fa-solid fa-volume-high';
  106. }
  107. }
  108. async function startMusic() {
  109. if (musicStarted) {
  110. return;
  111. }
  112. try {
  113. await backgroundMusic.play();
  114. musicStarted = true;
  115. } catch (error) {
  116. /*
  117. * Los navegadores pueden bloquear la reproducción
  118. * automática hasta que exista interacción del usuario.
  119. */
  120. }
  121. }
  122. function setVolume(value) {
  123. const newVolume = clampVolume(value);
  124. backgroundMusic.volume = newVolume;
  125. if (newVolume > 0) {
  126. backgroundMusic.muted = false;
  127. }
  128. localStorage.setItem(
  129. 'memoryMusicVolume',
  130. String(newVolume)
  131. );
  132. localStorage.setItem(
  133. 'memoryMusicMuted',
  134. String(backgroundMusic.muted)
  135. );
  136. updateVolumeInterface();
  137. startMusic();
  138. }
  139. function decreaseVolume() {
  140. setVolume(
  141. backgroundMusic.volume - 0.1
  142. );
  143. }
  144. function increaseVolume() {
  145. setVolume(
  146. backgroundMusic.volume + 0.1
  147. );
  148. }
  149. function toggleMute() {
  150. backgroundMusic.muted = !backgroundMusic.muted;
  151. localStorage.setItem(
  152. 'memoryMusicMuted',
  153. String(backgroundMusic.muted)
  154. );
  155. updateVolumeInterface();
  156. if (!backgroundMusic.muted) {
  157. startMusic();
  158. }
  159. }
  160. volumeDownButton.addEventListener(
  161. 'click',
  162. decreaseVolume
  163. );
  164. volumeUpButton.addEventListener(
  165. 'click',
  166. increaseVolume
  167. );
  168. muteButton.addEventListener(
  169. 'click',
  170. toggleMute
  171. );
  172. /*
  173. * Intentamos iniciar la música al cargar.
  174. * Si el navegador bloquea el autoplay,
  175. * arrancará con la primera interacción.
  176. */
  177. startMusic();
  178. document.addEventListener(
  179. 'pointerdown',
  180. () => {
  181. startMusic();
  182. },
  183. {
  184. once: true
  185. }
  186. );
  187. updateVolumeInterface();
  188. /* ================================
  189. MEMORY
  190. ================================ */
  191. function createCard(cardData) {
  192. const card = document.createElement('div');
  193. card.classList.add('memory-card');
  194. card.dataset.id = cardData.id;
  195. const frontFace = document.createElement('img');
  196. frontFace.classList.add('front-face');
  197. frontFace.src = cardData.imgSrc;
  198. frontFace.alt = T.cardAlt;
  199. const backFace = document.createElement('img');
  200. backFace.classList.add('back-face');
  201. backFace.src = IMG_BASE + 'carta.png';
  202. backFace.alt = `${T.card} ${cardData.id}`;
  203. card.appendChild(frontFace);
  204. card.appendChild(backFace);
  205. card.addEventListener(
  206. 'click',
  207. flipCard
  208. );
  209. memoryGame.appendChild(card);
  210. }
  211. function flipCard() {
  212. startMusic();
  213. if (lockBoard) {
  214. return;
  215. }
  216. if (this === firstCard) {
  217. return;
  218. }
  219. this.classList.add('flip');
  220. if (!hasFlippedCard) {
  221. hasFlippedCard = true;
  222. firstCard = this;
  223. return;
  224. }
  225. secondCard = this;
  226. checkForMatch();
  227. }
  228. function checkForMatch() {
  229. const isMatch =
  230. firstCard.dataset.id ===
  231. secondCard.dataset.id;
  232. if (isMatch) {
  233. disableCards();
  234. } else {
  235. unflipCards();
  236. }
  237. }
  238. function disableCards() {
  239. firstCard.removeEventListener(
  240. 'click',
  241. flipCard
  242. );
  243. secondCard.removeEventListener(
  244. 'click',
  245. flipCard
  246. );
  247. matchesFound++;
  248. if (matchesFound === cardsData.length) {
  249. showStartButton();
  250. }
  251. resetBoard();
  252. }
  253. function unflipCards() {
  254. lockBoard = true;
  255. setTimeout(
  256. () => {
  257. firstCard.classList.remove('flip');
  258. secondCard.classList.remove('flip');
  259. resetBoard();
  260. },
  261. 1000
  262. );
  263. }
  264. function resetBoard() {
  265. hasFlippedCard = false;
  266. lockBoard = false;
  267. firstCard = null;
  268. secondCard = null;
  269. }
  270. function initializeGame() {
  271. memoryGame.innerHTML = '';
  272. matchesFound = 0;
  273. startButton.style.display = 'none';
  274. const cards = [
  275. ...cardsData,
  276. ...cardsData
  277. ];
  278. for (
  279. let i = cards.length - 1;
  280. i > 0;
  281. i--
  282. ) {
  283. const j = Math.floor(
  284. Math.random() * (i + 1)
  285. );
  286. [
  287. cards[i],
  288. cards[j]
  289. ] = [
  290. cards[j],
  291. cards[i]
  292. ];
  293. }
  294. cards.forEach(
  295. cardData => {
  296. createCard(cardData);
  297. }
  298. );
  299. }
  300. function showStartButton() {
  301. startButton.style.display = 'flex';
  302. }
  303. startButton.addEventListener(
  304. 'click',
  305. initializeGame
  306. );
  307. initializeGame();