Advertisement
Guest User

Untitled

a guest
Mar 31st, 2025
599
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 333.97 KB | None | 0 0
  1. # LLM Collaboration
  2. Date: 2025-03-30 20:12:00
  3.  
  4. ## Initial Task
  5.  
  6. ```
  7. Let's create an amazing version of 2048, playable in a browser. Be sure to output one codeblock. Feel free to use any additional libraries you need.
  8. ```
  9.  
  10. ## Claude (Iteration 1)
  11.  
  12. # 2048 Game - Enhanced Browser Version
  13.  
  14. Here's an enhanced version of the classic 2048 game that's playable in a browser. This implementation includes animations, responsive design, score tracking, and a clean UI.
  15.  
  16. ```html
  17. <!DOCTYPE html>
  18. <html lang="en">
  19. <head>
  20. <meta charset="UTF-8">
  21. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  22. <title>Enhanced 2048</title>
  23. <style>
  24. :root {
  25. --grid-size: 4;
  26. --cell-size: 100px;
  27. --cell-gap: 12px;
  28. --border-radius: 8px;
  29. }
  30.  
  31. * {
  32. box-sizing: border-box;
  33. margin: 0;
  34. padding: 0;
  35. font-family: 'Arial', sans-serif;
  36. }
  37.  
  38. body {
  39. background-color: #faf8ef;
  40. display: flex;
  41. justify-content: center;
  42. align-items: center;
  43. height: 100vh;
  44. overflow: hidden;
  45. touch-action: none;
  46. }
  47.  
  48. .container {
  49. width: min(95vw, 600px);
  50. padding: 20px;
  51. border-radius: 10px;
  52. background-color: #bbada0;
  53. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  54. }
  55.  
  56. .header {
  57. display: flex;
  58. justify-content: space-between;
  59. align-items: center;
  60. margin-bottom: 20px;
  61. }
  62.  
  63. .title {
  64. color: #776e65;
  65. font-size: 2.5rem;
  66. font-weight: bold;
  67. }
  68.  
  69. .scores {
  70. display: flex;
  71. gap: 10px;
  72. }
  73.  
  74. .score-box {
  75. background-color: #bbada0;
  76. color: white;
  77. padding: 10px 15px;
  78. border-radius: 5px;
  79. display: flex;
  80. flex-direction: column;
  81. align-items: center;
  82. min-width: 80px;
  83. background-color: #8f7a66;
  84. }
  85.  
  86. .score-title {
  87. font-size: 0.8rem;
  88. text-transform: uppercase;
  89. }
  90.  
  91. .score-value {
  92. font-size: 1.5rem;
  93. font-weight: bold;
  94. }
  95.  
  96. .sub-header {
  97. display: flex;
  98. justify-content: space-between;
  99. margin-bottom: 20px;
  100. }
  101.  
  102. .game-intro {
  103. color: #776e65;
  104. max-width: 300px;
  105. font-size: 0.9rem;
  106. }
  107.  
  108. .new-game-btn {
  109. background-color: #8f7a66;
  110. color: white;
  111. border: none;
  112. border-radius: 5px;
  113. padding: 10px 15px;
  114. font-weight: bold;
  115. cursor: pointer;
  116. transition: background-color 0.3s;
  117. }
  118.  
  119. .new-game-btn:hover {
  120. background-color: #9f8b77;
  121. }
  122.  
  123. .game-container {
  124. position: relative;
  125. width: calc(var(--cell-size) * var(--grid-size) + var(--cell-gap) * (var(--grid-size) + 1));
  126. height: calc(var(--cell-size) * var(--grid-size) + var(--cell-gap) * (var(--grid-size) + 1));
  127. background-color: #bbada0;
  128. border-radius: var(--border-radius);
  129. padding: var(--cell-gap);
  130. margin: 0 auto;
  131. }
  132.  
  133. .cell {
  134. background-color: rgba(238, 228, 218, 0.35);
  135. border-radius: var(--border-radius);
  136. position: absolute;
  137. width: var(--cell-size);
  138. height: var(--cell-size);
  139. }
  140.  
  141. .tile {
  142. position: absolute;
  143. display: flex;
  144. justify-content: center;
  145. align-items: center;
  146. width: var(--cell-size);
  147. height: var(--cell-size);
  148. border-radius: var(--border-radius);
  149. font-size: 2rem;
  150. font-weight: bold;
  151. background-color: #eee4da;
  152. color: #776e65;
  153. transition: transform 100ms ease-in-out;
  154. animation: appear 200ms ease-in-out;
  155. z-index: 10;
  156. }
  157.  
  158. @keyframes appear {
  159. 0% {
  160. opacity: 0;
  161. transform: scale(0);
  162. }
  163. 100% {
  164. opacity: 1;
  165. transform: scale(1);
  166. }
  167. }
  168.  
  169. .tile.merged {
  170. animation: pop 200ms ease-in-out;
  171. }
  172.  
  173. @keyframes pop {
  174. 0% {
  175. transform: scale(1);
  176. }
  177. 50% {
  178. transform: scale(1.2);
  179. }
  180. 100% {
  181. transform: scale(1);
  182. }
  183. }
  184.  
  185. .tile[data-value="2"] {
  186. background-color: #eee4da;
  187. }
  188.  
  189. .tile[data-value="4"] {
  190. background-color: #ede0c8;
  191. }
  192.  
  193. .tile[data-value="8"] {
  194. background-color: #f2b179;
  195. color: white;
  196. }
  197.  
  198. .tile[data-value="16"] {
  199. background-color: #f59563;
  200. color: white;
  201. }
  202.  
  203. .tile[data-value="32"] {
  204. background-color: #f67c5f;
  205. color: white;
  206. }
  207.  
  208. .tile[data-value="64"] {
  209. background-color: #f65e3b;
  210. color: white;
  211. }
  212.  
  213. .tile[data-value="128"] {
  214. background-color: #edcf72;
  215. color: white;
  216. font-size: 1.8rem;
  217. }
  218.  
  219. .tile[data-value="256"] {
  220. background-color: #edcc61;
  221. color: white;
  222. font-size: 1.8rem;
  223. }
  224.  
  225. .tile[data-value="512"] {
  226. background-color: #edc850;
  227. color: white;
  228. font-size: 1.8rem;
  229. }
  230.  
  231. .tile[data-value="1024"] {
  232. background-color: #edc53f;
  233. color: white;
  234. font-size: 1.5rem;
  235. }
  236.  
  237. .tile[data-value="2048"] {
  238. background-color: #edc22e;
  239. color: white;
  240. font-size: 1.5rem;
  241. }
  242.  
  243. .game-message {
  244. display: none;
  245. position: absolute;
  246. top: 0;
  247. right: 0;
  248. bottom: 0;
  249. left: 0;
  250. background-color: rgba(238, 228, 218, 0.73);
  251. flex-direction: column;
  252. align-items: center;
  253. justify-content: center;
  254. text-align: center;
  255. border-radius: var(--border-radius);
  256. z-index: 100;
  257. }
  258.  
  259. .game-message.game-won, .game-message.game-over {
  260. display: flex;
  261. }
  262.  
  263. .game-message p {
  264. font-size: 2rem;
  265. font-weight: bold;
  266. color: #776e65;
  267. margin-bottom: 20px;
  268. }
  269.  
  270. .game-message .lower {
  271. display: flex;
  272. gap: 10px;
  273. margin-top: 20px;
  274. }
  275.  
  276. @media screen and (max-width: 520px) {
  277. :root {
  278. --cell-size: 65px;
  279. --cell-gap: 8px;
  280. }
  281.  
  282. .title {
  283. font-size: 2rem;
  284. }
  285.  
  286. .score-box {
  287. min-width: 60px;
  288. padding: 8px 10px;
  289. }
  290.  
  291. .score-value {
  292. font-size: 1.2rem;
  293. }
  294.  
  295. .game-intro {
  296. font-size: 0.8rem;
  297. max-width: 200px;
  298. }
  299.  
  300. .tile {
  301. font-size: 1.5rem;
  302. }
  303.  
  304. .tile[data-value="128"],
  305. .tile[data-value="256"],
  306. .tile[data-value="512"] {
  307. font-size: 1.3rem;
  308. }
  309.  
  310. .tile[data-value="1024"],
  311. .tile[data-value="2048"] {
  312. font-size: 1.1rem;
  313. }
  314. }
  315.  
  316. @media screen and (max-width: 340px) {
  317. :root {
  318. --cell-size: 55px;
  319. --cell-gap: 6px;
  320. }
  321.  
  322. .container {
  323. padding: 10px;
  324. }
  325.  
  326. .header {
  327. flex-direction: column;
  328. gap: 10px;
  329. margin-bottom: 10px;
  330. }
  331.  
  332. .sub-header {
  333. flex-direction: column;
  334. gap: 10px;
  335. align-items: center;
  336. margin-bottom: 10px;
  337. }
  338.  
  339. .game-intro {
  340. text-align: center;
  341. margin-bottom: 10px;
  342. }
  343. }
  344.  
  345. .footer {
  346. margin-top: 20px;
  347. text-align: center;
  348. color: #776e65;
  349. font-size: 0.8rem;
  350. }
  351. </style>
  352. </head>
  353. <body>
  354. <div class="container">
  355. <div class="header">
  356. <h1 class="title">2048</h1>
  357. <div class="scores">
  358. <div class="score-box">
  359. <div class="score-title">Score</div>
  360. <div class="score-value" id="score">0</div>
  361. </div>
  362. <div class="score-box">
  363. <div class="score-title">Best</div>
  364. <div class="score-value" id="best-score">0</div>
  365. </div>
  366. </div>
  367. </div>
  368. <div class="sub-header">
  369. <p class="game-intro">Join the tiles and get to <strong>2048!</strong> Use arrow keys or swipe to move.</p>
  370. <button class="new-game-btn" id="restart-button">New Game</button>
  371. </div>
  372. <div class="game-container" id="game-container">
  373. <div class="game-message" id="game-message">
  374. <p></p>
  375. <div class="lower">
  376. <button class="new-game-btn" id="retry-button">Try again</button>
  377. </div>
  378. </div>
  379. </div>
  380. <div class="footer">
  381. <p>Made with ❤️ • Swipe or use arrow keys to play</p>
  382. </div>
  383. </div>
  384.  
  385. <script>
  386. document.addEventListener('DOMContentLoaded', () => {
  387. const GRID_SIZE = 4;
  388. const CELL_SIZE = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--cell-size'));
  389. const CELL_GAP = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--cell-gap'));
  390.  
  391. let grid = [];
  392. let score = 0;
  393. let bestScore = localStorage.getItem('bestScore') || 0;
  394. let gameOver = false;
  395. let gameWon = false;
  396. let touchStartX = 0;
  397. let touchStartY = 0;
  398.  
  399. const gameContainer = document.getElementById('game-container');
  400. const scoreElement = document.getElementById('score');
  401. const bestScoreElement = document.getElementById('best-score');
  402. const messageContainer = document.getElementById('game-message');
  403. const restartButton = document.getElementById('restart-button');
  404. const retryButton = document.getElementById('retry-button');
  405.  
  406. bestScoreElement.textContent = bestScore;
  407.  
  408. // Initialize game
  409. function initGame() {
  410. createCellElements();
  411. resetGame();
  412. setupEventListeners();
  413. }
  414.  
  415. // Create cell elements
  416. function createCellElements() {
  417. gameContainer.innerHTML = '';
  418. messageContainer.classList.remove('game-won', 'game-over');
  419. messageContainer.querySelector('p').textContent = '';
  420. gameContainer.appendChild(messageContainer);
  421.  
  422. for (let i = 0; i < GRID_SIZE; i++) {
  423. for (let j = 0; j < GRID_SIZE; j++) {
  424. const cell = document.createElement('div');
  425. cell.classList.add('cell');
  426. cell.style.top = `${CELL_GAP + i * (CELL_SIZE + CELL_GAP)}px`;
  427. cell.style.left = `${CELL_GAP + j * (CELL_SIZE + CELL_GAP)}px`;
  428. gameContainer.appendChild(cell);
  429. }
  430. }
  431. }
  432.  
  433. // Reset game state
  434. function resetGame() {
  435. // Clear the grid
  436. grid = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(null));
  437. score = 0;
  438. gameOver = false;
  439. gameWon = false;
  440.  
  441. // Update score display
  442. scoreElement.textContent = score;
  443.  
  444. // Remove existing tiles
  445. const tiles = document.querySelectorAll('.tile');
  446. tiles.forEach(tile => tile.remove());
  447.  
  448. // Reset game message
  449. messageContainer.classList.remove('game-won', 'game-over');
  450.  
  451. // Add initial tiles
  452. addRandomTile();
  453. addRandomTile();
  454. }
  455.  
  456. // Setup event listeners
  457. function setupEventListeners() {
  458. document.addEventListener('keydown', handleKeyPress);
  459. restartButton.addEventListener('click', resetGame);
  460. retryButton.addEventListener('click', resetGame);
  461.  
  462. // Touch events for mobile
  463. gameContainer.addEventListener('touchstart', handleTouchStart, { passive: true });
  464. gameContainer.addEventListener('touchmove', handleTouchMove, { passive: false });
  465. gameContainer.addEventListener('touchend', handleTouchEnd, { passive: true });
  466. }
  467.  
  468. // Handle key presses
  469. function handleKeyPress(e) {
  470. if (gameOver || gameWon) return;
  471.  
  472. let moved = false;
  473.  
  474. switch(e.key) {
  475. case 'ArrowUp':
  476. moved = moveUp();
  477. break;
  478. case 'ArrowDown':
  479. moved = moveDown();
  480. break;
  481. case 'ArrowLeft':
  482. moved = moveLeft();
  483. break;
  484. case 'ArrowRight':
  485. moved = moveRight();
  486. break;
  487. default:
  488. return;
  489. }
  490.  
  491. if (moved) {
  492. setTimeout(() => {
  493. addRandomTile();
  494. checkGameStatus();
  495. }, 150);
  496. }
  497. }
  498.  
  499. // Touch event handlers
  500. function handleTouchStart(e) {
  501. touchStartX = e.touches[0].clientX;
  502. touchStartY = e.touches[0].clientY;
  503. }
  504.  
  505. function handleTouchMove(e) {
  506. if (gameOver || gameWon) return;
  507. e.preventDefault();
  508. }
  509.  
  510. function handleTouchEnd(e) {
  511. if (gameOver || gameWon) return;
  512.  
  513. const touchEndX = e.changedTouches[0].clientX;
  514. const touchEndY = e.changedTouches[0].clientY;
  515.  
  516. const deltaX = touchEndX - touchStartX;
  517. const deltaY = touchEndY - touchStartY;
  518.  
  519. let moved = false;
  520.  
  521. // Determine swipe direction based on the larger delta
  522. if (Math.abs(deltaX) > Math.abs(deltaY)) {
  523. if (deltaX > 20) {
  524. moved = moveRight();
  525. } else if (deltaX < -20) {
  526. moved = moveLeft();
  527. }
  528. } else {
  529. if (deltaY > 20) {
  530. moved = moveDown();
  531. } else if (deltaY < -20) {
  532. moved = moveUp();
  533. }
  534. }
  535.  
  536. if (moved) {
  537. setTimeout(() => {
  538. addRandomTile();
  539. checkGameStatus();
  540. }, 150);
  541. }
  542. }
  543.  
  544. // Movement functions
  545. function moveUp() {
  546. return moveTiles((i, j) => ({row: i, col: j}),
  547. (i) => i > 0,
  548. (i) => i - 1,
  549. () => -1);
  550. }
  551.  
  552. function moveDown() {
  553. return moveTiles((i, j) => ({row: GRID_SIZE - 1 - i, col: j}),
  554. (i) => i < GRID_SIZE - 1,
  555. (i) => i + 1,
  556. () => 1);
  557. }
  558.  
  559. function moveLeft() {
  560. return moveTiles((i, j) => ({row: j, col: i}),
  561. (i) => i > 0,
  562. (i) => i - 1,
  563. () => -1);
  564. }
  565.  
  566. function moveRight() {
  567. return moveTiles((i, j) => ({row: j, col: GRID_SIZE - 1 - i}),
  568. (i) => i < GRID_SIZE - 1,
  569. (i) => i + 1,
  570. () => 1);
  571. }
  572.  
  573. // Generic move tiles function
  574. function moveTiles(positionTransform, canMove, nextPos, direction) {
  575. let moved = false;
  576. const mergedPositions = new Set();
  577.  
  578. for (let i = 0; i < GRID_SIZE; i++) {
  579. for (let j = 0; j < GRID_SIZE; j++) {
  580. const { row, col } = positionTransform(i, j);
  581.  
  582. if (grid[row][col] === null) continue;
  583.  
  584. let currentRow = row;
  585. let currentCol = col;
  586. let dir = direction();
  587.  
  588. // Move as far as possible in the direction
  589. while (canMove(dir === 1 ? currentRow : currentCol)) {
  590. const nextRow = dir === 0 ? currentRow : (dir === 1 ? nextPos(currentRow) : currentRow);
  591. const nextCol = dir === 0 ? (dir === -1 ? nextPos(currentCol) : currentCol) : nextCol = currentCol;
  592.  
  593. // If next cell is empty, move there
  594. if (grid[nextRow][nextCol] === null) {
  595. grid[nextRow][nextCol] = grid[currentRow][currentCol];
  596. grid[currentRow][currentCol] = null;
  597. currentRow = nextRow;
  598. currentCol = nextCol;
  599. moved = true;
  600. }
  601. // If next cell has same value and hasn't been merged yet, merge them
  602. else if (grid[nextRow][nextCol].value === grid[currentRow][currentCol].value &&
  603. !mergedPositions.has(`${nextRow},${nextCol}`)) {
  604. const newValue = grid[currentRow][currentCol].value * 2;
  605. grid[nextRow][nextCol].value = newValue;
  606. grid[currentRow][currentCol] = null;
  607.  
  608. // Mark as merged and update score
  609. mergedPositions.add(`${nextRow},${nextCol}`);
  610. score += newValue;
  611. scoreElement.textContent = score;
  612.  
  613. // Update best score if needed
  614. if (score > bestScore) {
  615. bestScore = score;
  616. bestScoreElement.textContent = bestScore;
  617. localStorage.setItem('bestScore', bestScore);
  618. }
  619.  
  620. moved = true;
  621. break;
  622. } else {
  623. // Can't move further
  624. break;
  625. }
  626. }
  627. }
  628. }
  629.  
  630. if (moved) {
  631. updateTileDisplay();
  632. }
  633.  
  634. return moved;
  635. }
  636.  
  637. // Add a random tile to the grid
  638. function addRandomTile() {
  639. const emptyCells = [];
  640.  
  641. // Find all empty cells
  642. for (let i = 0; i < GRID_SIZE; i++) {
  643. for (let j = 0; j < GRID_SIZE; j++) {
  644. if (grid[i][j] === null) {
  645. emptyCells.push({row: i, col: j});
  646. }
  647. }
  648. }
  649.  
  650. // If there are empty cells, add a new tile
  651. if (emptyCells.length > 0) {
  652. const {row, col} = emptyCells[Math.floor(Math.random() * emptyCells.length)];
  653. const value = Math.random() < 0.9 ? 2 : 4;
  654.  
  655. grid[row][col] = { value, merged: false };
  656.  
  657. // Create and add the tile element
  658. const tile = document.createElement('div');
  659. tile.classList.add('tile');
  660. tile.dataset.value = value;
  661. tile.textContent = value;
  662. tile.style.top = `${CELL_GAP + row * (CELL_SIZE + CELL_GAP)}px`;
  663. tile.style.left = `${CELL_GAP + col * (CELL_SIZE + CELL_GAP)}px`;
  664.  
  665. gameContainer.appendChild(tile);
  666. }
  667. }
  668.  
  669. // Update the display of all tiles
  670. function updateTileDisplay() {
  671. // Remove all existing tiles
  672. const existingTiles = document.querySelectorAll('.tile');
  673. existingTiles.forEach(tile => tile.remove());
  674.  
  675. // Create new tiles based on grid state
  676. for (let i = 0; i < GRID_SIZE; i++) {
  677. for (let j = 0; j < GRID_SIZE; j++) {
  678. if (grid[i][j] !== null) {
  679. const value = grid[i][j].value;
  680. const merged = grid[i][j].merged;
  681.  
  682. const tile = document.createElement('div');
  683. tile.classList.add('tile');
  684. if (merged) tile.classList.add('merged');
  685. tile.dataset.value = value;
  686. tile.textContent = value;
  687. tile.style.top = `${CELL_GAP + i * (CELL_SIZE + CELL_GAP)}px`;
  688. tile.style.left = `${CELL_GAP + j * (CELL_SIZE + CELL_GAP)}px`;
  689.  
  690. gameContainer.appendChild(tile);
  691.  
  692. // Reset merged flag
  693. grid[i][j].merged = false;
  694. }
  695. }
  696. }
  697. }
  698.  
  699. // Check if the game is over or won
  700. function checkGameStatus() {
  701. // Check for 2048 tile (win condition)
  702. if (!gameWon) {
  703. for (let i = 0; i < GRID_SIZE; i++) {
  704. for (let j = 0; j < GRID_SIZE; j++) {
  705. if (grid[i][j] !== null && grid[i][j].value === 2048) {
  706. gameWon = true;
  707. messageContainer.classList.add('game-won');
  708. messageContainer.querySelector('p').textContent = 'You Win!';
  709. return;
  710. }
  711. }
  712. }
  713. }
  714.  
  715. // Check if any moves are possible
  716. // First, check if there are any empty cells
  717. for (let i = 0; i < GRID_SIZE; i++) {
  718. for (let j = 0; j < GRID_SIZE; j++) {
  719. if (grid[i][j] === null) return; // There's an empty cell, game not over
  720. }
  721. }
  722.  
  723. // Check if any adjacent cells have the same value
  724. for (let i = 0; i < GRID_SIZE; i++) {
  725. for (let j = 0; j < GRID_SIZE; j++) {
  726. const value = grid[i][j].value;
  727.  
  728. // Check right
  729. if (j < GRID_SIZE - 1 && grid[i][j + 1].value === value) return;
  730.  
  731. // Check down
  732. if (i < GRID_SIZE - 1 && grid[i + 1][j].value === value) return;
  733. }
  734. }
  735.  
  736. // If we reach here, game is over
  737. gameOver = true;
  738. messageContainer.classList.add('game-over');
  739. messageContainer.querySelector('p').textContent = 'Game Over!';
  740. }
  741.  
  742. // Start the game
  743. initGame();
  744. });
  745. </script>
  746. </body>
  747. </html>
  748. ```
  749.  
  750. This implementation includes:
  751.  
  752. 1. A responsive design that works on both desktop and mobile devices
  753. 2. Touch/swipe controls for mobile devices
  754. 3. Animations for tile movements and merges
  755. 4. Score tracking with persistent high score (saved in localStorage)
  756. 5. Game win/lose detection
  757. 6. Clean, modern UI with color-coded tiles
  758. 7. Restart functionality
  759.  
  760. To play the game:
  761. - On desktop: Use arrow keys (Up, Down, Left, Right)
  762. - On mobile: Swipe in the direction you want to move the tiles
  763.  
  764. The game follows the classic 2048 rules - combine matching tiles to reach the 2048 tile!
  765.  
  766. ## Gemini (Iteration 2)
  767.  
  768. Okay, let's review Claude's code and enhance it. Claude has provided a solid foundation with good structure, responsiveness, and basic animations.
  769.  
  770. **Critique of Claude's Version:**
  771.  
  772. 1. **Animation & DOM Manipulation:** The biggest issue is how animations and tile updates are handled. The `updateTileDisplay` function removes *all* existing tile elements and redraws them based on the `grid` array *after* the logic for a move is complete. This prevents smooth animation of individual tiles moving across the board. The CSS `transition: transform 100ms` is likely not having the intended effect because the elements it would apply to are destroyed and recreated. The `merged` class and animation are added, but again, applied to newly created elements, not the ones involved in the merge visually.
  773. 2. **`moveTiles` Function:** While clever to use a generic function with transforms, it's quite complex and hard to debug. The logic for determining `nextRow`/`nextCol` based on `dir` seems overly complicated and potentially error-prone (especially the `nextCol = nextCol = currentCol` part which looks like a typo/bug).
  774. 3. **Performance:** Constantly destroying and recreating DOM elements, especially in a timed loop (`setTimeout`), is less performant than moving existing elements.
  775. 4. **Game Win Flow:** Once 2048 is reached, the game stops. A common enhancement is allowing the player to continue playing ("Keep Playing" button).
  776. 5. **Accessibility:** Basic accessibility features (like ARIA roles or live regions for score updates/game status) are missing.
  777.  
  778. **Proposed Improvements:**
  779.  
  780. 1. **Refactor DOM Manipulation for Animations:**
  781. * Represent tiles in the JS `grid` not just by value, but potentially by linking them to their DOM elements.
  782. * Instead of `updateTileDisplay`, modify the `move` functions to:
  783. * Calculate the target position for each tile.
  784. * Update the `top` and `left` CSS properties of the *existing* tile DOM elements. Use CSS transitions on `top` and `left` for smooth movement.
  785. * When tiles merge, update the value (`textContent`, `data-value`) of the target tile element, add the `merged` class for the pop animation, and *remove* the DOM element of the tile that merged into it.
  786. * When a new tile appears, create only that single new element and add an "appear" animation.
  787. 2. **Simplify Movement Logic:** Potentially rewrite the movement logic for each direction more explicitly (e.g., `processMoveUp`, `processMoveLeft`) rather than the complex generic `moveTiles`. This might involve helper functions for sliding and merging within a single row/column.
  788. 3. **"Keep Playing" Feature:** Modify the win condition check to allow the game to continue after the "You Win!" message is displayed, if the user chooses.
  789. 4. **Add Basic Accessibility:** Use `aria-live` regions for score updates and game status messages.
  790. 5. **Code Structure:** Minor improvements for clarity and separation.
  791.  
  792. ---
  793.  
  794. Here is the improved version incorporating these changes:
  795.  
  796. ```html
  797. <!DOCTYPE html>
  798. <html lang="en">
  799. <head>
  800. <meta charset="UTF-8">
  801. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  802. <title>Enhanced 2048 Refactored</title>
  803. <style>
  804. :root {
  805. --grid-size: 4;
  806. --cell-size: 100px;
  807. --cell-gap: 12px;
  808. --border-radius: 8px;
  809. --animation-speed: 100ms; /* For movement */
  810. --appear-speed: 200ms;
  811. --pop-speed: 200ms;
  812. }
  813.  
  814. * {
  815. box-sizing: border-box;
  816. margin: 0;
  817. padding: 0;
  818. font-family: 'Arial', sans-serif;
  819. }
  820.  
  821. body {
  822. background-color: #faf8ef;
  823. display: flex;
  824. justify-content: center;
  825. align-items: center;
  826. min-height: 100vh; /* Use min-height for flexibility */
  827. overflow-y: auto; /* Allow scrolling if content overflows */
  828. touch-action: none; /* Prevent browser default touch actions */
  829. padding: 10px; /* Add padding for small screens */
  830. }
  831.  
  832. .container {
  833. width: min(95vw, 500px); /* Adjusted max-width */
  834. padding: 15px;
  835. border-radius: 10px;
  836. background-color: #bbada0;
  837. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  838. }
  839.  
  840. .header {
  841. display: flex;
  842. justify-content: space-between;
  843. align-items: center;
  844. margin-bottom: 20px;
  845. flex-wrap: wrap; /* Allow wrapping on small screens */
  846. gap: 10px;
  847. }
  848.  
  849. .title {
  850. color: #776e65;
  851. font-size: clamp(2rem, 8vw, 2.5rem); /* Responsive font size */
  852. font-weight: bold;
  853. }
  854.  
  855. .scores {
  856. display: flex;
  857. gap: 10px;
  858. }
  859.  
  860. .score-box {
  861. background-color: #8f7a66;
  862. color: white;
  863. padding: 8px 12px;
  864. border-radius: 5px;
  865. display: flex;
  866. flex-direction: column;
  867. align-items: center;
  868. min-width: 70px;
  869. text-align: center;
  870. }
  871.  
  872. .score-title {
  873. font-size: 0.7rem;
  874. text-transform: uppercase;
  875. margin-bottom: 2px;
  876. }
  877.  
  878. .score-value {
  879. font-size: clamp(1.2rem, 5vw, 1.5rem);
  880. font-weight: bold;
  881. }
  882.  
  883. .sub-header {
  884. display: flex;
  885. justify-content: space-between;
  886. align-items: center;
  887. margin-bottom: 20px;
  888. flex-wrap: wrap;
  889. gap: 10px;
  890. }
  891.  
  892. .game-intro {
  893. color: #776e65;
  894. flex-basis: 60%; /* Allow text to take more space */
  895. font-size: 0.9rem;
  896. line-height: 1.3;
  897. }
  898.  
  899. .new-game-btn {
  900. background-color: #8f7a66;
  901. color: white;
  902. border: none;
  903. border-radius: 5px;
  904. padding: 10px 15px;
  905. font-weight: bold;
  906. cursor: pointer;
  907. transition: background-color 0.2s ease;
  908. white-space: nowrap; /* Prevent button text wrapping */
  909. }
  910.  
  911. .new-game-btn:hover, .new-game-btn:focus {
  912. background-color: #9f8b77;
  913. outline: none;
  914. }
  915.  
  916. .game-container {
  917. position: relative;
  918. /* Calculate width/height based on CSS variables */
  919. width: calc(var(--grid-size) * (var(--cell-size) + var(--cell-gap)) + var(--cell-gap));
  920. height: calc(var(--grid-size) * (var(--cell-size) + var(--cell-gap)) + var(--cell-gap));
  921. background-color: #bbada0; /* Background for the grid area */
  922. border-radius: var(--border-radius);
  923. padding: var(--cell-gap);
  924. margin: 0 auto; /* Center the grid */
  925. overflow: hidden; /* Hide parts of tiles moving outside */
  926. }
  927.  
  928. .grid-background {
  929. position: absolute;
  930. top: var(--cell-gap);
  931. left: var(--cell-gap);
  932. bottom: var(--cell-gap);
  933. right: var(--cell-gap);
  934. display: grid;
  935. grid-template-columns: repeat(var(--grid-size), var(--cell-size));
  936. grid-template-rows: repeat(var(--grid-size), var(--cell-size));
  937. gap: var(--cell-gap);
  938. }
  939.  
  940. .cell {
  941. background-color: rgba(238, 228, 218, 0.35);
  942. border-radius: var(--border-radius);
  943. }
  944.  
  945. .tile {
  946. position: absolute;
  947. display: flex;
  948. justify-content: center;
  949. align-items: center;
  950. width: var(--cell-size);
  951. height: var(--cell-size);
  952. border-radius: var(--border-radius);
  953. font-size: clamp(1.5rem, 7vw, 2rem); /* Responsive font size */
  954. font-weight: bold;
  955. background-color: #eee4da; /* Default tile color */
  956. color: #776e65; /* Default text color */
  957. /* Animate position changes */
  958. transition: top var(--animation-speed) ease-in-out,
  959. left var(--animation-speed) ease-in-out;
  960. z-index: 10;
  961. /* Prevent text selection */
  962. user-select: none;
  963. }
  964.  
  965. /* Animation for new tiles */
  966. .tile.new-tile {
  967. animation: appear var(--appear-speed) ease-in-out;
  968. }
  969.  
  970. @keyframes appear {
  971. 0% {
  972. opacity: 0;
  973. transform: scale(0);
  974. }
  975. 100% {
  976. opacity: 1;
  977. transform: scale(1);
  978. }
  979. }
  980.  
  981. /* Animation for merged tiles */
  982. .tile.merged-tile {
  983. animation: pop var(--pop-speed) ease-in-out;
  984. z-index: 20; /* Ensure merged tile pops above others */
  985. }
  986.  
  987. @keyframes pop {
  988. 0% {
  989. transform: scale(1);
  990. }
  991. 50% {
  992. transform: scale(1.2);
  993. }
  994. 100% {
  995. transform: scale(1);
  996. }
  997. }
  998.  
  999. /* Tile Colors - Using data attributes */
  1000. .tile[data-value="2"] { background-color: #eee4da; color: #776e65; }
  1001. .tile[data-value="4"] { background-color: #ede0c8; color: #776e65; }
  1002. .tile[data-value="8"] { background-color: #f2b179; color: #f9f6f2; }
  1003. .tile[data-value="16"] { background-color: #f59563; color: #f9f6f2; }
  1004. .tile[data-value="32"] { background-color: #f67c5f; color: #f9f6f2; }
  1005. .tile[data-value="64"] { background-color: #f65e3b; color: #f9f6f2; }
  1006. .tile[data-value="128"] { background-color: #edcf72; color: #f9f6f2; font-size: clamp(1.3rem, 6vw, 1.8rem); }
  1007. .tile[data-value="256"] { background-color: #edcc61; color: #f9f6f2; font-size: clamp(1.3rem, 6vw, 1.8rem); }
  1008. .tile[data-value="512"] { background-color: #edc850; color: #f9f6f2; font-size: clamp(1.3rem, 6vw, 1.8rem); }
  1009. .tile[data-value="1024"] { background-color: #edc53f; color: #f9f6f2; font-size: clamp(1.1rem, 5vw, 1.5rem); }
  1010. .tile[data-value="2048"] { background-color: #edc22e; color: #f9f6f2; font-size: clamp(1.1rem, 5vw, 1.5rem); }
  1011. .tile[data-value="4096"] { background-color: #3c3a32; color: #f9f6f2; font-size: clamp(1.1rem, 5vw, 1.5rem); }
  1012. .tile[data-value="8192"] { background-color: #3c3a32; color: #f9f6f2; font-size: clamp(1.1rem, 5vw, 1.5rem); }
  1013.  
  1014.  
  1015. .game-message {
  1016. /* Start hidden */
  1017. display: none;
  1018. position: absolute;
  1019. top: 0; right: 0; bottom: 0; left: 0;
  1020. background-color: rgba(238, 228, 218, 0.73);
  1021. flex-direction: column;
  1022. align-items: center;
  1023. justify-content: center;
  1024. text-align: center;
  1025. border-radius: var(--border-radius);
  1026. z-index: 100;
  1027. opacity: 0; /* Start transparent for fade-in */
  1028. transition: opacity 0.3s ease-in-out;
  1029. animation: fadein 0.3s ease-in-out forwards;
  1030. }
  1031.  
  1032. .game-message.active {
  1033. display: flex;
  1034. opacity: 1; /* Fade in */
  1035. }
  1036.  
  1037. @keyframes fadein {
  1038. from { opacity: 0; }
  1039. to { opacity: 1; }
  1040. }
  1041.  
  1042. .game-message p {
  1043. font-size: clamp(1.8rem, 7vw, 2rem);
  1044. font-weight: bold;
  1045. color: #776e65;
  1046. margin-bottom: 20px;
  1047. }
  1048.  
  1049. .game-message .lower {
  1050. display: flex;
  1051. gap: 10px;
  1052. margin-top: 10px;
  1053. }
  1054.  
  1055. /* Responsive adjustments */
  1056. @media screen and (max-width: 520px) {
  1057. :root {
  1058. --cell-size: 65px;
  1059. --cell-gap: 8px;
  1060. }
  1061. .game-intro { flex-basis: 100%; text-align: center; margin-bottom: 10px; }
  1062. .new-game-btn { padding: 8px 12px; }
  1063. }
  1064.  
  1065. @media screen and (max-width: 380px) {
  1066. :root {
  1067. --cell-size: 55px;
  1068. --cell-gap: 6px;
  1069. }
  1070. .container { padding: 10px; }
  1071. .header { justify-content: center; } /* Center title and scores */
  1072. .title { text-align: center; width: 100%; margin-bottom: 5px;}
  1073. .sub-header { justify-content: center; }
  1074. .score-box { min-width: 60px; padding: 6px 10px; }
  1075. }
  1076.  
  1077. .footer {
  1078. margin-top: 20px;
  1079. text-align: center;
  1080. color: #776e65;
  1081. font-size: 0.8rem;
  1082. }
  1083. </style>
  1084. </head>
  1085. <body>
  1086. <div class="container">
  1087. <header class="header">
  1088. <h1 class="title">2048</h1>
  1089. <div class="scores">
  1090. <div class="score-box">
  1091. <div class="score-title">Score</div>
  1092. <div class="score-value" id="score" aria-live="polite">0</div>
  1093. </div>
  1094. <div class="score-box">
  1095. <div class="score-title">Best</div>
  1096. <div class="score-value" id="best-score">0</div>
  1097. </div>
  1098. </div>
  1099. </header>
  1100. <div class="sub-header">
  1101. <p class="game-intro">Join tiles, get to <strong>2048!</strong> Use <strong>arrow keys</strong> or <strong>swipe</strong>.</p>
  1102. <button class="new-game-btn" id="restart-button">New Game</button>
  1103. </div>
  1104.  
  1105. <div class="game-container" id="game-container">
  1106. <div class="grid-background" id="grid-background">
  1107. <!-- Background cells are generated here by JS -->
  1108. </div>
  1109. <div class="tile-container" id="tile-container">
  1110. <!-- Tiles are added here by JS -->
  1111. </div>
  1112. <div class="game-message" id="game-message" aria-live="assertive">
  1113. <p id="message-text"></p>
  1114. <div class="lower">
  1115. <button class="new-game-btn" id="retry-button">Try again</button>
  1116. <button class="new-game-btn" id="keep-playing-button" style="display: none;">Keep Playing</button>
  1117. </div>
  1118. </div>
  1119. </div>
  1120.  
  1121. <footer class="footer">
  1122. <p>Refactored Version • Swipe or use arrow keys</p>
  1123. </footer>
  1124. </div>
  1125.  
  1126. <script>
  1127. document.addEventListener('DOMContentLoaded', () => {
  1128. const GRID_SIZE = 4;
  1129. const WINNING_VALUE = 2048;
  1130. const MOVE_ANIMATION_DURATION = 100; // ms, matches CSS --animation-speed
  1131. const APPEAR_ANIMATION_DURATION = 200; // ms, matches CSS --appear-speed
  1132.  
  1133. // --- DOM Elements ---
  1134. const gameContainer = document.getElementById('game-container');
  1135. const gridBackground = document.getElementById('grid-background');
  1136. const tileContainer = document.getElementById('tile-container');
  1137. const scoreElement = document.getElementById('score');
  1138. const bestScoreElement = document.getElementById('best-score');
  1139. const messageContainer = document.getElementById('game-message');
  1140. const messageText = document.getElementById('message-text');
  1141. const restartButton = document.getElementById('restart-button');
  1142. const retryButton = document.getElementById('retry-button');
  1143. const keepPlayingButton = document.getElementById('keep-playing-button');
  1144.  
  1145. // --- Game State ---
  1146. let grid; // Will be a 2D array representing cells (Tile objects or null)
  1147. let score = 0;
  1148. let bestScore = localStorage.getItem('bestScore2048') || 0;
  1149. let isGameOver = false;
  1150. let hasWon = false; // Tracks if 2048 has been reached
  1151. let keepPlayingMode = false; // Tracks if user chose to continue after winning
  1152. let isMoving = false; // Prevent input during animations
  1153.  
  1154. // --- Touch Input State ---
  1155. let touchStartX = 0;
  1156. let touchStartY = 0;
  1157. let touchEndX = 0;
  1158. let touchEndY = 0;
  1159.  
  1160. // --- Tile Class ---
  1161. class Tile {
  1162. constructor(value, row, col) {
  1163. this.value = value;
  1164. this.row = row;
  1165. this.col = col;
  1166. this.element = this.createElement();
  1167. this.updatePosition(false); // Initial position without animation
  1168. this.mergedFrom = null; // Track merge source for animation coordination
  1169. tileContainer.appendChild(this.element);
  1170. }
  1171.  
  1172. createElement() {
  1173. const tileElement = document.createElement('div');
  1174. tileElement.classList.add('tile');
  1175. this.updateValue(this.value); // Set initial text and data-value
  1176. // Add new tile animation class, remove after animation
  1177. tileElement.classList.add('new-tile');
  1178. setTimeout(() => {
  1179. tileElement.classList.remove('new-tile');
  1180. }, APPEAR_ANIMATION_DURATION);
  1181. return tileElement;
  1182. }
  1183.  
  1184. updateValue(newValue) {
  1185. this.value = newValue;
  1186. this.element.textContent = newValue;
  1187. this.element.dataset.value = newValue; // For CSS styling
  1188. }
  1189.  
  1190. updatePosition(animate = true) {
  1191. const { top, left } = this.calculatePixelPosition();
  1192. this.element.style.transition = animate ? `top ${MOVE_ANIMATION_DURATION}ms ease-in-out, left ${MOVE_ANIMATION_DURATION}ms ease-in-out` : 'none';
  1193. this.element.style.top = `${top}px`;
  1194. this.element.style.left = `${left}px`;
  1195. }
  1196.  
  1197. calculatePixelPosition() {
  1198. // Need to get these values dynamically in case they change (e.g., responsive)
  1199. const cellSize = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--cell-size'));
  1200. const cellGap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--cell-gap'));
  1201. const top = this.row * (cellSize + cellGap) + cellGap;
  1202. const left = this.col * (cellSize + cellGap) + cellGap;
  1203. return { top, left };
  1204. }
  1205.  
  1206. remove(merged = false) {
  1207. if (merged) {
  1208. // If merged, let the pop animation finish before removing
  1209. this.element.classList.add('merged-tile');
  1210. this.element.addEventListener('animationend', () => {
  1211. if (this.element.parentNode) {
  1212. tileContainer.removeChild(this.element);
  1213. }
  1214. }, { once: true });
  1215. } else {
  1216. if (this.element.parentNode) {
  1217. tileContainer.removeChild(this.element);
  1218. }
  1219. }
  1220. }
  1221.  
  1222. playMergeAnimation() {
  1223. this.element.classList.add('merged-tile');
  1224. // Remove class after animation to allow re-triggering
  1225. this.element.addEventListener('animationend', () => {
  1226. this.element.classList.remove('merged-tile');
  1227. }, { once: true });
  1228. }
  1229. }
  1230.  
  1231. // --- Grid Initialization ---
  1232. function createGridCells() {
  1233. gridBackground.innerHTML = ''; // Clear previous cells
  1234. for (let i = 0; i < GRID_SIZE * GRID_SIZE; i++) {
  1235. const cell = document.createElement('div');
  1236. cell.classList.add('cell');
  1237. gridBackground.appendChild(cell);
  1238. }
  1239. }
  1240.  
  1241. function initializeGrid() {
  1242. grid = Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(null));
  1243. }
  1244.  
  1245. // --- Game Setup ---
  1246. function setupGame() {
  1247. isMoving = true; // Prevent input during setup
  1248. initializeGrid();
  1249. tileContainer.innerHTML = ''; // Clear existing tiles
  1250. updateScore(0);
  1251. bestScoreElement.textContent = bestScore;
  1252. isGameOver = false;
  1253. hasWon = false;
  1254. keepPlayingMode = false;
  1255. hideMessage();
  1256. addRandomTile();
  1257. addRandomTile();
  1258. isMoving = false; // Allow input
  1259. }
  1260.  
  1261. // --- Score Management ---
  1262. function updateScore(newScore) {
  1263. score = newScore;
  1264. scoreElement.textContent = score;
  1265. if (score > bestScore) {
  1266. bestScore = score;
  1267. bestScoreElement.textContent = bestScore;
  1268. localStorage.setItem('bestScore2048', bestScore);
  1269. }
  1270. }
  1271.  
  1272. // --- Tile Management ---
  1273. function getEmptyCells() {
  1274. const emptyCells = [];
  1275. for (let r = 0; r < GRID_SIZE; r++) {
  1276. for (let c = 0; c < GRID_SIZE; c++) {
  1277. if (grid[r][c] === null) {
  1278. emptyCells.push({ r, c });
  1279. }
  1280. }
  1281. }
  1282. return emptyCells;
  1283. }
  1284.  
  1285. function addRandomTile() {
  1286. const emptyCells = getEmptyCells();
  1287. if (emptyCells.length === 0) return; // No space left
  1288.  
  1289. const { r, c } = emptyCells[Math.floor(Math.random() * emptyCells.length)];
  1290. const value = Math.random() < 0.9 ? 2 : 4;
  1291. const newTile = new Tile(value, r, c);
  1292. grid[r][c] = newTile;
  1293. }
  1294.  
  1295. // --- Movement Logic ---
  1296. function move(direction) {
  1297. if (isMoving || isGameOver) return;
  1298.  
  1299. isMoving = true;
  1300. let moved = false;
  1301. let currentScore = score; // Track score changes from this move
  1302.  
  1303. // Prepare for move: reset merge tracking
  1304. for (let r = 0; r < GRID_SIZE; r++) {
  1305. for (let c = 0; c < GRID_SIZE; c++) {
  1306. if (grid[r][c]) grid[r][c].mergedFrom = null;
  1307. }
  1308. }
  1309.  
  1310. // Define traversal order based on direction
  1311. const traversals = getTraversals(direction);
  1312.  
  1313. traversals.rows.forEach(r => {
  1314. traversals.cols.forEach(c => {
  1315. const currentTile = grid[r][c];
  1316. if (currentTile) {
  1317. const { furthestRow, furthestCol, nextTile } = findFurthestPosition(r, c, direction);
  1318.  
  1319. if (nextTile && nextTile.value === currentTile.value && !nextTile.mergedFrom) {
  1320. // --- Merge ---
  1321. const mergedValue = currentTile.value * 2;
  1322. nextTile.updateValue(mergedValue);
  1323. nextTile.mergedFrom = currentTile; // Mark as merged
  1324. nextTile.playMergeAnimation(); // Play pop animation on target
  1325.  
  1326. grid[r][c] = null; // Remove current tile from grid
  1327. currentTile.remove(true); // Remove current tile element (with animation flag)
  1328.  
  1329. currentScore += mergedValue;
  1330. moved = true;
  1331.  
  1332. // Check for win condition immediately after merge
  1333. if (!hasWon && mergedValue === WINNING_VALUE) {
  1334. hasWon = true;
  1335. }
  1336.  
  1337. } else if (furthestRow !== r || furthestCol !== c) {
  1338. // --- Move ---
  1339. grid[furthestRow][furthestCol] = currentTile;
  1340. grid[r][c] = null;
  1341. currentTile.row = furthestRow;
  1342. currentTile.col = furthestCol;
  1343. currentTile.updatePosition(); // Animate move
  1344. moved = true;
  1345. }
  1346. }
  1347. });
  1348. });
  1349.  
  1350. if (moved) {
  1351. updateScore(currentScore); // Update score display after all merges calculation
  1352.  
  1353. // Use setTimeout to allow move/merge animations to mostly complete
  1354. setTimeout(() => {
  1355. addRandomTile();
  1356. if (!canMove()) {
  1357. checkGameOver();
  1358. } else if (hasWon && !keepPlayingMode) {
  1359. showWinMessage();
  1360. }
  1361. isMoving = false; // Re-enable input
  1362. }, MOVE_ANIMATION_DURATION + 50); // Wait slightly longer than move animation
  1363. } else {
  1364. isMoving = false; // No move occurred, re-enable input immediately
  1365. }
  1366. }
  1367.  
  1368. function getTraversals(direction) {
  1369. const rows = Array.from({ length: GRID_SIZE }, (_, i) => i);
  1370. const cols = Array.from({ length: GRID_SIZE }, (_, i) => i);
  1371.  
  1372. if (direction === 'right') cols.reverse();
  1373. if (direction === 'down') rows.reverse();
  1374.  
  1375. return { rows, cols };
  1376. }
  1377.  
  1378. function findFurthestPosition(r, c, direction) {
  1379. let currentRow = r;
  1380. let currentCol = c;
  1381. let nextRow, nextCol;
  1382.  
  1383. do {
  1384. currentRow = nextRow !== undefined ? nextRow : r;
  1385. currentCol = nextCol !== undefined ? nextCol : c;
  1386. const { nr, nc } = getNextPosition(currentRow, currentCol, direction);
  1387. nextRow = nr;
  1388. nextCol = nc;
  1389. } while (isWithinBounds(nextRow, nextCol) && grid[nextRow][nextCol] === null);
  1390.  
  1391. // The loop went one step too far or hit a tile
  1392. const furthestRow = currentRow;
  1393. const furthestCol = currentCol;
  1394. const nextTile = isWithinBounds(nextRow, nextCol) ? grid[nextRow][nextCol] : null;
  1395.  
  1396. return { furthestRow, furthestCol, nextTile };
  1397. }
  1398.  
  1399. function getNextPosition(r, c, direction) {
  1400. switch (direction) {
  1401. case 'up': return { nr: r - 1, nc: c };
  1402. case 'down': return { nr: r + 1, nc: c };
  1403. case 'left': return { nr: r, nc: c - 1 };
  1404. case 'right': return { nr: r, nc: c + 1 };
  1405. default: return { nr: r, nc: c };
  1406. }
  1407. }
  1408.  
  1409. function isWithinBounds(r, c) {
  1410. return r >= 0 && r < GRID_SIZE && c >= 0 && c < GRID_SIZE;
  1411. }
  1412.  
  1413.  
  1414. // --- Game Status Checks ---
  1415. function canMove() {
  1416. if (getEmptyCells().length > 0) return true; // Can always move if empty cells exist
  1417.  
  1418. // Check for possible merges horizontally and vertically
  1419. for (let r = 0; r < GRID_SIZE; r++) {
  1420. for (let c = 0; c < GRID_SIZE; c++) {
  1421. const tile = grid[r][c];
  1422. if (!tile) continue; // Should not happen if no empty cells, but safe check
  1423.  
  1424. // Check right
  1425. if (c < GRID_SIZE - 1 && grid[r][c + 1] && grid[r][c + 1].value === tile.value) {
  1426. return true;
  1427. }
  1428. // Check down
  1429. if (r < GRID_SIZE - 1 && grid[r + 1][c] && grid[r + 1][c].value === tile.value) {
  1430. return true;
  1431. }
  1432. }
  1433. }
  1434. return false; // No empty cells and no possible merges
  1435. }
  1436.  
  1437. function checkGameOver() {
  1438. if (!isGameOver && !canMove()) {
  1439. isGameOver = true;
  1440. showGameOverMessage();
  1441. }
  1442. }
  1443.  
  1444. // --- Message Display ---
  1445. function showMessage(text, type) {
  1446. messageText.textContent = text;
  1447. messageContainer.className = 'game-message active'; // Reset classes and add active
  1448. keepPlayingButton.style.display = 'none'; // Hide by default
  1449.  
  1450. if (type === 'win') {
  1451. messageContainer.classList.add('game-won');
  1452. if (!keepPlayingMode) {
  1453. keepPlayingButton.style.display = 'inline-block'; // Show keep playing
  1454. }
  1455. } else if (type === 'over') {
  1456. messageContainer.classList.add('game-over');
  1457. }
  1458. }
  1459.  
  1460. function showWinMessage() {
  1461. showMessage('You Win!', 'win');
  1462. }
  1463. function showGameOverMessage() {
  1464. showMessage('Game Over!', 'over');
  1465. }
  1466.  
  1467. function hideMessage() {
  1468. messageContainer.classList.remove('active');
  1469. // Optional: Delay removal of other classes if needed for fade out
  1470. // setTimeout(() => {
  1471. // messageContainer.classList.remove('game-won', 'game-over');
  1472. // }, 300); // Match fade out duration
  1473. }
  1474.  
  1475.  
  1476. // --- Event Listeners ---
  1477. function handleKeyDown(e) {
  1478. if (isMoving || isGameOver) return;
  1479.  
  1480. switch (e.key) {
  1481. case 'ArrowUp':
  1482. case 'w': // Add WASD support
  1483. e.preventDefault(); // Prevent page scrolling
  1484. move('up');
  1485. break;
  1486. case 'ArrowDown':
  1487. case 's':
  1488. e.preventDefault();
  1489. move('down');
  1490. break;
  1491. case 'ArrowLeft':
  1492. case 'a':
  1493. e.preventDefault();
  1494. move('left');
  1495. break;
  1496. case 'ArrowRight':
  1497. case 'd':
  1498. e.preventDefault();
  1499. move('right');
  1500. break;
  1501. }
  1502. }
  1503.  
  1504. function handleTouchStart(e) {
  1505. if (isMoving || isGameOver || e.touches.length > 1) return; // Ignore multi-touch
  1506. touchStartX = e.touches[0].clientX;
  1507. touchStartY = e.touches[0].clientY;
  1508. touchEndX = touchStartX; // Reset end points
  1509. touchEndY = touchStartY;
  1510. // e.preventDefault(); // Prevent scroll bounce effect on start, maybe not needed with touch-action: none
  1511. }
  1512.  
  1513. function handleTouchMove(e) {
  1514. if (isMoving || isGameOver || e.touches.length > 1) return;
  1515. // Update end points for continuous tracking if needed, but mainly for the final calculation
  1516. touchEndX = e.touches[0].clientX;
  1517. touchEndY = e.touches[0].clientY;
  1518. e.preventDefault(); // Prevent scrolling *during* swipe detection
  1519. }
  1520.  
  1521. function handleTouchEnd(e) {
  1522. if (isMoving || isGameOver || e.changedTouches.length > 1) return;
  1523.  
  1524. const deltaX = touchEndX - touchStartX;
  1525. const deltaY = touchEndY - touchStartY;
  1526. const absDeltaX = Math.abs(deltaX);
  1527. const absDeltaY = Math.abs(deltaY);
  1528. const swipeThreshold = 30; // Minimum distance for a swipe
  1529.  
  1530. if (Math.max(absDeltaX, absDeltaY) < swipeThreshold) return; // Not a swipe
  1531.  
  1532. let direction;
  1533. if (absDeltaX > absDeltaY) {
  1534. // Horizontal swipe
  1535. direction = deltaX > 0 ? 'right' : 'left';
  1536. } else {
  1537. // Vertical swipe
  1538. direction = deltaY > 0 ? 'down' : 'up';
  1539. }
  1540. move(direction);
  1541. }
  1542.  
  1543. // --- Button Actions ---
  1544. restartButton.addEventListener('click', setupGame);
  1545. retryButton.addEventListener('click', setupGame);
  1546. keepPlayingButton.addEventListener('click', () => {
  1547. keepPlayingMode = true;
  1548. hideMessage();
  1549. isMoving = false; // Ensure input is re-enabled
  1550. });
  1551.  
  1552. // --- Initialize ---
  1553. createGridCells(); // Create the static background cells once
  1554. setupGame(); // Setup initial game state
  1555.  
  1556. // Add keyboard listeners
  1557. document.addEventListener('keydown', handleKeyDown);
  1558.  
  1559. // Add touch listeners (use passive: false for move to allow preventDefault)
  1560. gameContainer.addEventListener('touchstart', handleTouchStart, { passive: true });
  1561. gameContainer.addEventListener('touchmove', handleTouchMove, { passive: false });
  1562. gameContainer.addEventListener('touchend', handleTouchEnd, { passive: true });
  1563. // Listen for resize to recalculate tile positions if necessary (though CSS handles most)
  1564. // window.addEventListener('resize', () => { /* Could force redraw if needed */ });
  1565.  
  1566. });
  1567. </script>
  1568. </body>
  1569. </html>
  1570. ```
  1571.  
  1572. **Explanation of Changes:**
  1573.  
  1574. 1. **HTML Structure:**
  1575. * Added a `div.grid-background` and `div.tile-container` inside `game-container`. The background holds the static empty cell visuals, and the `tile-container` holds the actual moving tile elements. This separation makes managing absolutely positioned tiles easier.
  1576. * Added `aria-live="polite"` to the score display for screen readers.
  1577. * Added `aria-live="assertive"` to the game message overlay.
  1578. * Added a "Keep Playing" button (`#keep-playing-button`), initially hidden.
  1579. * Added `clamp()` for responsive font sizes in CSS.
  1580. * Added `min-height: 100vh` and `overflow-y: auto` to `body` for better handling on different screen sizes.
  1581.  
  1582. 2. **CSS Enhancements:**
  1583. * Defined `--animation-speed`, `--appear-speed`, `--pop-speed` variables.
  1584. * Tiles now transition `top` and `left` properties for movement.
  1585. * Renamed `appear` animation class to `new-tile` for clarity.
  1586. * Renamed `pop` animation class to `merged-tile`.
  1587. * Added `z-index` to ensure merged tiles pop visually above others.
  1588. * Improved responsiveness with `clamp()` and minor layout adjustments in media queries.
  1589. * Added styles for tiles > 2048 (e.g., 4096).
  1590. * Game message now fades in using `opacity` and `transition`/`animation`.
  1591.  
  1592. 3. **JavaScript Refactoring:**
  1593. * **`Tile` Class:** Introduced a `Tile` class to manage each tile's state (value, row, col) and its corresponding DOM element. This class handles creating the element, updating its value/position, playing animations, and removing itself.
  1594. * **DOM Manipulation:**
  1595. * Tiles are now represented by `Tile` objects stored in the `grid` array.
  1596. * `addRandomTile` creates a `new Tile()` instance, which appends its element to the `tile-container`.
  1597. * Movement logic (`move` function) now updates the `row`, `col` properties of `Tile` objects and calls their `updatePosition()` method, which changes the `top`/`left` style, triggering the CSS transition.
  1598. * Merging involves updating the target `Tile`'s value, calling its `playMergeAnimation()`, and calling the source `Tile`'s `remove(true)` method (which handles removal after animation).
  1599. * **Movement Logic (`move` function):**
  1600. * Rewrote the core movement logic. It's now more explicit:
  1601. * Determines traversal order based on direction (`getTraversals`).
  1602. * Iterates through cells in the correct order.
  1603. * For each tile, finds its furthest destination or potential merge target (`findFurthestPosition`).
  1604. * Performs either a move (updating grid and tile position) or a merge (updating target tile, removing source tile, updating score).
  1605. * Uses helper functions (`getTraversals`, `findFurthestPosition`, `getNextPosition`, `isWithinBounds`) for clarity.
  1606. * Uses a `mergedFrom` property on the `Tile` object to prevent double merges in a single move.
  1607. * **Animation Coordination:** Uses `setTimeout` after a move completes to allow CSS transitions/animations to largely finish before adding the next random tile and checking game state. The durations match the CSS variables. The `Tile` class manages adding/removing animation classes (`new-tile`, `merged-tile`).
  1608. * **Game State Management:** Introduced `isMoving` flag to prevent concurrent moves or input during animations. Added `hasWon` and `keepPlayingMode` flags.
  1609. * **"Keep Playing":** The win message now shows a "Keep Playing" button. Clicking it sets `keepPlayingMode = true` and hides the message, allowing the game to continue. The win check (`hasWon`) ensures the message only appears once unless the game is reset.
  1610. * **Game Over Check:** `checkGameOver` is called only if a move occurred and `canMove()` returns false.
  1611. * **Touch Handling:** Improved swipe detection logic slightly for clarity and added a threshold. Uses `passive: false` on `touchmove` to correctly allow `preventDefault()` and stop scrolling during swipe detection.
  1612. * **Event Handling:** Added WASD key support alongside arrow keys. `preventDefault` is used for arrow keys to stop page scrolling.
  1613. * **Initialization:** `createGridCells` now runs once to set up the background. `setupGame` handles resetting the state and adding initial tiles.
  1614.  
  1615. This refactored version provides a much smoother user experience due to proper animation handling, is arguably easier to maintain with the `Tile` class, and includes the common "Keep Playing" feature.
  1616.  
  1617. ## Claude (Iteration 2)
  1618.  
  1619. # Enhanced 2048 Game - Further Improvements
  1620.  
  1621. I've reviewed Gemini's refactored version of the 2048 game and will build upon it. Gemini made excellent improvements to the animation system, code organization, and added the "Keep Playing" feature. Let me enhance it further with additional features and optimizations.
  1622.  
  1623. ## Analysis of Gemini's Implementation
  1624.  
  1625. Gemini's refactoring addressed several key issues:
  1626. - Created a proper Tile class with DOM element management
  1627. - Implemented smooth animations for tile movement and merges
  1628. - Added accessibility features with aria-live regions
  1629. - Improved responsiveness and touch handling
  1630. - Added the "Keep Playing" feature after reaching 2048
  1631.  
  1632. ## My Additional Enhancements
  1633.  
  1634. I'll further improve the game with:
  1635.  
  1636. 1. **Game history and undo functionality**
  1637. 2. **Keyboard and touch accessibility improvements**
  1638. 3. **Game statistics tracking**
  1639. 4. **Dark mode support**
  1640. 5. **Save game state persistence**
  1641. 6. **Performance optimizations**
  1642. 7. **Better animation control**
  1643. 8. **Visual and UX enhancements**
  1644.  
  1645. ## The Enhanced Implementation
  1646.  
  1647. ```html
  1648. <!DOCTYPE html>
  1649. <html lang="en">
  1650. <head>
  1651. <meta charset="UTF-8">
  1652. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  1653. <title>2048 - Advanced Edition</title>
  1654. <style>
  1655. :root {
  1656. --grid-size: 4;
  1657. --cell-size: 100px;
  1658. --cell-gap: 12px;
  1659. --border-radius: 8px;
  1660. --animation-speed: 150ms; /* Slightly increased for smoother feel */
  1661. --appear-speed: 200ms;
  1662. --pop-speed: 200ms;
  1663.  
  1664. /* Color scheme - light mode (default) */
  1665. --bg-color: #faf8ef;
  1666. --container-bg: #bbada0;
  1667. --cell-bg: rgba(238, 228, 218, 0.35);
  1668. --title-color: #776e65;
  1669. --text-color: #776e65;
  1670. --button-bg: #8f7a66;
  1671. --button-hover-bg: #9f8b77;
  1672. --button-text: #f9f6f2;
  1673. --overlay-bg: rgba(238, 228, 218, 0.73);
  1674. --tile-2-bg: #eee4da;
  1675. --tile-2-text: #776e65;
  1676. --tile-4-bg: #ede0c8;
  1677. --tile-4-text: #776e65;
  1678. --tile-8-bg: #f2b179;
  1679. --tile-8-text: #f9f6f2;
  1680. --tile-16-bg: #f59563;
  1681. --tile-16-text: #f9f6f2;
  1682. --tile-32-bg: #f67c5f;
  1683. --tile-32-text: #f9f6f2;
  1684. --tile-64-bg: #f65e3b;
  1685. --tile-64-text: #f9f6f2;
  1686. --tile-128-bg: #edcf72;
  1687. --tile-128-text: #f9f6f2;
  1688. --tile-256-bg: #edcc61;
  1689. --tile-256-text: #f9f6f2;
  1690. --tile-512-bg: #edc850;
  1691. --tile-512-text: #f9f6f2;
  1692. --tile-1024-bg: #edc53f;
  1693. --tile-1024-text: #f9f6f2;
  1694. --tile-2048-bg: #edc22e;
  1695. --tile-2048-text: #f9f6f2;
  1696. --tile-super-bg: #3c3a32;
  1697. --tile-super-text: #f9f6f2;
  1698. }
  1699.  
  1700. /* Dark mode color scheme */
  1701. [data-theme="dark"] {
  1702. --bg-color: #1f2937;
  1703. --container-bg: #374151;
  1704. --cell-bg: rgba(55, 65, 81, 0.7);
  1705. --title-color: #e5e7eb;
  1706. --text-color: #e5e7eb;
  1707. --button-bg: #4b5563;
  1708. --button-hover-bg: #6b7280;
  1709. --button-text: #f9fafb;
  1710. --overlay-bg: rgba(31, 41, 55, 0.8);
  1711. --tile-2-bg: #9ca3af;
  1712. --tile-2-text: #1f2937;
  1713. --tile-4-bg: #d1d5db;
  1714. --tile-4-text: #1f2937;
  1715. /* Other tile colors remain the same for recognition */
  1716. }
  1717.  
  1718. @media (prefers-color-scheme: dark) {
  1719. :root:not([data-theme="light"]) {
  1720. --bg-color: #1f2937;
  1721. --container-bg: #374151;
  1722. --cell-bg: rgba(55, 65, 81, 0.7);
  1723. --title-color: #e5e7eb;
  1724. --text-color: #e5e7eb;
  1725. --button-bg: #4b5563;
  1726. --button-hover-bg: #6b7280;
  1727. --button-text: #f9fafb;
  1728. --overlay-bg: rgba(31, 41, 55, 0.8);
  1729. --tile-2-bg: #9ca3af;
  1730. --tile-2-text: #1f2937;
  1731. --tile-4-bg: #d1d5db;
  1732. --tile-4-text: #1f2937;
  1733. }
  1734. }
  1735.  
  1736. * {
  1737. box-sizing: border-box;
  1738. margin: 0;
  1739. padding: 0;
  1740. font-family: 'Arial', sans-serif;
  1741. }
  1742.  
  1743. body {
  1744. background-color: var(--bg-color);
  1745. color: var(--text-color);
  1746. display: flex;
  1747. flex-direction: column;
  1748. justify-content: center;
  1749. align-items: center;
  1750. min-height: 100vh;
  1751. overflow-y: auto;
  1752. touch-action: none;
  1753. padding: 10px;
  1754. transition: background-color 0.3s ease, color 0.3s ease;
  1755. }
  1756.  
  1757. .container {
  1758. width: min(95vw, 500px);
  1759. padding: 15px;
  1760. border-radius: 10px;
  1761. background-color: var(--container-bg);
  1762. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
  1763. transition: background-color 0.3s ease;
  1764. }
  1765.  
  1766. .header {
  1767. display: flex;
  1768. justify-content: space-between;
  1769. align-items: center;
  1770. margin-bottom: 20px;
  1771. flex-wrap: wrap;
  1772. gap: 10px;
  1773. }
  1774.  
  1775. .title-container {
  1776. display: flex;
  1777. align-items: center;
  1778. gap: 10px;
  1779. }
  1780.  
  1781. .title {
  1782. color: var(--title-color);
  1783. font-size: clamp(2rem, 8vw, 2.5rem);
  1784. font-weight: bold;
  1785. transition: color 0.3s ease;
  1786. }
  1787.  
  1788. .theme-toggle {
  1789. background: none;
  1790. border: none;
  1791. cursor: pointer;
  1792. font-size: 1.5rem;
  1793. color: var(--text-color);
  1794. transition: transform 0.3s ease;
  1795. padding: 5px;
  1796. margin-left: 10px;
  1797. }
  1798.  
  1799. .theme-toggle:hover {
  1800. transform: rotate(30deg);
  1801. }
  1802.  
  1803. .scores {
  1804. display: flex;
  1805. gap: 10px;
  1806. }
  1807.  
  1808. .score-box {
  1809. background-color: var(--button-bg);
  1810. color: var(--button-text);
  1811. padding: 8px 12px;
  1812. border-radius: 5px;
  1813. display: flex;
  1814. flex-direction: column;
  1815. align-items: center;
  1816. min-width: 70px;
  1817. text-align: center;
  1818. transition: background-color 0.3s ease;
  1819. }
  1820.  
  1821. .score-title {
  1822. font-size: 0.7rem;
  1823. text-transform: uppercase;
  1824. margin-bottom: 2px;
  1825. }
  1826.  
  1827. .score-value {
  1828. font-size: clamp(1.2rem, 5vw, 1.5rem);
  1829. font-weight: bold;
  1830. }
  1831.  
  1832. .sub-header {
  1833. display: flex;
  1834. justify-content: space-between;
  1835. align-items: center;
  1836. margin-bottom: 20px;
  1837. flex-wrap: wrap;
  1838. gap: 10px;
  1839. }
  1840.  
  1841. .game-intro {
  1842. color: var(--text-color);
  1843. flex-basis: 60%;
  1844. font-size: 0.9rem;
  1845. line-height: 1.3;
  1846. transition: color 0.3s ease;
  1847. }
  1848.  
  1849. .button-container {
  1850. display: flex;
  1851. gap: 8px;
  1852. flex-wrap: wrap;
  1853. }
  1854.  
  1855. .btn {
  1856. background-color: var(--button-bg);
  1857. color: var(--button-text);
  1858. border: none;
  1859. border-radius: 5px;
  1860. padding: 10px 15px;
  1861. font-weight: bold;
  1862. cursor: pointer;
  1863. transition: background-color 0.2s ease, transform 0.1s ease;
  1864. white-space: nowrap;
  1865. display: flex;
  1866. align-items: center;
  1867. justify-content: center;
  1868. min-width: 40px;
  1869. }
  1870.  
  1871. .btn:hover, .btn:focus {
  1872. background-color: var(--button-hover-bg);
  1873. outline: none;
  1874. }
  1875.  
  1876. .btn:active {
  1877. transform: scale(0.95);
  1878. }
  1879.  
  1880. .btn:disabled {
  1881. opacity: 0.5;
  1882. cursor: not-allowed;
  1883. }
  1884.  
  1885. .btn-icon {
  1886. font-size: 1.2rem;
  1887. line-height: 1;
  1888. }
  1889.  
  1890. .game-container {
  1891. position: relative;
  1892. width: calc(var(--grid-size) * (var(--cell-size) + var(--cell-gap)) + var(--cell-gap));
  1893. height: calc(var(--grid-size) * (var(--cell-size) + var(--cell-gap)) + var(--cell-gap));
  1894. background-color: var(--container-bg);
  1895. border-radius: var(--border-radius);
  1896. padding: var(--cell-gap);
  1897. margin: 0 auto;
  1898. overflow: hidden;
  1899. transition: background-color 0.3s ease;
  1900. }
  1901.  
  1902. .grid-background {
  1903. position: absolute;
  1904. top: var(--cell-gap);
  1905. left: var(--cell-gap);
  1906. bottom: var(--cell-gap);
  1907. right: var(--cell-gap);
  1908. display: grid;
  1909. grid-template-columns: repeat(var(--grid-size), var(--cell-size));
  1910. grid-template-rows: repeat(var(--grid-size), var(--cell-size));
  1911. gap: var(--cell-gap);
  1912. }
  1913.  
  1914. .cell {
  1915. background-color: var(--cell-bg);
  1916. border-radius: var(--border-radius);
  1917. transition: background-color 0.3s ease;
  1918. }
  1919.  
  1920. .tile {
  1921. position: absolute;
  1922. display: flex;
  1923. justify-content: center;
  1924. align-items: center;
  1925. width: var(--cell-size);
  1926. height: var(--cell-size);
  1927. border-radius: var(--border-radius);
  1928. font-size: clamp(1.5rem, 7vw, 2rem);
  1929. font-weight: bold;
  1930. background-color: var(--tile-2-bg);
  1931. color: var(--tile-2-text);
  1932. transition: top var(--animation-speed) ease-in-out,
  1933. left var(--animation-speed) ease-in-out,
  1934. background-color 0.3s ease,
  1935. color 0.3s ease;
  1936. z-index: 10;
  1937. user-select: none;
  1938. will-change: transform, opacity; /* Performance hint */
  1939. }
  1940.  
  1941. .tile.new-tile {
  1942. animation: appear var(--appear-speed) ease-in-out;
  1943. }
  1944.  
  1945. @keyframes appear {
  1946. 0% {
  1947. opacity: 0;
  1948. transform: scale(0);
  1949. }
  1950. 100% {
  1951. opacity: 1;
  1952. transform: scale(1);
  1953. }
  1954. }
  1955.  
  1956. .tile.merged-tile {
  1957. animation: pop var(--pop-speed) ease-in-out;
  1958. z-index: 20;
  1959. }
  1960.  
  1961. @keyframes pop {
  1962. 0% {
  1963. transform: scale(1);
  1964. }
  1965. 50% {
  1966. transform: scale(1.2);
  1967. }
  1968. 100% {
  1969. transform: scale(1);
  1970. }
  1971. }
  1972.  
  1973. /* Tile Colors - Using CSS variables for theme support */
  1974. .tile[data-value="2"] { background-color: var(--tile-2-bg); color: var(--tile-2-text); }
  1975. .tile[data-value="4"] { background-color: var(--tile-4-bg); color: var(--tile-4-text); }
  1976. .tile[data-value="8"] { background-color: var(--tile-8-bg); color: var(--tile-8-text); }
  1977. .tile[data-value="16"] { background-color: var(--tile-16-bg); color: var(--tile-16-text); }
  1978. .tile[data-value="32"] { background-color: var(--tile-32-bg); color: var(--tile-32-text); }
  1979. .tile[data-value="64"] { background-color: var(--tile-64-bg); color: var(--tile-64-text); }
  1980. .tile[data-value="128"] { background-color: var(--tile-128-bg); color: var(--tile-128-text); font-size: clamp(1.3rem, 6vw, 1.8rem); }
  1981. .tile[data-value="256"] { background-color: var(--tile-256-bg); color: var(--tile-256-text); font-size: clamp(1.3rem, 6vw, 1.8rem); }
  1982. .tile[data-value="512"] { background-color: var(--tile-512-bg); color: var(--tile-512-text); font-size: clamp(1.3rem, 6vw, 1.8rem); }
  1983. .tile[data-value="1024"] { background-color: var(--tile-1024-bg); color: var(--tile-1024-text); font-size: clamp(1.1rem, 5vw, 1.5rem); }
  1984. .tile[data-value="2048"] { background-color: var(--tile-2048-bg); color: var(--tile-2048-text); font-size: clamp(1.1rem, 5vw, 1.5rem); }
  1985. .tile[data-value="4096"],
  1986. .tile[data-value="8192"],
  1987. .tile[data-value="16384"],
  1988. .tile[data-value="32768"],
  1989. .tile[data-value="65536"] {
  1990. background-color: var(--tile-super-bg);
  1991. color: var(--tile-super-text);
  1992. font-size: clamp(1rem, 4.5vw, 1.4rem);
  1993. }
  1994.  
  1995. .game-message {
  1996. display: none;
  1997. position: absolute;
  1998. top: 0; right: 0; bottom: 0; left: 0;
  1999. background-color: var(--overlay-bg);
  2000. flex-direction: column;
  2001. align-items: center;
  2002. justify-content: center;
  2003. text-align: center;
  2004. border-radius: var(--border-radius);
  2005. z-index: 100;
  2006. opacity: 0;
  2007. transition: opacity 0.3s ease-in-out, background-color 0.3s ease;
  2008. animation: fadein 0.3s ease-in-out forwards;
  2009. }
  2010.  
  2011. .game-message.active {
  2012. display: flex;
  2013. opacity: 1;
  2014. }
  2015.  
  2016. @keyframes fadein {
  2017. from { opacity: 0; }
  2018. to { opacity: 1; }
  2019. }
  2020.  
  2021. .game-message p {
  2022. font-size: clamp(1.8rem, 7vw, 2rem);
  2023. font-weight: bold;
  2024. color: var(--title-color);
  2025. margin-bottom: 20px;
  2026. transition: color 0.3s ease;
  2027. }
  2028.  
  2029. .game-message .lower {
  2030. display: flex;
  2031. gap: 10px;
  2032. margin-top: 10px;
  2033. }
  2034.  
  2035. .score-addition {
  2036. position: absolute;
  2037. animation: score-pop 0.8s ease-in-out;
  2038. font-weight: bold;
  2039. color: var(--title-color);
  2040. z-index: 100;
  2041. pointer-events: none;
  2042. font-size: 1.2rem;
  2043. opacity: 0;
  2044. }
  2045.  
  2046. @keyframes score-pop {
  2047. 0% {
  2048. opacity: 1;
  2049. transform: translate(-50%, 0);
  2050. }
  2051. 100% {
  2052. opacity: 0;
  2053. transform: translate(-50%, -20px);
  2054. }
  2055. }
  2056.  
  2057. .stats-container {
  2058. margin-top: 10px;
  2059. background-color: var(--container-bg);
  2060. border-radius: 8px;
  2061. padding: 10px 15px;
  2062. display: flex;
  2063. flex-direction: column;
  2064. gap: 5px;
  2065. width: 100%;
  2066. transition: background-color 0.3s ease;
  2067. }
  2068.  
  2069. .stats-title {
  2070. font-weight: bold;
  2071. color: var(--title-color);
  2072. font-size: 1.1rem;
  2073. margin-bottom: 5px;
  2074. display: flex;
  2075. justify-content: space-between;
  2076. align-items: center;
  2077. transition: color 0.3s ease;
  2078. }
  2079.  
  2080. .stats-row {
  2081. display: flex;
  2082. justify-content: space-between;
  2083. font-size: 0.9rem;
  2084. color: var(--text-color);
  2085. transition: color 0.3s ease;
  2086. }
  2087.  
  2088. .stats-value {
  2089. font-weight: bold;
  2090. }
  2091.  
  2092. .stats-toggle {
  2093. font-size: 1rem;
  2094. cursor: pointer;
  2095. background: none;
  2096. border: none;
  2097. color: var(--text-color);
  2098. transition: transform 0.3s ease, color 0.3s ease;
  2099. }
  2100.  
  2101. .stats-toggle:hover {
  2102. transform: rotate(180deg);
  2103. }
  2104.  
  2105. .stats-content {
  2106. overflow: hidden;
  2107. max-height: 0;
  2108. transition: max-height 0.3s ease-in-out;
  2109. }
  2110.  
  2111. .stats-content.expanded {
  2112. max-height: 200px; /* Adjust based on content */
  2113. }
  2114.  
  2115. /* Responsive adjustments */
  2116. @media screen and (max-width: 520px) {
  2117. :root {
  2118. --cell-size: 65px;
  2119. --cell-gap: 8px;
  2120. }
  2121. .game-intro { flex-basis: 100%; text-align: center; margin-bottom: 10px; }
  2122. .btn { padding: 8px 12px; }
  2123. .button-container { justify-content: center; width: 100%; }
  2124. }
  2125.  
  2126. @media screen and (max-width: 380px) {
  2127. :root {
  2128. --cell-size: 55px;
  2129. --cell-gap: 6px;
  2130. }
  2131. .container { padding: 10px; }
  2132. .header { justify-content: center; }
  2133. .title { text-align: center; width: 100%; margin-bottom: 5px;}
  2134. .sub-header { justify-content: center; }
  2135. .score-box { min-width: 60px; padding: 6px 10px; }
  2136. .button-container { flex-wrap: wrap; }
  2137. }
  2138.  
  2139. .footer {
  2140. margin-top: 20px;
  2141. text-align: center;
  2142. color: var(--text-color);
  2143. font-size: 0.8rem;
  2144. transition: color 0.3s ease;
  2145. display: flex;
  2146. flex-direction: column;
  2147. gap: 5px;
  2148. }
  2149.  
  2150. /* Keyboard shortcuts overlay */
  2151. .keyboard-shortcuts {
  2152. position: fixed;
  2153. top: 0;
  2154. left: 0;
  2155. right: 0;
  2156. bottom: 0;
  2157. background-color: var(--overlay-bg);
  2158. z-index: 1000;
  2159. display: none;
  2160. justify-content: center;
  2161. align-items: center;
  2162. transition: background-color 0.3s ease;
  2163. }
  2164.  
  2165. .keyboard-shortcuts.active {
  2166. display: flex;
  2167. }
  2168.  
  2169. .shortcuts-content {
  2170. background-color: var(--bg-color);
  2171. border-radius: 10px;
  2172. padding: 20px;
  2173. max-width: 90%;
  2174. width: 400px;
  2175. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
  2176. position: relative;
  2177. transition: background-color 0.3s ease;
  2178. }
  2179.  
  2180. .shortcuts-title {
  2181. font-size: 1.5rem;
  2182. font-weight: bold;
  2183. margin-bottom: 15px;
  2184. color: var(--title-color);
  2185. transition: color 0.3s ease;
  2186. }
  2187.  
  2188. .shortcut-grid {
  2189. display: grid;
  2190. grid-template-columns: auto 1fr;
  2191. gap: 10px 20px;
  2192. margin-bottom: 20px;
  2193. }
  2194.  
  2195. .key {
  2196. background-color: var(--container-bg);
  2197. color: var(--text-color);
  2198. padding: 5px 10px;
  2199. border-radius: 5px;
  2200. font-family: monospace;
  2201. text-align: center;
  2202. font-weight: bold;
  2203. transition: background-color 0.3s ease, color 0.3s ease;
  2204. }
  2205.  
  2206. .close-shortcuts {
  2207. position: absolute;
  2208. top: 10px;
  2209. right: 10px;
  2210. background: none;
  2211. border: none;
  2212. font-size: 1.5rem;
  2213. cursor: pointer;
  2214. color: var(--text-color);
  2215. transition: transform 0.3s ease, color 0.3s ease;
  2216. }
  2217.  
  2218. .close-shortcuts:hover {
  2219. transform: rotate(90deg);
  2220. }
  2221.  
  2222. /* Animation controls */
  2223. .animation-toggle {
  2224. border: none;
  2225. background: none;
  2226. cursor: pointer;
  2227. font-size: 1.2rem;
  2228. color: var(--text-color);
  2229. transition: color 0.3s ease;
  2230. display: flex;
  2231. align-items: center;
  2232. gap: 5px;
  2233. }
  2234.  
  2235. .animation-toggle:hover {
  2236. color: var(--title-color);
  2237. }
  2238.  
  2239. .animation-status {
  2240. font-size: 0.8rem;
  2241. }
  2242.  
  2243. /* Toast notification */
  2244. .toast {
  2245. position: fixed;
  2246. bottom: 20px;
  2247. left: 50%;
  2248. transform: translateX(-50%);
  2249. background-color: var(--container-bg);
  2250. color: var(--text-color);
  2251. padding: 10px 20px;
  2252. border-radius: 5px;
  2253. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
  2254. z-index: 1000;
  2255. transition: opacity 0.3s ease, transform 0.3s ease, background-color 0.3s ease, color 0.3s ease;
  2256. opacity: 0;
  2257. transform: translate(-50%, 20px);
  2258. pointer-events: none;
  2259. }
  2260.  
  2261. .toast.active {
  2262. opacity: 1;
  2263. transform: translate(-50%, 0);
  2264. }
  2265.  
  2266. /* Accessibility focus styles */
  2267. button:focus-visible, a:focus-visible {
  2268. outline: 3px solid #4c9aff;
  2269. outline-offset: 2px;
  2270. }
  2271.  
  2272. /* Skip link for keyboard users */
  2273. .skip-link {
  2274. position: absolute;
  2275. top: -40px;
  2276. left: 0;
  2277. background: var(--button-bg);
  2278. color: var(--button-text);
  2279. padding: 8px;
  2280. z-index: 100;
  2281. transition: top 0.3s ease;
  2282. }
  2283.  
  2284. .skip-link:focus {
  2285. top: 0;
  2286. }
  2287. </style>
  2288. </head>
  2289. <body>
  2290. <a href="#game-container" class="skip-link">Skip to game</a>
  2291. <div class="container">
  2292. <header class="header">
  2293. <div class="title-container">
  2294. <h1 class="title">2048</h1>
  2295. <button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode">🌓</button>
  2296. </div>
  2297. <div class="scores">
  2298. <div class="score-box">
  2299. <div class="score-title">Score</div>
  2300. <div class="score-value" id="score" aria-live="polite">0</div>
  2301. </div>
  2302. <div class="score-box">
  2303. <div class="score-title">Best</div>
  2304. <div class="score-value" id="best-score">0</div>
  2305. </div>
  2306. </div>
  2307. </header>
  2308. <div class="sub-header">
  2309. <p class="game-intro">Join tiles, get to <strong>2048!</strong> Use <strong>arrow keys</strong> or <strong>swipe</strong>.</p>
  2310. <div class="button-container">
  2311. <button class="btn" id="restart-button" aria-label="New Game">New</button>
  2312. <button class="btn" id="undo-button" aria-label="Undo Move" disabled>↩️</button>
  2313. <button class="btn" id="help-button" aria-label="Show Keyboard Shortcuts">?</button>
  2314. </div>
  2315. </div>
  2316.  
  2317. <div class="game-container" id="game-container" tabindex="0">
  2318. <div class="grid-background" id="grid-background">
  2319. <!-- Background cells are generated here by JS -->
  2320. </div>
  2321. <div class="tile-container" id="tile-container">
  2322. <!-- Tiles are added here by JS -->
  2323. </div>
  2324. <div class="game-message" id="game-message" aria-live="assertive">
  2325. <p id="message-text"></p>
  2326. <div class="lower">
  2327. <button class="btn" id="retry-button">Try again</button>
  2328. <button class="btn" id="keep-playing-button" style="display: none;">Keep Playing</button>
  2329. </div>
  2330. </div>
  2331. </div>
  2332.  
  2333. <div class="stats-container" id="stats-container">
  2334. <div class="stats-title">
  2335. Game Statistics
  2336. <button class="stats-toggle" id="stats-toggle" aria-label="Toggle statistics visibility">▼</button>
  2337. </div>
  2338. <div class="stats-content" id="stats-content">
  2339. <div class="stats-row">
  2340. <span>Games Played:</span>
  2341. <span class="stats-value" id="games-played">0</span>
  2342. </div>
  2343. <div class="stats-row">
  2344. <span>Highest Tile:</span>
  2345. <span class="stats-value" id="highest-tile">0</span>
  2346. </div>
  2347. <div class="stats-row">
  2348. <span>Total Moves:</span>
  2349. <span class="stats-value" id="total-moves">0</span>
  2350. </div>
  2351. <div class="stats-row">
  2352. <span>Total Score:</span>
  2353. <span class="stats-value" id="total-score">0</span>
  2354. </div>
  2355. <div class="stats-row">
  2356. <span>Avg Score/Game:</span>
  2357. <span class="stats-value" id="avg-score">0</span>
  2358. </div>
  2359. </div>
  2360. </div>
  2361.  
  2362. <footer class="footer">
  2363. <div class="animation-controls">
  2364. <button class="animation-toggle" id="animation-toggle">
  2365. <span class="btn-icon">✨</span>
  2366. <span class="animation-status" id="animation-status">Animations: On</span>
  2367. </button>
  2368. </div>
  2369. <p>Advanced Edition • <a href="#" id="keyboard-link">Keyboard shortcuts</a></p>
  2370. </footer>
  2371. </div>
  2372.  
  2373. <div class="keyboard-shortcuts" id="keyboard-shortcuts">
  2374. <div class="shortcuts-content">
  2375. <button class="close-shortcuts" id="close-shortcuts" aria-label="Close keyboard shortcuts">×</button>
  2376. <h2 class="shortcuts-title">Keyboard Shortcuts</h2>
  2377. <div class="shortcut-grid">
  2378. <div class="key">↑</div><div>Move Up</div>
  2379. <div class="key">↓</div><div>Move Down</div>
  2380. <div class="key">←</div><div>Move Left</div>
  2381. <div class="key">→</div><div>Move Right</div>
  2382. <div class="key">W</div><div>Move Up</div>
  2383. <div class="key">S</div><div>Move Down</div>
  2384. <div class="key">A</div><div>Move Left</div>
  2385. <div class="key">D</div><div>Move Right</div>
  2386. <div class="key">R</div><div>Restart Game</div>
  2387. <div class="key">U</div><div>Undo Move</div>
  2388. <div class="key">Z</div><div>Undo Move</div>
  2389. <div class="key">M</div><div>Toggle Dark Mode</div>
  2390. <div class="key">Esc</div><div>Close Dialogs</div>
  2391. <div class="key">?</div><div>Show This Help</div>
  2392. </div>
  2393. </div>
  2394. </div>
  2395.  
  2396. <div class="toast" id="toast"></div>
  2397.  
  2398. <script>
  2399. document.addEventListener('DOMContentLoaded', () => {
  2400. const GRID_SIZE = 4;
  2401. const WINNING_VALUE = 2048;
  2402. const MOVE_ANIMATION_DURATION = 150; // ms, matches CSS --animation-speed
  2403. const APPEAR_ANIMATION_DURATION = 200; // ms, matches CSS --appear-speed
  2404. const MAX_UNDO_STEPS = 20; // Maximum number of moves to store in history
  2405.  
  2406. // --- DOM Elements ---
  2407. const gameContainer = document.getElementById('game-container');
  2408. const gridBackground = document.getElementById('grid-background');
  2409. const tileContainer = document.getElementById('tile-container');
  2410. const scoreElement = document.getElementById('score');
  2411. const bestScoreElement = document.getElementById('best-score');
  2412. const messageContainer = document.getElementById('game-message');
  2413. const messageText = document.getElementById('message-text');
  2414. const restartButton = document.getElementById('restart-button');
  2415. const retryButton = document.getElementById('retry-button');
  2416. const keepPlayingButton = document.getElementById('keep-playing-button');
  2417. const undoButton = document.getElementById('undo-button');
  2418. const themeToggle = document.getElementById('theme-toggle');
  2419. const helpButton = document.getElementById('help-button');
  2420. const keyboardShortcuts = document.getElementById('keyboard-shortcuts');
  2421. const closeShortcuts = document.getElementById('close-shortcuts');
  2422. const keyboardLink = document.getElementById('keyboard-link');
  2423. const toast = document.getElementById('toast');
  2424. const statsToggle = document.getElementById('stats-toggle');
  2425. const statsContent = document.getElementById('stats-content');
  2426. const gamesPlayedElement = document.getElementById('games-played');
  2427. const highestTileElement = document.getElementById('highest-tile');
  2428. const totalMovesElement = document.getElementById('total-moves');
  2429. const totalScoreElement = document.getElementById('total-score');
  2430. const avgScoreElement = document.getElementById('avg-score');
  2431. const animationToggle = document.getElementById('animation-toggle');
  2432. const animationStatus = document.getElementById('animation-status');
  2433.  
  2434. // --- Game State ---
  2435. let grid; // Will be a 2D array representing cells (Tile objects or null)
  2436. let score = 0;
  2437. let bestScore = parseInt(localStorage.getItem('bestScore2048') || 0);
  2438. let isGameOver = false;
  2439. let hasWon = false; // Tracks if 2048 has been reached
  2440. let keepPlayingMode = false; // Tracks if user chose to continue after winning
  2441. let isMoving = false; // Prevent input during animations
  2442. let gameHistory = []; // Array to store game states for undo
  2443. let animationsEnabled = localStorage.getItem('animationsEnabled') !== 'false'; // Default to true
  2444. let darkMode = localStorage.getItem('darkMode') === 'true' ||
  2445. (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && localStorage.getItem('darkMode') !== 'false');
  2446.  
  2447. // --- Statistics ---
  2448. let stats = {
  2449. gamesPlayed: 0,
  2450. highestTile: 0,
  2451. totalMoves: 0,
  2452. totalScore: 0
  2453. };
  2454.  
  2455. // --- Touch Input State ---
  2456. let touchStartX = 0;
  2457. let touchStartY = 0;
  2458. let touchEndX = 0;
  2459. let touchEndY = 0;
  2460.  
  2461. // --- Tile Class ---
  2462. class Tile {
  2463. constructor(value, row, col) {
  2464. this.value = value;
  2465. this.row = row;
  2466. this.col = col;
  2467. this.element = this.createElement();
  2468. this.updatePosition(false); // Initial position without animation
  2469. this.mergedFrom = null; // Track merge source for animation coordination
  2470. tileContainer.appendChild(this.element);
  2471. }
  2472.  
  2473. createElement() {
  2474. const tileElement = document.createElement('div');
  2475. tileElement.classList.add('tile');
  2476. this.updateValue(this.value); // Set initial text and data-value
  2477. // Add new tile animation class if animations are enabled
  2478. if (animationsEnabled) {
  2479. tileElement.classList.add('new-tile');
  2480. setTimeout(() => {
  2481. tileElement.classList.remove('new-tile');
  2482. }, APPEAR_ANIMATION_DURATION);
  2483. }
  2484. return tileElement;
  2485. }
  2486.  
  2487. updateValue(newValue) {
  2488. this.value = newValue;
  2489. this.element.textContent = newValue;
  2490. this.element.dataset.value = newValue; // For CSS styling
  2491.  
  2492. // Update statistics if needed
  2493. if (newValue > stats.highestTile) {
  2494. stats.highestTile = newValue;
  2495. saveStats();
  2496. updateStatsDisplay();
  2497. }
  2498. }
  2499.  
  2500. updatePosition(animate = true) {
  2501. const { top, left } = this.calculatePixelPosition();
  2502. // Only enable transitions if animations are enabled
  2503. this.element.style.transition = (animate && animationsEnabled)
  2504. ? `top ${MOVE_ANIMATION_DURATION}ms ease-in-out, left ${MOVE_ANIMATION_DURATION}ms ease-in-out`
  2505. : 'none';
  2506. this.element.style.top = `${top}px`;
  2507. this.element.style.left = `${left}px`;
  2508. }
  2509.  
  2510. calculatePixelPosition() {
  2511. // Get these values dynamically in case they change (e.g., responsive)
  2512. const cellSize = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--cell-size'));
  2513. const cellGap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--cell-gap'));
  2514. const top = this.row * (cellSize + cellGap) + cellGap;
  2515. const left = this.col * (cellSize + cellGap) + cellGap;
  2516. return { top, left };
  2517. }
  2518.  
  2519. remove(merged = false) {
  2520. if (merged && animationsEnabled) {
  2521. // If merged and animations enabled, let the pop animation finish before removing
  2522. this.element.classList.add('merged-tile');
  2523. this.element.addEventListener('animationend', () => {
  2524. if (this.element.parentNode) {
  2525. tileContainer.removeChild(this.element);
  2526. }
  2527. }, { once: true });
  2528. } else {
  2529. // Otherwise, remove immediately
  2530. if (this.element.parentNode) {
  2531. tileContainer.removeChild(this.element);
  2532. }
  2533. }
  2534. }
  2535.  
  2536. playMergeAnimation() {
  2537. if (animationsEnabled) {
  2538. this.element.classList.add('merged-tile');
  2539. // Remove class after animation to allow re-triggering
  2540. this.element.addEventListener('animationend', () => {
  2541. this.element.classList.remove('merged-tile');
  2542. }, { once: true });
  2543. }
  2544. }
  2545.  
  2546. // Clone for history/undo
  2547. clone() {
  2548. return {
  2549. value: this.value,
  2550. row: this.row,
  2551. col: this.col
  2552. };
  2553. }
  2554. }
  2555.  
  2556. // --- Grid Initialization ---
  2557. function createGridCells() {
  2558. gridBackground.innerHTML = ''; // Clear previous cells
  2559. for (let i = 0; i < GRID_SIZE * GRID_SIZE; i++) {
  2560. const cell = document.createElement('div');
  2561. cell.classList.add('cell');
  2562. gridBackground.appendChild(cell);
  2563. }
  2564. }
  2565.  
  2566. function initializeGrid() {
  2567. grid = Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(null));
  2568. }
  2569.  
  2570. // --- Game Setup ---
  2571. function setupGame() {
  2572. isMoving = true; // Prevent input during setup
  2573. initializeGrid();
  2574. tileContainer.innerHTML = ''; // Clear existing tiles
  2575. updateScore(0);
  2576. bestScoreElement.textContent = bestScore;
  2577. isGameOver = false;
  2578. hasWon = false;
  2579. keepPlayingMode = false;
  2580. hideMessage();
  2581. gameHistory = []; // Clear history
  2582. updateUndoButton();
  2583.  
  2584. // Increment games played on new game (but not on first load)
  2585. if (tileContainer.childElementCount > 0) {
  2586. stats.gamesPlayed++;
  2587. saveStats();
  2588. updateStatsDisplay();
  2589. }
  2590.  
  2591. addRandomTile();
  2592. addRandomTile();
  2593.  
  2594. // Save initial state to history
  2595. saveGameState();
  2596.  
  2597. isMoving = false; // Allow input
  2598. }
  2599.  
  2600. // --- Score Management ---
  2601. function updateScore(newScore, addition = 0) {
  2602. const oldScore = score;
  2603. score = newScore;
  2604. scoreElement.textContent = score;
  2605.  
  2606. // Show score addition animation
  2607. if (addition > 0 && animationsEnabled) {
  2608. const scoreAdditionElement = document.createElement('div');
  2609. scoreAdditionElement.classList.add('score-addition');
  2610. scoreAdditionElement.textContent = `+${addition}`;
  2611.  
  2612. // Position near the score display
  2613. const scoreRect = scoreElement.getBoundingClientRect();
  2614. scoreAdditionElement.style.top = `${scoreRect.top - 10}px`;
  2615. scoreAdditionElement.style.left = `${scoreRect.left + scoreRect.width / 2}px`;
  2616.  
  2617. document.body.appendChild(scoreAdditionElement);
  2618.  
  2619. // Remove after animation completes
  2620. setTimeout(() => {
  2621. if (scoreAdditionElement.parentNode) {
  2622. document.body.removeChild(scoreAdditionElement);
  2623. }
  2624. }, 800);
  2625. }
  2626.  
  2627. // Update best score if needed
  2628. if (score > bestScore) {
  2629. bestScore = score;
  2630. bestScoreElement.textContent = bestScore;
  2631. localStorage.setItem('bestScore2048', bestScore);
  2632. }
  2633.  
  2634. // Update total score in stats
  2635. stats.totalScore += (newScore - oldScore);
  2636. saveStats();
  2637. updateStatsDisplay();
  2638. }
  2639.  
  2640. // --- Tile Management ---
  2641. function getEmptyCells() {
  2642. const emptyCells = [];
  2643. for (let r = 0; r < GRID_SIZE; r++) {
  2644. for (let c = 0; c < GRID_SIZE; c++) {
  2645. if (grid[r][c] === null) {
  2646. emptyCells.push({ r, c });
  2647. }
  2648. }
  2649. }
  2650. return emptyCells;
  2651. }
  2652.  
  2653. function addRandomTile() {
  2654. const emptyCells = getEmptyCells();
  2655. if (emptyCells.length === 0) return; // No space left
  2656.  
  2657. const { r, c } = emptyCells[Math.floor(Math.random() * emptyCells.length)];
  2658. const value = Math.random() < 0.9 ? 2 : 4;
  2659. const newTile = new Tile(value, r, c);
  2660. grid[r][c] = newTile;
  2661. }
  2662.  
  2663. // --- Game State Management for Undo ---
  2664. function saveGameState() {
  2665. // Create a deep copy of the current grid state
  2666. const gridCopy = Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(null));
  2667.  
  2668. for (let r = 0; r < GRID_SIZE; r++) {
  2669. for (let c = 0; c < GRID_SIZE; c++) {
  2670. if (grid[r][c] !== null) {
  2671. gridCopy[r][c] = grid[r][c].clone();
  2672. }
  2673. }
  2674. }
  2675.  
  2676. // Add to history, limiting size
  2677. gameHistory.push({
  2678. grid: gridCopy,
  2679. score: score
  2680. });
  2681.  
  2682. // Keep history at a reasonable size
  2683. if (gameHistory.length > MAX_UNDO_STEPS) {
  2684. gameHistory.shift();
  2685. }
  2686.  
  2687. // Enable/disable undo button
  2688. updateUndoButton();
  2689. }
  2690.  
  2691. function updateUndoButton() {
  2692. undoButton.disabled = gameHistory.length <= 1;
  2693. }
  2694.  
  2695. function undoMove() {
  2696. if (isMoving || gameHistory.length <= 1) return;
  2697.  
  2698. isMoving = true;
  2699.  
  2700. // Remove the current state
  2701. gameHistory.pop();
  2702.  
  2703. // Get the previous state
  2704. const previousState = gameHistory[gameHistory.length - 1];
  2705.  
  2706. // Clear current tiles
  2707. tileContainer.innerHTML = '';
  2708.  
  2709. // Restore grid and create new tile elements
  2710. initializeGrid();
  2711.  
  2712. for (let r = 0; r < GRID_SIZE; r++) {
  2713. for (let c = 0; c < GRID_SIZE; c++) {
  2714. if (previousState.grid[r][c] !== null) {
  2715. const { value, row, col } = previousState.grid[r][c];
  2716. grid[r][c] = new Tile(value, row, col);
  2717. }
  2718. }
  2719. }
  2720.  
  2721. // Restore score
  2722. updateScore(previousState.score);
  2723.  
  2724. // Update undo button state
  2725. updateUndoButton();
  2726.  
  2727. // Re-enable input
  2728. isMoving = false;
  2729.  
  2730. // Increment total moves in stats
  2731. stats.totalMoves++;
  2732. saveStats();
  2733. updateStatsDisplay();
  2734.  
  2735. // Show toast notification
  2736. showToast("Move undone");
  2737. }
  2738.  
  2739. // --- Movement Logic ---
  2740. function move(direction) {
  2741. if (isMoving || isGameOver) return;
  2742.  
  2743. isMoving = true;
  2744. let moved = false;
  2745. let currentScore = score; // Track score changes from this move
  2746. let scoreAddition = 0; // Track score added in this move
  2747.  
  2748. // Prepare for move: reset merge tracking
  2749. for (let r = 0; r < GRID_SIZE; r++) {
  2750. for (let c = 0; c < GRID_SIZE; c++) {
  2751. if (grid[r][c]) grid[r][c].mergedFrom = null;
  2752. }
  2753. }
  2754.  
  2755. // Define traversal order based on direction
  2756. const traversals = getTraversals(direction);
  2757.  
  2758. traversals.rows.forEach(r => {
  2759. traversals.cols.forEach(c => {
  2760. const currentTile = grid[r][c];
  2761. if (currentTile) {
  2762. const { furthestRow, furthestCol, nextTile } = findFurthestPosition(r, c, direction);
  2763.  
  2764. if (nextTile && nextTile.value === currentTile.value && !nextTile.mergedFrom) {
  2765. // --- Merge ---
  2766. const mergedValue = currentTile.value * 2;
  2767. nextTile.updateValue(mergedValue);
  2768. nextTile.mergedFrom = currentTile; // Mark as merged
  2769. nextTile.playMergeAnimation(); // Play pop animation on target
  2770.  
  2771. grid[r][c] = null; // Remove current tile from grid
  2772. currentTile.remove(true); // Remove current tile element (with animation flag)
  2773.  
  2774. scoreAddition += mergedValue;
  2775. currentScore += mergedValue;
  2776. moved = true;
  2777.  
  2778. // Check for win condition immediately after merge
  2779. if (!hasWon && mergedValue === WINNING_VALUE) {
  2780. hasWon = true;
  2781. }
  2782.  
  2783. } else if (furthestRow !== r || furthestCol !== c) {
  2784. // --- Move ---
  2785. grid[furthestRow][furthestCol] = currentTile;
  2786. grid[r][c] = null;
  2787. currentTile.row = furthestRow;
  2788. currentTile.col = furthestCol;
  2789. currentTile.updatePosition(); // Animate move
  2790. moved = true;
  2791. }
  2792. }
  2793. });
  2794. });
  2795.  
  2796. if (moved) {
  2797. // Save game state before adding the new tile
  2798. saveGameState();
  2799.  
  2800. // Update score display with animation
  2801. updateScore(currentScore, scoreAddition);
  2802.  
  2803. // Increment total moves in stats
  2804. stats.totalMoves++;
  2805. saveStats();
  2806. updateStatsDisplay();
  2807.  
  2808. // Use setTimeout to allow move/merge animations to mostly complete
  2809. const delay = animationsEnabled ? MOVE_ANIMATION_DURATION + 50 : 0;
  2810. setTimeout(() => {
  2811. addRandomTile();
  2812. if (!canMove()) {
  2813. checkGameOver();
  2814. } else if (hasWon && !keepPlayingMode) {
  2815. showWinMessage();
  2816. }
  2817. isMoving = false; // Re-enable input
  2818. }, delay);
  2819. } else {
  2820. isMoving = false; // No move occurred, re-enable input immediately
  2821. }
  2822.  
  2823. return moved;
  2824. }
  2825.  
  2826. function getTraversals(direction) {
  2827. const rows = Array.from({ length: GRID_SIZE }, (_, i) => i);
  2828. const cols = Array.from({ length: GRID_SIZE }, (_, i) => i);
  2829.  
  2830. if (direction === 'right') cols.reverse();
  2831. if (direction === 'down') rows.reverse();
  2832.  
  2833. return { rows, cols };
  2834. }
  2835.  
  2836. function findFurthestPosition(r, c, direction) {
  2837. let currentRow = r;
  2838. let currentCol = c;
  2839. let nextRow, nextCol;
  2840.  
  2841. do {
  2842. currentRow = nextRow !== undefined ? nextRow : r;
  2843. currentCol = nextCol !== undefined ? nextCol : c;
  2844. const { nr, nc } = getNextPosition(currentRow, currentCol, direction);
  2845. nextRow = nr;
  2846. nextCol = nc;
  2847. } while (isWithinBounds(nextRow, nextCol) && grid[nextRow][nextCol] === null);
  2848.  
  2849. // The loop went one step too far or hit a tile
  2850. const furthestRow = currentRow;
  2851. const furthestCol = currentCol;
  2852. const nextTile = isWithinBounds(nextRow, nextCol) ? grid[nextRow][nextCol] : null;
  2853.  
  2854. return { furthestRow, furthestCol, nextTile };
  2855. }
  2856.  
  2857. function getNextPosition(r, c, direction) {
  2858. switch (direction) {
  2859. case 'up': return { nr: r - 1, nc: c };
  2860. case 'down': return { nr: r + 1, nc: c };
  2861. case 'left': return { nr: r, nc: c - 1 };
  2862. case 'right': return { nr: r, nc: c + 1 };
  2863. default: return { nr: r, nc: c };
  2864. }
  2865. }
  2866.  
  2867. function isWithinBounds(r, c) {
  2868. return r >= 0 && r < GRID_SIZE && c >= 0 && c < GRID_SIZE;
  2869. }
  2870.  
  2871. // --- Game Status Checks ---
  2872. function canMove() {
  2873. if (getEmptyCells().length > 0) return true; // Can always move if empty cells exist
  2874.  
  2875. // Check for possible merges horizontally and vertically
  2876. for (let r = 0; r < GRID_SIZE; r++) {
  2877. for (let c = 0; c < GRID_SIZE; c++) {
  2878. const tile = grid[r][c];
  2879. if (!tile) continue; // Should not happen if no empty cells, but safe check
  2880.  
  2881. // Check right
  2882. if (c < GRID_SIZE - 1 && grid[r][c + 1] && grid[r][c + 1].value === tile.value) {
  2883. return true;
  2884. }
  2885. // Check down
  2886. if (r < GRID_SIZE - 1 && grid[r + 1][c] && grid[r + 1][c].value === tile.value) {
  2887. return true;
  2888. }
  2889. }
  2890. }
  2891. return false; // No empty cells and no possible merges
  2892. }
  2893.  
  2894. function checkGameOver() {
  2895. if (!isGameOver && !canMove()) {
  2896. isGameOver = true;
  2897. showGameOverMessage();
  2898. }
  2899. }
  2900.  
  2901. // --- Message Display ---
  2902. function showMessage(text, type) {
  2903. messageText.textContent = text;
  2904. messageContainer.className = 'game-message active'; // Reset classes and add active
  2905. keepPlayingButton.style.display = 'none'; // Hide by default
  2906.  
  2907. if (type === 'win') {
  2908. messageContainer.classList.add('game-won');
  2909. if (!keepPlayingMode) {
  2910. keepPlayingButton.style.display = 'inline-block'; // Show keep playing
  2911. }
  2912. } else if (type === 'over') {
  2913. messageContainer.classList.add('game-over');
  2914. }
  2915. }
  2916.  
  2917. function showWinMessage() {
  2918. showMessage('You Win!', 'win');
  2919. }
  2920.  
  2921. function showGameOverMessage() {
  2922. showMessage('Game Over!', 'over');
  2923. }
  2924.  
  2925. function hideMessage() {
  2926. messageContainer.classList.remove('active', 'game-won', 'game-over');
  2927. }
  2928.  
  2929. // --- Toast Notification ---
  2930. function showToast(message, duration = 2000) {
  2931. toast.textContent = message;
  2932. toast.classList.add('active');
  2933.  
  2934. // Clear any existing timeout
  2935. if (toast.timeoutId) {
  2936. clearTimeout(toast.timeoutId);
  2937. }
  2938.  
  2939. // Hide after duration
  2940. toast.timeoutId = setTimeout(() => {
  2941. toast.classList.remove('active');
  2942. }, duration);
  2943. }
  2944.  
  2945. // --- Theme Management ---
  2946. function toggleTheme() {
  2947. darkMode = !darkMode;
  2948. updateTheme();
  2949. localStorage.setItem('darkMode', darkMode);
  2950. showToast(darkMode ? 'Dark mode enabled' : 'Light mode enabled');
  2951. }
  2952.  
  2953. function updateTheme() {
  2954. document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light');
  2955. }
  2956.  
  2957. // --- Animation Management ---
  2958. function toggleAnimations() {
  2959. animationsEnabled = !animationsEnabled;
  2960. updateAnimationStatus();
  2961. localStorage.setItem('animationsEnabled', animationsEnabled);
  2962. showToast(animationsEnabled ? 'Animations enabled' : 'Animations disabled');
  2963. }
  2964.  
  2965. function updateAnimationStatus() {
  2966. animationStatus.textContent = `Animations: ${animationsEnabled ? 'On' : 'Off'}`;
  2967. }
  2968.  
  2969. // --- Statistics Management ---
  2970. function loadStats() {
  2971. const savedStats = localStorage.getItem('gameStats2048');
  2972. if (savedStats) {
  2973. try {
  2974. stats = JSON.parse(savedStats);
  2975. } catch (e) {
  2976. console.error('Error parsing saved statistics:', e);
  2977. // Use default stats if parse error
  2978. }
  2979. }
  2980. }
  2981.  
  2982. function saveStats() {
  2983. localStorage.setItem('gameStats2048', JSON.stringify(stats));
  2984. }
  2985.  
  2986. function updateStatsDisplay() {
  2987. gamesPlayedElement.textContent = stats.gamesPlayed;
  2988. highestTileElement.textContent = stats.highestTile;
  2989. totalMovesElement.textContent = stats.totalMoves;
  2990. totalScoreElement.textContent = stats.totalScore;
  2991.  
  2992. // Calculate average score per game
  2993. const avgScore = stats.gamesPlayed > 0
  2994. ? Math.round(stats.totalScore / stats.gamesPlayed)
  2995. : 0;
  2996. avgScoreElement.textContent = avgScore;
  2997. }
  2998.  
  2999. function toggleStats() {
  3000. statsContent.classList.toggle('expanded');
  3001. statsToggle.textContent = statsContent.classList.contains('expanded') ? '▲' : '▼';
  3002. }
  3003.  
  3004. // --- Keyboard Shortcuts Help ---
  3005. function showKeyboardShortcuts() {
  3006. keyboardShortcuts.classList.add('active');
  3007. }
  3008.  
  3009. function hideKeyboardShortcuts() {
  3010. keyboardShortcuts.classList.remove('active');
  3011. }
  3012.  
  3013. // --- Event Listeners ---
  3014. function handleKeyDown(e) {
  3015. if (isMoving) return;
  3016.  
  3017. // Close keyboard shortcuts dialog with Escape
  3018. if (e.key === 'Escape' && keyboardShortcuts.classList.contains('active')) {
  3019. hideKeyboardShortcuts();
  3020. e.preventDefault();
  3021. return;
  3022. }
  3023.  
  3024. // Game is over, only allow restart or undo
  3025. if (isGameOver) {
  3026. if (e.key === 'r' || e.key === 'R') {
  3027. setupGame();
  3028. e.preventDefault();
  3029. } else if ((e.key === 'u' || e.key === 'U' || e.key === 'z' || e.key === 'Z') && !undoButton.disabled) {
  3030. undoMove();
  3031. e.preventDefault();
  3032. }
  3033. return;
  3034. }
  3035.  
  3036. switch (e.key) {
  3037. case 'ArrowUp':
  3038. case 'w':
  3039. case 'W':
  3040. e.preventDefault(); // Prevent page scrolling
  3041. move('up');
  3042. break;
  3043. case 'ArrowDown':
  3044. case 's':
  3045. case 'S':
  3046. e.preventDefault();
  3047. move('down');
  3048. break;
  3049. case 'ArrowLeft':
  3050. case 'a':
  3051. case 'A':
  3052. e.preventDefault();
  3053. move('left');
  3054. break;
  3055. case 'ArrowRight':
  3056. case 'd':
  3057. case 'D':
  3058. e.preventDefault();
  3059. move('right');
  3060. break;
  3061. case 'r':
  3062. case 'R':
  3063. e.preventDefault();
  3064. setupGame();
  3065. break;
  3066. case 'u':
  3067. case 'U':
  3068. case 'z':
  3069. case 'Z':
  3070. e.preventDefault();
  3071. if (!undoButton.disabled) {
  3072. undoMove();
  3073. }
  3074. break;
  3075. case 'm':
  3076. case 'M':
  3077. e.preventDefault();
  3078. toggleTheme();
  3079. break;
  3080. case '?':
  3081. e.preventDefault();
  3082. showKeyboardShortcuts();
  3083. break;
  3084. }
  3085. }
  3086.  
  3087. function handleTouchStart(e) {
  3088. if (isMoving || e.touches.length > 1) return; // Ignore multi-touch
  3089. touchStartX = e.touches[0].clientX;
  3090. touchStartY = e.touches[0].clientY;
  3091. touchEndX = touchStartX; // Reset end points
  3092. touchEndY = touchStartY;
  3093. }
  3094.  
  3095. function handleTouchMove(e) {
  3096. if (isMoving || e.touches.length > 1) return;
  3097. // Update end points for continuous tracking if needed, but mainly for the final calculation
  3098. touchEndX = e.touches[0].clientX;
  3099. touchEndY = e.touches[0].clientY;
  3100. e.preventDefault(); // Prevent scrolling *during* swipe detection
  3101. }
  3102.  
  3103. function handleTouchEnd(e) {
  3104. if (isMoving || e.changedTouches.length > 1) return;
  3105.  
  3106. const deltaX = touchEndX - touchStartX;
  3107. const deltaY = touchEndY - touchStartY;
  3108. const absDeltaX = Math.abs(deltaX);
  3109. const absDeltaY = Math.abs(deltaY);
  3110. const swipeThreshold = 30; // Minimum distance for a swipe
  3111.  
  3112. if (Math.max(absDeltaX, absDeltaY) < swipeThreshold) return; // Not a swipe
  3113.  
  3114. let direction;
  3115. if (absDeltaX > absDeltaY) {
  3116. // Horizontal swipe
  3117. direction = deltaX > 0 ? 'right' : 'left';
  3118. } else {
  3119. // Vertical swipe
  3120. direction = deltaY > 0 ? 'down' : 'up';
  3121. }
  3122. move(direction);
  3123. }
  3124.  
  3125. // --- Game Container Focus Management ---
  3126. function handleGameContainerFocus() {
  3127. // Add a visual indicator or enhance accessibility when game container is focused
  3128. gameContainer.classList.add('focused');
  3129. }
  3130.  
  3131. function handleGameContainerBlur() {
  3132. gameContainer.classList.remove('focused');
  3133. }
  3134.  
  3135. // --- Save/Load Game State ---
  3136. function saveGame() {
  3137. // Create a saveable representation of the current game state
  3138. const saveData = {
  3139. grid: Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(null)),
  3140. score: score,
  3141. bestScore: bestScore,
  3142. hasWon: hasWon,
  3143. keepPlayingMode: keepPlayingMode
  3144. };
  3145.  
  3146. // Save tile values and positions
  3147. for (let r = 0; r < GRID_SIZE; r++) {
  3148. for (let c = 0; c < GRID_SIZE; c++) {
  3149. if (grid[r][c] !== null) {
  3150. saveData.grid[r][c] = grid[r][c].clone();
  3151. }
  3152. }
  3153. }
  3154.  
  3155. localStorage.setItem('savedGame2048', JSON.stringify(saveData));
  3156. }
  3157.  
  3158. function loadGame() {
  3159. const savedGame = localStorage.getItem('savedGame2048');
  3160. if (!savedGame) return false;
  3161.  
  3162. try {
  3163. const saveData = JSON.parse(savedGame);
  3164.  
  3165. // Reset game state
  3166. initializeGrid();
  3167. tileContainer.innerHTML = '';
  3168.  
  3169. // Restore score and game state
  3170. score = saveData.score;
  3171. scoreElement.textContent = score;
  3172. bestScore = saveData.bestScore;
  3173. bestScoreElement.textContent = bestScore;
  3174. hasWon = saveData.hasWon;
  3175. keepPlayingMode = saveData.keepPlayingMode;
  3176.  
  3177. // Restore tiles
  3178. for (let r = 0; r < GRID_SIZE; r++) {
  3179. for (let c = 0; c < GRID_SIZE; c++) {
  3180. if (saveData.grid[r][c] !== null) {
  3181. const { value, row, col } = saveData.grid[r][c];
  3182. grid[r][c] = new Tile(value, row, col);
  3183. }
  3184. }
  3185. }
  3186.  
  3187. // Save initial state to history
  3188. saveGameState();
  3189.  
  3190. // Check game status
  3191. if (!canMove()) {
  3192. isGameOver = true;
  3193. showGameOverMessage();
  3194. } else if (hasWon && !keepPlayingMode) {
  3195. showWinMessage();
  3196. }
  3197.  
  3198. return true;
  3199. } catch (e) {
  3200. console.error('Error loading saved game:', e);
  3201. return false;
  3202. }
  3203. }
  3204.  
  3205. // --- Window Events ---
  3206. function handleVisibilityChange() {
  3207. if (document.visibilityState === 'hidden') {
  3208. // Save game when tab is hidden/inactive
  3209. saveGame();
  3210. }
  3211. }
  3212.  
  3213. function handleBeforeUnload() {
  3214. // Save game when page is closed/refreshed
  3215. saveGame();
  3216. }
  3217.  
  3218. // --- Initialize ---
  3219. function initialize() {
  3220. // Set up theme
  3221. updateTheme();
  3222.  
  3223. // Set up animation status
  3224. updateAnimationStatus();
  3225.  
  3226. // Create grid cells
  3227. createGridCells();
  3228.  
  3229. // Load statistics
  3230. loadStats();
  3231. updateStatsDisplay();
  3232.  
  3233. // Try to load saved game, or start a new one
  3234. const gameLoaded = loadGame();
  3235. if (!gameLoaded) {
  3236. setupGame();
  3237. }
  3238.  
  3239. // Add event listeners
  3240. document.addEventListener('keydown', handleKeyDown);
  3241.  
  3242. // Game container event listeners
  3243. gameContainer.addEventListener('touchstart', handleTouchStart, { passive: true });
  3244. gameContainer.addEventListener('touchmove', handleTouchMove, { passive: false });
  3245. gameContainer.addEventListener('touchend', handleTouchEnd, { passive: true });
  3246. gameContainer.addEventListener('focus', handleGameContainerFocus);
  3247. gameContainer.addEventListener('blur', handleGameContainerBlur);
  3248.  
  3249. // Button event listeners
  3250. restartButton.addEventListener('click', setupGame);
  3251. retryButton.addEventListener('click', setupGame);
  3252. undoButton.addEventListener('click', undoMove);
  3253. keepPlayingButton.addEventListener('click', () => {
  3254. keepPlayingMode = true;
  3255. hideMessage();
  3256. isMoving = false; // Ensure input is re-enabled
  3257. });
  3258. themeToggle.addEventListener('click', toggleTheme);
  3259. helpButton.addEventListener('click', showKeyboardShortcuts);
  3260. closeShortcuts.addEventListener('click', hideKeyboardShortcuts);
  3261. keyboardLink.addEventListener('click', (e) => {
  3262. e.preventDefault();
  3263. showKeyboardShortcuts();
  3264. });
  3265. statsToggle.addEventListener('click', toggleStats);
  3266. animationToggle.addEventListener('click', toggleAnimations);
  3267.  
  3268. // Window event listeners
  3269. document.addEventListener('visibilitychange', handleVisibilityChange);
  3270. window.addEventListener('beforeunload', handleBeforeUnload);
  3271. }
  3272.  
  3273. // Start the game
  3274. initialize();
  3275. });
  3276. </script>
  3277. </body>
  3278. </html>
  3279. ```
  3280.  
  3281. ## Key Enhancements Explained
  3282.  
  3283. ### 1. Game History and Undo Functionality
  3284. - Added a `gameHistory` array to store previous game states
  3285. - Implemented `saveGameState()` to capture a deep copy of the grid and score
  3286. - Created `undoMove()` to restore a previous state
  3287. - Added keyboard shortcuts (U/Z) for undo
  3288. - Limited history to `MAX_UNDO_STEPS` to prevent memory issues
  3289.  
  3290. ### 2. Dark Mode Support
  3291. - Added CSS variables for all colors with light/dark variants
  3292. - Implemented theme toggle with localStorage persistence
  3293. - Added system preference detection via `prefers-color-scheme`
  3294. - Smooth transitions between themes
  3295.  
  3296. ### 3. Game State Persistence
  3297. - Added `saveGame()` and `loadGame()` functions
  3298. - Game state is saved when tab becomes inactive or before page unload
  3299. - Game state is loaded on startup if available
  3300.  
  3301. ### 4. Statistics Tracking
  3302. - Added statistics panel with toggle
  3303. - Tracks games played, highest tile, total moves, total score, average score
  3304. - Statistics persist in localStorage
  3305.  
  3306. ### 5. Keyboard and Touch Accessibility
  3307. - Added keyboard shortcuts help dialog
  3308. - Improved keyboard navigation support
  3309. - Added a skip link for keyboard users
  3310. - Enhanced focus styles for better visibility
  3311. - Added ARIA attributes for screen readers
  3312.  
  3313. ### 6. Animation Control
  3314. - Added toggle to enable/disable animations
  3315. - Persists preference in localStorage
  3316. - Improves performance on lower-end devices
  3317.  
  3318. ### 7. Visual and UX Enhancements
  3319. - Added score addition animation (+100 popup)
  3320. - Added toast notifications for user feedback
  3321. - Improved responsive design
  3322. - Added transition effects for smoother experience
  3323. - Support for larger tiles (beyond 2048)
  3324.  
  3325. ### 8. Performance Optimizations
  3326. - Added `will-change` hints for better rendering performance
  3327. - Conditional animation based on user preference
  3328. - More efficient DOM manipulation
  3329. - Optimized event handling
  3330.  
  3331. ### 9. Code Structure Improvements
  3332. - Better organization of related functions
  3333. - More consistent naming conventions
  3334. - Added comments for better maintainability
  3335. - Improved error handling
  3336.  
  3337. ## Conclusion
  3338.  
  3339. This enhanced version of 2048 builds on Gemini's excellent refactoring by adding several quality-of-life features and optimizations. The game is now more accessible, customizable, and user-friendly while maintaining the core gameplay experience. The addition of undo functionality, game state persistence, and statistics tracking adds depth to the experience, while the performance optimizations and animation controls ensure it runs well on a wide range of devices.
  3340.  
  3341. ## Gemini (Iteration 3)
  3342.  
  3343. Okay, Claude, that's a fantastic enhancement! You've added a wealth of features that significantly improve the game's usability, longevity, and polish. The dark mode, undo, persistence, stats, and accessibility additions are all excellent.
  3344.  
  3345. I've reviewed your code and will now build upon it, focusing primarily on architectural improvements for better organization and maintainability, along with some minor refinements.
  3346.  
  3347. ## Analysis of Claude's Implementation
  3348.  
  3349. **Strengths:**
  3350.  
  3351. * **Feature Completeness:** You've implemented all the requested features effectively (undo, stats, persistence, dark mode, animation control, etc.).
  3352. * **Robustness:** Includes error handling for loading, persistence triggers (`visibilitychange`, `beforeunload`), and history limits.
  3353. * **User Experience:** Toast notifications, score animations, clear visual states (win/lose), and responsive design create a polished feel.
  3354. * **Accessibility:** Good use of ARIA, keyboard navigation, focus management, and skip links.
  3355. * **Theming:** Excellent use of CSS variables for easy theming and dark mode.
  3356. * **`Tile` Class:** The `Tile` class remains a good abstraction for managing individual tiles.
  3357.  
  3358. **Areas for Architectural Improvement:**
  3359.  
  3360. * **Global Scope Pollution:** Many variables and functions reside in the global scope (within the DOMContentLoaded listener). This can make the code harder to manage as it grows.
  3361. * **Encapsulation:** Game logic, state, and UI updates are somewhat intertwined in the main script scope. Encapsulating these concerns would improve organization.
  3362. * **Animation Synchronization:** While using `setTimeout` after moves is common, it relies on fixed durations. A more robust approach could use Promises or animation events for better synchronization, although the current method is often sufficient.
  3363. * **Readability:** Some functions like `move` and `initialize` are quite long.
  3364.  
  3365. ## My Refinements: Introducing a `Game` Class
  3366.  
  3367. My main contribution will be to refactor the core game logic and state into a `Game` class. This addresses the global scope and encapsulation issues, making the code more modular and easier to understand.
  3368.  
  3369. **Key Changes:**
  3370.  
  3371. 1. **`Game` Class:** Encapsulates game state (`grid`, `score`, `bestScore`, `history`, `stats`, `settings`, etc.) and core logic methods (`move`, `undo`, `addRandomTile`, `saveState`, `loadState`, `checkStatus`, etc.).
  3372. 2. **UI Interaction:** The `Game` class will interact with a dedicated `UIManager` (or directly with DOM elements passed during construction) to update the display. For simplicity here, I'll have the `Game` class manage its required DOM elements directly.
  3373. 3. **Constants:** Grouped constants for better clarity.
  3374. 4. **Event Handling:** Event listeners will primarily call methods on the `Game` instance.
  3375. 5. **Minor Refinements:** Added constants for magic numbers, slightly improved comments, basic localStorage error handling. Clarified the `totalMoves` statistic name/comment.
  3376.  
  3377. ```html
  3378. <!DOCTYPE html>
  3379. <html lang="en">
  3380. <head>
  3381. <meta charset="UTF-8">
  3382. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  3383. <title>2048 - Collaborative Edition</title>
  3384. <style>
  3385. /* CSS remains largely the same as Claude's version */
  3386. /* ... (Claude's CSS styles - no major changes needed here) ... */
  3387. :root {
  3388. --grid-size: 4;
  3389. --cell-size: 100px;
  3390. --cell-gap: 12px;
  3391. --border-radius: 8px;
  3392. --animation-speed: 150ms; /* Slightly increased for smoother feel */
  3393. --appear-speed: 200ms;
  3394. --pop-speed: 200ms;
  3395.  
  3396. /* Color scheme - light mode (default) */
  3397. --bg-color: #faf8ef;
  3398. --container-bg: #bbada0;
  3399. --cell-bg: rgba(238, 228, 218, 0.35);
  3400. --title-color: #776e65;
  3401. --text-color: #776e65;
  3402. --button-bg: #8f7a66;
  3403. --button-hover-bg: #9f8b77;
  3404. --button-text: #f9f6f2;
  3405. --overlay-bg: rgba(238, 228, 218, 0.73);
  3406. --tile-2-bg: #eee4da;
  3407. --tile-2-text: #776e65;
  3408. --tile-4-bg: #ede0c8;
  3409. --tile-4-text: #776e65;
  3410. --tile-8-bg: #f2b179;
  3411. --tile-8-text: #f9f6f2;
  3412. --tile-16-bg: #f59563;
  3413. --tile-16-text: #f9f6f2;
  3414. --tile-32-bg: #f67c5f;
  3415. --tile-32-text: #f9f6f2;
  3416. --tile-64-bg: #f65e3b;
  3417. --tile-64-text: #f9f6f2;
  3418. --tile-128-bg: #edcf72;
  3419. --tile-128-text: #f9f6f2;
  3420. --tile-256-bg: #edcc61;
  3421. --tile-256-text: #f9f6f2;
  3422. --tile-512-bg: #edc850;
  3423. --tile-512-text: #f9f6f2;
  3424. --tile-1024-bg: #edc53f;
  3425. --tile-1024-text: #f9f6f2;
  3426. --tile-2048-bg: #edc22e;
  3427. --tile-2048-text: #f9f6f2;
  3428. --tile-super-bg: #3c3a32;
  3429. --tile-super-text: #f9f6f2;
  3430. }
  3431.  
  3432. /* Dark mode color scheme */
  3433. [data-theme="dark"] {
  3434. --bg-color: #1f2937;
  3435. --container-bg: #374151;
  3436. --cell-bg: rgba(55, 65, 81, 0.7);
  3437. --title-color: #e5e7eb;
  3438. --text-color: #e5e7eb;
  3439. --button-bg: #4b5563;
  3440. --button-hover-bg: #6b7280;
  3441. --button-text: #f9fafb;
  3442. --overlay-bg: rgba(31, 41, 55, 0.8);
  3443. --tile-2-bg: #9ca3af;
  3444. --tile-2-text: #1f2937;
  3445. --tile-4-bg: #d1d5db;
  3446. --tile-4-text: #1f2937;
  3447. /* Other tile colors remain the same for recognition */
  3448. }
  3449.  
  3450. @media (prefers-color-scheme: dark) {
  3451. :root:not([data-theme="light"]) {
  3452. --bg-color: #1f2937;
  3453. --container-bg: #374151;
  3454. --cell-bg: rgba(55, 65, 81, 0.7);
  3455. --title-color: #e5e7eb;
  3456. --text-color: #e5e7eb;
  3457. --button-bg: #4b5563;
  3458. --button-hover-bg: #6b7280;
  3459. --button-text: #f9fafb;
  3460. --overlay-bg: rgba(31, 41, 55, 0.8);
  3461. --tile-2-bg: #9ca3af;
  3462. --tile-2-text: #1f2937;
  3463. --tile-4-bg: #d1d5db;
  3464. --tile-4-text: #1f2937;
  3465. }
  3466. }
  3467.  
  3468. * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Arial', sans-serif; }
  3469. body { background-color: var(--bg-color); color: var(--text-color); display: flex; flex-direction: column; justify-content: center; align-items: center; min-height: 100vh; overflow-y: auto; touch-action: none; padding: 10px; transition: background-color 0.3s ease, color 0.3s ease; }
  3470. .container { width: min(95vw, 500px); padding: 15px; border-radius: 10px; background-color: var(--container-bg); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); transition: background-color 0.3s ease; }
  3471. .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 10px; }
  3472. .title-container { display: flex; align-items: center; gap: 10px; }
  3473. .title { color: var(--title-color); font-size: clamp(2rem, 8vw, 2.5rem); font-weight: bold; transition: color 0.3s ease; }
  3474. .theme-toggle { background: none; border: none; cursor: pointer; font-size: 1.5rem; color: var(--text-color); transition: transform 0.3s ease; padding: 5px; margin-left: 10px; }
  3475. .theme-toggle:hover { transform: rotate(30deg); }
  3476. .scores { display: flex; gap: 10px; }
  3477. .score-box { background-color: var(--button-bg); color: var(--button-text); padding: 8px 12px; border-radius: 5px; display: flex; flex-direction: column; align-items: center; min-width: 70px; text-align: center; transition: background-color 0.3s ease; }
  3478. .score-title { font-size: 0.7rem; text-transform: uppercase; margin-bottom: 2px; }
  3479. .score-value { font-size: clamp(1.2rem, 5vw, 1.5rem); font-weight: bold; }
  3480. .sub-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 10px; }
  3481. .game-intro { color: var(--text-color); flex-basis: 60%; font-size: 0.9rem; line-height: 1.3; transition: color 0.3s ease; }
  3482. .button-container { display: flex; gap: 8px; flex-wrap: wrap; }
  3483. .btn { background-color: var(--button-bg); color: var(--button-text); border: none; border-radius: 5px; padding: 10px 15px; font-weight: bold; cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; white-space: nowrap; display: flex; align-items: center; justify-content: center; min-width: 40px; }
  3484. .btn:hover, .btn:focus { background-color: var(--button-hover-bg); outline: none; }
  3485. .btn:active { transform: scale(0.95); }
  3486. .btn:disabled { opacity: 0.5; cursor: not-allowed; }
  3487. .btn-icon { font-size: 1.2rem; line-height: 1; }
  3488. .game-container { position: relative; width: calc(var(--grid-size) * (var(--cell-size) + var(--cell-gap)) + var(--cell-gap)); height: calc(var(--grid-size) * (var(--cell-size) + var(--cell-gap)) + var(--cell-gap)); background-color: var(--container-bg); border-radius: var(--border-radius); padding: var(--cell-gap); margin: 0 auto; overflow: hidden; transition: background-color 0.3s ease; }
  3489. .game-container.focused { outline: 3px solid #4c9aff; outline-offset: 3px; } /* Focus outline for game container */
  3490. .grid-background { position: absolute; top: var(--cell-gap); left: var(--cell-gap); bottom: var(--cell-gap); right: var(--cell-gap); display: grid; grid-template-columns: repeat(var(--grid-size), var(--cell-size)); grid-template-rows: repeat(var(--grid-size), var(--cell-size)); gap: var(--cell-gap); }
  3491. .cell { background-color: var(--cell-bg); border-radius: var(--border-radius); transition: background-color 0.3s ease; }
  3492. .tile-container { position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 5;} /* Ensure tiles are above grid */
  3493. .tile { position: absolute; display: flex; justify-content: center; align-items: center; width: var(--cell-size); height: var(--cell-size); border-radius: var(--border-radius); font-size: clamp(1.5rem, 7vw, 2rem); font-weight: bold; background-color: var(--tile-2-bg); color: var(--tile-2-text); transition: top var(--animation-speed) ease-in-out, left var(--animation-speed) ease-in-out, background-color 0.3s ease, color 0.3s ease; z-index: 10; user-select: none; will-change: transform, opacity; }
  3494. .tile.new-tile { animation: appear var(--appear-speed) ease-in-out; }
  3495. @keyframes appear { 0% { opacity: 0; transform: scale(0); } 100% { opacity: 1; transform: scale(1); } }
  3496. .tile.merged-tile { animation: pop var(--pop-speed) ease-in-out; z-index: 20; }
  3497. @keyframes pop { 0% { transform: scale(1); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } }
  3498. .tile[data-value="2"] { background-color: var(--tile-2-bg); color: var(--tile-2-text); }
  3499. .tile[data-value="4"] { background-color: var(--tile-4-bg); color: var(--tile-4-text); }
  3500. .tile[data-value="8"] { background-color: var(--tile-8-bg); color: var(--tile-8-text); }
  3501. .tile[data-value="16"] { background-color: var(--tile-16-bg); color: var(--tile-16-text); }
  3502. .tile[data-value="32"] { background-color: var(--tile-32-bg); color: var(--tile-32-text); }
  3503. .tile[data-value="64"] { background-color: var(--tile-64-bg); color: var(--tile-64-text); }
  3504. .tile[data-value="128"] { background-color: var(--tile-128-bg); color: var(--tile-128-text); font-size: clamp(1.3rem, 6vw, 1.8rem); }
  3505. .tile[data-value="256"] { background-color: var(--tile-256-bg); color: var(--tile-256-text); font-size: clamp(1.3rem, 6vw, 1.8rem); }
  3506. .tile[data-value="512"] { background-color: var(--tile-512-bg); color: var(--tile-512-text); font-size: clamp(1.3rem, 6vw, 1.8rem); }
  3507. .tile[data-value="1024"] { background-color: var(--tile-1024-bg); color: var(--tile-1024-text); font-size: clamp(1.1rem, 5vw, 1.5rem); }
  3508. .tile[data-value="2048"] { background-color: var(--tile-2048-bg); color: var(--tile-2048-text); font-size: clamp(1.1rem, 5vw, 1.5rem); }
  3509. .tile[data-value="4096"], .tile[data-value="8192"], .tile[data-value="16384"], .tile[data-value="32768"], .tile[data-value="65536"] { background-color: var(--tile-super-bg); color: var(--tile-super-text); font-size: clamp(1rem, 4.5vw, 1.4rem); }
  3510. .game-message { display: none; position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: var(--overlay-bg); flex-direction: column; align-items: center; justify-content: center; text-align: center; border-radius: var(--border-radius); z-index: 100; opacity: 0; transition: opacity 0.3s ease-in-out, background-color 0.3s ease; animation: fadein 0.3s ease-in-out forwards; }
  3511. .game-message.active { display: flex; opacity: 1; }
  3512. @keyframes fadein { from { opacity: 0; } to { opacity: 1; } }
  3513. .game-message p { font-size: clamp(1.8rem, 7vw, 2rem); font-weight: bold; color: var(--title-color); margin-bottom: 20px; transition: color 0.3s ease; }
  3514. .game-message .lower { display: flex; gap: 10px; margin-top: 10px; }
  3515. .score-addition { position: fixed; /* Use fixed for positioning relative to viewport */ animation: score-pop 0.8s ease-in-out forwards; font-weight: bold; color: var(--title-color); z-index: 100; pointer-events: none; font-size: 1.2rem; opacity: 0; transition: color 0.3s ease; }
  3516. @keyframes score-pop { 0% { opacity: 1; transform: translate(-50%, 0); } 100% { opacity: 0; transform: translate(-50%, -30px); } } /* Adjusted transform */
  3517. .stats-container { margin-top: 10px; background-color: var(--container-bg); border-radius: 8px; padding: 10px 15px; display: flex; flex-direction: column; gap: 5px; width: 100%; transition: background-color 0.3s ease; }
  3518. .stats-title { font-weight: bold; color: var(--title-color); font-size: 1.1rem; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; transition: color 0.3s ease; }
  3519. .stats-row { display: flex; justify-content: space-between; font-size: 0.9rem; color: var(--text-color); transition: color 0.3s ease; }
  3520. .stats-value { font-weight: bold; }
  3521. .stats-toggle { font-size: 1rem; cursor: pointer; background: none; border: none; color: var(--text-color); transition: transform 0.3s ease, color 0.3s ease; }
  3522. .stats-toggle:hover { transform: rotate(180deg); }
  3523. .stats-content { overflow: hidden; max-height: 0; transition: max-height 0.3s ease-in-out; }
  3524. .stats-content.expanded { max-height: 200px; /* Adjust based on content */ }
  3525. @media screen and (max-width: 520px) { :root { --cell-size: 65px; --cell-gap: 8px; } .game-intro { flex-basis: 100%; text-align: center; margin-bottom: 10px; } .btn { padding: 8px 12px; } .button-container { justify-content: center; width: 100%; } }
  3526. @media screen and (max-width: 380px) { :root { --cell-size: 55px; --cell-gap: 6px; } .container { padding: 10px; } .header { justify-content: center; } .title { text-align: center; width: 100%; margin-bottom: 5px;} .sub-header { justify-content: center; } .score-box { min-width: 60px; padding: 6px 10px; } .button-container { flex-wrap: wrap; } }
  3527. .footer { margin-top: 20px; text-align: center; color: var(--text-color); font-size: 0.8rem; transition: color 0.3s ease; display: flex; flex-direction: column; gap: 5px; align-items: center;}
  3528. .animation-controls { display: flex; align-items: center; gap: 5px; }
  3529. .animation-toggle { border: none; background: none; cursor: pointer; font-size: 1.2rem; color: var(--text-color); transition: color 0.3s ease; display: flex; align-items: center; gap: 5px; }
  3530. .animation-toggle:hover { color: var(--title-color); }
  3531. .animation-status { font-size: 0.8rem; }
  3532. .keyboard-shortcuts { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--overlay-bg); z-index: 1000; display: none; justify-content: center; align-items: center; transition: background-color 0.3s ease; }
  3533. .keyboard-shortcuts.active { display: flex; }
  3534. .shortcuts-content { background-color: var(--bg-color); border-radius: 10px; padding: 20px; max-width: 90%; width: 400px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); position: relative; transition: background-color 0.3s ease; }
  3535. .shortcuts-title { font-size: 1.5rem; font-weight: bold; margin-bottom: 15px; color: var(--title-color); transition: color 0.3s ease; }
  3536. .shortcut-grid { display: grid; grid-template-columns: auto 1fr; gap: 10px 20px; margin-bottom: 20px; color: var(--text-color); } /* Added text color */
  3537. .key { background-color: var(--container-bg); color: var(--text-color); padding: 5px 10px; border-radius: 5px; font-family: monospace; text-align: center; font-weight: bold; transition: background-color 0.3s ease, color 0.3s ease; }
  3538. .close-shortcuts { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-color); transition: transform 0.3s ease, color 0.3s ease; }
  3539. .close-shortcuts:hover { transform: rotate(90deg); }
  3540. .toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: var(--container-bg); color: var(--text-color); padding: 10px 20px; border-radius: 5px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); z-index: 1000; transition: opacity 0.3s ease, transform 0.3s ease, background-color 0.3s ease, color 0.3s ease; opacity: 0; transform: translate(-50%, 20px); pointer-events: none; }
  3541. .toast.active { opacity: 1; transform: translate(-50%, 0); }
  3542. button:focus-visible, a:focus-visible, [tabindex="0"]:focus-visible { outline: 3px solid #4c9aff; outline-offset: 2px; } /* Added game container focus */
  3543. .skip-link { position: absolute; top: -40px; left: 0; background: var(--button-bg); color: var(--button-text); padding: 8px; z-index: 100; transition: top 0.3s ease; }
  3544. .skip-link:focus { top: 0; }
  3545. </style>
  3546. </head>
  3547. <body>
  3548. <a href="#game-container" class="skip-link">Skip to game</a>
  3549. <div class="container">
  3550. <header class="header">
  3551. <div class="title-container">
  3552. <h1 class="title">2048</h1>
  3553. <button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode">🌓</button>
  3554. </div>
  3555. <div class="scores">
  3556. <div class="score-box">
  3557. <div class="score-title">Score</div>
  3558. <div class="score-value" id="score" aria-live="polite">0</div>
  3559. </div>
  3560. <div class="score-box">
  3561. <div class="score-title">Best</div>
  3562. <div class="score-value" id="best-score">0</div>
  3563. </div>
  3564. </div>
  3565. </header>
  3566. <div class="sub-header">
  3567. <p class="game-intro">Join tiles, get to <strong>2048!</strong> Use <strong>arrow keys</strong> or <strong>swipe</strong>.</p>
  3568. <div class="button-container">
  3569. <button class="btn" id="restart-button" aria-label="New Game">New</button>
  3570. <button class="btn" id="undo-button" aria-label="Undo Move" disabled>↩️</button>
  3571. <button class="btn" id="help-button" aria-label="Show Keyboard Shortcuts">?</button>
  3572. </div>
  3573. </div>
  3574.  
  3575. <div class="game-container" id="game-container" tabindex="0" aria-label="Game Board">
  3576. <div class="grid-background" id="grid-background"></div>
  3577. <div class="tile-container" id="tile-container"></div>
  3578. <div class="game-message" id="game-message" aria-live="assertive">
  3579. <p id="message-text"></p>
  3580. <div class="lower">
  3581. <button class="btn" id="retry-button">Try again</button>
  3582. <button class="btn" id="keep-playing-button" style="display: none;">Keep Playing</button>
  3583. </div>
  3584. </div>
  3585. </div>
  3586.  
  3587. <div class="stats-container" id="stats-container">
  3588. <div class="stats-title">
  3589. Game Statistics
  3590. <button class="stats-toggle" id="stats-toggle" aria-label="Toggle statistics visibility" aria-expanded="false">▼</button>
  3591. </div>
  3592. <div class="stats-content" id="stats-content">
  3593. <div class="stats-row"><span>Games Played:</span><span class="stats-value" id="games-played">0</span></div>
  3594. <div class="stats-row"><span>Highest Tile:</span><span class="stats-value" id="highest-tile">0</span></div>
  3595. <div class="stats-row"><span>Total Moves Made:</span><span class="stats-value" id="total-moves">0</span></div>
  3596. <div class="stats-row"><span>Total Score Accumulated:</span><span class="stats-value" id="total-score">0</span></div>
  3597. <div class="stats-row"><span>Avg Score/Game:</span><span class="stats-value" id="avg-score">0</span></div>
  3598. </div>
  3599. </div>
  3600.  
  3601. <footer class="footer">
  3602. <div class="animation-controls">
  3603. <button class="animation-toggle" id="animation-toggle" aria-pressed="true">
  3604. <span class="btn-icon">✨</span>
  3605. <span class="animation-status" id="animation-status">Animations: On</span>
  3606. </button>
  3607. </div>
  3608. <p>Collaborative Edition • <a href="#" id="keyboard-link">Keyboard shortcuts</a></p>
  3609. </footer>
  3610. </div>
  3611.  
  3612. <div class="keyboard-shortcuts" id="keyboard-shortcuts" role="dialog" aria-modal="true" aria-labelledby="shortcuts-title">
  3613. <div class="shortcuts-content">
  3614. <button class="close-shortcuts" id="close-shortcuts" aria-label="Close keyboard shortcuts">×</button>
  3615. <h2 class="shortcuts-title" id="shortcuts-title">Keyboard Shortcuts</h2>
  3616. <div class="shortcut-grid">
  3617. <div class="key">↑ ↓ ← →</div><div>Move Tiles</div>
  3618. <div class="key">W A S D</div><div>Move Tiles</div>
  3619. <div class="key">R</div><div>Restart Game</div>
  3620. <div class="key">U / Z</div><div>Undo Move</div>
  3621. <div class="key">M</div><div>Toggle Dark Mode</div>
  3622. <div class="key">T</div><div>Toggle Animations</div>
  3623. <div class="key">Esc</div><div>Close Dialogs</div>
  3624. <div class="key">?</div><div>Show This Help</div>
  3625. </div>
  3626. </div>
  3627. </div>
  3628.  
  3629. <div class="toast" id="toast" aria-live="assertive"></div>
  3630.  
  3631. <script>
  3632. document.addEventListener('DOMContentLoaded', () => {
  3633. // --- Constants ---
  3634. const GRID_SIZE = 4;
  3635. const WINNING_VALUE = 2048;
  3636. const MOVE_ANIMATION_DURATION = 150; // ms, matches CSS --animation-speed
  3637. const APPEAR_ANIMATION_DURATION = 200; // ms, matches CSS --appear-speed
  3638. const POP_ANIMATION_DURATION = 200; // ms, matches CSS --pop-speed
  3639. const MAX_UNDO_STEPS = 20;
  3640. const SWIPE_THRESHOLD = 30; // Min distance for swipe
  3641. const LOCAL_STORAGE_KEYS = {
  3642. BEST_SCORE: 'bestScore2048Collab',
  3643. GAME_STATE: 'savedGame2048Collab',
  3644. STATS: 'gameStats2048Collab',
  3645. DARK_MODE: 'darkModeCollab',
  3646. ANIMATIONS: 'animationsEnabledCollab'
  3647. };
  3648.  
  3649. // --- Tile Class (Remains largely the same as Claude's) ---
  3650. class Tile {
  3651. constructor(value, row, col, parentContainer, settingsRef) {
  3652. this.value = value;
  3653. this.row = row;
  3654. this.col = col;
  3655. this.parentContainer = parentContainer;
  3656. this.settings = settingsRef; // Reference to game settings (like animationsEnabled)
  3657. this.element = this.createElement();
  3658. this.updatePosition(false); // Initial position without animation
  3659. this.mergedFrom = null; // Track merge source
  3660. this.parentContainer.appendChild(this.element);
  3661. }
  3662.  
  3663. createElement() {
  3664. const tileElement = document.createElement('div');
  3665. tileElement.classList.add('tile');
  3666. this.updateValue(this.value); // Set initial text and data-value
  3667.  
  3668. if (this.settings.animationsEnabled) {
  3669. tileElement.classList.add('new-tile');
  3670. // No need for timeout removal here, CSS animation handles visibility
  3671. }
  3672. return tileElement;
  3673. }
  3674.  
  3675. updateValue(newValue) {
  3676. this.value = newValue;
  3677. this.element.textContent = newValue;
  3678. this.element.dataset.value = newValue; // For CSS styling
  3679. }
  3680.  
  3681. updatePosition(animate = true) {
  3682. const { top, left } = this.calculatePixelPosition();
  3683. // Use transition only if animations are enabled AND requested
  3684. this.element.style.transition = (animate && this.settings.animationsEnabled)
  3685. ? `top ${MOVE_ANIMATION_DURATION}ms ease-in-out, left ${MOVE_ANIMATION_DURATION}ms ease-in-out`
  3686. : 'none';
  3687. this.element.style.top = `${top}px`;
  3688. this.element.style.left = `${left}px`;
  3689. }
  3690.  
  3691. calculatePixelPosition() {
  3692. const cellSize = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--cell-size'));
  3693. const cellGap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--cell-gap'));
  3694. const top = this.row * (cellSize + cellGap) + cellGap;
  3695. const left = this.col * (cellSize + cellGap) + cellGap;
  3696. return { top, left };
  3697. }
  3698.  
  3699. remove(merged = false) {
  3700. // Use transitionend for move animations, animationend for pop/appear
  3701. const removeAction = () => {
  3702. if (this.element.parentNode) {
  3703. this.parentContainer.removeChild(this.element);
  3704. }
  3705. };
  3706.  
  3707. if (merged && this.settings.animationsEnabled) {
  3708. // Let pop animation finish
  3709. this.element.addEventListener('animationend', removeAction, { once: true });
  3710. } else {
  3711. // If not merged or animations off, remove immediately or after move transition
  3712. if(this.settings.animationsEnabled && this.element.style.transition !== 'none') {
  3713. this.element.addEventListener('transitionend', removeAction, {once: true});
  3714. } else {
  3715. removeAction();
  3716. }
  3717. }
  3718. }
  3719.  
  3720. playMergeAnimation() {
  3721. if (this.settings.animationsEnabled) {
  3722. this.element.classList.add('merged-tile');
  3723. this.element.addEventListener('animationend', () => {
  3724. this.element.classList.remove('merged-tile');
  3725. }, { once: true });
  3726. }
  3727. }
  3728.  
  3729. clone() {
  3730. return { value: this.value, row: this.row, col: this.col };
  3731. }
  3732. }
  3733.  
  3734. // --- Game Class ---
  3735. class Game {
  3736. constructor(elements) {
  3737. this.elements = elements; // Store references to DOM elements
  3738. this.grid = [];
  3739. this.score = 0;
  3740. this.bestScore = 0;
  3741. this.isGameOver = false;
  3742. this.hasWon = false;
  3743. this.keepPlayingMode = false;
  3744. this.isMoving = false;
  3745. this.gameHistory = [];
  3746. this.stats = { gamesPlayed: 0, highestTile: 0, totalMoves: 0, totalScore: 0 };
  3747. this.settings = {
  3748. animationsEnabled: true,
  3749. darkMode: false
  3750. };
  3751. // Touch state needs to be part of the instance
  3752. this.touchStartX = 0;
  3753. this.touchStartY = 0;
  3754. this.touchEndX = 0;
  3755. this.touchEndY = 0;
  3756. }
  3757.  
  3758. // --- Initialization and Setup ---
  3759. init() {
  3760. this.loadSettings();
  3761. this.loadStats();
  3762. this.loadBestScore();
  3763. this.updateTheme();
  3764. this.updateAnimationStatus();
  3765. this.createGridCells();
  3766. this.setupEventHandlers();
  3767.  
  3768. if (!this.loadGame()) {
  3769. this.startNewGame(false); // Don't increment stats on initial load
  3770. }
  3771. this.updateUI();
  3772. }
  3773.  
  3774. startNewGame(incrementStats = true) {
  3775. this.isMoving = true; // Prevent input during setup
  3776. this.initializeGrid();
  3777. this.elements.tileContainer.innerHTML = ''; // Clear existing tiles
  3778. this.score = 0;
  3779. this.isGameOver = false;
  3780. this.hasWon = false;
  3781. this.keepPlayingMode = false;
  3782. this.hideMessage();
  3783. this.gameHistory = []; // Clear history
  3784.  
  3785. if (incrementStats) {
  3786. this.stats.gamesPlayed++;
  3787. this.saveStats();
  3788. }
  3789.  
  3790. this.addRandomTile();
  3791. this.addRandomTile();
  3792. this.saveGameState(); // Save initial state
  3793. this.updateUI();
  3794. this.isMoving = false; // Allow input
  3795. }
  3796.  
  3797. initializeGrid() {
  3798. this.grid = Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(null));
  3799. }
  3800.  
  3801. createGridCells() {
  3802. this.elements.gridBackground.innerHTML = ''; // Clear previous cells
  3803. for (let i = 0; i < GRID_SIZE * GRID_SIZE; i++) {
  3804. const cell = document.createElement('div');
  3805. cell.classList.add('cell');
  3806. this.elements.gridBackground.appendChild(cell);
  3807. }
  3808. }
  3809.  
  3810. // --- State Management (Save/Load) ---
  3811. saveSettings() {
  3812. try {
  3813. localStorage.setItem(LOCAL_STORAGE_KEYS.DARK_MODE, this.settings.darkMode);
  3814. localStorage.setItem(LOCAL_STORAGE_KEYS.ANIMATIONS, this.settings.animationsEnabled);
  3815. } catch (e) {
  3816. console.error("Error saving settings to localStorage:", e);
  3817. this.showToast("Could not save settings.");
  3818. }
  3819. }
  3820.  
  3821. loadSettings() {
  3822. const darkModeSaved = localStorage.getItem(LOCAL_STORAGE_KEYS.DARK_MODE);
  3823. const animationsSaved = localStorage.getItem(LOCAL_STORAGE_KEYS.ANIMATIONS);
  3824.  
  3825. // Determine initial dark mode
  3826. this.settings.darkMode = darkModeSaved === 'true' ||
  3827. (darkModeSaved === null && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
  3828.  
  3829. // Determine initial animation setting
  3830. this.settings.animationsEnabled = animationsSaved !== 'false'; // Default true
  3831. }
  3832.  
  3833. loadBestScore() {
  3834. this.bestScore = parseInt(localStorage.getItem(LOCAL_STORAGE_KEYS.BEST_SCORE) || 0);
  3835. }
  3836.  
  3837. saveBestScore() {
  3838. try {
  3839. localStorage.setItem(LOCAL_STORAGE_KEYS.BEST_SCORE, this.bestScore);
  3840. } catch (e) {
  3841. console.error("Error saving best score to localStorage:", e);
  3842. }
  3843. }
  3844.  
  3845. loadStats() {
  3846. const savedStats = localStorage.getItem(LOCAL_STORAGE_KEYS.STATS);
  3847. if (savedStats) {
  3848. try {
  3849. this.stats = JSON.parse(savedStats);
  3850. } catch (e) {
  3851. console.error('Error parsing saved statistics:', e);
  3852. this.stats = { gamesPlayed: 0, highestTile: 0, totalMoves: 0, totalScore: 0 }; // Reset if corrupt
  3853. }
  3854. }
  3855. }
  3856.  
  3857. saveStats() {
  3858. try {
  3859. localStorage.setItem(LOCAL_STORAGE_KEYS.STATS, JSON.stringify(this.stats));
  3860. } catch (e) {
  3861. console.error("Error saving stats to localStorage:", e);
  3862. }
  3863. }
  3864.  
  3865. saveGame() {
  3866. if (this.isGameOver) { // Don't save finished games
  3867. localStorage.removeItem(LOCAL_STORAGE_KEYS.GAME_STATE);
  3868. return;
  3869. }
  3870. const gridSaveData = Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(null));
  3871. for (let r = 0; r < GRID_SIZE; r++) {
  3872. for (let c = 0; c < GRID_SIZE; c++) {
  3873. if (this.grid[r][c]) {
  3874. gridSaveData[r][c] = this.grid[r][c].clone();
  3875. }
  3876. }
  3877. }
  3878. const saveData = {
  3879. grid: gridSaveData,
  3880. score: this.score,
  3881. hasWon: this.hasWon,
  3882. keepPlayingMode: this.keepPlayingMode,
  3883. // No need to save bestScore here, it's saved separately
  3884. };
  3885. try {
  3886. localStorage.setItem(LOCAL_STORAGE_KEYS.GAME_STATE, JSON.stringify(saveData));
  3887. } catch (e) {
  3888. console.error("Error saving game state to localStorage:", e);
  3889. this.showToast("Could not save game progress.");
  3890. }
  3891. }
  3892.  
  3893. loadGame() {
  3894. const savedGame = localStorage.getItem(LOCAL_STORAGE_KEYS.GAME_STATE);
  3895. if (!savedGame) return false;
  3896.  
  3897. try {
  3898. const saveData = JSON.parse(savedGame);
  3899. this.initializeGrid();
  3900. this.elements.tileContainer.innerHTML = '';
  3901.  
  3902. this.score = saveData.score;
  3903. this.hasWon = saveData.hasWon;
  3904. this.keepPlayingMode = saveData.keepPlayingMode;
  3905.  
  3906. for (let r = 0; r < GRID_SIZE; r++) {
  3907. for (let c = 0; c < GRID_SIZE; c++) {
  3908. if (saveData.grid[r][c]) {
  3909. const { value, row, col } = saveData.grid[r][c];
  3910. // Pass parent container and settings reference to Tile constructor
  3911. this.grid[r][c] = new Tile(value, row, col, this.elements.tileContainer, this.settings);
  3912. }
  3913. }
  3914. }
  3915. this.saveGameState(); // Save loaded state as the first step in history
  3916. this.checkGameStatus(); // Check win/loss state after loading
  3917. return true;
  3918. } catch (e) {
  3919. console.error('Error loading saved game:', e);
  3920. localStorage.removeItem(LOCAL_STORAGE_KEYS.GAME_STATE); // Clear corrupted data
  3921. return false;
  3922. }
  3923. }
  3924.  
  3925. // --- Game Logic ---
  3926. addRandomTile() {
  3927. const emptyCells = this.getEmptyCells();
  3928. if (emptyCells.length === 0) return;
  3929.  
  3930. const { r, c } = emptyCells[Math.floor(Math.random() * emptyCells.length)];
  3931. const value = Math.random() < 0.9 ? 2 : 4;
  3932. // Pass parent container and settings reference to Tile constructor
  3933. const newTile = new Tile(value, r, c, this.elements.tileContainer, this.settings);
  3934. this.grid[r][c] = newTile;
  3935.  
  3936. // Update highest tile stat if needed
  3937. if (value > this.stats.highestTile) {
  3938. this.stats.highestTile = value;
  3939. this.saveStats();
  3940. this.updateStatsDisplay();
  3941. }
  3942. }
  3943.  
  3944. getEmptyCells() {
  3945. const emptyCells = [];
  3946. for (let r = 0; r < GRID_SIZE; r++) {
  3947. for (let c = 0; c < GRID_SIZE; c++) {
  3948. if (this.grid[r][c] === null) {
  3949. emptyCells.push({ r, c });
  3950. }
  3951. }
  3952. }
  3953. return emptyCells;
  3954. }
  3955.  
  3956. move(direction) {
  3957. if (this.isMoving || this.isGameOver) return false;
  3958.  
  3959. this.isMoving = true;
  3960. let moved = false;
  3961. let scoreAddition = 0;
  3962. const initialScore = this.score;
  3963.  
  3964. // Reset merge tracking
  3965. for (let r = 0; r < GRID_SIZE; r++) {
  3966. for (let c = 0; c < GRID_SIZE; c++) {
  3967. if (this.grid[r][c]) this.grid[r][c].mergedFrom = null;
  3968. }
  3969. }
  3970.  
  3971. const traversals = this.getTraversals(direction);
  3972.  
  3973. traversals.rows.forEach(r => {
  3974. traversals.cols.forEach(c => {
  3975. const currentTile = this.grid[r][c];
  3976. if (currentTile) {
  3977. const { furthestRow, furthestCol, nextTile } = this.findFurthestPosition(r, c, direction);
  3978.  
  3979. if (nextTile && nextTile.value === currentTile.value && !nextTile.mergedFrom) {
  3980. // --- Merge ---
  3981. const mergedValue = currentTile.value * 2;
  3982. nextTile.updateValue(mergedValue);
  3983. nextTile.mergedFrom = currentTile;
  3984. nextTile.playMergeAnimation();
  3985.  
  3986. this.grid[r][c] = null;
  3987. currentTile.remove(true); // Remove with merge flag
  3988.  
  3989. scoreAddition += mergedValue;
  3990. moved = true;
  3991.  
  3992. if (!this.hasWon && mergedValue === WINNING_VALUE) {
  3993. this.hasWon = true;
  3994. }
  3995. if (mergedValue > this.stats.highestTile) {
  3996. this.stats.highestTile = mergedValue;
  3997. this.saveStats(); // Save stats immediately on update
  3998. }
  3999. } else if (furthestRow !== r || furthestCol !== c) {
  4000. // --- Move ---
  4001. this.grid[furthestRow][furthestCol] = currentTile;
  4002. this.grid[r][c] = null;
  4003. currentTile.row = furthestRow;
  4004. currentTile.col = furthestCol;
  4005. currentTile.updatePosition(); // Animate move
  4006. moved = true;
  4007. }
  4008. }
  4009. });
  4010. });
  4011.  
  4012. if (moved) {
  4013. this.updateScore(this.score + scoreAddition, scoreAddition);
  4014. this.stats.totalMoves++; // Increment moves made in this session/lifetime
  4015. this.saveStats();
  4016. this.saveGameState(); // Save state *before* adding new tile
  4017.  
  4018. const delay = this.settings.animationsEnabled ? MOVE_ANIMATION_DURATION + 50 : 0;
  4019. setTimeout(() => {
  4020. this.addRandomTile();
  4021. this.checkGameStatus();
  4022. this.updateUI(); // Update score, buttons etc.
  4023. this.isMoving = false;
  4024. }, delay);
  4025. } else {
  4026. this.isMoving = false; // No move occurred
  4027. }
  4028. return moved; // Return whether a move happened
  4029. }
  4030.  
  4031. getTraversals(direction) {
  4032. const rows = Array.from({ length: GRID_SIZE }, (_, i) => i);
  4033. const cols = Array.from({ length: GRID_SIZE }, (_, i) => i);
  4034. if (direction === 'right') cols.reverse();
  4035. if (direction === 'down') rows.reverse();
  4036. return { rows, cols };
  4037. }
  4038.  
  4039. findFurthestPosition(r, c, direction) {
  4040. let currentRow = r, currentCol = c, nextRow, nextCol;
  4041. do {
  4042. currentRow = nextRow !== undefined ? nextRow : r;
  4043. currentCol = nextCol !== undefined ? nextCol : c;
  4044. const { nr, nc } = this.getNextPosition(currentRow, currentCol, direction);
  4045. nextRow = nr;
  4046. nextCol = nc;
  4047. } while (this.isWithinBounds(nextRow, nextCol) && this.grid[nextRow][nextCol] === null);
  4048.  
  4049. const furthestRow = currentRow;
  4050. const furthestCol = currentCol;
  4051. const nextTile = this.isWithinBounds(nextRow, nextCol) ? this.grid[nextRow][nextCol] : null;
  4052. return { furthestRow, furthestCol, nextTile };
  4053. }
  4054.  
  4055. getNextPosition(r, c, direction) {
  4056. switch (direction) {
  4057. case 'up': return { nr: r - 1, nc: c };
  4058. case 'down': return { nr: r + 1, nc: c };
  4059. case 'left': return { nr: r, nc: c - 1 };
  4060. case 'right': return { nr: r, nc: c + 1 };
  4061. default: return { nr: r, nc: c };
  4062. }
  4063. }
  4064.  
  4065. isWithinBounds(r, c) {
  4066. return r >= 0 && r < GRID_SIZE && c >= 0 && c < GRID_SIZE;
  4067. }
  4068.  
  4069. canMove() {
  4070. if (this.getEmptyCells().length > 0) return true;
  4071. for (let r = 0; r < GRID_SIZE; r++) {
  4072. for (let c = 0; c < GRID_SIZE; c++) {
  4073. const tile = this.grid[r][c];
  4074. if (!tile) continue;
  4075. // Check right and down - sufficient to check all adjacencies
  4076. if (c < GRID_SIZE - 1 && this.grid[r][c + 1]?.value === tile.value) return true;
  4077. if (r < GRID_SIZE - 1 && this.grid[r + 1][c]?.value === tile.value) return true;
  4078. }
  4079. }
  4080. return false;
  4081. }
  4082.  
  4083. checkGameStatus() {
  4084. if (this.isGameOver) return; // Already over
  4085.  
  4086. const canMove = this.canMove();
  4087.  
  4088. if (this.hasWon && !this.keepPlayingMode) {
  4089. this.showMessage('You Win!', 'win');
  4090. // Don't set isGameOver, allow keep playing
  4091. } else if (!canMove) {
  4092. this.isGameOver = true;
  4093. this.showMessage('Game Over!', 'over');
  4094. // Clear saved game state as the game is finished
  4095. localStorage.removeItem(LOCAL_STORAGE_KEYS.GAME_STATE);
  4096. }
  4097. }
  4098.  
  4099. // --- History and Undo ---
  4100. saveGameState() {
  4101. const gridCopy = Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(null));
  4102. for (let r = 0; r < GRID_SIZE; r++) {
  4103. for (let c = 0; c < GRID_SIZE; c++) {
  4104. if (this.grid[r][c]) {
  4105. gridCopy[r][c] = this.grid[r][c].clone();
  4106. }
  4107. }
  4108. }
  4109. this.gameHistory.push({ grid: gridCopy, score: this.score });
  4110. if (this.gameHistory.length > MAX_UNDO_STEPS) {
  4111. this.gameHistory.shift();
  4112. }
  4113. this.updateUndoButton();
  4114. }
  4115.  
  4116. undoMove() {
  4117. if (this.isMoving || this.gameHistory.length <= 1) return;
  4118.  
  4119. this.isMoving = true;
  4120. this.hideMessage(); // Hide any win/loss message if undoing
  4121. this.isGameOver = false; // Game is no longer over if we undo
  4122.  
  4123. this.gameHistory.pop(); // Remove current state
  4124. const previousState = this.gameHistory[this.gameHistory.length - 1];
  4125.  
  4126. this.elements.tileContainer.innerHTML = ''; // Clear display
  4127. this.initializeGrid(); // Clear logical grid
  4128.  
  4129. for (let r = 0; r < GRID_SIZE; r++) {
  4130. for (let c = 0; c < GRID_SIZE; c++) {
  4131. if (previousState.grid[r][c]) {
  4132. const { value, row, col } = previousState.grid[r][c];
  4133. // Restore tile object - pass container and settings
  4134. this.grid[r][c] = new Tile(value, row, col, this.elements.tileContainer, this.settings);
  4135. }
  4136. }
  4137. }
  4138.  
  4139. // Restore score *without* triggering score addition animation/stat update
  4140. this.score = previousState.score;
  4141. this.elements.scoreElement.textContent = this.score;
  4142.  
  4143. // Note: We don't decrement totalMoves or totalScore stats on undo.
  4144. // These stats track lifetime activity.
  4145.  
  4146. this.updateUI(); // Update buttons, etc.
  4147. this.showToast("Move undone");
  4148. this.isMoving = false;
  4149. }
  4150.  
  4151. // --- UI Updates ---
  4152. updateUI() {
  4153. this.updateScoreDisplay();
  4154. this.updateBestScoreDisplay();
  4155. this.updateUndoButton();
  4156. this.updateStatsDisplay();
  4157. // Ensure game message state is correct (e.g., hidden if not win/loss)
  4158. if (!this.isGameOver && !(this.hasWon && !this.keepPlayingMode)) {
  4159. this.hideMessage();
  4160. }
  4161. }
  4162.  
  4163. updateScore(newScore, addition = 0) {
  4164. const oldScore = this.score;
  4165. this.score = newScore;
  4166. this.updateScoreDisplay();
  4167.  
  4168. if (addition > 0 && this.settings.animationsEnabled) {
  4169. this.showScoreAddition(addition);
  4170. }
  4171.  
  4172. if (this.score > this.bestScore) {
  4173. this.bestScore = this.score;
  4174. this.saveBestScore();
  4175. this.updateBestScoreDisplay();
  4176. }
  4177.  
  4178. // Update total score stat only if score increased
  4179. if(newScore > oldScore) {
  4180. this.stats.totalScore += (newScore - oldScore);
  4181. this.saveStats();
  4182. this.updateStatsDisplay(); // Update display if stats changed
  4183. }
  4184. }
  4185.  
  4186. updateScoreDisplay() {
  4187. this.elements.scoreElement.textContent = this.score;
  4188. }
  4189.  
  4190. updateBestScoreDisplay() {
  4191. this.elements.bestScoreElement.textContent = this.bestScore;
  4192. }
  4193.  
  4194. updateUndoButton() {
  4195. this.elements.undoButton.disabled = this.gameHistory.length <= 1 || this.isMoving;
  4196. }
  4197.  
  4198. updateStatsDisplay() {
  4199. this.elements.gamesPlayedElement.textContent = this.stats.gamesPlayed;
  4200. this.elements.highestTileElement.textContent = this.stats.highestTile;
  4201. this.elements.totalMovesElement.textContent = this.stats.totalMoves;
  4202. this.elements.totalScoreElement.textContent = this.stats.totalScore;
  4203. const avgScore = this.stats.gamesPlayed > 0
  4204. ? Math.round(this.stats.totalScore / this.stats.gamesPlayed)
  4205. : 0;
  4206. this.elements.avgScoreElement.textContent = avgScore;
  4207. }
  4208.  
  4209. showMessage(text, type) {
  4210. this.elements.messageText.textContent = text;
  4211. this.elements.messageContainer.className = 'game-message active'; // Reset classes
  4212. this.elements.keepPlayingButton.style.display = 'none';
  4213.  
  4214. if (type === 'win') {
  4215. this.elements.messageContainer.classList.add('game-won');
  4216. if (!this.keepPlayingMode) {
  4217. this.elements.keepPlayingButton.style.display = 'inline-block';
  4218. }
  4219. } else if (type === 'over') {
  4220. this.elements.messageContainer.classList.add('game-over');
  4221. }
  4222. }
  4223.  
  4224. hideMessage() {
  4225. this.elements.messageContainer.classList.remove('active', 'game-won', 'game-over');
  4226. }
  4227.  
  4228. showScoreAddition(addition) {
  4229. const scoreAdditionElement = document.createElement('div');
  4230. scoreAdditionElement.classList.add('score-addition');
  4231. scoreAdditionElement.textContent = `+${addition}`;
  4232.  
  4233. const scoreRect = this.elements.scoreElement.getBoundingClientRect();
  4234. // Position relative to viewport using fixed positioning
  4235. scoreAdditionElement.style.position = 'fixed';
  4236. scoreAdditionElement.style.top = `${scoreRect.top - 10}px`; // Position above score box
  4237. scoreAdditionElement.style.left = `${scoreRect.left + scoreRect.width / 2}px`; // Centered horizontally
  4238.  
  4239. document.body.appendChild(scoreAdditionElement);
  4240.  
  4241. scoreAdditionElement.addEventListener('animationend', () => {
  4242. if (scoreAdditionElement.parentNode) {
  4243. document.body.removeChild(scoreAdditionElement);
  4244. }
  4245. }, { once: true });
  4246. }
  4247.  
  4248. showToast(message, duration = 2000) {
  4249. const toastElement = this.elements.toast;
  4250. toastElement.textContent = message;
  4251. toastElement.classList.add('active');
  4252.  
  4253. if (toastElement.timeoutId) clearTimeout(toastElement.timeoutId);
  4254.  
  4255. toastElement.timeoutId = setTimeout(() => {
  4256. toastElement.classList.remove('active');
  4257. }, duration);
  4258. }
  4259.  
  4260. // --- Settings Toggles ---
  4261. toggleTheme() {
  4262. this.settings.darkMode = !this.settings.darkMode;
  4263. this.updateTheme();
  4264. this.saveSettings();
  4265. this.showToast(this.settings.darkMode ? 'Dark mode enabled' : 'Light mode enabled');
  4266. }
  4267.  
  4268. updateTheme() {
  4269. document.documentElement.setAttribute('data-theme', this.settings.darkMode ? 'dark' : 'light');
  4270. }
  4271.  
  4272. toggleAnimations() {
  4273. this.settings.animationsEnabled = !this.settings.animationsEnabled;
  4274. this.updateAnimationStatus();
  4275. this.saveSettings();
  4276. this.showToast(this.settings.animationsEnabled ? 'Animations enabled' : 'Animations disabled');
  4277. }
  4278.  
  4279. updateAnimationStatus() {
  4280. this.elements.animationStatus.textContent = `Animations: ${this.settings.animationsEnabled ? 'On' : 'Off'}`;
  4281. this.elements.animationToggle.setAttribute('aria-pressed', this.settings.animationsEnabled);
  4282. }
  4283.  
  4284. toggleStats() {
  4285. const isExpanded = this.elements.statsContent.classList.toggle('expanded');
  4286. this.elements.statsToggle.textContent = isExpanded ? '▲' : '▼';
  4287. this.elements.statsToggle.setAttribute('aria-expanded', isExpanded);
  4288. }
  4289.  
  4290. showKeyboardShortcuts() {
  4291. this.elements.keyboardShortcuts.classList.add('active');
  4292. this.elements.closeShortcuts.focus(); // Focus the close button
  4293. }
  4294.  
  4295. hideKeyboardShortcuts() {
  4296. this.elements.keyboardShortcuts.classList.remove('active');
  4297. this.elements.helpButton.focus(); // Return focus to the help button
  4298. }
  4299.  
  4300. // --- Event Handlers ---
  4301. setupEventHandlers() {
  4302. // Keyboard
  4303. document.addEventListener('keydown', this.handleKeyDown.bind(this));
  4304.  
  4305. // Touch
  4306. this.elements.gameContainer.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: true });
  4307. this.elements.gameContainer.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
  4308. this.elements.gameContainer.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: true });
  4309.  
  4310. // Focus Management for Game Container
  4311. this.elements.gameContainer.addEventListener('focus', () => this.elements.gameContainer.classList.add('focused'));
  4312. this.elements.gameContainer.addEventListener('blur', () => this.elements.gameContainer.classList.remove('focused'));
  4313.  
  4314.  
  4315. // Buttons
  4316. this.elements.restartButton.addEventListener('click', () => this.startNewGame());
  4317. this.elements.retryButton.addEventListener('click', () => this.startNewGame());
  4318. this.elements.undoButton.addEventListener('click', () => this.undoMove());
  4319. this.elements.keepPlayingButton.addEventListener('click', () => {
  4320. this.keepPlayingMode = true;
  4321. this.hideMessage();
  4322. this.isMoving = false; // Ensure input enabled
  4323. this.showToast("Keep playing!");
  4324. });
  4325. this.elements.themeToggle.addEventListener('click', () => this.toggleTheme());
  4326. this.elements.helpButton.addEventListener('click', () => this.showKeyboardShortcuts());
  4327. this.elements.closeShortcuts.addEventListener('click', () => this.hideKeyboardShortcuts());
  4328. this.elements.keyboardLink.addEventListener('click', (e) => {
  4329. e.preventDefault();
  4330. this.showKeyboardShortcuts();
  4331. });
  4332. this.elements.statsToggle.addEventListener('click', () => this.toggleStats());
  4333. this.elements.animationToggle.addEventListener('click', () => this.toggleAnimations());
  4334.  
  4335. // Window events for saving state
  4336. document.addEventListener('visibilitychange', () => {
  4337. if (document.visibilityState === 'hidden') this.saveGame();
  4338. });
  4339. window.addEventListener('beforeunload', () => this.saveGame());
  4340. }
  4341.  
  4342. handleKeyDown(e) {
  4343. // Handle shortcuts dialog first
  4344. if (this.elements.keyboardShortcuts.classList.contains('active')) {
  4345. if (e.key === 'Escape') {
  4346. e.preventDefault();
  4347. this.hideKeyboardShortcuts();
  4348. }
  4349. return; // Don't process game keys if dialog is open
  4350. }
  4351.  
  4352. if (this.isMoving) return; // Ignore input during animation
  4353.  
  4354. // Always allow help
  4355. if (e.key === '?') {
  4356. e.preventDefault();
  4357. this.showKeyboardShortcuts();
  4358. return;
  4359. }
  4360. // Always allow theme/animation toggle
  4361. if (e.key === 'm' || e.key === 'M') {
  4362. e.preventDefault();
  4363. this.toggleTheme();
  4364. return;
  4365. }
  4366. if (e.key === 't' || e.key === 'T') { // Added 'T' for toggle animations
  4367. e.preventDefault();
  4368. this.toggleAnimations();
  4369. return;
  4370. }
  4371.  
  4372. // Game Over specific actions
  4373. if (this.isGameOver) {
  4374. if (e.key === 'r' || e.key === 'R') {
  4375. e.preventDefault();
  4376. this.startNewGame();
  4377. }
  4378. // Allow undo even when game over if possible
  4379. else if ((e.key === 'u' || e.key === 'U' || e.key === 'z' || e.key === 'Z') && !this.elements.undoButton.disabled) {
  4380. e.preventDefault();
  4381. this.undoMove();
  4382. }
  4383. return; // No other actions when game over
  4384. }
  4385.  
  4386. // Active Game Actions
  4387. let direction = null;
  4388. switch (e.key) {
  4389. case 'ArrowUp': case 'w': case 'W': direction = 'up'; break;
  4390. case 'ArrowDown': case 's': case 'S': direction = 'down'; break;
  4391. case 'ArrowLeft': case 'a': case 'A': direction = 'left'; break;
  4392. case 'ArrowRight': case 'd': case 'D': direction = 'right'; break;
  4393. case 'r': case 'R':
  4394. e.preventDefault(); this.startNewGame(); return;
  4395. case 'u': case 'U': case 'z': case 'Z':
  4396. e.preventDefault(); if (!this.elements.undoButton.disabled) this.undoMove(); return;
  4397. }
  4398.  
  4399. if (direction) {
  4400. e.preventDefault(); // Prevent scrolling
  4401. this.move(direction);
  4402. }
  4403. }
  4404.  
  4405. handleTouchStart(e) {
  4406. if (this.isMoving || e.touches.length > 1) return;
  4407. this.touchStartX = e.touches[0].clientX;
  4408. this.touchStartY = e.touches[0].clientY;
  4409. this.touchEndX = this.touchStartX;
  4410. this.touchEndY = this.touchStartY;
  4411. }
  4412.  
  4413. handleTouchMove(e) {
  4414. if (this.isMoving || e.touches.length > 1) return;
  4415. this.touchEndX = e.touches[0].clientX;
  4416. this.touchEndY = e.touches[0].clientY;
  4417. // Prevent scrolling ONLY if a swipe is likely vertical/horizontal
  4418. const deltaX = this.touchEndX - this.touchStartX;
  4419. const deltaY = this.touchEndY - this.touchStartY;
  4420. if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) { // Simple threshold
  4421. e.preventDefault();
  4422. }
  4423. }
  4424.  
  4425. handleTouchEnd(e) {
  4426. if (this.isMoving || this.isGameOver || e.changedTouches.length > 1) return;
  4427.  
  4428. const deltaX = this.touchEndX - this.touchStartX;
  4429. const deltaY = this.touchEndY - this.touchStartY;
  4430. const absDeltaX = Math.abs(deltaX);
  4431. const absDeltaY = Math.abs(deltaY);
  4432.  
  4433. if (Math.max(absDeltaX, absDeltaY) < SWIPE_THRESHOLD) return; // Not a swipe
  4434.  
  4435. let direction = absDeltaX > absDeltaY
  4436. ? (deltaX > 0 ? 'right' : 'left')
  4437. : (deltaY > 0 ? 'down' : 'up');
  4438.  
  4439. this.move(direction);
  4440. }
  4441. }
  4442.  
  4443. // --- App Initialization ---
  4444. const gameElements = {
  4445. gameContainer: document.getElementById('game-container'),
  4446. gridBackground: document.getElementById('grid-background'),
  4447. tileContainer: document.getElementById('tile-container'),
  4448. scoreElement: document.getElementById('score'),
  4449. bestScoreElement: document.getElementById('best-score'),
  4450. messageContainer: document.getElementById('game-message'),
  4451. messageText: document.getElementById('message-text'),
  4452. restartButton: document.getElementById('restart-button'),
  4453. retryButton: document.getElementById('retry-button'),
  4454. keepPlayingButton: document.getElementById('keep-playing-button'),
  4455. undoButton: document.getElementById('undo-button'),
  4456. themeToggle: document.getElementById('theme-toggle'),
  4457. helpButton: document.getElementById('help-button'),
  4458. keyboardShortcuts: document.getElementById('keyboard-shortcuts'),
  4459. closeShortcuts: document.getElementById('close-shortcuts'),
  4460. keyboardLink: document.getElementById('keyboard-link'),
  4461. toast: document.getElementById('toast'),
  4462. statsToggle: document.getElementById('stats-toggle'),
  4463. statsContent: document.getElementById('stats-content'),
  4464. gamesPlayedElement: document.getElementById('games-played'),
  4465. highestTileElement: document.getElementById('highest-tile'),
  4466. totalMovesElement: document.getElementById('total-moves'),
  4467. totalScoreElement: document.getElementById('total-score'),
  4468. avgScoreElement: document.getElementById('avg-score'),
  4469. animationToggle: document.getElementById('animation-toggle'),
  4470. animationStatus: document.getElementById('animation-status')
  4471. };
  4472.  
  4473. const game = new Game(gameElements);
  4474. game.init();
  4475.  
  4476. });
  4477. </script>
  4478. </body>
  4479. </html>
  4480. ```
  4481.  
  4482. ## Explanation of Changes
  4483.  
  4484. 1. **`Game` Class:**
  4485. * The core logic and state are now encapsulated within the `Game` class.
  4486. * DOM element references are passed into the constructor and stored in `this.elements`.
  4487. * Game state variables (`grid`, `score`, `isGameOver`, `history`, `stats`, `settings`, etc.) are now properties of the `Game` instance (`this.score`, `this.settings.darkMode`, etc.).
  4488. * All major functions (`move`, `undoMove`, `addRandomTile`, `saveGame`, `loadGame`, `updateScore`, `showMessage`, etc.) are now methods of the `Game` class.
  4489. * Touch state variables (`touchStartX`, etc.) are also moved into the class instance to avoid global scope.
  4490.  
  4491. 2. **Initialization (`Game.init`)**:
  4492. * A new `init` method handles the setup sequence: loading settings/stats/scores, creating the grid, setting up event handlers, and loading a saved game or starting a new one.
  4493. * This makes the initial `DOMContentLoaded` callback much cleaner, simply creating the `Game` instance and calling `init()`.
  4494.  
  4495. 3. **Event Handling:**
  4496. * Event listeners (for keys, buttons, touch, window events) now call methods on the `game` instance (e.g., `this.handleKeyDown.bind(this)`, `() => game.startNewGame()`). Using `.bind(this)` ensures that `this` inside the handler refers to the `Game` instance.
  4497.  
  4498. 4. **`Tile` Class Modifications:**
  4499. * The `Tile` constructor now accepts the `parentContainer` (the `#tile-container` div) and a reference to the `game.settings` object.
  4500. * This allows the `Tile` instance to know where to append itself and to check `settings.animationsEnabled` directly when deciding whether to apply animation classes or transitions.
  4501.  
  4502. 5. **Constants:** Defined constants for `SWIPE_THRESHOLD` and grouped `LOCAL_STORAGE_KEYS` for better maintainability and to avoid typos.
  4503.  
  4504. 6. **UI Updates:** UI update methods (`updateScoreDisplay`, `updateBestScoreDisplay`, `updateUndoButton`, etc.) are now part of the `Game` class, clearly separating logic from presentation updates. A central `updateUI` method is called after actions to refresh relevant parts of the display.
  4505.  
  4506. 7. **Persistence:** Save/load methods are now part of the `Game` class. Basic `try...catch` blocks are added around `localStorage` operations to prevent errors if storage is disabled or full, showing a toast message on failure. Saving now explicitly avoids saving completed games.
  4507.  
  4508. 8. **Accessibility:** Added `aria-label` to the game board container. Added `aria-expanded` to the stats toggle button and `aria-pressed` to the animation toggle. Improved focus management for the keyboard shortcuts dialog. Added focus style for the game container itself.
  4509.  
  4510. 9. **Clarity:** Renamed "Total Moves" statistic to "Total Moves Made" and "Total Score" to "Total Score Accumulated" for slightly better clarity. Added `T` as a keyboard shortcut for toggling animations.
  4511.  
  4512. This refactoring into a `Game` class significantly improves the structure and maintainability of the code without drastically changing the features or core logic you implemented. It provides a solid foundation for any future enhancements.
  4513.  
  4514. ## Claude (Iteration 3)
  4515.  
  4516. # Enhanced 2048 Game - Final Collaborative Edition
  4517.  
  4518. I've reviewed Gemini's excellent refactoring of our code, which significantly improved the architecture by introducing a proper `Game` class. This provides better encapsulation and organization. I'll now build upon this foundation to further enhance the game's architecture, fix some issues, and add new features.
  4519.  
  4520. ## Key Improvements
  4521.  
  4522. 1. **Architectural Refinements**
  4523. - Add a proper `UIManager` class to separate UI concerns from game logic
  4524. - Implement Promise-based animation handling
  4525. - Add a `GridManager` class to handle grid operations
  4526. - Improve event handling with a central event manager
  4527.  
  4528. 2. **New Features**
  4529. - Auto-save functionality with timestamps and named saves
  4530. - Game replay functionality
  4531. - Achievement system
  4532. - Game board size options (3x3, 4x4, 5x5, 6x6)
  4533. - Difficulty levels (affects probability of 4s vs 2s)
  4534.  
  4535. 3. **Bug Fixes & Optimizations**
  4536. - Fix animation synchronization issues
  4537. - Improve touch handling
  4538. - Optimize performance with better DOM management
  4539. - Better error handling
  4540.  
  4541. 4. **Accessibility & UX Improvements**
  4542. - Enhanced keyboard navigation
  4543. - Improved focus management
  4544. - Better screen reader support
  4545. - More descriptive game messages
  4546.  
  4547. ## The Final Implementation
  4548.  
  4549. Here's the enhanced version:
  4550.  
  4551. ```html
  4552. <!DOCTYPE html>
  4553. <html lang="en">
  4554. <head>
  4555. <meta charset="UTF-8">
  4556. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  4557. <title>2048 - Ultimate Edition</title>
  4558. <style>
  4559. :root {
  4560. --grid-size: 4;
  4561. --cell-size: 100px;
  4562. --cell-gap: 12px;
  4563. --border-radius: 8px;
  4564. --animation-speed: 150ms;
  4565. --appear-speed: 200ms;
  4566. --pop-speed: 200ms;
  4567.  
  4568. /* Color scheme - light mode (default) */
  4569. --bg-color: #faf8ef;
  4570. --container-bg: #bbada0;
  4571. --cell-bg: rgba(238, 228, 218, 0.35);
  4572. --title-color: #776e65;
  4573. --text-color: #776e65;
  4574. --button-bg: #8f7a66;
  4575. --button-hover-bg: #9f8b77;
  4576. --button-text: #f9f6f2;
  4577. --overlay-bg: rgba(238, 228, 218, 0.73);
  4578. --tile-2-bg: #eee4da;
  4579. --tile-2-text: #776e65;
  4580. --tile-4-bg: #ede0c8;
  4581. --tile-4-text: #776e65;
  4582. --tile-8-bg: #f2b179;
  4583. --tile-8-text: #f9f6f2;
  4584. --tile-16-bg: #f59563;
  4585. --tile-16-text: #f9f6f2;
  4586. --tile-32-bg: #f67c5f;
  4587. --tile-32-text: #f9f6f2;
  4588. --tile-64-bg: #f65e3b;
  4589. --tile-64-text: #f9f6f2;
  4590. --tile-128-bg: #edcf72;
  4591. --tile-128-text: #f9f6f2;
  4592. --tile-256-bg: #edcc61;
  4593. --tile-256-text: #f9f6f2;
  4594. --tile-512-bg: #edc850;
  4595. --tile-512-text: #f9f6f2;
  4596. --tile-1024-bg: #edc53f;
  4597. --tile-1024-text: #f9f6f2;
  4598. --tile-2048-bg: #edc22e;
  4599. --tile-2048-text: #f9f6f2;
  4600. --tile-super-bg: #3c3a32;
  4601. --tile-super-text: #f9f6f2;
  4602. --modal-bg: rgba(250, 248, 239, 0.95);
  4603. --modal-text: #776e65;
  4604. --modal-border: #bbada0;
  4605. --achievement-bg: #8f7a66;
  4606. --achievement-text: #f9f6f2;
  4607. }
  4608.  
  4609. /* Dark mode color scheme */
  4610. [data-theme="dark"] {
  4611. --bg-color: #1f2937;
  4612. --container-bg: #374151;
  4613. --cell-bg: rgba(55, 65, 81, 0.7);
  4614. --title-color: #e5e7eb;
  4615. --text-color: #e5e7eb;
  4616. --button-bg: #4b5563;
  4617. --button-hover-bg: #6b7280;
  4618. --button-text: #f9fafb;
  4619. --overlay-bg: rgba(31, 41, 55, 0.8);
  4620. --tile-2-bg: #9ca3af;
  4621. --tile-2-text: #1f2937;
  4622. --tile-4-bg: #d1d5db;
  4623. --tile-4-text: #1f2937;
  4624. --modal-bg: rgba(31, 41, 55, 0.95);
  4625. --modal-text: #e5e7eb;
  4626. --modal-border: #4b5563;
  4627. --achievement-bg: #4b5563;
  4628. --achievement-text: #f9fafb;
  4629. }
  4630.  
  4631. @media (prefers-color-scheme: dark) {
  4632. :root:not([data-theme="light"]) {
  4633. --bg-color: #1f2937;
  4634. --container-bg: #374151;
  4635. --cell-bg: rgba(55, 65, 81, 0.7);
  4636. --title-color: #e5e7eb;
  4637. --text-color: #e5e7eb;
  4638. --button-bg: #4b5563;
  4639. --button-hover-bg: #6b7280;
  4640. --button-text: #f9fafb;
  4641. --overlay-bg: rgba(31, 41, 55, 0.8);
  4642. --tile-2-bg: #9ca3af;
  4643. --tile-2-text: #1f2937;
  4644. --tile-4-bg: #d1d5db;
  4645. --tile-4-text: #1f2937;
  4646. --modal-bg: rgba(31, 41, 55, 0.95);
  4647. --modal-text: #e5e7eb;
  4648. --modal-border: #4b5563;
  4649. --achievement-bg: #4b5563;
  4650. --achievement-text: #f9fafb;
  4651. }
  4652. }
  4653.  
  4654. * {
  4655. box-sizing: border-box;
  4656. margin: 0;
  4657. padding: 0;
  4658. font-family: 'Arial', sans-serif;
  4659. }
  4660.  
  4661. body {
  4662. background-color: var(--bg-color);
  4663. color: var(--text-color);
  4664. display: flex;
  4665. flex-direction: column;
  4666. justify-content: center;
  4667. align-items: center;
  4668. min-height: 100vh;
  4669. overflow-y: auto;
  4670. touch-action: none;
  4671. padding: 10px;
  4672. transition: background-color 0.3s ease, color 0.3s ease;
  4673. }
  4674.  
  4675. .container {
  4676. width: min(95vw, 500px);
  4677. padding: 15px;
  4678. border-radius: 10px;
  4679. background-color: var(--container-bg);
  4680. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
  4681. transition: background-color 0.3s ease;
  4682. }
  4683.  
  4684. .header {
  4685. display: flex;
  4686. justify-content: space-between;
  4687. align-items: center;
  4688. margin-bottom: 20px;
  4689. flex-wrap: wrap;
  4690. gap: 10px;
  4691. }
  4692.  
  4693. .title-container {
  4694. display: flex;
  4695. align-items: center;
  4696. gap: 10px;
  4697. }
  4698.  
  4699. .title {
  4700. color: var(--title-color);
  4701. font-size: clamp(2rem, 8vw, 2.5rem);
  4702. font-weight: bold;
  4703. transition: color 0.3s ease;
  4704. }
  4705.  
  4706. .theme-toggle {
  4707. background: none;
  4708. border: none;
  4709. cursor: pointer;
  4710. font-size: 1.5rem;
  4711. color: var(--text-color);
  4712. transition: transform 0.3s ease;
  4713. padding: 5px;
  4714. margin-left: 10px;
  4715. }
  4716.  
  4717. .theme-toggle:hover {
  4718. transform: rotate(30deg);
  4719. }
  4720.  
  4721. .scores {
  4722. display: flex;
  4723. gap: 10px;
  4724. }
  4725.  
  4726. .score-box {
  4727. background-color: var(--button-bg);
  4728. color: var(--button-text);
  4729. padding: 8px 12px;
  4730. border-radius: 5px;
  4731. display: flex;
  4732. flex-direction: column;
  4733. align-items: center;
  4734. min-width: 70px;
  4735. text-align: center;
  4736. transition: background-color 0.3s ease;
  4737. }
  4738.  
  4739. .score-title {
  4740. font-size: 0.7rem;
  4741. text-transform: uppercase;
  4742. margin-bottom: 2px;
  4743. }
  4744.  
  4745. .score-value {
  4746. font-size: clamp(1.2rem, 5vw, 1.5rem);
  4747. font-weight: bold;
  4748. }
  4749.  
  4750. .sub-header {
  4751. display: flex;
  4752. justify-content: space-between;
  4753. align-items: center;
  4754. margin-bottom: 20px;
  4755. flex-wrap: wrap;
  4756. gap: 10px;
  4757. }
  4758.  
  4759. .game-intro {
  4760. color: var(--text-color);
  4761. flex-basis: 60%;
  4762. font-size: 0.9rem;
  4763. line-height: 1.3;
  4764. transition: color 0.3s ease;
  4765. }
  4766.  
  4767. .button-container {
  4768. display: flex;
  4769. gap: 8px;
  4770. flex-wrap: wrap;
  4771. }
  4772.  
  4773. .btn {
  4774. background-color: var(--button-bg);
  4775. color: var(--button-text);
  4776. border: none;
  4777. border-radius: 5px;
  4778. padding: 10px 15px;
  4779. font-weight: bold;
  4780. cursor: pointer;
  4781. transition: background-color 0.2s ease, transform 0.1s ease;
  4782. white-space: nowrap;
  4783. display: flex;
  4784. align-items: center;
  4785. justify-content: center;
  4786. min-width: 40px;
  4787. }
  4788.  
  4789. .btn:hover, .btn:focus {
  4790. background-color: var(--button-hover-bg);
  4791. outline: none;
  4792. }
  4793.  
  4794. .btn:active {
  4795. transform: scale(0.95);
  4796. }
  4797.  
  4798. .btn:disabled {
  4799. opacity: 0.5;
  4800. cursor: not-allowed;
  4801. }
  4802.  
  4803. .btn-icon {
  4804. font-size: 1.2rem;
  4805. line-height: 1;
  4806. }
  4807.  
  4808. .dropdown {
  4809. position: relative;
  4810. display: inline-block;
  4811. }
  4812.  
  4813. .dropdown-content {
  4814. display: none;
  4815. position: absolute;
  4816. background-color: var(--bg-color);
  4817. min-width: 160px;
  4818. box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
  4819. z-index: 1000;
  4820. border-radius: 5px;
  4821. right: 0;
  4822. }
  4823.  
  4824. .dropdown-content a {
  4825. color: var(--text-color);
  4826. padding: 12px 16px;
  4827. text-decoration: none;
  4828. display: block;
  4829. transition: background-color 0.2s;
  4830. }
  4831.  
  4832. .dropdown-content a:hover {
  4833. background-color: var(--container-bg);
  4834. }
  4835.  
  4836. .dropdown:hover .dropdown-content {
  4837. display: block;
  4838. }
  4839.  
  4840. .game-container {
  4841. position: relative;
  4842. width: calc(var(--grid-size) * (var(--cell-size) + var(--cell-gap)) + var(--cell-gap));
  4843. height: calc(var(--grid-size) * (var(--cell-size) + var(--cell-gap)) + var(--cell-gap));
  4844. background-color: var(--container-bg);
  4845. border-radius: var(--border-radius);
  4846. padding: var(--cell-gap);
  4847. margin: 0 auto;
  4848. overflow: hidden;
  4849. transition: width 0.3s ease, height 0.3s ease, background-color 0.3s ease;
  4850. }
  4851.  
  4852. .game-container.focused {
  4853. outline: 3px solid #4c9aff;
  4854. outline-offset: 3px;
  4855. }
  4856.  
  4857. .grid-background {
  4858. position: absolute;
  4859. top: var(--cell-gap);
  4860. left: var(--cell-gap);
  4861. bottom: var(--cell-gap);
  4862. right: var(--cell-gap);
  4863. display: grid;
  4864. grid-template-columns: repeat(var(--grid-size), var(--cell-size));
  4865. grid-template-rows: repeat(var(--grid-size), var(--cell-size));
  4866. gap: var(--cell-gap);
  4867. transition: grid-template-columns 0.3s ease, grid-template-rows 0.3s ease;
  4868. }
  4869.  
  4870. .cell {
  4871. background-color: var(--cell-bg);
  4872. border-radius: var(--border-radius);
  4873. transition: background-color 0.3s ease;
  4874. }
  4875.  
  4876. .tile-container {
  4877. position: absolute;
  4878. top: 0;
  4879. left: 0;
  4880. right: 0;
  4881. bottom: 0;
  4882. z-index: 5;
  4883. }
  4884.  
  4885. .tile {
  4886. position: absolute;
  4887. display: flex;
  4888. justify-content: center;
  4889. align-items: center;
  4890. width: var(--cell-size);
  4891. height: var(--cell-size);
  4892. border-radius: var(--border-radius);
  4893. font-size: clamp(1.5rem, 7vw, 2rem);
  4894. font-weight: bold;
  4895. background-color: var(--tile-2-bg);
  4896. color: var(--tile-2-text);
  4897. transition: top var(--animation-speed) ease-in-out,
  4898. left var(--animation-speed) ease-in-out,
  4899. width var(--animation-speed) ease-in-out,
  4900. height var(--animation-speed) ease-in-out,
  4901. background-color 0.3s ease,
  4902. color 0.3s ease;
  4903. z-index: 10;
  4904. user-select: none;
  4905. will-change: transform, opacity;
  4906. }
  4907.  
  4908. .tile.new-tile {
  4909. animation: appear var(--appear-speed) ease-in-out;
  4910. }
  4911.  
  4912. @keyframes appear {
  4913. 0% {
  4914. opacity: 0;
  4915. transform: scale(0);
  4916. }
  4917. 100% {
  4918. opacity: 1;
  4919. transform: scale(1);
  4920. }
  4921. }
  4922.  
  4923. .tile.merged-tile {
  4924. animation: pop var(--pop-speed) ease-in-out;
  4925. z-index: 20;
  4926. }
  4927.  
  4928. @keyframes pop {
  4929. 0% {
  4930. transform: scale(1);
  4931. }
  4932. 50% {
  4933. transform: scale(1.2);
  4934. }
  4935. 100% {
  4936. transform: scale(1);
  4937. }
  4938. }
  4939.  
  4940. .tile[data-value="2"] { background-color: var(--tile-2-bg); color: var(--tile-2-text); }
  4941. .tile[data-value="4"] { background-color: var(--tile-4-bg); color: var(--tile-4-text); }
  4942. .tile[data-value="8"] { background-color: var(--tile-8-bg); color: var(--tile-8-text); }
  4943. .tile[data-value="16"] { background-color: var(--tile-16-bg); color: var(--tile-16-text); }
  4944. .tile[data-value="32"] { background-color: var(--tile-32-bg); color: var(--tile-32-text); }
  4945. .tile[data-value="64"] { background-color: var(--tile-64-bg); color: var(--tile-64-text); }
  4946. .tile[data-value="128"] { background-color: var(--tile-128-bg); color: var(--tile-128-text); font-size: clamp(1.3rem, 6vw, 1.8rem); }
  4947. .tile[data-value="256"] { background-color: var(--tile-256-bg); color: var(--tile-256-text); font-size: clamp(1.3rem, 6vw, 1.8rem); }
  4948. .tile[data-value="512"] { background-color: var(--tile-512-bg); color: var(--tile-512-text); font-size: clamp(1.3rem, 6vw, 1.8rem); }
  4949. .tile[data-value="1024"] { background-color: var(--tile-1024-bg); color: var(--tile-1024-text); font-size: clamp(1.1rem, 5vw, 1.5rem); }
  4950. .tile[data-value="2048"] { background-color: var(--tile-2048-bg); color: var(--tile-2048-text); font-size: clamp(1.1rem, 5vw, 1.5rem); }
  4951. .tile[data-value="4096"],
  4952. .tile[data-value="8192"],
  4953. .tile[data-value="16384"],
  4954. .tile[data-value="32768"],
  4955. .tile[data-value="65536"] {
  4956. background-color: var(--tile-super-bg);
  4957. color: var(--tile-super-text);
  4958. font-size: clamp(1rem, 4.5vw, 1.4rem);
  4959. }
  4960.  
  4961. .game-message {
  4962. display: none;
  4963. position: absolute;
  4964. top: 0;
  4965. right: 0;
  4966. bottom: 0;
  4967. left: 0;
  4968. background-color: var(--overlay-bg);
  4969. flex-direction: column;
  4970. align-items: center;
  4971. justify-content: center;
  4972. text-align: center;
  4973. border-radius: var(--border-radius);
  4974. z-index: 100;
  4975. opacity: 0;
  4976. transition: opacity 0.3s ease-in-out, background-color 0.3s ease;
  4977. animation: fadein 0.3s ease-in-out forwards;
  4978. }
  4979.  
  4980. .game-message.active {
  4981. display: flex;
  4982. opacity: 1;
  4983. }
  4984.  
  4985. @keyframes fadein {
  4986. from { opacity: 0; }
  4987. to { opacity: 1; }
  4988. }
  4989.  
  4990. .game-message p {
  4991. font-size: clamp(1.8rem, 7vw, 2rem);
  4992. font-weight: bold;
  4993. color: var(--title-color);
  4994. margin-bottom: 20px;
  4995. transition: color 0.3s ease;
  4996. }
  4997.  
  4998. .game-message .lower {
  4999. display: flex;
  5000. gap: 10px;
  5001. margin-top: 10px;
  5002. flex-wrap: wrap;
  5003. justify-content: center;
  5004. }
  5005.  
  5006. .score-addition {
  5007. position: fixed;
  5008. animation: score-pop 0.8s ease-in-out forwards;
  5009. font-weight: bold;
  5010. color: var(--title-color);
  5011. z-index: 100;
  5012. pointer-events: none;
  5013. font-size: 1.2rem;
  5014. opacity: 0;
  5015. transition: color 0.3s ease;
  5016. }
  5017.  
  5018. @keyframes score-pop {
  5019. 0% { opacity: 1; transform: translate(-50%, 0); }
  5020. 100% { opacity: 0; transform: translate(-50%, -30px); }
  5021. }
  5022.  
  5023. .stats-container {
  5024. margin-top: 10px;
  5025. background-color: var(--container-bg);
  5026. border-radius: 8px;
  5027. padding: 10px 15px;
  5028. display: flex;
  5029. flex-direction: column;
  5030. gap: 5px;
  5031. width: 100%;
  5032. transition: background-color 0.3s ease;
  5033. }
  5034.  
  5035. .stats-title {
  5036. font-weight: bold;
  5037. color: var(--title-color);
  5038. font-size: 1.1rem;
  5039. margin-bottom: 5px;
  5040. display: flex;
  5041. justify-content: space-between;
  5042. align-items: center;
  5043. transition: color 0.3s ease;
  5044. }
  5045.  
  5046. .stats-row {
  5047. display: flex;
  5048. justify-content: space-between;
  5049. font-size: 0.9rem;
  5050. color: var(--text-color);
  5051. transition: color 0.3s ease;
  5052. }
  5053.  
  5054. .stats-value {
  5055. font-weight: bold;
  5056. }
  5057.  
  5058. .stats-toggle {
  5059. font-size: 1rem;
  5060. cursor: pointer;
  5061. background: none;
  5062. border: none;
  5063. color: var(--text-color);
  5064. transition: transform 0.3s ease, color 0.3s ease;
  5065. }
  5066.  
  5067. .stats-toggle:hover {
  5068. transform: rotate(180deg);
  5069. }
  5070.  
  5071. .stats-content {
  5072. overflow: hidden;
  5073. max-height: 0;
  5074. transition: max-height 0.3s ease-in-out;
  5075. }
  5076.  
  5077. .stats-content.expanded {
  5078. max-height: 300px;
  5079. }
  5080.  
  5081. /* Modal styles */
  5082. .modal {
  5083. display: none;
  5084. position: fixed;
  5085. top: 0;
  5086. left: 0;
  5087. right: 0;
  5088. bottom: 0;
  5089. background-color: rgba(0, 0, 0, 0.5);
  5090. z-index: 1000;
  5091. justify-content: center;
  5092. align-items: center;
  5093. padding: 20px;
  5094. }
  5095.  
  5096. .modal.active {
  5097. display: flex;
  5098. }
  5099.  
  5100. .modal-content {
  5101. background-color: var(--modal-bg);
  5102. border-radius: 10px;
  5103. padding: 20px;
  5104. max-width: 90%;
  5105. width: 400px;
  5106. max-height: 80vh;
  5107. overflow-y: auto;
  5108. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
  5109. position: relative;
  5110. color: var(--modal-text);
  5111. border: 2px solid var(--modal-border);
  5112. }
  5113.  
  5114. .modal-title {
  5115. font-size: 1.5rem;
  5116. font-weight: bold;
  5117. margin-bottom: 15px;
  5118. color: var(--title-color);
  5119. }
  5120.  
  5121. .modal-close {
  5122. position: absolute;
  5123. top: 10px;
  5124. right: 10px;
  5125. background: none;
  5126. border: none;
  5127. font-size: 1.5rem;
  5128. cursor: pointer;
  5129. color: var(--text-color);
  5130. transition: transform 0.3s ease;
  5131. }
  5132.  
  5133. .modal-close:hover {
  5134. transform: rotate(90deg);
  5135. }
  5136.  
  5137. .settings-group {
  5138. margin-bottom: 15px;
  5139. }
  5140.  
  5141. .settings-label {
  5142. display: block;
  5143. margin-bottom: 5px;
  5144. font-weight: bold;
  5145. }
  5146.  
  5147. .settings-select {
  5148. width: 100%;
  5149. padding: 8px;
  5150. border-radius: 5px;
  5151. border: 1px solid var(--container-bg);
  5152. background-color: var(--bg-color);
  5153. color: var(--text-color);
  5154. margin-bottom: 10px;
  5155. }
  5156.  
  5157. /* Game Size Controls */
  5158. .size-controls {
  5159. display: flex;
  5160. justify-content: space-between;
  5161. margin-top: 10px;
  5162. margin-bottom: 10px;
  5163. }
  5164.  
  5165. .size-btn {
  5166. background-color: var(--button-bg);
  5167. color: var(--button-text);
  5168. border: none;
  5169. border-radius: 5px;
  5170. padding: 8px 12px;
  5171. cursor: pointer;
  5172. transition: background-color 0.2s;
  5173. }
  5174.  
  5175. .size-btn:hover {
  5176. background-color: var(--button-hover-bg);
  5177. }
  5178.  
  5179. .size-btn.active {
  5180. background-color: var(--title-color);
  5181. }
  5182.  
  5183. /* Saved Games List */
  5184. .saved-games-list {
  5185. max-height: 300px;
  5186. overflow-y: auto;
  5187. margin-top: 10px;
  5188. }
  5189.  
  5190. .saved-game-item {
  5191. display: flex;
  5192. justify-content: space-between;
  5193. align-items: center;
  5194. padding: 10px;
  5195. border-bottom: 1px solid var(--container-bg);
  5196. transition: background-color 0.2s;
  5197. }
  5198.  
  5199. .saved-game-item:hover {
  5200. background-color: var(--container-bg);
  5201. }
  5202.  
  5203. .saved-game-info {
  5204. flex-grow: 1;
  5205. }
  5206.  
  5207. .saved-game-title {
  5208. font-weight: bold;
  5209. margin-bottom: 5px;
  5210. }
  5211.  
  5212. .saved-game-meta {
  5213. font-size: 0.8rem;
  5214. color: var(--text-color);
  5215. opacity: 0.8;
  5216. }
  5217.  
  5218. .saved-game-actions {
  5219. display: flex;
  5220. gap: 5px;
  5221. }
  5222.  
  5223. /* Achievement notification */
  5224. .achievement {
  5225. position: fixed;
  5226. top: 20px;
  5227. right: 20px;
  5228. background-color: var(--achievement-bg);
  5229. color: var(--achievement-text);
  5230. padding: 15px;
  5231. border-radius: 5px;
  5232. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
  5233. z-index: 1000;
  5234. transition: transform 0.3s ease, opacity 0.3s ease;
  5235. transform: translateX(120%);
  5236. opacity: 0;
  5237. max-width: 300px;
  5238. }
  5239.  
  5240. .achievement.active {
  5241. transform: translateX(0);
  5242. opacity: 1;
  5243. }
  5244.  
  5245. .achievement-title {
  5246. font-weight: bold;
  5247. margin-bottom: 5px;
  5248. font-size: 1.1rem;
  5249. }
  5250.  
  5251. .achievement-description {
  5252. font-size: 0.9rem;
  5253. }
  5254.  
  5255. /* Achievements list */
  5256. .achievements-list {
  5257. display: grid;
  5258. grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  5259. gap: 10px;
  5260. margin-top: 10px;
  5261. }
  5262.  
  5263. .achievement-item {
  5264. background-color: var(--container-bg);
  5265. border-radius: 5px;
  5266. padding: 10px;
  5267. opacity: 0.6;
  5268. transition: opacity 0.3s;
  5269. }
  5270.  
  5271. .achievement-item.unlocked {
  5272. opacity: 1;
  5273. }
  5274.  
  5275. .achievement-item-title {
  5276. font-weight: bold;
  5277. margin-bottom: 5px;
  5278. }
  5279.  
  5280. .achievement-item-description {
  5281. font-size: 0.9rem;
  5282. }
  5283.  
  5284. /* Game history controls */
  5285. .history-controls {
  5286. display: flex;
  5287. justify-content: center;
  5288. gap: 10px;
  5289. margin-top: 10px;
  5290. }
  5291.  
  5292. /* Responsive adjustments */
  5293. @media screen and (max-width: 520px) {
  5294. :root {
  5295. --cell-size: 65px;
  5296. --cell-gap: 8px;
  5297. }
  5298. .game-intro { flex-basis: 100%; text-align: center; margin-bottom: 10px; }
  5299. .btn { padding: 8px 12px; }
  5300. .button-container { justify-content: center; width: 100%; }
  5301. }
  5302.  
  5303. @media screen and (max-width: 420px) {
  5304. :root {
  5305. --cell-size: 60px;
  5306. --cell-gap: 7px;
  5307. }
  5308. }
  5309.  
  5310. @media screen and (max-width: 380px) {
  5311. :root {
  5312. --cell-size: 55px;
  5313. --cell-gap: 6px;
  5314. }
  5315. .container { padding: 10px; }
  5316. .header { justify-content: center; }
  5317. .title { text-align: center; width: 100%; margin-bottom: 5px;}
  5318. .sub-header { justify-content: center; }
  5319. .score-box { min-width: 60px; padding: 6px 10px; }
  5320. .button-container { flex-wrap: wrap; }
  5321. }
  5322.  
  5323. @media screen and (max-width: 340px) {
  5324. :root {
  5325. --cell-size: 50px;
  5326. --cell-gap: 5px;
  5327. }
  5328. }
  5329.  
  5330. .footer {
  5331. margin-top: 20px;
  5332. text-align: center;
  5333. color: var(--text-color);
  5334. font-size: 0.8rem;
  5335. transition: color 0.3s ease;
  5336. display: flex;
  5337. flex-direction: column;
  5338. gap: 5px;
  5339. align-items: center;
  5340. }
  5341.  
  5342. .controls-row {
  5343. display: flex;
  5344. gap: 10px;
  5345. flex-wrap: wrap;
  5346. justify-content: center;
  5347. }
  5348.  
  5349. .animation-toggle {
  5350. border: none;
  5351. background: none;
  5352. cursor: pointer;
  5353. font-size: 1.2rem;
  5354. color: var(--text-color);
  5355. transition: color 0.3s ease;
  5356. display: flex;
  5357. align-items: center;
  5358. gap: 5px;
  5359. }
  5360.  
  5361. .animation-toggle:hover {
  5362. color: var(--title-color);
  5363. }
  5364.  
  5365. .animation-status {
  5366. font-size: 0.8rem;
  5367. }
  5368.  
  5369. .keyboard-shortcuts {
  5370. position: fixed;
  5371. top: 0;
  5372. left: 0;
  5373. right: 0;
  5374. bottom: 0;
  5375. background-color: var(--overlay-bg);
  5376. z-index: 1000;
  5377. display: none;
  5378. justify-content: center;
  5379. align-items: center;
  5380. transition: background-color 0.3s ease;
  5381. }
  5382.  
  5383. .keyboard-shortcuts.active {
  5384. display: flex;
  5385. }
  5386.  
  5387. .shortcuts-content {
  5388. background-color: var(--bg-color);
  5389. border-radius: 10px;
  5390. padding: 20px;
  5391. max-width: 90%;
  5392. width: 400px;
  5393. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
  5394. position: relative;
  5395. transition: background-color 0.3s ease;
  5396. }
  5397.  
  5398. .shortcuts-title {
  5399. font-size: 1.5rem;
  5400. font-weight: bold;
  5401. margin-bottom: 15px;
  5402. color: var(--title-color);
  5403. transition: color 0.3s ease;
  5404. }
  5405.  
  5406. .shortcut-grid {
  5407. display: grid;
  5408. grid-template-columns: auto 1fr;
  5409. gap: 10px 20px;
  5410. margin-bottom: 20px;
  5411. color: var(--text-color);
  5412. }
  5413.  
  5414. .key {
  5415. background-color: var(--container-bg);
  5416. color: var(--text-color);
  5417. padding: 5px 10px;
  5418. border-radius: 5px;
  5419. font-family: monospace;
  5420. text-align: center;
  5421. font-weight: bold;
  5422. transition: background-color 0.3s ease, color 0.3s ease;
  5423. }
  5424.  
  5425. .close-shortcuts {
  5426. position: absolute;
  5427. top: 10px;
  5428. right: 10px;
  5429. background: none;
  5430. border: none;
  5431. font-size: 1.5rem;
  5432. cursor: pointer;
  5433. color: var(--text-color);
  5434. transition: transform 0.3s ease, color 0.3s ease;
  5435. }
  5436.  
  5437. .close-shortcuts:hover {
  5438. transform: rotate(90deg);
  5439. }
  5440.  
  5441. .toast {
  5442. position: fixed;
  5443. bottom: 20px;
  5444. left: 50%;
  5445. transform: translateX(-50%);
  5446. background-color: var(--container-bg);
  5447. color: var(--text-color);
  5448. padding: 10px 20px;
  5449. border-radius: 5px;
  5450. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
  5451. z-index: 1000;
  5452. transition: opacity 0.3s ease, transform 0.3s ease, background-color 0.3s ease, color 0.3s ease;
  5453. opacity: 0;
  5454. transform: translate(-50%, 20px);
  5455. pointer-events: none;
  5456. }
  5457.  
  5458. .toast.active {
  5459. opacity: 1;
  5460. transform: translate(-50%, 0);
  5461. }
  5462.  
  5463. button:focus-visible, a:focus-visible, [tabindex="0"]:focus-visible {
  5464. outline: 3px solid #4c9aff;
  5465. outline-offset: 2px;
  5466. }
  5467.  
  5468. .skip-link {
  5469. position: absolute;
  5470. top: -40px;
  5471. left: 0;
  5472. background: var(--button-bg);
  5473. color: var(--button-text);
  5474. padding: 8px;
  5475. z-index: 100;
  5476. transition: top 0.3s ease;
  5477. }
  5478.  
  5479. .skip-link:focus {
  5480. top: 0;
  5481. }
  5482. </style>
  5483. </head>
  5484. <body>
  5485. <a href="#game-container" class="skip-link">Skip to game</a>
  5486. <div class="container">
  5487. <header class="header">
  5488. <div class="title-container">
  5489. <h1 class="title">2048</h1>
  5490. <button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode">🌓</button>
  5491. </div>
  5492. <div class="scores">
  5493. <div class="score-box">
  5494. <div class="score-title">Score</div>
  5495. <div class="score-value" id="score" aria-live="polite">0</div>
  5496. </div>
  5497. <div class="score-box">
  5498. <div class="score-title">Best</div>
  5499. <div class="score-value" id="best-score">0</div>
  5500. </div>
  5501. </div>
  5502. </header>
  5503. <div class="sub-header">
  5504. <p class="game-intro">Join tiles, get to <strong>2048!</strong> Use <strong>arrow keys</strong> or <strong>swipe</strong>.</p>
  5505. <div class="button-container">
  5506. <button class="btn" id="restart-button" aria-label="New Game">New</button>
  5507. <button class="btn" id="undo-button" aria-label="Undo Move" disabled>↩️</button>
  5508. <div class="dropdown">
  5509. <button class="btn" id="menu-button" aria-haspopup="true" aria-expanded="false">☰</button>
  5510. <div class="dropdown-content" id="menu-dropdown">
  5511. <a href="#" id="settings-link">Settings</a>
  5512. <a href="#" id="save-link">Save Game</a>
  5513. <a href="#" id="load-link">Load Game</a>
  5514. <a href="#" id="achievements-link">Achievements</a>
  5515. <a href="#" id="help-link">Help</a>
  5516. </div>
  5517. </div>
  5518. </div>
  5519. </div>
  5520.  
  5521. <div class="game-container" id="game-container" tabindex="0" aria-label="Game Board">
  5522. <div class="grid-background" id="grid-background"></div>
  5523. <div class="tile-container" id="tile-container"></div>
  5524. <div class="game-message" id="game-message" aria-live="assertive">
  5525. <p id="message-text"></p>
  5526. <div class="lower">
  5527. <button class="btn" id="retry-button">Try again</button>
  5528. <button class="btn" id="keep-playing-button" style="display: none;">Keep Playing</button>
  5529. <button class="btn" id="save-score-button" style="display: none;">Save Score</button>
  5530. </div>
  5531. </div>
  5532. </div>
  5533.  
  5534. <div class="stats-container" id="stats-container">
  5535. <div class="stats-title">
  5536. Game Statistics
  5537. <button class="stats-toggle" id="stats-toggle" aria-label="Toggle statistics visibility" aria-expanded="false">▼</button>
  5538. </div>
  5539. <div class="stats-content" id="stats-content">
  5540. <div class="stats-row"><span>Games Played:</span><span class="stats-value" id="games-played">0</span></div>
  5541. <div class="stats-row"><span>Highest Tile:</span><span class="stats-value" id="highest-tile">0</span></div>
  5542. <div class="stats-row"><span>Total Moves Made:</span><span class="stats-value" id="total-moves">0</span></div>
  5543. <div class="stats-row"><span>Total Score Accumulated:</span><span class="stats-value" id="total-score">0</span></div>
  5544. <div class="stats-row"><span>Avg Score/Game:</span><span class="stats-value" id="avg-score">0</span></div>
  5545. <div class="stats-row"><span>Current Difficulty:</span><span class="stats-value" id="current-difficulty">Normal</span></div>
  5546. <div class="stats-row"><span>Current Grid Size:</span><span class="stats-value" id="current-grid-size">4×4</span></div>
  5547. <div class="stats-row"><span>Achievements Unlocked:</span><span class="stats-value" id="achievements-unlocked">0/10</span></div>
  5548. </div>
  5549. </div>
  5550.  
  5551. <footer class="footer">
  5552. <div class="controls-row">
  5553. <button class="animation-toggle" id="animation-toggle" aria-pressed="true">
  5554. <span class="btn-icon">✨</span>
  5555. <span class="animation-status" id="animation-status">Animations: On</span>
  5556. </button>
  5557. </div>
  5558. <p>Ultimate Edition • <a href="#" id="keyboard-link">Keyboard shortcuts</a></p>
  5559. </footer>
  5560. </div>
  5561.  
  5562. <!-- Keyboard Shortcuts Modal -->
  5563. <div class="keyboard-shortcuts" id="keyboard-shortcuts" role="dialog" aria-modal="true" aria-labelledby="shortcuts-title">
  5564. <div class="shortcuts-content">
  5565. <button class="close-shortcuts" id="close-shortcuts" aria-label="Close keyboard shortcuts">×</button>
  5566. <h2 class="shortcuts-title" id="shortcuts-title">Keyboard Shortcuts</h2>
  5567. <div class="shortcut-grid">
  5568. <div class="key">↑ ↓ ← →</div><div>Move Tiles</div>
  5569. <div class="key">W A S D</div><div>Move Tiles</div>
  5570. <div class="key">R</div><div>Restart Game</div>
  5571. <div class="key">U / Z</div><div>Undo Move</div>
  5572. <div class="key">M</div><div>Toggle Dark Mode</div>
  5573. <div class="key">T</div><div>Toggle Animations</div>
  5574. <div class="key">S</div><div>Save Game</div>
  5575. <div class="key">L</div><div>Load Game</div>
  5576. <div class="key">O</div><div>Open Settings</div>
  5577. <div class="key">Esc</div><div>Close Dialogs</div>
  5578. <div class="key">?</div><div>Show This Help</div>
  5579. </div>
  5580. </div>
  5581. </div>
  5582.  
  5583. <!-- Settings Modal -->
  5584. <div class="modal" id="settings-modal" role="dialog" aria-modal="true" aria-labelledby="settings-title">
  5585. <div class="modal-content">
  5586. <button class="modal-close" id="settings-close" aria-label="Close settings">×</button>
  5587. <h2 class="modal-title" id="settings-title">Game Settings</h2>
  5588.  
  5589. <div class="settings-group">
  5590. <label class="settings-label" for="difficulty-select">Difficulty</label>
  5591. <select class="settings-select" id="difficulty-select">
  5592. <option value="easy">Easy (10% chance of 4s)</option>
  5593. <option value="normal" selected>Normal (20% chance of 4s)</option>
  5594. <option value="hard">Hard (30% chance of 4s)</option>
  5595. <option value="expert">Expert (40% chance of 4s)</option>
  5596. </select>
  5597. </div>
  5598.  
  5599. <div class="settings-group">
  5600. <label class="settings-label">Grid Size</label>
  5601. <div class="size-controls">
  5602. <button class="size-btn" data-size="3">3×3</button>
  5603. <button class="size-btn active" data-size="4">4×4</button>
  5604. <button class="size-btn" data-size="5">5×5</button>
  5605. <button class="size-btn" data-size="6">6×6</button>
  5606. </div>
  5607. <p><small>Note: Changing grid size will restart the game.</small></p>
  5608. </div>
  5609.  
  5610. <div class="settings-group">
  5611. <button class="btn" id="apply-settings">Apply Settings</button>
  5612. </div>
  5613. </div>
  5614. </div>
  5615.  
  5616. <!-- Save Game Modal -->
  5617. <div class="modal" id="save-modal" role="dialog" aria-modal="true" aria-labelledby="save-title">
  5618. <div class="modal-content">
  5619. <button class="modal-close" id="save-close" aria-label="Close save dialog">×</button>
  5620. <h2 class="modal-title" id="save-title">Save Game</h2>
  5621.  
  5622. <div class="settings-group">
  5623. <label class="settings-label" for="save-name">Save Name</label>
  5624. <input type="text" id="save-name" class="settings-select" placeholder="My Game">
  5625. </div>
  5626.  
  5627. <div class="settings-group">
  5628. <button class="btn" id="save-game-btn">Save Game</button>
  5629. </div>
  5630. </div>
  5631. </div>
  5632.  
  5633. <!-- Load Game Modal -->
  5634. <div class="modal" id="load-modal" role="dialog" aria-modal="true" aria-labelledby="load-title">
  5635. <div class="modal-content">
  5636. <button class="modal-close" id="load-close" aria-label="Close load dialog">×</button>
  5637. <h2 class="modal-title" id="load-title">Load Game</h2>
  5638.  
  5639. <div id="saved-games-list" class="saved-games-list">
  5640. <!-- Saved games will be listed here -->
  5641. <p id="no-saves-message">No saved games found.</p>
  5642. </div>
  5643. </div>
  5644. </div>
  5645.  
  5646. <!-- Achievements Modal -->
  5647. <div class="modal" id="achievements-modal" role="dialog" aria-modal="true" aria-labelledby="achievements-title">
  5648. <div class="modal-content">
  5649. <button class="modal-close" id="achievements-close" aria-label="Close achievements">×</button>
  5650. <h2 class="modal-title" id="achievements-title">Achievements</h2>
  5651.  
  5652. <div id="achievements-list" class="achievements-list">
  5653. <!-- Achievements will be listed here -->
  5654. </div>
  5655. </div>
  5656. </div>
  5657.  
  5658. <!-- Achievement Notification -->
  5659. <div class="achievement" id="achievement-notification">
  5660. <div class="achievement-title">Achievement Unlocked!</div>
  5661. <div class="achievement-description" id="achievement-description"></div>
  5662. </div>
  5663.  
  5664. <!-- Toast Notification -->
  5665. <div class="toast" id="toast" aria-live="assertive"></div>
  5666.  
  5667. <script>
  5668. document.addEventListener('DOMContentLoaded', () => {
  5669. // --- Constants ---
  5670. const CONSTANTS = {
  5671. DEFAULT_GRID_SIZE: 4,
  5672. WINNING_VALUE: 2048,
  5673. ANIMATION_DURATIONS: {
  5674. MOVE: 150, // ms
  5675. APPEAR: 200, // ms
  5676. POP: 200 // ms
  5677. },
  5678. MAX_UNDO_STEPS: 30,
  5679. SWIPE_THRESHOLD: 30,
  5680. STORAGE_KEYS: {
  5681. BEST_SCORE: 'bestScore2048Ultimate',
  5682. GAME_STATE: 'savedGame2048Ultimate',
  5683. STATS: 'gameStats2048Ultimate',
  5684. SETTINGS: 'settings2048Ultimate',
  5685. ACHIEVEMENTS: 'achievements2048Ultimate',
  5686. SAVED_GAMES: 'savedGames2048Ultimate'
  5687. },
  5688. DIFFICULTY_LEVELS: {
  5689. easy: { name: 'Easy', fourProbability: 0.1 },
  5690. normal: { name: 'Normal', fourProbability: 0.2 },
  5691. hard: { name: 'Hard', fourProbability: 0.3 },
  5692. expert: { name: 'Expert', fourProbability: 0.4 }
  5693. },
  5694. GRID_SIZES: [3, 4, 5, 6],
  5695. CELL_SIZES: {
  5696. 3: { cell: '120px', gap: '15px' },
  5697. 4: { cell: '100px', gap: '12px' },
  5698. 5: { cell: '80px', gap: '10px' },
  5699. 6: { cell: '65px', gap: '8px' }
  5700. }
  5701. };
  5702.  
  5703. // --- Event Manager ---
  5704. class EventManager {
  5705. constructor() {
  5706. this.events = {};
  5707. }
  5708.  
  5709. on(event, callback) {
  5710. if (!this.events[event]) {
  5711. this.events[event] = [];
  5712. }
  5713. this.events[event].push(callback);
  5714. return this; // For chaining
  5715. }
  5716.  
  5717. off(event, callback) {
  5718. if (!this.events[event]) return this;
  5719.  
  5720. if (callback) {
  5721. this.events[event] = this.events[event].filter(cb => cb !== callback);
  5722. } else {
  5723. delete this.events[event];
  5724. }
  5725. return this;
  5726. }
  5727.  
  5728. emit(event, ...args) {
  5729. if (!this.events[event]) return;
  5730.  
  5731. this.events[event].forEach(callback => {
  5732. callback(...args);
  5733. });
  5734. return this;
  5735. }
  5736. }
  5737.  
  5738. // --- UI Manager ---
  5739. class UIManager {
  5740. constructor(elements, eventManager) {
  5741. this.elements = elements;
  5742. this.events = eventManager;
  5743. this.activeModals = [];
  5744. this.toastTimeout = null;
  5745. this.achievementTimeout = null;
  5746.  
  5747. // Initialize UI event listeners
  5748. this.setupUIEvents();
  5749. }
  5750.  
  5751. setupUIEvents() {
  5752. // Button event listeners
  5753. this.elements.restartButton.addEventListener('click', () => this.events.emit('restart'));
  5754. this.elements.retryButton.addEventListener('click', () => this.events.emit('restart'));
  5755. this.elements.undoButton.addEventListener('click', () => this.events.emit('undo'));
  5756. this.elements.keepPlayingButton.addEventListener('click', () => this.events.emit('keepPlaying'));
  5757. this.elements.themeToggle.addEventListener('click', () => this.events.emit('toggleTheme'));
  5758. this.elements.animationToggle.addEventListener('click', () => this.events.emit('toggleAnimations'));
  5759. this.elements.statsToggle.addEventListener('click', () => this.toggleStats());
  5760.  
  5761. // Menu items
  5762. this.elements.settingsLink.addEventListener('click', (e) => {
  5763. e.preventDefault();
  5764. this.showModal('settings');
  5765. });
  5766. this.elements.saveLink.addEventListener('click', (e) => {
  5767. e.preventDefault();
  5768. this.showModal('save');
  5769. });
  5770. this.elements.loadLink.addEventListener('click', (e) => {
  5771. e.preventDefault();
  5772. this.showModal('load');
  5773. });
  5774. this.elements.achievementsLink.addEventListener('click', (e) => {
  5775. e.preventDefault();
  5776. this.showModal('achievements');
  5777. });
  5778. this.elements.helpLink.addEventListener('click', (e) => {
  5779. e.preventDefault();
  5780. this.showKeyboardShortcuts();
  5781. });
  5782.  
  5783. // Modal close buttons
  5784. document.querySelectorAll('.modal-close').forEach(button => {
  5785. button.addEventListener('click', () => {
  5786. this.closeAllModals();
  5787. });
  5788. });
  5789.  
  5790. // Settings modal
  5791. this.elements.applySettings.addEventListener('click', () => {
  5792. const difficulty = this.elements.difficultySelect.value;
  5793. const gridSizeBtn = document.querySelector('.size-btn.active');
  5794. const gridSize = gridSizeBtn ? parseInt(gridSizeBtn.dataset.size) : CONSTANTS.DEFAULT_GRID_SIZE;
  5795.  
  5796. this.events.emit('applySettings', { difficulty, gridSize });
  5797. this.closeAllModals();
  5798. });
  5799.  
  5800. // Grid size buttons
  5801. document.querySelectorAll('.size-btn').forEach(button => {
  5802. button.addEventListener('click', () => {
  5803. document.querySelectorAll('.size-btn').forEach(btn => btn.classList.remove('active'));
  5804. button.classList.add('active');
  5805. });
  5806. });
  5807.  
  5808. // Save game button
  5809. this.elements.saveGameBtn.addEventListener('click', () => {
  5810. const saveName = this.elements.saveName.value.trim() || `Game ${new Date().toLocaleString()}`;
  5811. this.events.emit('saveGame', saveName);
  5812. this.closeAllModals();
  5813. });
  5814.  
  5815. // Keyboard shortcuts
  5816. this.elements.keyboardLink.addEventListener('click', (e) => {
  5817. e.preventDefault();
  5818. this.showKeyboardShortcuts();
  5819. });
  5820. this.elements.closeShortcuts.addEventListener('click', () => {
  5821. this.hideKeyboardShortcuts();
  5822. });
  5823. }
  5824.  
  5825. updateScore(score, bestScore, addition = 0) {
  5826. this.elements.scoreElement.textContent = score;
  5827. this.elements.bestScoreElement.textContent = bestScore;
  5828.  
  5829. if (addition > 0) {
  5830. this.showScoreAddition(addition);
  5831. }
  5832. }
  5833.  
  5834. updateUndoButton(enabled) {
  5835. this.elements.undoButton.disabled = !enabled;
  5836. }
  5837.  
  5838. updateStatsDisplay(stats, settings) {
  5839. this.elements.gamesPlayedElement.textContent = stats.gamesPlayed;
  5840. this.elements.highestTileElement.textContent = stats.highestTile;
  5841. this.elements.totalMovesElement.textContent = stats.totalMoves;
  5842. this.elements.totalScoreElement.textContent = stats.totalScore;
  5843.  
  5844. const avgScore = stats.gamesPlayed > 0
  5845. ? Math.round(stats.totalScore / stats.gamesPlayed)
  5846. : 0;
  5847. this.elements.avgScoreElement.textContent = avgScore;
  5848.  
  5849. this.elements.currentDifficulty.textContent =
  5850. CONSTANTS.DIFFICULTY_LEVELS[settings.difficulty].name;
  5851. this.elements.currentGridSize.textContent = `${settings.gridSize}×${settings.gridSize}`;
  5852.  
  5853. // Update achievements count
  5854. const achievementsCount = this.countUnlockedAchievements();
  5855. this.elements.achievementsUnlocked.textContent = `${achievementsCount}/10`;
  5856. }
  5857.  
  5858. countUnlockedAchievements() {
  5859. try {
  5860. const achievements = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.ACHIEVEMENTS) || '{}');
  5861. return Object.values(achievements).filter(a => a.unlocked).length;
  5862. } catch (e) {
  5863. return 0;
  5864. }
  5865. }
  5866.  
  5867. showScoreAddition(addition) {
  5868. const scoreAdditionElement = document.createElement('div');
  5869. scoreAdditionElement.classList.add('score-addition');
  5870. scoreAdditionElement.textContent = `+${addition}`;
  5871.  
  5872. const scoreRect = this.elements.scoreElement.getBoundingClientRect();
  5873. scoreAdditionElement.style.position = 'fixed';
  5874. scoreAdditionElement.style.top = `${scoreRect.top - 10}px`;
  5875. scoreAdditionElement.style.left = `${scoreRect.left + scoreRect.width / 2}px`;
  5876.  
  5877. document.body.appendChild(scoreAdditionElement);
  5878.  
  5879. scoreAdditionElement.addEventListener('animationend', () => {
  5880. if (scoreAdditionElement.parentNode) {
  5881. document.body.removeChild(scoreAdditionElement);
  5882. }
  5883. }, { once: true });
  5884. }
  5885.  
  5886. showMessage(text, type) {
  5887. this.elements.messageText.textContent = text;
  5888. this.elements.messageContainer.className = 'game-message active';
  5889. this.elements.keepPlayingButton.style.display = 'none';
  5890. this.elements.saveScoreButton.style.display = 'none';
  5891.  
  5892. if (type === 'win') {
  5893. this.elements.messageContainer.classList.add('game-won');
  5894. this.elements.keepPlayingButton.style.display = 'inline-block';
  5895. this.elements.saveScoreButton.style.display = 'inline-block';
  5896. } else if (type === 'over') {
  5897. this.elements.messageContainer.classList.add('game-over');
  5898. this.elements.saveScoreButton.style.display = 'inline-block';
  5899. }
  5900. }
  5901.  
  5902. hideMessage() {
  5903. this.elements.messageContainer.classList.remove('active', 'game-won', 'game-over');
  5904. }
  5905.  
  5906. showToast(message, duration = 2000) {
  5907. const toastElement = this.elements.toast;
  5908. toastElement.textContent = message;
  5909. toastElement.classList.add('active');
  5910.  
  5911. if (this.toastTimeout) clearTimeout(this.toastTimeout);
  5912.  
  5913. this.toastTimeout = setTimeout(() => {
  5914. toastElement.classList.remove('active');
  5915. }, duration);
  5916. }
  5917.  
  5918. showAchievement(achievement) {
  5919. const notification = this.elements.achievementNotification;
  5920. const description = this.elements.achievementDescription;
  5921.  
  5922. description.textContent = achievement.description;
  5923. notification.classList.add('active');
  5924.  
  5925. if (this.achievementTimeout) clearTimeout(this.achievementTimeout);
  5926.  
  5927. this.achievementTimeout = setTimeout(() => {
  5928. notification.classList.remove('active');
  5929. }, 3000);
  5930. }
  5931.  
  5932. updateAnimationStatus(enabled) {
  5933. this.elements.animationStatus.textContent = `Animations: ${enabled ? 'On' : 'Off'}`;
  5934. this.elements.animationToggle.setAttribute('aria-pressed', enabled);
  5935. }
  5936.  
  5937. updateTheme(darkMode) {
  5938. document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light');
  5939. }
  5940.  
  5941. toggleStats() {
  5942. const isExpanded = this.elements.statsContent.classList.toggle('expanded');
  5943. this.elements.statsToggle.textContent = isExpanded ? '▲' : '▼';
  5944. this.elements.statsToggle.setAttribute('aria-expanded', isExpanded);
  5945. }
  5946.  
  5947. showKeyboardShortcuts() {
  5948. this.elements.keyboardShortcuts.classList.add('active');
  5949. this.elements.closeShortcuts.focus();
  5950. }
  5951.  
  5952. hideKeyboardShortcuts() {
  5953. this.elements.keyboardShortcuts.classList.remove('active');
  5954. this.elements.helpLink.focus();
  5955. }
  5956.  
  5957. showModal(modalType) {
  5958. const modalId = `${modalType}-modal`;
  5959. const modal = document.getElementById(modalId);
  5960.  
  5961. if (!modal) return;
  5962.  
  5963. // Close any open modals first
  5964. this.closeAllModals();
  5965.  
  5966. modal.classList.add('active');
  5967. this.activeModals.push(modalId);
  5968.  
  5969. // Focus the first focusable element
  5970. const focusable = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  5971. if (focusable) focusable.focus();
  5972.  
  5973. // If it's the load modal, refresh the saved games list
  5974. if (modalType === 'load') {
  5975. this.refreshSavedGamesList();
  5976. } else if (modalType === 'achievements') {
  5977. this.refreshAchievementsList();
  5978. }
  5979. }
  5980.  
  5981. closeAllModals() {
  5982. this.activeModals.forEach(modalId => {
  5983. const modal = document.getElementById(modalId);
  5984. if (modal) modal.classList.remove('active');
  5985. });
  5986.  
  5987. this.activeModals = [];
  5988. this.elements.gameContainer.focus();
  5989. }
  5990.  
  5991. refreshSavedGamesList() {
  5992. const listElement = document.getElementById('saved-games-list');
  5993. const noSavesMessage = document.getElementById('no-saves-message');
  5994.  
  5995. try {
  5996. const savedGames = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.SAVED_GAMES) || '[]');
  5997.  
  5998. if (savedGames.length === 0) {
  5999. noSavesMessage.style.display = 'block';
  6000. return;
  6001. }
  6002.  
  6003. noSavesMessage.style.display = 'none';
  6004.  
  6005. // Clear previous list
  6006. listElement.innerHTML = '';
  6007.  
  6008. // Add each saved game to the list
  6009. savedGames.forEach((save, index) => {
  6010. const item = document.createElement('div');
  6011. item.className = 'saved-game-item';
  6012.  
  6013. item.innerHTML = `
  6014. <div class="saved-game-info">
  6015. <div class="saved-game-title">${save.name}</div>
  6016. <div class="saved-game-meta">
  6017. Score: ${save.score} | Grid: ${save.gridSize}×${save.gridSize} |
  6018. ${new Date(save.timestamp).toLocaleString()}
  6019. </div>
  6020. </div>
  6021. <div class="saved-game-actions">
  6022. <button class="btn load-game-btn" data-index="${index}">Load</button>
  6023. <button class="btn delete-game-btn" data-index="${index}">Delete</button>
  6024. </div>
  6025. `;
  6026.  
  6027. listElement.appendChild(item);
  6028. });
  6029.  
  6030. // Add event listeners to the buttons
  6031. document.querySelectorAll('.load-game-btn').forEach(button => {
  6032. button.addEventListener('click', () => {
  6033. const index = parseInt(button.dataset.index);
  6034. this.events.emit('loadSavedGame', index);
  6035. this.closeAllModals();
  6036. });
  6037. });
  6038.  
  6039. document.querySelectorAll('.delete-game-btn').forEach(button => {
  6040. button.addEventListener('click', () => {
  6041. const index = parseInt(button.dataset.index);
  6042. this.events.emit('deleteSavedGame', index);
  6043. this.refreshSavedGamesList();
  6044. });
  6045. });
  6046. } catch (e) {
  6047. console.error('Error loading saved games:', e);
  6048. noSavesMessage.style.display = 'block';
  6049. }
  6050. }
  6051.  
  6052. refreshAchievementsList() {
  6053. const listElement = document.getElementById('achievements-list');
  6054.  
  6055. try {
  6056. const achievements = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.ACHIEVEMENTS) || '{}');
  6057. const achievementsList = [
  6058. { id: 'first_win', title: 'First Victory', description: 'Reach the 2048 tile for the first time' },
  6059. { id: 'super_tile', title: 'Super Tile', description: 'Create a tile with value 4096 or higher' },
  6060. { id: 'persistent', title: 'Persistent', description: 'Play 10 games' },
  6061. { id: 'master', title: 'Master', description: 'Play 50 games' },
  6062. { id: 'high_score', title: 'High Scorer', description: 'Reach a score of 20,000 or more' },
  6063. { id: 'efficient', title: 'Efficient', description: 'Win a game with fewer than 300 moves' },
  6064. { id: 'comeback', title: 'Comeback Kid', description: 'Undo a move and still win the game' },
  6065. { id: 'explorer', title: 'Explorer', description: 'Play on all grid sizes' },
  6066. { id: 'challenge_seeker', title: 'Challenge Seeker', description: 'Win on Hard or Expert difficulty' },
  6067. { id: 'completionist', title: 'Completionist', description: 'Unlock all other achievements' }
  6068. ];
  6069.  
  6070. // Clear previous list
  6071. listElement.innerHTML = '';
  6072.  
  6073. // Add each achievement to the list
  6074. achievementsList.forEach(achievement => {
  6075. const isUnlocked = achievements[achievement.id]?.unlocked || false;
  6076.  
  6077. const item = document.createElement('div');
  6078. item.className = `achievement-item ${isUnlocked ? 'unlocked' : ''}`;
  6079.  
  6080. item.innerHTML = `
  6081. <div class="achievement-item-title">${isUnlocked ? '🏆' : '🔒'} ${achievement.title}</div>
  6082. <div class="achievement-item-description">${achievement.description}</div>
  6083. `;
  6084.  
  6085. listElement.appendChild(item);
  6086. });
  6087. } catch (e) {
  6088. console.error('Error loading achievements:', e);
  6089. listElement.innerHTML = '<p>Error loading achievements.</p>';
  6090. }
  6091. }
  6092.  
  6093. updateGridSize(size) {
  6094. // Update CSS variables for grid size
  6095. const cellSize = CONSTANTS.CELL_SIZES[size].cell;
  6096. const cellGap = CONSTANTS.CELL_SIZES[size].gap;
  6097.  
  6098. document.documentElement.style.setProperty('--grid-size', size);
  6099. document.documentElement.style.setProperty('--cell-size', cellSize);
  6100. document.documentElement.style.setProperty('--cell-gap', cellGap);
  6101.  
  6102. // Resize the grid background with proper number of cells
  6103. this.elements.gridBackground.innerHTML = '';
  6104. for (let i = 0; i < size * size; i++) {
  6105. const cell = document.createElement('div');
  6106. cell.classList.add('cell');
  6107. this.elements.gridBackground.appendChild(cell);
  6108. }
  6109. }
  6110. }
  6111.  
  6112. // --- Grid Manager ---
  6113. class GridManager {
  6114. constructor(gridSize, tileContainer, settings) {
  6115. this.gridSize = gridSize;
  6116. this.tileContainer = tileContainer;
  6117. this.settings = settings;
  6118. this.grid = [];
  6119. this.initializeGrid();
  6120. }
  6121.  
  6122. initializeGrid() {
  6123. this.grid = Array(this.gridSize).fill(null).map(() => Array(this.gridSize).fill(null));
  6124. }
  6125.  
  6126. setGridSize(newSize) {
  6127. this.gridSize = newSize;
  6128. this.initializeGrid();
  6129. }
  6130.  
  6131. clearGrid() {
  6132. this.tileContainer.innerHTML = '';
  6133. this.initializeGrid();
  6134. }
  6135.  
  6136. getEmptyCells() {
  6137. const emptyCells = [];
  6138. for (let r = 0; r < this.gridSize; r++) {
  6139. for (let c = 0; c < this.gridSize; c++) {
  6140. if (this.grid[r][c] === null) {
  6141. emptyCells.push({ r, c });
  6142. }
  6143. }
  6144. }
  6145. return emptyCells;
  6146. }
  6147.  
  6148. addRandomTile() {
  6149. const emptyCells = this.getEmptyCells();
  6150. if (emptyCells.length === 0) return null;
  6151.  
  6152. const { r, c } = emptyCells[Math.floor(Math.random() * emptyCells.length)];
  6153.  
  6154. // Determine value based on difficulty
  6155. const fourProbability = CONSTANTS.DIFFICULTY_LEVELS[this.settings.difficulty].fourProbability;
  6156. const value = Math.random() < fourProbability ? 4 : 2;
  6157.  
  6158. const newTile = new Tile(value, r, c, this.tileContainer, this.settings);
  6159. this.grid[r][c] = newTile;
  6160.  
  6161. return newTile;
  6162. }
  6163.  
  6164. canMove() {
  6165. if (this.getEmptyCells().length > 0) return true;
  6166.  
  6167. for (let r = 0; r < this.gridSize; r++) {
  6168. for (let c = 0; c < this.gridSize; c++) {
  6169. const tile = this.grid[r][c];
  6170. if (!tile) continue;
  6171.  
  6172. // Check right and down - sufficient to check all adjacencies
  6173. if (c < this.gridSize - 1 && this.grid[r][c + 1]?.value === tile.value) return true;
  6174. if (r < this.gridSize - 1 && this.grid[r + 1][c]?.value === tile.value) return true;
  6175. }
  6176. }
  6177. return false;
  6178. }
  6179.  
  6180. getTraversals(direction) {
  6181. const rows = Array.from({ length: this.gridSize }, (_, i) => i);
  6182. const cols = Array.from({ length: this.gridSize }, (_, i) => i);
  6183.  
  6184. if (direction === 'right') cols.reverse();
  6185. if (direction === 'down') rows.reverse();
  6186.  
  6187. return { rows, cols };
  6188. }
  6189.  
  6190. findFurthestPosition(r, c, direction) {
  6191. let currentRow = r, currentCol = c, nextRow, nextCol;
  6192.  
  6193. do {
  6194. currentRow = nextRow !== undefined ? nextRow : r;
  6195. currentCol = nextCol !== undefined ? nextCol : c;
  6196.  
  6197. const { nr, nc } = this.getNextPosition(currentRow, currentCol, direction);
  6198. nextRow = nr;
  6199. nextCol = nc;
  6200. } while (
  6201. this.isWithinBounds(nextRow, nextCol) &&
  6202. this.grid[nextRow][nextCol] === null
  6203. );
  6204.  
  6205. const furthestRow = currentRow;
  6206. const furthestCol = currentCol;
  6207. const nextTile = this.isWithinBounds(nextRow, nextCol) ? this.grid[nextRow][nextCol] : null;
  6208.  
  6209. return { furthestRow, furthestCol, nextTile };
  6210. }
  6211.  
  6212. getNextPosition(r, c, direction) {
  6213. switch (direction) {
  6214. case 'up': return { nr: r - 1, nc: c };
  6215. case 'down': return { nr: r + 1, nc: c };
  6216. case 'left': return { nr: r, nc: c - 1 };
  6217. case 'right': return { nr: r, nc: c + 1 };
  6218. default: return { nr: r, nc: c };
  6219. }
  6220. }
  6221.  
  6222. isWithinBounds(r, c) {
  6223. return r >= 0 && r < this.gridSize && c >= 0 && c < this.gridSize;
  6224. }
  6225.  
  6226. resetMergeStates() {
  6227. for (let r = 0; r < this.gridSize; r++) {
  6228. for (let c = 0; c < this.gridSize; c++) {
  6229. if (this.grid[r][c]) {
  6230. this.grid[r][c].mergedFrom = null;
  6231. }
  6232. }
  6233. }
  6234. }
  6235.  
  6236. // Create a deep copy of the grid for history
  6237. cloneGrid() {
  6238. const gridCopy = Array(this.gridSize).fill(null).map(() => Array(this.gridSize).fill(null));
  6239.  
  6240. for (let r = 0; r < this.gridSize; r++) {
  6241. for (let c = 0; c < this.gridSize; c++) {
  6242. if (this.grid[r][c]) {
  6243. gridCopy[r][c] = this.grid[r][c].clone();
  6244. }
  6245. }
  6246. }
  6247.  
  6248. return gridCopy;
  6249. }
  6250.  
  6251. // Restore grid from a saved state
  6252. restoreGrid(savedGrid) {
  6253. this.clearGrid();
  6254.  
  6255. for (let r = 0; r < this.gridSize; r++) {
  6256. for (let c = 0; c < this.gridSize; c++) {
  6257. if (savedGrid[r] && savedGrid[r][c]) {
  6258. const { value, row, col } = savedGrid[r][c];
  6259. this.grid[r][c] = new Tile(value, row, col, this.tileContainer, this.settings);
  6260. }
  6261. }
  6262. }
  6263. }
  6264.  
  6265. // Process a move in the given direction
  6266. async move(direction) {
  6267. this.resetMergeStates();
  6268.  
  6269. let moved = false;
  6270. let scoreAddition = 0;
  6271. const mergedTiles = [];
  6272.  
  6273. const traversals = this.getTraversals(direction);
  6274.  
  6275. traversals.rows.forEach(r => {
  6276. traversals.cols.forEach(c => {
  6277. const currentTile = this.grid[r][c];
  6278. if (!currentTile) return;
  6279.  
  6280. const { furthestRow, furthestCol, nextTile } = this.findFurthestPosition(r, c, direction);
  6281.  
  6282. if (nextTile && nextTile.value === currentTile.value && !nextTile.mergedFrom) {
  6283. // --- Merge ---
  6284. const mergedValue = currentTile.value * 2;
  6285. nextTile.updateValue(mergedValue);
  6286. nextTile.mergedFrom = currentTile;
  6287.  
  6288. this.grid[r][c] = null;
  6289. currentTile.row = nextTile.row;
  6290. currentTile.col = nextTile.col;
  6291. currentTile.updatePosition();
  6292.  
  6293. // Track for animation synchronization
  6294. mergedTiles.push({ tile: nextTile, originalTile: currentTile });
  6295.  
  6296. scoreAddition += mergedValue;
  6297. moved = true;
  6298.  
  6299. } else if (furthestRow !== r || furthestCol !== c) {
  6300. // --- Move ---
  6301. this.grid[furthestRow][furthestCol] = currentTile;
  6302. this.grid[r][c] = null;
  6303. currentTile.row = furthestRow;
  6304. currentTile.col = furthestCol;
  6305. currentTile.updatePosition();
  6306. moved = true;
  6307. }
  6308. });
  6309. });
  6310.  
  6311. // Return a promise that resolves when animations complete
  6312. return new Promise(resolve => {
  6313. if (!moved) {
  6314. resolve({ moved: false, scoreAddition: 0 });
  6315. return;
  6316. }
  6317.  
  6318. if (this.settings.animationsEnabled) {
  6319. // Play merge animations
  6320. mergedTiles.forEach(({ tile, originalTile }) => {
  6321. tile.playMergeAnimation();
  6322. originalTile.remove(true);
  6323. });
  6324.  
  6325. // Wait for animations to complete
  6326. setTimeout(() => {
  6327. resolve({ moved, scoreAddition });
  6328. }, CONSTANTS.ANIMATION_DURATIONS.MOVE + 50);
  6329. } else {
  6330. // If animations are disabled, remove merged tiles immediately
  6331. mergedTiles.forEach(({ originalTile }) => {
  6332. originalTile.remove(false);
  6333. });
  6334. resolve({ moved, scoreAddition });
  6335. }
  6336. });
  6337. }
  6338. }
  6339.  
  6340. // --- Achievement Manager ---
  6341. class AchievementManager {
  6342. constructor(eventManager) {
  6343. this.events = eventManager;
  6344. this.achievements = this.loadAchievements();
  6345.  
  6346. // Setup achievement checks
  6347. this.setupAchievementListeners();
  6348. }
  6349.  
  6350. loadAchievements() {
  6351. try {
  6352. return JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.ACHIEVEMENTS) || '{}');
  6353. } catch (e) {
  6354. console.error('Error loading achievements:', e);
  6355. return {};
  6356. }
  6357. }
  6358.  
  6359. saveAchievements() {
  6360. try {
  6361. localStorage.setItem(CONSTANTS.STORAGE_KEYS.ACHIEVEMENTS, JSON.stringify(this.achievements));
  6362. } catch (e) {
  6363. console.error('Error saving achievements:', e);
  6364. }
  6365. }
  6366.  
  6367. setupAchievementListeners() {
  6368. // Listen for game events to check for achievements
  6369. this.events.on('gameWon', (data) => {
  6370. this.checkFirstWin();
  6371. this.checkEfficientWin(data.moves);
  6372. this.checkDifficultyWin(data.difficulty);
  6373. if (data.undoUsed) this.checkComebackWin();
  6374. });
  6375.  
  6376. this.events.on('gameOver', () => {
  6377. this.checkPersistent();
  6378. this.checkMaster();
  6379. });
  6380.  
  6381. this.events.on('newHighScore', (score) => {
  6382. this.checkHighScore(score);
  6383. });
  6384.  
  6385. this.events.on('newHighTile', (value) => {
  6386. this.checkSuperTile(value);
  6387. });
  6388.  
  6389. this.events.on('gridSizeChanged', () => {
  6390. this.checkExplorer();
  6391. });
  6392.  
  6393. // Check completionist periodically
  6394. this.events.on('achievementUnlocked', () => {
  6395. this.checkCompletionist();
  6396. });
  6397. }
  6398.  
  6399. unlockAchievement(id, title, description) {
  6400. if (this.achievements[id]?.unlocked) return false; // Already unlocked
  6401.  
  6402. this.achievements[id] = {
  6403. unlocked: true,
  6404. timestamp: Date.now()
  6405. };
  6406.  
  6407. this.saveAchievements();
  6408.  
  6409. // Emit event for UI notification
  6410. this.events.emit('achievementUnlocked', { id, title, description });
  6411.  
  6412. return true;
  6413. }
  6414.  
  6415. checkFirstWin() {
  6416. this.unlockAchievement(
  6417. 'first_win',
  6418. 'First Victory',
  6419. 'Reach the 2048 tile for the first time'
  6420. );
  6421. }
  6422.  
  6423. checkSuperTile(value) {
  6424. if (value >= 4096) {
  6425. this.unlockAchievement(
  6426. 'super_tile',
  6427. 'Super Tile',
  6428. 'Create a tile with value 4096 or higher'
  6429. );
  6430. }
  6431. }
  6432.  
  6433. checkPersistent() {
  6434. try {
  6435. const stats = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.STATS) || '{}');
  6436. if (stats.gamesPlayed >= 10) {
  6437. this.unlockAchievement(
  6438. 'persistent',
  6439. 'Persistent',
  6440. 'Play 10 games'
  6441. );
  6442. }
  6443. } catch (e) {
  6444. console.error('Error checking persistent achievement:', e);
  6445. }
  6446. }
  6447.  
  6448. checkMaster() {
  6449. try {
  6450. const stats = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.STATS) || '{}');
  6451. if (stats.gamesPlayed >= 50) {
  6452. this.unlockAchievement(
  6453. 'master',
  6454. 'Master',
  6455. 'Play 50 games'
  6456. );
  6457. }
  6458. } catch (e) {
  6459. console.error('Error checking master achievement:', e);
  6460. }
  6461. }
  6462.  
  6463. checkHighScore(score) {
  6464. if (score >= 20000) {
  6465. this.unlockAchievement(
  6466. 'high_score',
  6467. 'High Scorer',
  6468. 'Reach a score of 20,000 or more'
  6469. );
  6470. }
  6471. }
  6472.  
  6473. checkEfficientWin(moves) {
  6474. if (moves && moves < 300) {
  6475. this.unlockAchievement(
  6476. 'efficient',
  6477. 'Efficient',
  6478. 'Win a game with fewer than 300 moves'
  6479. );
  6480. }
  6481. }
  6482.  
  6483. checkComebackWin() {
  6484. this.unlockAchievement(
  6485. 'comeback',
  6486. 'Comeback Kid',
  6487. 'Undo a move and still win the game'
  6488. );
  6489. }
  6490.  
  6491. checkExplorer() {
  6492. try {
  6493. const settings = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.SETTINGS) || '{}');
  6494. const playedSizes = settings.playedGridSizes || [];
  6495.  
  6496. if (playedSizes.length >= 4) { // All 4 grid sizes
  6497. this.unlockAchievement(
  6498. 'explorer',
  6499. 'Explorer',
  6500. 'Play on all grid sizes'
  6501. );
  6502. }
  6503. } catch (e) {
  6504. console.error('Error checking explorer achievement:', e);
  6505. }
  6506. }
  6507.  
  6508. checkDifficultyWin(difficulty) {
  6509. if (difficulty === 'hard' || difficulty === 'expert') {
  6510. this.unlockAchievement(
  6511. 'challenge_seeker',
  6512. 'Challenge Seeker',
  6513. 'Win on Hard or Expert difficulty'
  6514. );
  6515. }
  6516. }
  6517.  
  6518. checkCompletionist() {
  6519. // Check if all other achievements are unlocked
  6520. const requiredAchievements = [
  6521. 'first_win', 'super_tile', 'persistent', 'master',
  6522. 'high_score', 'efficient', 'comeback', 'explorer', 'challenge_seeker'
  6523. ];
  6524.  
  6525. const allUnlocked = requiredAchievements.every(id => this.achievements[id]?.unlocked);
  6526.  
  6527. if (allUnlocked) {
  6528. this.unlockAchievement(
  6529. 'completionist',
  6530. 'Completionist',
  6531. 'Unlock all other achievements'
  6532. );
  6533. }
  6534. }
  6535. }
  6536.  
  6537. // --- Tile Class ---
  6538. class Tile {
  6539. constructor(value, row, col, parentContainer, settings) {
  6540. this.value = value;
  6541. this.row = row;
  6542. this.col = col;
  6543. this.parentContainer = parentContainer;
  6544. this.settings = settings;
  6545. this.element = this.createElement();
  6546. this.updatePosition(false);
  6547. this.mergedFrom = null;
  6548. this.parentContainer.appendChild(this.element);
  6549. }
  6550.  
  6551. createElement() {
  6552. const tileElement = document.createElement('div');
  6553. tileElement.classList.add('tile');
  6554. this.updateValue(this.value);
  6555.  
  6556. if (this.settings.animationsEnabled) {
  6557. tileElement.classList.add('new-tile');
  6558. }
  6559.  
  6560. return tileElement;
  6561. }
  6562.  
  6563. updateValue(newValue) {
  6564. this.value = newValue;
  6565. this.element.textContent = newValue;
  6566. this.element.dataset.value = newValue;
  6567. }
  6568.  
  6569. updatePosition(animate = true) {
  6570. const { top, left } = this.calculatePixelPosition();
  6571.  
  6572. this.element.style.transition = (animate && this.settings.animationsEnabled)
  6573. ? `top ${CONSTANTS.ANIMATION_DURATIONS.MOVE}ms ease-in-out, left ${CONSTANTS.ANIMATION_DURATIONS.MOVE}ms ease-in-out`
  6574. : 'none';
  6575.  
  6576. this.element.style.top = `${top}px`;
  6577. this.element.style.left = `${left}px`;
  6578. }
  6579.  
  6580. calculatePixelPosition() {
  6581. const cellSize = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--cell-size'));
  6582. const cellGap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--cell-gap'));
  6583.  
  6584. const top = this.row * (cellSize + cellGap) + cellGap;
  6585. const left = this.col * (cellSize + cellGap) + cellGap;
  6586.  
  6587. return { top, left };
  6588. }
  6589.  
  6590. remove(merged = false) {
  6591. const removeAction = () => {
  6592. if (this.element.parentNode) {
  6593. this.parentContainer.removeChild(this.element);
  6594. }
  6595. };
  6596.  
  6597. if (merged && this.settings.animationsEnabled) {
  6598. // Let pop animation finish
  6599. this.element.addEventListener('animationend', removeAction, { once: true });
  6600. } else if (this.settings.animationsEnabled && this.element.style.transition !== 'none') {
  6601. // Wait for move transition
  6602. this.element.addEventListener('transitionend', removeAction, { once: true });
  6603. } else {
  6604. // Remove immediately
  6605. removeAction();
  6606. }
  6607. }
  6608.  
  6609. playMergeAnimation() {
  6610. if (this.settings.animationsEnabled) {
  6611. this.element.classList.add('merged-tile');
  6612. this.element.addEventListener('animationend', () => {
  6613. this.element.classList.remove('merged-tile');
  6614. }, { once: true });
  6615. }
  6616. }
  6617.  
  6618. clone() {
  6619. return { value: this.value, row: this.row, col: this.col };
  6620. }
  6621. }
  6622.  
  6623. // --- Game Class ---
  6624. class Game {
  6625. constructor(uiManager, eventManager) {
  6626. this.ui = uiManager;
  6627. this.events = eventManager;
  6628.  
  6629. // Game state
  6630. this.grid = null;
  6631. this.score = 0;
  6632. this.bestScore = 0;
  6633. this.isGameOver = false;
  6634. this.hasWon = false;
  6635. this.keepPlayingMode = false;
  6636. this.isMoving = false;
  6637. this.gameHistory = [];
  6638. this.moveCount = 0;
  6639. this.undoUsed = false;
  6640.  
  6641. // Game settings
  6642. this.settings = {
  6643. animationsEnabled: true,
  6644. darkMode: false,
  6645. difficulty: 'normal',
  6646. gridSize: CONSTANTS.DEFAULT_GRID_SIZE,
  6647. playedGridSizes: []
  6648. };
  6649.  
  6650. // Statistics
  6651. this.stats = {
  6652. gamesPlayed: 0,
  6653. highestTile: 0,
  6654. totalMoves: 0,
  6655. totalScore: 0
  6656. };
  6657.  
  6658. // Touch state
  6659. this.touchStartX = 0;
  6660. this.touchStartY = 0;
  6661. this.touchEndX = 0;
  6662. this.touchEndY = 0;
  6663.  
  6664. // Setup event listeners
  6665. this.setupEventListeners();
  6666. }
  6667.  
  6668. init() {
  6669. // Load saved data
  6670. this.loadSettings();
  6671. this.loadStats();
  6672. this.loadBestScore();
  6673.  
  6674. // Update UI based on settings
  6675. this.ui.updateTheme(this.settings.darkMode);
  6676. this.ui.updateAnimationStatus(this.settings.animationsEnabled);
  6677. this.ui.updateGridSize(this.settings.gridSize);
  6678.  
  6679. // Initialize grid manager
  6680. this.grid = new GridManager(
  6681. this.settings.gridSize,
  6682. this.elements.tileContainer,
  6683. this.settings
  6684. );
  6685.  
  6686. // Try to load saved game or start new
  6687. if (!this.loadGame()) {
  6688. this.startNewGame(false);
  6689. }
  6690.  
  6691. // Update UI with initial state
  6692. this.updateUI();
  6693. }
  6694.  
  6695. setupEventListeners() {
  6696. // Listen for UI events
  6697. this.events.on('restart', () => this.startNewGame());
  6698. this.events.on('undo', () => this.undoMove());
  6699. this.events.on('keepPlaying', () => this.keepPlaying());
  6700. this.events.on('toggleTheme', () => this.toggleTheme());
  6701. this.events.on('toggleAnimations', () => this.toggleAnimations());
  6702. this.events.on('applySettings', (settings) => this.applySettings(settings));
  6703. this.events.on('saveGame', (name) => this.saveNamedGame(name));
  6704. this.events.on('loadSavedGame', (index) => this.loadSavedGame(index));
  6705. this.events.on('deleteSavedGame', (index) => this.deleteSavedGame(index));
  6706.  
  6707. // Keyboard events
  6708. document.addEventListener('keydown', this.handleKeyDown.bind(this));
  6709.  
  6710. // Touch events
  6711. this.elements.gameContainer.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: true });
  6712. this.elements.gameContainer.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
  6713. this.elements.gameContainer.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: true });
  6714.  
  6715. // Focus management
  6716. this.elements.gameContainer.addEventListener('focus', () => {
  6717. this.elements.gameContainer.classList.add('focused');
  6718. });
  6719. this.elements.gameContainer.addEventListener('blur', () => {
  6720. this.elements.gameContainer.classList.remove('focused');
  6721. });
  6722.  
  6723. // Window events for auto-save
  6724. document.addEventListener('visibilitychange', () => {
  6725. if (document.visibilityState === 'hidden') this.saveGame();
  6726. });
  6727. window.addEventListener('beforeunload', () => this.saveGame());
  6728.  
  6729. // Achievement notification
  6730. this.events.on('achievementUnlocked', (achievement) => {
  6731. this.ui.showAchievement(achievement);
  6732. });
  6733. }
  6734.  
  6735. startNewGame(incrementStats = true) {
  6736. this.isMoving = true;
  6737.  
  6738. // Clear game state
  6739. this.grid.clearGrid();
  6740. this.score = 0;
  6741. this.isGameOver = false;
  6742. this.hasWon = false;
  6743. this.keepPlayingMode = false;
  6744. this.gameHistory = [];
  6745. this.moveCount = 0;
  6746. this.undoUsed = false;
  6747.  
  6748. this.ui.hideMessage();
  6749.  
  6750. // Increment stats if requested
  6751. if (incrementStats) {
  6752. this.stats.gamesPlayed++;
  6753. this.saveStats();
  6754. }
  6755.  
  6756. // Track this grid size as played
  6757. if (!this.settings.playedGridSizes.includes(this.settings.gridSize)) {
  6758. this.settings.playedGridSizes.push(this.settings.gridSize);
  6759. this.saveSettings();
  6760. this.events.emit('gridSizeChanged');
  6761. }
  6762.  
  6763. // Add initial tiles
  6764. this.grid.addRandomTile();
  6765. this.grid.addRandomTile();
  6766.  
  6767. // Save initial state
  6768. this.saveGameState();
  6769.  
  6770. // Update UI
  6771. this.updateUI();
  6772.  
  6773. this.isMoving = false;
  6774. }
  6775.  
  6776. async move(direction) {
  6777. if (this.isMoving || this.isGameOver) return false;
  6778.  
  6779. this.isMoving = true;
  6780.  
  6781. // Process the move
  6782. const { moved, scoreAddition } = await this.grid.move(direction);
  6783.  
  6784. if (moved) {
  6785. // Update score
  6786. this.updateScore(this.score + scoreAddition, scoreAddition);
  6787.  
  6788. // Increment move counter
  6789. this.moveCount++;
  6790. this.stats.totalMoves++;
  6791. this.saveStats();
  6792.  
  6793. // Save game state before adding new tile
  6794. this.saveGameState();
  6795.  
  6796. // Add a new random tile
  6797. const newTile = this.grid.addRandomTile();
  6798.  
  6799. // Check if this tile is a new highest
  6800. if (newTile && newTile.value > this.stats.highestTile) {
  6801. this.stats.highestTile = newTile.value;
  6802. this.saveStats();
  6803. this.events.emit('newHighTile', newTile.value);
  6804. }
  6805.  
  6806. // Check game status
  6807. this.checkGameStatus();
  6808.  
  6809. // Update UI
  6810. this.updateUI();
  6811. }
  6812.  
  6813. this.isMoving = false;
  6814. return moved;
  6815. }
  6816.  
  6817. undoMove() {
  6818. if (this.isMoving || this.gameHistory.length <= 1) return;
  6819.  
  6820. this.isMoving = true;
  6821. this.undoUsed = true;
  6822.  
  6823. // Hide any game messages
  6824. this.ui.hideMessage();
  6825. this.isGameOver = false;
  6826.  
  6827. // Remove current state
  6828. this.gameHistory.pop();
  6829.  
  6830. // Get previous state
  6831. const previousState = this.gameHistory[this.gameHistory.length - 1];
  6832.  
  6833. // Restore grid
  6834. this.grid.restoreGrid(previousState.grid);
  6835.  
  6836. // Restore score
  6837. this.score = previousState.score;
  6838.  
  6839. // Update UI
  6840. this.updateUI();
  6841. this.ui.showToast("Move undone");
  6842.  
  6843. this.isMoving = false;
  6844. }
  6845.  
  6846. keepPlaying() {
  6847. this.keepPlayingMode = true;
  6848. this.ui.hideMessage();
  6849. this.isMoving = false;
  6850. this.ui.showToast("Keep playing!");
  6851. }
  6852.  
  6853. checkGameStatus() {
  6854. if (this.isGameOver) return;
  6855.  
  6856. // Check for 2048 tile
  6857. if (!this.hasWon) {
  6858. for (let r = 0; r < this.grid.gridSize; r++) {
  6859. for (let c = 0; c < this.grid.gridSize; c++) {
  6860. const tile = this.grid.grid[r][c];
  6861. if (tile && tile.value === CONSTANTS.WINNING_VALUE) {
  6862. this.hasWon = true;
  6863. if (!this.keepPlayingMode) {
  6864. this.ui.showMessage("You Win!", "win");
  6865. }
  6866. this.events.emit('gameWon', {
  6867. score: this.score,
  6868. moves: this.moveCount,
  6869. difficulty: this.settings.difficulty,
  6870. gridSize: this.settings.gridSize,
  6871. undoUsed: this.undoUsed
  6872. });
  6873. return;
  6874. }
  6875. }
  6876. }
  6877. }
  6878.  
  6879. // Check if no moves are possible
  6880. if (!this.grid.canMove()) {
  6881. this.isGameOver = true;
  6882. this.ui.showMessage("Game Over!", "over");
  6883. this.events.emit('gameOver', {
  6884. score: this.score,
  6885. moves: this.moveCount,
  6886. highestTile: this.stats.highestTile
  6887. });
  6888. }
  6889. }
  6890.  
  6891. updateScore(newScore, addition = 0) {
  6892. const oldScore = this.score;
  6893. this.score = newScore;
  6894.  
  6895. // Update best score if needed
  6896. if (this.score > this.bestScore) {
  6897. this.bestScore = this.score;
  6898. this.saveBestScore();
  6899. this.events.emit('newHighScore', this.score);
  6900. }
  6901.  
  6902. // Update total score in stats
  6903. if (newScore > oldScore) {
  6904. this.stats.totalScore += (newScore - oldScore);
  6905. this.saveStats();
  6906. }
  6907.  
  6908. // Update UI
  6909. this.ui.updateScore(this.score, this.bestScore, addition);
  6910. }
  6911.  
  6912. saveGameState() {
  6913. this.gameHistory.push({
  6914. grid: this.grid.cloneGrid(),
  6915. score: this.score
  6916. });
  6917.  
  6918. if (this.gameHistory.length > CONSTANTS.MAX_UNDO_STEPS) {
  6919. this.gameHistory.shift();
  6920. }
  6921.  
  6922. this.ui.updateUndoButton(this.gameHistory.length > 1 && !this.isMoving);
  6923. }
  6924.  
  6925. updateUI() {
  6926. this.ui.updateScore(this.score, this.bestScore);
  6927. this.ui.updateUndoButton(this.gameHistory.length > 1 && !this.isMoving);
  6928. this.ui.updateStatsDisplay(this.stats, this.settings);
  6929. }
  6930.  
  6931. // Settings Management
  6932. loadSettings() {
  6933. try {
  6934. const savedSettings = localStorage.getItem(CONSTANTS.STORAGE_KEYS.SETTINGS);
  6935. if (savedSettings) {
  6936. const parsedSettings = JSON.parse(savedSettings);
  6937. this.settings = { ...this.settings, ...parsedSettings };
  6938. } else {
  6939. // Set default dark mode based on user preference
  6940. this.settings.darkMode = window.matchMedia &&
  6941. window.matchMedia('(prefers-color-scheme: dark)').matches;
  6942. }
  6943. } catch (e) {
  6944. console.error('Error loading settings:', e);
  6945. }
  6946. }
  6947.  
  6948. saveSettings() {
  6949. try {
  6950. localStorage.setItem(CONSTANTS.STORAGE_KEYS.SETTINGS, JSON.stringify(this.settings));
  6951. } catch (e) {
  6952. console.error('Error saving settings:', e);
  6953. this.ui.showToast("Could not save settings");
  6954. }
  6955. }
  6956.  
  6957. toggleTheme() {
  6958. this.settings.darkMode = !this.settings.darkMode;
  6959. this.ui.updateTheme(this.settings.darkMode);
  6960. this.saveSettings();
  6961. this.ui.showToast(this.settings.darkMode ? 'Dark mode enabled' : 'Light mode enabled');
  6962. }
  6963.  
  6964. toggleAnimations() {
  6965. this.settings.animationsEnabled = !this.settings.animationsEnabled;
  6966. this.ui.updateAnimationStatus(this.settings.animationsEnabled);
  6967. this.saveSettings();
  6968. this.ui.showToast(this.settings.animationsEnabled ? 'Animations enabled' : 'Animations disabled');
  6969. }
  6970.  
  6971. applySettings(newSettings) {
  6972. const needsRestart = newSettings.gridSize !== this.settings.gridSize;
  6973.  
  6974. // Update settings
  6975. this.settings.difficulty = newSettings.difficulty;
  6976.  
  6977. if (needsRestart) {
  6978. this.settings.gridSize = newSettings.gridSize;
  6979. this.ui.updateGridSize(newSettings.gridSize);
  6980. this.grid.setGridSize(newSettings.gridSize);
  6981. this.startNewGame();
  6982. }
  6983.  
  6984. this.saveSettings();
  6985. this.ui.updateStatsDisplay(this.stats, this.settings);
  6986. this.ui.showToast('Settings applied');
  6987. }
  6988.  
  6989. // Stats Management
  6990. loadStats() {
  6991. try {
  6992. const savedStats = localStorage.getItem(CONSTANTS.STORAGE_KEYS.STATS);
  6993. if (savedStats) {
  6994. this.stats = JSON.parse(savedStats);
  6995. }
  6996. } catch (e) {
  6997. console.error('Error loading stats:', e);
  6998. }
  6999. }
  7000.  
  7001. saveStats() {
  7002. try {
  7003. localStorage.setItem(CONSTANTS.STORAGE_KEYS.STATS, JSON.stringify(this.stats));
  7004. } catch (e) {
  7005. console.error('Error saving stats:', e);
  7006. }
  7007. }
  7008.  
  7009. loadBestScore() {
  7010. this.bestScore = parseInt(localStorage.getItem(CONSTANTS.STORAGE_KEYS.BEST_SCORE) || 0);
  7011. }
  7012.  
  7013. saveBestScore() {
  7014. try {
  7015. localStorage.setItem(CONSTANTS.STORAGE_KEYS.BEST_SCORE, this.bestScore);
  7016. } catch (e) {
  7017. console.error('Error saving best score:', e);
  7018. }
  7019. }
  7020.  
  7021. // Game Save/Load
  7022. saveGame() {
  7023. if (this.isGameOver) {
  7024. localStorage.removeItem(CONSTANTS.STORAGE_KEYS.GAME_STATE);
  7025. return;
  7026. }
  7027.  
  7028. try {
  7029. const saveData = {
  7030. grid: this.grid.cloneGrid(),
  7031. score: this.score,
  7032. bestScore: this.bestScore,
  7033. hasWon: this.hasWon,
  7034. keepPlayingMode: this.keepPlayingMode,
  7035. moveCount: this.moveCount,
  7036. undoUsed: this.undoUsed,
  7037. gridSize: this.settings.gridSize,
  7038. difficulty: this.settings.difficulty,
  7039. timestamp: Date.now()
  7040. };
  7041.  
  7042. localStorage.setItem(CONSTANTS.STORAGE_KEYS.GAME_STATE, JSON.stringify(saveData));
  7043. } catch (e) {
  7044. console.error('Error saving game:', e);
  7045. }
  7046. }
  7047.  
  7048. loadGame() {
  7049. try {
  7050. const savedGame = localStorage.getItem(CONSTANTS.STORAGE_KEYS.GAME_STATE);
  7051. if (!savedGame) return false;
  7052.  
  7053. const saveData = JSON.parse(savedGame);
  7054.  
  7055. // Check if the grid size matches current settings
  7056. if (saveData.gridSize !== this.settings.gridSize) {
  7057. this.ui.updateGridSize(saveData.gridSize);
  7058. this.settings.gridSize = saveData.gridSize;
  7059. this.grid.setGridSize(saveData.gridSize);
  7060. }
  7061.  
  7062. // Restore game state
  7063. this.score = saveData.score;
  7064. this.hasWon = saveData.hasWon;
  7065. this.keepPlayingMode = saveData.keepPlayingMode;
  7066. this.moveCount = saveData.moveCount || 0;
  7067. this.undoUsed = saveData.undoUsed || false;
  7068.  
  7069. // Restore difficulty if present
  7070. if (saveData.difficulty) {
  7071. this.settings.difficulty = saveData.difficulty;
  7072. }
  7073.  
  7074. // Restore grid
  7075. this.grid.restoreGrid(saveData.grid);
  7076.  
  7077. // Save initial state for undo
  7078. this.saveGameState();
  7079.  
  7080. // Check game status
  7081. this.checkGameStatus();
  7082.  
  7083. return true;
  7084. } catch (e) {
  7085. console.error('Error loading saved game:', e);
  7086. localStorage.removeItem(CONSTANTS.STORAGE_KEYS.GAME_STATE);
  7087. return false;
  7088. }
  7089. }
  7090.  
  7091. saveNamedGame(name) {
  7092. try {
  7093. // Get existing saved games
  7094. const savedGames = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.SAVED_GAMES) || '[]');
  7095.  
  7096. // Create save data
  7097. const saveData = {
  7098. name,
  7099. grid: this.grid.cloneGrid(),
  7100. score: this.score,
  7101. hasWon: this.hasWon,
  7102. keepPlayingMode: this.keepPlayingMode,
  7103. moveCount: this.moveCount,
  7104. undoUsed: this.undoUsed,
  7105. gridSize: this.settings.gridSize,
  7106. difficulty: this.settings.difficulty,
  7107. timestamp: Date.now()
  7108. };
  7109.  
  7110. // Add to saved games
  7111. savedGames.push(saveData);
  7112.  
  7113. // Limit to 10 saves
  7114. if (savedGames.length > 10) {
  7115. savedGames.shift();
  7116. }
  7117.  
  7118. // Save to localStorage
  7119. localStorage.setItem(CONSTANTS.STORAGE_KEYS.SAVED_GAMES, JSON.stringify(savedGames));
  7120.  
  7121. this.ui.showToast(`Game saved as "${name}"`);
  7122. } catch (e) {
  7123. console.error('Error saving named game:', e);
  7124. this.ui.showToast('Error saving game');
  7125. }
  7126. }
  7127.  
  7128. loadSavedGame(index) {
  7129. try {
  7130. const savedGames = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.SAVED_GAMES) || '[]');
  7131.  
  7132. if (!savedGames[index]) {
  7133. this.ui.showToast('Save not found');
  7134. return;
  7135. }
  7136.  
  7137. const saveData = savedGames[index];
  7138.  
  7139. // Update grid size if needed
  7140. if (saveData.gridSize !== this.settings.gridSize) {
  7141. this.ui.updateGridSize(saveData.gridSize);
  7142. this.settings.gridSize = saveData.gridSize;
  7143. this.grid.setGridSize(saveData.gridSize);
  7144. }
  7145.  
  7146. // Restore game state
  7147. this.score = saveData.score;
  7148. this.hasWon = saveData.hasWon;
  7149. this.keepPlayingMode = saveData.keepPlayingMode;
  7150. this.moveCount = saveData.moveCount || 0;
  7151. this.undoUsed = saveData.undoUsed || false;
  7152. this.isGameOver = false;
  7153.  
  7154. // Restore difficulty if present
  7155. if (saveData.difficulty) {
  7156. this.settings.difficulty = saveData.difficulty;
  7157. }
  7158.  
  7159. // Restore grid
  7160. this.grid.restoreGrid(saveData.grid);
  7161.  
  7162. // Clear history and save current state
  7163. this.gameHistory = [];
  7164. this.saveGameState();
  7165.  
  7166. // Update UI
  7167. this.ui.hideMessage();
  7168. this.updateUI();
  7169.  
  7170. // Check game status
  7171. this.checkGameStatus();
  7172.  
  7173. this.ui.showToast(`Loaded game "${saveData.name}"`);
  7174. } catch (e) {
  7175. console.error('Error loading saved game:', e);
  7176. this.ui.showToast('Error loading game');
  7177. }
  7178. }
  7179.  
  7180. deleteSavedGame(index) {
  7181. try {
  7182. const savedGames = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.SAVED_GAMES) || '[]');
  7183.  
  7184. if (!savedGames[index]) {
  7185. this.ui.showToast('Save not found');
  7186. return;
  7187. }
  7188.  
  7189. const name = savedGames[index].name;
  7190. savedGames.splice(index, 1);
  7191.  
  7192. localStorage.setItem(CONSTANTS.STORAGE_KEYS.SAVED_GAMES, JSON.stringify(savedGames));
  7193.  
  7194. this.ui.showToast(`Deleted save "${name}"`);
  7195. } catch (e) {
  7196. console.error('Error deleting saved game:', e);
  7197. this.ui.showToast('Error deleting save');
  7198. }
  7199. }
  7200.  
  7201. // Input Handling
  7202. handleKeyDown(e) {
  7203. // Handle keyboard shortcuts dialog first
  7204. if (this.elements.keyboardShortcuts.classList.contains('active')) {
  7205. if (e.key === 'Escape') {
  7206. e.preventDefault();
  7207. this.ui.hideKeyboardShortcuts();
  7208. }
  7209. return;
  7210. }
  7211.  
  7212. // Handle any active modals
  7213. if (this.ui.activeModals.length > 0) {
  7214. if (e.key === 'Escape') {
  7215. e.preventDefault();
  7216. this.ui.closeAllModals();
  7217. }
  7218. return;
  7219. }
  7220.  
  7221. if (this.isMoving) return;
  7222.  
  7223. // Global shortcuts
  7224. switch (e.key) {
  7225. case '?':
  7226. e.preventDefault();
  7227. this.ui.showKeyboardShortcuts();
  7228. return;
  7229. case 'm':
  7230. case 'M':
  7231. e.preventDefault();
  7232. this.toggleTheme();
  7233. return;
  7234. case 't':
  7235. case 'T':
  7236. e.preventDefault();
  7237. this.toggleAnimations();
  7238. return;
  7239. case 's':
  7240. case 'S':
  7241. if (!e.ctrlKey) { // Don't interfere with Ctrl+S
  7242. e.preventDefault();
  7243. this.ui.showModal('save');
  7244. }
  7245. return;
  7246. case 'l':
  7247. case 'L':
  7248. e.preventDefault();
  7249. this.ui.showModal('load');
  7250. return;
  7251. case 'o':
  7252. case 'O':
  7253. e.preventDefault();
  7254. this.ui.showModal('settings');
  7255. return;
  7256. }
  7257.  
  7258. // Game-specific shortcuts
  7259. if (this.isGameOver) {
  7260. if (e.key === 'r' || e.key === 'R') {
  7261. e.preventDefault();
  7262. this.startNewGame();
  7263. } else if ((e.key === 'u' || e.key === 'U' || e.key === 'z' || e.key === 'Z') && !this.elements.undoButton.disabled) {
  7264. e.preventDefault();
  7265. this.undoMove();
  7266. }
  7267. return;
  7268. }
  7269.  
  7270. // Movement keys
  7271. let direction = null;
  7272.  
  7273. switch (e.key) {
  7274. case 'ArrowUp':
  7275. case 'w':
  7276. case 'W':
  7277. direction = 'up';
  7278. break;
  7279. case 'ArrowDown':
  7280. case 's':
  7281. case 'S':
  7282. direction = 'down';
  7283. break;
  7284. case 'ArrowLeft':
  7285. case 'a':
  7286. case 'A':
  7287. direction = 'left';
  7288. break;
  7289. case 'ArrowRight':
  7290. case 'd':
  7291. case 'D':
  7292. direction = 'right';
  7293. break;
  7294. case 'r':
  7295. case 'R':
  7296. e.preventDefault();
  7297. this.startNewGame();
  7298. return;
  7299. case 'u':
  7300. case 'U':
  7301. case 'z':
  7302. case 'Z':
  7303. e.preventDefault();
  7304. if (!this.elements.undoButton.disabled) {
  7305. this.undoMove();
  7306. }
  7307. return;
  7308. }
  7309.  
  7310. if (direction) {
  7311. e.preventDefault();
  7312. this.move(direction);
  7313. }
  7314. }
  7315.  
  7316. handleTouchStart(e) {
  7317. if (this.isMoving || e.touches.length > 1) return;
  7318.  
  7319. this.touchStartX = e.touches[0].clientX;
  7320. this.touchStartY = e.touches[0].clientY;
  7321. this.touchEndX = this.touchStartX;
  7322. this.touchEndY = this.touchStartY;
  7323. }
  7324.  
  7325. handleTouchMove(e) {
  7326. if (this.isMoving || e.touches.length > 1) return;
  7327.  
  7328. this.touchEndX = e.touches[0].clientX;
  7329. this.touchEndY = e.touches[0].clientY;
  7330.  
  7331. // Prevent scrolling if a swipe is detected
  7332. const deltaX = this.touchEndX - this.touchStartX;
  7333. const deltaY = this.touchEndY - this.touchStartY;
  7334.  
  7335. if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
  7336. e.preventDefault();
  7337. }
  7338. }
  7339.  
  7340. handleTouchEnd(e) {
  7341. if (this.isMoving || this.isGameOver || e.changedTouches.length > 1) return;
  7342.  
  7343. const deltaX = this.touchEndX - this.touchStartX;
  7344. const deltaY = this.touchEndY - this.touchStartY;
  7345. const absDeltaX = Math.abs(deltaX);
  7346. const absDeltaY = Math.abs(deltaY);
  7347.  
  7348. if (Math.max(absDeltaX, absDeltaY) < CONSTANTS.SWIPE_THRESHOLD) return;
  7349.  
  7350. let direction = absDeltaX > absDeltaY
  7351. ? (deltaX > 0 ? 'right' : 'left')
  7352. : (deltaY > 0 ? 'down' : 'up');
  7353.  
  7354. this.move(direction);
  7355. }
  7356.  
  7357. get elements() {
  7358. return this.ui.elements;
  7359. }
  7360. }
  7361.  
  7362. // --- App Initialization ---
  7363. function initializeGame() {
  7364. // Collect DOM elements
  7365. const elements = {
  7366. // Game board elements
  7367. gameContainer: document.getElementById('game-container'),
  7368. gridBackground: document.getElementById('grid-background'),
  7369. tileContainer: document.getElementById('tile-container'),
  7370.  
  7371. // Score elements
  7372. scoreElement: document.getElementById('score'),
  7373. bestScoreElement: document.getElementById('best-score'),
  7374.  
  7375. // Game message elements
  7376. messageContainer: document.getElementById('game-message'),
  7377. messageText: document.getElementById('message-text'),
  7378.  
  7379. // Buttons
  7380. restartButton: document.getElementById('restart-button'),
  7381. retryButton: document.getElementById('retry-button'),
  7382. keepPlayingButton: document.getElementById('keep-playing-button'),
  7383. undoButton: document.getElementById('undo-button'),
  7384. saveScoreButton: document.getElementById('save-score-button'),
  7385.  
  7386. // Menu elements
  7387. menuButton: document.getElementById('menu-button'),
  7388. menuDropdown: document.getElementById('menu-dropdown'),
  7389. settingsLink: document.getElementById('settings-link'),
  7390. saveLink: document.getElementById('save-link'),
  7391. loadLink: document.getElementById('load-link'),
  7392. achievementsLink: document.getElementById('achievements-link'),
  7393. helpLink: document.getElementById('help-link'),
  7394.  
  7395. // Theme/animation control
  7396. themeToggle: document.getElementById('theme-toggle'),
  7397. animationToggle: document.getElementById('animation-toggle'),
  7398. animationStatus: document.getElementById('animation-status'),
  7399.  
  7400. // Keyboard shortcuts
  7401. keyboardLink: document.getElementById('keyboard-link'),
  7402. keyboardShortcuts: document.getElementById('keyboard-shortcuts'),
  7403. closeShortcuts: document.getElementById('close-shortcuts'),
  7404.  
  7405. // Toast notification
  7406. toast: document.getElementById('toast'),
  7407.  
  7408. // Achievement notification
  7409. achievementNotification: document.getElementById('achievement-notification'),
  7410. achievementDescription: document.getElementById('achievement-description'),
  7411.  
  7412. // Stats elements
  7413. statsToggle: document.getElementById('stats-toggle'),
  7414. statsContent: document.getElementById('stats-content'),
  7415. gamesPlayedElement: document.getElementById('games-played'),
  7416. highestTileElement: document.getElementById('highest-tile'),
  7417. totalMovesElement: document.getElementById('total-moves'),
  7418. totalScoreElement: document.getElementById('total-score'),
  7419. avgScoreElement: document.getElementById('avg-score'),
  7420. currentDifficulty: document.getElementById('current-difficulty'),
  7421. currentGridSize: document.getElementById('current-grid-size'),
  7422. achievementsUnlocked: document.getElementById('achievements-unlocked'),
  7423.  
  7424. // Settings modal
  7425. difficultySelect: document.getElementById('difficulty-select'),
  7426. applySettings: document.getElementById('apply-settings'),
  7427.  
  7428. // Save modal
  7429. saveName: document.getElementById('save-name'),
  7430. saveGameBtn: document.getElementById('save-game-btn')
  7431. };
  7432.  
  7433. // Create event manager
  7434. const eventManager = new EventManager();
  7435.  
  7436. // Create UI manager
  7437. const uiManager = new UIManager(elements, eventManager);
  7438.  
  7439. // Create achievement manager
  7440. const achievementManager = new AchievementManager(eventManager);
  7441.  
  7442. // Create game
  7443. const game = new Game(uiManager, eventManager);
  7444.  
  7445. // Initialize game
  7446. game.init();
  7447. }
  7448.  
  7449. // Start the game
  7450. initializeGame();
  7451. });
  7452. </script>
  7453. </body>
  7454. </html>
  7455. ```
  7456.  
  7457. ## Architectural Improvements Explanation
  7458.  
  7459. ### 1. Better Separation of Concerns
  7460.  
  7461. I've completely restructured the code using a proper component-based architecture with these key classes:
  7462.  
  7463. - **EventManager**: Manages event communication between components
  7464. - **UIManager**: Handles all UI updates and user interaction
  7465. - **GridManager**: Manages the game grid and tile movements
  7466. - **AchievementManager**: Handles achievement tracking and unlocking
  7467. - **Game**: Coordinates the overall game flow and state
  7468.  
  7469. This separation of concerns makes the code much more maintainable and extensible.
  7470.  
  7471. ### 2. Event-Driven Architecture
  7472.  
  7473. The new implementation uses an event-driven architecture where components communicate through events rather than direct method calls. This provides several benefits:
  7474.  
  7475. - Loose coupling between components
  7476. - Easier to add new features without modifying existing code
  7477. - Better testability
  7478. - More flexible communication patterns
  7479.  
  7480. ### 3. Promise-Based Animation Handling
  7481.  
  7482. Instead of using fixed timeouts for animations, I've implemented a Promise-based approach:
  7483.  
  7484. ```javascript
  7485. async move(direction) {
  7486. // Process the move and return a Promise
  7487. const { moved, scoreAddition } = await this.grid.move(direction);
  7488.  
  7489. if (moved) {
  7490. // This code runs only after animations complete
  7491. }
  7492. }
  7493. ```
  7494.  
  7495. This ensures animations complete properly before the next game state update, preventing visual glitches.
  7496.  
  7497. ### 4. Improved Error Handling
  7498.  
  7499. I've added more robust error handling throughout the codebase, particularly around localStorage operations:
  7500.  
  7501. ```javascript
  7502. try {
  7503. localStorage.setItem(CONSTANTS.STORAGE_KEYS.SETTINGS, JSON.stringify(this.settings));
  7504. } catch (e) {
  7505. console.error('Error saving settings:', e);
  7506. this.ui.showToast("Could not save settings");
  7507. }
  7508. ```
  7509.  
  7510. This prevents the game from crashing if storage is full or unavailable.
  7511.  
  7512. ## New Features
  7513.  
  7514. ### 1. Achievements System
  7515.  
  7516. I've added a comprehensive achievements system with 10 achievements:
  7517.  
  7518. - First Victory: Reach 2048
  7519. - Super Tile: Create a 4096+ tile
  7520. - Persistent: Play 10 games
  7521. - Master: Play 50 games
  7522. - High Scorer: Reach 20,000+ points
  7523. - Efficient: Win with fewer than 300 moves
  7524. - Comeback Kid: Undo a move and still win
  7525. - Explorer: Play on all grid sizes
  7526. - Challenge Seeker: Win on Hard/Expert difficulty
  7527. - Completionist: Unlock all other achievements
  7528.  
  7529. The system includes:
  7530. - Achievement tracking logic
  7531. - UI for viewing achievements
  7532. - Popup notifications when achievements are unlocked
  7533.  
  7534. ### 2. Named Game Saves
  7535.  
  7536. Players can now save multiple games with custom names:
  7537.  
  7538. ```javascript
  7539. saveNamedGame(name) {
  7540. // Save the current game with a name
  7541. const saveData = {
  7542. name,
  7543. grid: this.grid.cloneGrid(),
  7544. score: this.score,
  7545. // ...other game state
  7546. timestamp: Date.now()
  7547. };
  7548.  
  7549. // Add to saved games list
  7550. savedGames.push(saveData);
  7551. }
  7552. ```
  7553.  
  7554. The UI allows loading and deleting saved games from a list.
  7555.  
  7556. ### 3. Variable Grid Sizes
  7557.  
  7558. Players can now choose from 4 different grid sizes:
  7559. - 3×3: Smaller, faster games
  7560. - 4×4: Classic experience
  7561. - 5×5: More strategic
  7562. - 6×6: Ultimate challenge
  7563.  
  7564. The grid size affects:
  7565. - Cell size (responsive)
  7566. - Game difficulty
  7567. - Strategy required
  7568.  
  7569. ### 4. Difficulty Levels
  7570.  
  7571. I've added four difficulty levels that affect the probability of spawning 4s vs 2s:
  7572. - Easy: 10% chance of 4s
  7573. - Normal: 20% chance of 4s
  7574. - Hard: 30% chance of 4s
  7575. - Expert: 40% chance of 4s
  7576.  
  7577. ### 5. Enhanced Settings Menu
  7578.  
  7579. The settings modal allows players to customize:
  7580. - Difficulty level
  7581. - Grid size
  7582. - (Animation and theme settings were already available)
  7583.  
  7584. ## UX and Accessibility Improvements
  7585.  
  7586. 1. **Better Keyboard Navigation**: Improved focus management in modals and added more keyboard shortcuts
  7587.  
  7588. 2. **Enhanced Notifications**: Toast messages and achievement notifications provide better feedback
  7589.  
  7590. 3. **Responsive Design Improvements**: The game now adapts better to various screen sizes and adjusts cell sizes based on grid size
  7591.  
  7592. 4. **Improved ARIA Attributes**: Added more ARIA roles, states, and properties for better screen reader support
  7593.  
  7594. 5. **Better Animation Synchronization**: Animations now complete properly before the next game state update
  7595.  
  7596. ## Performance Optimizations
  7597.  
  7598. 1. **Reduced DOM Manipulation**: The new structure minimizes unnecessary DOM operations
  7599.  
  7600. 2. **Event Delegation**: Used event delegation where appropriate to reduce the number of event listeners
  7601.  
  7602. 3. **Better Touch Handling**: Improved touch detection for better mobile experience
  7603.  
  7604. 4. **Optimized Rendering**: Used CSS transitions and animations more efficiently
  7605.  
  7606. ## Conclusion
  7607.  
  7608. This final implementation builds upon all the previous work while adding significant architectural improvements and new features. The code is now more maintainable, extensible, and provides a much richer user experience while maintaining the core gameplay that makes 2048 so engaging.
  7609.  
  7610. ## Final Collaboration Summary
  7611.  
  7612. # 2048 Game: Collaborative Evolution and Final Architecture
  7613.  
  7614. ## 1. Summary of Design Evolution
  7615.  
  7616. Our collaborative development of the 2048 game progressed through four distinct phases, each building upon the previous one with significant improvements:
  7617.  
  7618. ### Phase 1: Initial Implementation (Claude)
  7619. - Basic game mechanics with functional grid and tile movement
  7620. - Responsive design with CSS variables
  7621. - Simple animations for tile appearance and merges
  7622. - Score tracking with localStorage persistence
  7623. - Basic undo functionality
  7624. - Dark mode support
  7625. - Mobile touch controls
  7626.  
  7627. The initial implementation focused on creating a working game with essential features, but had limitations in code organization and animation handling.
  7628.  
  7629. ### Phase 2: Refactoring and Enhancement (Gemini)
  7630. - Introduced the `Tile` class for better encapsulation
  7631. - Improved animation system to maintain tile identity during moves
  7632. - Fixed issues with animation synchronization
  7633. - Added "Keep Playing" feature after reaching 2048
  7634. - Improved accessibility with ARIA attributes
  7635. - Enhanced keyboard navigation
  7636.  
  7637. Gemini's contribution significantly improved the animation system and code organization, particularly through the introduction of the `Tile` class, but the code was still largely procedural.
  7638.  
  7639. ### Phase 3: Architectural Improvements (Claude)
  7640. - Added game history with undo functionality
  7641. - Enhanced statistics tracking
  7642. - Implemented comprehensive save/load functionality
  7643. - Added achievement system framework
  7644. - Improved dark mode with system preference detection
  7645. - Added score addition animations
  7646. - Enhanced touch handling
  7647.  
  7648. My third iteration added many quality-of-life features while maintaining the same basic architecture, focusing on user experience enhancements.
  7649.  
  7650. ### Phase 4: Complete Architectural Refactoring (Gemini)
  7651. - Introduced the `Game` class for better encapsulation
  7652. - Separated UI concerns from game logic
  7653. - Improved event handling with bound methods
  7654. - Enhanced localStorage error handling
  7655. - Reorganized constants and configuration
  7656. - Added ARIA improvements for accessibility
  7657.  
  7658. Gemini's second contribution represented a major architectural improvement by properly encapsulating game state and logic in a dedicated class.
  7659.  
  7660. ### Phase 5: Final Implementation (Claude)
  7661. - Implemented full component-based architecture with multiple classes
  7662. - Added event-driven communication between components
  7663. - Implemented Promise-based animation handling
  7664. - Added comprehensive achievement system
  7665. - Introduced variable grid sizes and difficulty levels
  7666. - Added named game saves with management UI
  7667. - Enhanced keyboard shortcuts and accessibility
  7668.  
  7669. The final implementation represents a complete architectural overhaul, focusing on separation of concerns, extensibility, and rich feature set.
  7670.  
  7671. ## 2. Final Architecture and Implementation
  7672.  
  7673. The final 2048 game implementation is built on a component-based, event-driven architecture with five main classes:
  7674.  
  7675. ### EventManager
  7676. - Implements a simple publish/subscribe pattern
  7677. - Allows components to communicate without direct dependencies
  7678. - Provides `on()`, `off()`, and `emit()` methods for event handling
  7679.  
  7680. ```javascript
  7681. class EventManager {
  7682. constructor() {
  7683. this.events = {};
  7684. }
  7685.  
  7686. on(event, callback) {
  7687. if (!this.events[event]) {
  7688. this.events[event] = [];
  7689. }
  7690. this.events[event].push(callback);
  7691. return this;
  7692. }
  7693.  
  7694. off(event, callback) {
  7695. // Remove event listener
  7696. }
  7697.  
  7698. emit(event, ...args) {
  7699. // Trigger event callbacks
  7700. }
  7701. }
  7702. ```
  7703.  
  7704. ### UIManager
  7705. - Manages all DOM interactions and UI updates
  7706. - Handles modals, notifications, and user input
  7707. - Updates visual elements based on game state
  7708. - Manages theme switching and animation toggling
  7709.  
  7710. ```javascript
  7711. class UIManager {
  7712. constructor(elements, eventManager) {
  7713. this.elements = elements;
  7714. this.events = eventManager;
  7715. // Initialize UI
  7716. }
  7717.  
  7718. // UI update methods
  7719. updateScore(score, bestScore, addition) { /* ... */ }
  7720. showMessage(text, type) { /* ... */ }
  7721. updateGridSize(size) { /* ... */ }
  7722. showAchievement(achievement) { /* ... */ }
  7723.  
  7724. // Modal management
  7725. showModal(modalType) { /* ... */ }
  7726. closeAllModals() { /* ... */ }
  7727.  
  7728. // Event setup
  7729. setupUIEvents() { /* ... */ }
  7730. }
  7731. ```
  7732.  
  7733. ### GridManager
  7734. - Manages the game grid and all tile operations
  7735. - Handles tile creation, movement, and merging
  7736. - Provides grid state inspection methods
  7737. - Implements Promise-based move operations for animation synchronization
  7738.  
  7739. ```javascript
  7740. class GridManager {
  7741. constructor(gridSize, tileContainer, settings) {
  7742. this.gridSize = gridSize;
  7743. this.tileContainer = tileContainer;
  7744. this.settings = settings;
  7745. this.grid = [];
  7746. this.initializeGrid();
  7747. }
  7748.  
  7749. // Grid operations
  7750. initializeGrid() { /* ... */ }
  7751. addRandomTile() { /* ... */ }
  7752. canMove() { /* ... */ }
  7753.  
  7754. // Movement logic
  7755. async move(direction) {
  7756. // Returns a Promise that resolves when animations complete
  7757. return new Promise(resolve => {
  7758. // Process move and animations
  7759. resolve({ moved, scoreAddition });
  7760. });
  7761. }
  7762.  
  7763. // Grid state management
  7764. cloneGrid() { /* ... */ }
  7765. restoreGrid(savedGrid) { /* ... */ }
  7766. }
  7767. ```
  7768.  
  7769. ### AchievementManager
  7770. - Tracks player achievements
  7771. - Checks for achievement conditions
  7772. - Triggers achievement notifications
  7773. - Persists achievement state
  7774.  
  7775. ```javascript
  7776. class AchievementManager {
  7777. constructor(eventManager) {
  7778. this.events = eventManager;
  7779. this.achievements = this.loadAchievements();
  7780. this.setupAchievementListeners();
  7781. }
  7782.  
  7783. // Achievement tracking
  7784. unlockAchievement(id, title, description) { /* ... */ }
  7785.  
  7786. // Achievement checks
  7787. checkFirstWin() { /* ... */ }
  7788. checkSuperTile(value) { /* ... */ }
  7789. checkHighScore(score) { /* ... */ }
  7790. // Other achievement checks
  7791. }
  7792. ```
  7793.  
  7794. ### Tile
  7795. - Represents an individual tile on the grid
  7796. - Manages its own DOM element and animations
  7797. - Handles position updates and value changes
  7798.  
  7799. ```javascript
  7800. class Tile {
  7801. constructor(value, row, col, parentContainer, settings) {
  7802. this.value = value;
  7803. this.row = row;
  7804. this.col = col;
  7805. this.parentContainer = parentContainer;
  7806. this.settings = settings;
  7807. this.element = this.createElement();
  7808. // Initialize tile
  7809. }
  7810.  
  7811. // Tile operations
  7812. updateValue(newValue) { /* ... */ }
  7813. updatePosition(animate) { /* ... */ }
  7814. playMergeAnimation() { /* ... */ }
  7815. remove(merged) { /* ... */ }
  7816. clone() { /* ... */ }
  7817. }
  7818. ```
  7819.  
  7820. ### Game
  7821. - Coordinates overall game flow and state
  7822. - Manages game settings and statistics
  7823. - Handles user input and game logic
  7824. - Coordinates communication between components
  7825.  
  7826. ```javascript
  7827. class Game {
  7828. constructor(uiManager, eventManager) {
  7829. this.ui = uiManager;
  7830. this.events = eventManager;
  7831.  
  7832. // Game state
  7833. this.grid = null;
  7834. this.score = 0;
  7835. this.bestScore = 0;
  7836. // Other state properties
  7837.  
  7838. this.setupEventListeners();
  7839. }
  7840.  
  7841. // Game flow methods
  7842. init() { /* ... */ }
  7843. startNewGame(incrementStats) { /* ... */ }
  7844. async move(direction) { /* ... */ }
  7845. undoMove() { /* ... */ }
  7846. checkGameStatus() { /* ... */ }
  7847.  
  7848. // Settings and stats management
  7849. loadSettings() { /* ... */ }
  7850. saveSettings() { /* ... */ }
  7851. applySettings(newSettings) { /* ... */ }
  7852.  
  7853. // Save/load functionality
  7854. saveGame() { /* ... */ }
  7855. loadGame() { /* ... */ }
  7856. saveNamedGame(name) { /* ... */ }
  7857.  
  7858. // Input handling
  7859. handleKeyDown(e) { /* ... */ }
  7860. handleTouchStart(e) { /* ... */ }
  7861. handleTouchMove(e) { /* ... */ }
  7862. handleTouchEnd(e) { /* ... */ }
  7863. }
  7864. ```
  7865.  
  7866. ### Key Architectural Features
  7867.  
  7868. 1. **Event-Driven Communication**: Components communicate through events rather than direct method calls, reducing coupling.
  7869.  
  7870. 2. **Promise-Based Animation**: Move operations return Promises that resolve when animations complete, ensuring proper synchronization.
  7871.  
  7872. 3. **Separation of Concerns**: Each class has a single responsibility, making the code more maintainable.
  7873.  
  7874. 4. **Centralized Configuration**: Game constants are defined in a single CONSTANTS object for easy modification.
  7875.  
  7876. 5. **Persistent State Management**: Game state, settings, and achievements are saved to localStorage with proper error handling.
  7877.  
  7878. 6. **Responsive Design**: The UI adapts to different screen sizes and grid configurations.
  7879.  
  7880. 7. **Accessibility Support**: Comprehensive ARIA attributes, keyboard navigation, and focus management.
  7881.  
  7882. ## 3. Areas for Further Improvement
  7883.  
  7884. Despite the significant improvements made through our iterations, several areas could be enhanced with additional work:
  7885.  
  7886. ### 1. Testing Framework
  7887. - Implement unit tests for core game logic
  7888. - Add integration tests for UI components
  7889. - Create automated accessibility tests
  7890.  
  7891. ### 2. Performance Optimizations
  7892. - Implement virtual DOM-like approach for large grid sizes
  7893. - Use Web Workers for computationally intensive operations
  7894. - Add requestAnimationFrame for smoother animations
  7895. - Implement proper garbage collection for removed tiles
  7896.  
  7897. ### 3. Enhanced Accessibility
  7898. - Add more descriptive ARIA live regions for game state changes
  7899. - Implement a screen reader-specific game mode with verbal cues
  7900. - Add high contrast theme option
  7901. - Implement alternative input methods (e.g., voice commands)
  7902.  
  7903. ### 4. Multiplayer Features
  7904. - Add online leaderboards
  7905. - Implement multiplayer race mode
  7906. - Add friend challenges with shared game states
  7907. - Implement spectator mode
  7908.  
  7909. ### 5. Progressive Web App (PWA) Features
  7910. - Add offline support with Service Workers
  7911. - Implement push notifications for achievements
  7912. - Add "Add to Home Screen" functionality
  7913. - Optimize asset loading and caching
  7914.  
  7915. ### 6. Advanced Game Features
  7916. - Add time-based challenges
  7917. - Implement different game modes (e.g., zen mode, countdown mode)
  7918. - Add power-ups or special tiles
  7919. - Create custom themes with user-defined colors
  7920.  
  7921. ### 7. Code Improvements
  7922. - Implement TypeScript for better type safety
  7923. - Use Web Components for better encapsulation
  7924. - Add proper documentation with JSDoc
  7925. - Implement state management with reducers for more predictable state changes
  7926.  
  7927. ### 8. Analytics and Telemetry
  7928. - Add anonymous usage statistics
  7929. - Implement heat maps for most common moves
  7930. - Track common patterns leading to high scores
  7931. - Add player insights based on gameplay
  7932.  
  7933. ### 9. Internationalization
  7934. - Add support for multiple languages
  7935. - Implement RTL support for appropriate languages
  7936. - Add cultural variations of the game
  7937.  
  7938. ### 10. Advanced UI Features
  7939. - Add animations between game states (win/lose transitions)
  7940. - Implement confetti effect for achievements
  7941. - Add sound effects with volume control
  7942. - Create a tutorial mode for new players
  7943.  
  7944. By addressing these areas, the 2048 game could be further enhanced to provide an even better user experience, improved performance, and greater accessibility, while maintaining the clean architecture established in our final implementation.
  7945.  
  7946.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement