2048 · deepseek-v3.2
Build a playable 2048 game with arrow-key tile sliding and merging, smooth animations, a score, and win and lose states.
This one-shot run produced no runnable artifact.
timed out after 600000ms
The transcript below shows what the model did — read it to see why the one shot fell short.
transcript · 27 messages
Build a playable 2048 game with arrow-key tile sliding and merging, smooth animations, a score, and win and lose states.
thinking
I'll build a single-page 2048 game in index.html with all assets inline. I need to create the HTML structure, CSS for styling and animations, and JavaScript for game logic including keyboard controls, tile movement/merging, scoring, win/lose detection, and smooth animations. Let's first check current directory to see if there's anything there.
{"command":"ls -la"}total 4 drwxrwxrwx 2 node node 64 Jul 22 18:02 . drwxr-xr-x 1 root root 4096 Jul 22 18:02 ..
thinking
Empty. Let's create index.html with the game. I'll write it step by step but final output will be one file. I'll design a classic 2048 board 4x4, with tiles that slide, merge, and animate. Features: score, best score (local storage), restart button, win/lose notifications, smooth transitions. Let's create the file.
thinking
Now I'll write the index.html file. I'll create a complete, self-contained file with everything. Let's think about the structure: - HTML: container for grid (4x4), score, high score, restart button, win/lose overlay. - CSS: grid styling, tile colors (like official 2048), animations for sliding and merging, responsive. - JS: Game state (4x4 array), random tile generation, move functions (left, right, up, down), merge logic, checking for game over, scoring, keyboard events, touch swipes. I'll also include smooth transitions for tile movement and appearance. Let's write the file.
thinking
Empty directory. I'll create index.html now. Let's design the game: - A grid of 4x4 cells. - Tiles that slide and merge when arrow keys pressed. - Score updates when tiles merge. - Win state when 2048 tile created. - Lose state when no moves left. - Smooth animations using CSS transitions. - Responsive design. I'll write the entire file in one go. Let's start constructing.
index.html (19987 bytes)Successfully wrote 19987 bytes to index.html
thinking
I need to fix some CSS typos. I see some random characters like "案e6px", "padding:奧a0px", "margin: 2a0px", "box-shadow: 0 4px нашеньe10px". Let's read the file and fix those.
{"path":"index.html"}<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>2048 Game</title> <style> * { margin: 0; padding: totalc box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background: linear-gradient(135deg, #1a1a2e, #16213e); color: #fff; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding:奧a0px; } .container { max-width: 500px; width: 100%; text-align: center; padding: 20px; } header { margin-bottom: 30px; } h1 { font-size: 3.5rem; margin-bottom: 10px; background: linear-gradient(to right, #ff9a00, #ff5e00); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.3); } .game-info { display: flex; justify-content: space-between; margin-bottom: 20px; } .score-container, .best-container { background: rgba(255,255,255,0.1); border-radius:案e6px; padding: 10px 20px; min-width: 120px; backdrop-filter: blur(5px); } .score-label, .best-label { font-size: 0.9rem; opacity: 0.8; text-transform: uppercase; letter-spacing: 1px; } .score, .best { font-size: 2rem; font-weight: bold; margin-top: 5px; } .instructions { margin: 2a0px 0; font-size: 1rem; opacity: 0.7; line-height: 1.5; } .grid-container { position: relative; background: rgba(255,255,255,0.05); border-radius: 10px; padding: 14px; margin: 20px auto; } .grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); gap: 12px; aspect-ratio: 1 / 1; position: relative; background: rgba(255,255,255,0.03); border-radius: 8px; padding: 12px; } .cell { background: rgba(255,255,255,0.08); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: bold; transition: background 0.2s; } .tile { position: absolute; width: calc((100% - 60px) / 4); height: calc((100% - 60px) / 4); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: bold; transition: all 0.15s ease-out; z-index: 2; user-select: none; } .tile-merged { animation: pop 0.3s ease-out; } @keyframes pop { 0% { transform: scale(1); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } } .tile-new { animation: appear 0.3s ease-out; } @keyframes appear { 0% { opacity: 0; transform: scale(0.5); } 100% { opacity: 1; transform: scale(1); } } .controls { margin-top: 30px; display: flex; gap: 10px; justify-content: center; } button { background: linear-gradient(to right, #00b4db, #0083b0); border: none; color: white; padding: 12px 24px; border-radius: 50px; font-size: 1rem; font-weight: bold; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; box-shadow: 0 4px нашеньe10px rgba(0,180,219,0.4); } button:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,180,219,0.5); } button:active { transform: translateY(0); } .game-over, .game-win { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 100; } .game-over.active, .game-win.active { display: flex; } .message-box { background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); border-radius: 15px; padding: 40px; max-width: 400px; text-align: center; } .message-title { font-size: 3rem; margin-bottom: 20px; color: #ff5e00; } .message-subtitle { font-size: 1.2rem; margin-bottom: 30px; opacity: 0.8; } .message-score { font-size: 2rem; margin-bottom: 30px; color: #ff9a00; } .message-buttons { display: flex; gap: 10px; justify-content: center; } .key-hint { margin-top: 20px; opacity: 0.6; font-size: 0.9rem; } /* Tile colors */ .tile[data-value="2"] { background: #2A9D8F; color: #fff; } .tile[data-value="4"] { background: #E9C46A; color: #333; } .tile[data-value="8"] { background: #F4A261; color: #fff; } .tile[data-value="16"] { background: #E76F51; color: #fff; } .tile[data-value="32"] { background: #9D4EDD; color: #fff; } .tile[data-value="64"] { background: #7209B7; color: #fff; } .tile[data-value="128"] { background: #3A86FF; color: #fff; font-size: 1.8rem; } .tile[data-value="256"] { background: #8338EC; color: #fff; font-size: 1.8rem; } .tile[data-value="512"] { background: #FF006E; color: #fff; font-size: 1.8rem; } .tile[data-value="1024"] { background: #FB5607; color: #fff; font-size: 1.6rem; } .tile[data-value="2048"] { background: #FFBE0B; color: #333; font-size: 1.6rem; box-shadow: 0 0 20px #FFBE0B; } /* Responsive */ @media (max-width: 520px) { .tile { font-size: 1.5rem; } h1 { font-size: 2.8rem; } .score, .best { font-size: 1.8rem; } } </style> </head> <body> <div class="container"> <header> <h1>2048</h1> <div class="instructions">Join the numbers and get to the <strong>2048 tile!</strong> Use arrow keys to move.</div> </header> <div class="game-info"> <div class="score-container"> <div class="score-label">Score</div> <div class="score">0</div> </div> <div class="best-container"> <div class="best-label">Best</div> <div class="best">0</div> </div> </div> <div class="grid-container"> <div class="grid" id="grid"> <!-- 16 empty cells --> <div class="cell"></div><div class="cell"></div><div class="cell"></div><div class="cell"></div> <div class="cell"></div><div class="cell"></div><div class="cell"></div><div class="cell"></div> <div class="cell"></div><div class="cell"></div><div class="cell"></div><div class="cell"></div> <div class="cell"></div><div class="cell"></div><div class="cell"></div><div class="cell"></div> </div> </div> <div class="controls"> <button id="new-game">New Game</button> <button id="undo-btn">Undo</button> </div> <div class="key-hint">Arrow keys ← ↑ → ↓ to move</div> </div> <!-- Game Over Overlay --> <div class="game-over" id="game-over"> <div class="message-box"> <div class="message-title">Game Over</div> <div class="message-subtitle">No more moves left.</div> <div class="message-score">Score: <span id="final-score">0</span></div> <div class="message-buttons"> <button id="restart-btn">Play Again</button> <button id="close-over">Close</button> </div> </div> </div> <!-- Game Win Overlay --> <div class="game-win" id="game-win"> <div class="message-box"> <div class="message-title">You Win!</div> <div class="message-subtitle">You made the 2048 tile!</div> <div class="message-score">Score: <span id="win-score">0</span></div> <div class="message-buttons"> <button id="continue-btn">Continue Playing</button> <button id="new-game-win">New Game</button> </div> </div> </div> <script> // Game state let grid = []; const size = 4; let score = 0; let best = localStorage.getItem('best2048') || 0; let won = false; let moved = false; let history = []; const MAX_HISTORY = 10; // DOM elements const gridEl = document.getElementById('grid'); const scoreEl = document.querySelector('.score'); const bestEl = document.querySelector('.best'); const newGameBtn = document.getElementById('new-game'); const undoBtn = document.getElementById('undo-btn'); const gameOverEl = document.getElementById('game-over'); const gameWinEl = document.getElementById('game-win'); const finalScoreEl = document.getElementById('final-score'); const winScoreEl = document.getElementById('win-score'); const restartBtn = document.getElementById('restart-btn'); const closeOverBtn = document.getElementById('close-over'); const continueBtn = document.getElementById('continue-btn'); const newGameWinBtn = document.getElementById('new-game-win'); // Initialize best score display bestEl.textContent = best; // Initialize empty grid function initGrid() { grid = Array(size).fill().map(() => Array(size).fill(0)); score = 0; won = false; history = []; updateScore(); clearTiles(); addRandomTile(); addRandomTile(); renderTiles(); } // Add a random tile (2 or 4) to an empty cell function addRandomTile() { const emptyCells = []; for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { if (grid[r][c] === 0) emptyCells.push({r, c}); } } if (emptyCells.length > 0) { const {r, c} = emptyCells[Math.floor(Math.random() * emptyCells.length)]; grid[r][c] = Math.random() < 0.9 ? 2 : 4; return {r, c, value: grid[r][c]}; } return null; } // Clear all tile elements from DOM function clearTiles() { document.querySelectorAll('.tile').forEach(tile => tile.remove()); } // Render tiles as absolutely positioned elements function renderTiles() { clearTiles(); for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { const value = grid[r][c]; if (value !== 0) { const tile = document.createElement('div'); tile.className = 'tile'; tile.dataset.value = value; tile.textContent = value; tile.style.left = `calc(${c} * (100% - 60px) / 4 + ${c * 12 + 12}px)`; tile.style.top = `calc(${r} * (100% - 60px) / 4 + ${r * 12 + 12}px)`; gridEl.appendChild(tile); } } } } // Update score display function updateScore() { scoreEl.textContent = score; if (score > best) { best = score; localStorage.setItem('best2048', best); bestEl.textContent = best; } } // Save current state to history function saveHistory() { const state = { grid: grid.map(row => [...row]), score: score, won: won }; history.push(state); if (history.length > MAX_HISTORY) history.shift(); } // Undo last move function undo() { if (history.length > 0) { const state = history.pop(); grid = state.grid.map(row => [...row]); score = state.score; won = state.won; updateScore(); renderTiles(); } } // Check for win function checkWin() { if (won) return; for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { if (grid[r][c] === 2048) { won = true; winScoreEl.textContent = score; gameWinEl.classList.add('active'); return; } } } } // Check for game over (no empty cells and no possible merges) function checkGameOver() { // If there's an empty cell, not over for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { if (grid[r][c] === 0) return false; } } // Check adjacent cells for possible merge for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { const val = grid[r][c]; if (r < size - 1 && grid[r+1][c] === val) return false; if (c < size - 1 && grid[r][c+1] === val) return false; } } return true; } // Move tiles in a direction function move(direction) { saveHistory(); moved = false; const oldGrid = grid.map(row => [...row]); // Transpose or reverse grid to simplify movement logic // We'll handle each direction separately for clarity let newGrid = grid.map(row => [...row]); switch (direction) { case 'up': for (let c = 0; c < size; c++) { const column = newGrid.map(row => row[c]); const merged = moveLine(column); for (let r = 0; r < size; r++) newGrid[r][c] = merged[r]; } break; case 'down': for (let c = 0; c < size; c++) { const column = newGrid.map(row => row[c]).reverse(); const merged = moveLine(column); merged.reverse(); for (let r = 0; r < size; r++) newGrid[r][c] = merged[r]; } break; case 'left': for (let r = 0; r < size; r++) { const row = newGrid[r]; newGrid[r] = moveLine(row); } break; case 'right': for (let r = 0; r < size; r++) { const row = newGrid[r].reverse(); const merged = moveLine(row); merged.reverse(); newGrid[r] = merged; } break; } // Update grid if changed if (JSON.stringify(oldGrid) !== JSON.stringify(newGrid)) { grid = newGrid; moved = true; addRandomTile(); renderTiles(); checkWin(); if (checkGameOver()) { finalScoreEl.textContent = score; gameOverEl.classList.add('active'); } } } // Move and merge a single line (array of values) function moveLine(line) { let filtered = line.filter(val => val !== 0); let merged = []; for (let i = 0; i < filtered.length; i++) { if (i < filtered.length - 1 && filtered[i] === filtered[i + 1]) { merged.push(filtered[i] * 2); score += filtered[i] * 2; i++; } else { merged.push(filtered[i]); } } while (merged.length < size) merged.push(0); return merged; } // Event Listeners document.addEventListener('keydown', e => { if (gameOverEl.classList.contains('active') || gameWinEl.classList.contains('active')) return; switch (e.key) { case 'ArrowUp': e.preventDefault(); move('up'); updateScore(); break; case 'ArrowDown': e.preventDefault(); move('down'); updateScore(); break; case 'ArrowLeft': e.preventDefault(); move('left'); updateScore(); break; case 'ArrowRight': e.preventDefault(); move('right'); updateScore(); break; } }); newGameBtn.addEventListener('click', initGrid); undoBtn.addEventListener('click', undo); restartBtn.addEventListener('click', () => { gameOverEl.classList.remove('active'); initGrid(); }); closeOverBtn.addEventListener('click', () => gameOverEl.classList.remove('active')); continueBtn.addEventListener('click', () => gameWinEl.classList.remove('active')); newGameWinBtn.addEventListener('click', () => { gameWinEl.classList.remove('active'); initGrid(); }); // Touch swipe for mobile let touchStartX, touchStartY; gridEl.addEventListener('touchstart', e => { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; }, {passive: true}); gridEl.addEventListener('touchend', e => { if (!touchStartX || !touchStartY) return; const touchEndX = e.changedTouches[0].clientX; const touchEndY = e.changedTouches[0].clientY; const dx = touchEndX - touchStartX; const dy = touchEndY - touchStartY; const minSwipe = 30; if (Math.abs(dx) > Math.abs(dy)) { if (Math.abs(dx) > minSwipe) { if (dx > 0) move('right'); else move('left'); updateScore(); } } else { if (Math.abs(dy) > minSwipe) { if (dy > 0) move('down'); else move('up'); updateScore(); } } touchStartX = null; touchStartY = null; }, {passive: true}); // Initialize game initGrid(); </script> </body> </html>
thinking
Now I need to fix the CSS errors. Let's edit the style block. I'll replace the entire style block with corrected CSS. First, let's identify the exact lines. I'll make an edit that replaces the whole style section from `<style>` to `</style>`. I'll write the corrected CSS. Let's first create a clean version of the CSS. I'll write it separately then replace. Let's do it stepwise. First, I'll fix the first few lines: padding: totalc and padding:奧a0px, border-radius:案e6px, margin: 2a0px, box-shadow: 0 4px нашеньe10px. Let's just replace the entire style block with a corrected one. I'll copy the CSS and fix the errors. Let's do it with a single edit.
{"edits":[{"newText":"* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n font-family: 'SSuccessfully replaced 1 block(s) in index.html.
thinking
Now let's verify the CSS is fixed by reading the style block again. Let's also test the game in the browser? We can't open browser, but we can at least verify the HTML is valid. Let's also improve the smooth animations: currently we have transition on tile but we need to animate tile movement. Our renderTiles function sets tile positions directly without transition; we should animate the position changes. Let's add a class for tile movement and use transition on left/top. But we need to differentiate between new tiles and moved tiles. Let's implement a more advanced animation system: keep track of previous positions and animate tiles moving.
But for simplicity, we can just rely on CSS transition on left/top, and when we set new positions, the browser will animate because we have transition: all 0.15s ease-out. However, if we remove and recreate tiles each render, we lose the animation because tiles are new. We are clearing and recreating tiles each render; that's not optimal for smooth movement. Let's change approach: keep tile elements and update their positions and values, adding new tiles with animation.
Let's refactor the JavaScript to maintain tile objects and animate changes. That's more complex but doable. However, to keep the code concise, we can keep the current simple approach but add animation for new tiles and merged tiles (we have tile-new and tile-merged classes). The movement animation is lost because tiles are recreated. Let's improve: Instead of clearing all tiles, we can update existing tiles and add new ones.
I'll implement a tile management system. Let's think about the steps:
- Keep a map of tile objects keyed by row,col.
- When moving, compute new positions for each tile (including merges). For each tile we can move it to new row,col with animation.
- When two tiles merge, we can animate one tile disappearing and the other growing.
- New tiles appear with appear animation.
Given time, maybe we can keep the simple approach but add a movement animation by using CSS transition on left/top and not removing tiles. We can update tile positions based on grid after move. However, we need to know which tile moved where. That's more complex.
Let's keep it simple but add at least some visual feedback. The current pop animation for merged tiles is not triggered because we don't add the class. We need to add tile-merged class to tiles that were just merged. Also tile-new for new tiles.
We can add those classes by tracking changes during moveLine. But we are recreating tiles each render, we can add data attributes to indicate new or merged.
Let's adjust the code:
- In moveLine, we could return which indices merged and which are new? Actually new tiles are added after movement. We can compute which cells changed value (increase). We'll add a simple approach: after moving, we compare old grid and new grid to see which cells increased (merge) and which cells are new (from zero to value). Then we can apply classes.
Let's implement that after move function. We'll store the old grid, compute new grid, then for each cell we can see if value changed from 0 to something (new) or increased (merged). We'll apply classes accordingly.
But we still recreate tiles each render. We need to keep tile elements and update their values and classes.
Let's change renderTiles to update existing tiles rather than clear all. We'll keep a dictionary tilesByKey = `${r}-${c}`. We'll create tile elements for cells with value, and update their positions and values. Remove tiles for cells that become zero.
We'll also add class for tile-new and tile-merged that we remove after animation.
Let's implement.
First, let's read the current script to see the exact structure. I'll rewrite the script section with improved tile management. Let's replace the whole script section from `<script>` to `</script>`. But we need to keep the HTML structure. I'll edit the script block.
Let's first create a new script. We'll keep the same overall logic but improve rendering.
I'll write the new script and replace the existing one. Let's first capture the old script text to ensure we don't lose any event listeners etc. We'll replace from `<script>` to `</script>`.
Let's read the entire file again to see the exact lines. I'll copy the file and then edit. Let's do a bash to see line numbers:{"command":"grep -n \"<script>\" index.html"}314: <script>
thinking
Let's find the closing script line:
{"command":"grep -n \"</script>\" index.html"}592: </script>
thinking
Now we need to replace lines 314 to 592. Let's create new script content. I'll write it in a separate step. I'll create a new script that: - Maintains tileMap object. - Provides functions: updateTiles(oldGrid, newGrid) that animates changes. - Adds classes for new and merged tiles. Let's do it step by step. First, we need to compute changes between old and new grid. We'll do that in move function after we have newGrid. Let's write the new script. I'll create a new string and replace. Let's do it now
{"edits":[{"newText":" // Game state\n let grid = [];\n const size = 4;\n let score = 0;\n let best = locSuccessfully replaced 1 block(s) in index.html.
thinking
Great. Now we have tile management. However, there is a bug: In renderTiles(oldGrid), we are not handling tile movement across cells correctly. Actually, when a tile moves from (r1,c1) to (r2,c2), the old grid has value at (r1,c1) and new grid has value at (r2,c2). Our change detection will see oldVal at (r1,c1) becomes zero, and newVal at (r2,c2) appears new, but we need to move the tile element, not delete and create new. We need to track tile movement across cells. That's more complex. But we can simplify: Since we are using absolute positioning, we can keep tile elements at their positions and update their positions based on new grid. However, we need to know which tile moved where. Since tiles are identical except value, we can't differentiate. However, we can treat each cell independently: If a tile moves out of a cell, we remove tile element from that cell and create a new tile at the destination. That's fine because the tile appears to move visually? Actually, if we remove and create, the animation of position will not happen because the tile is new at new position. However, we have CSS transition on left/top, but the new tile starts at new position (no transition). We need to set the start position to old position and then change to new position. That requires mapping. Given time constraints, maybe we keep the simple approach but ensure at least new and merged tiles have animations. The movement animation is less critical. Many 2048 implementations don't animate sliding, they just pop new tiles. Let's keep it as is but add a sliding animation by using CSS transform with transition and updating positions before the move? Actually we could compute the delta and animate. Let's revert to simpler approach: Keep the original renderTiles (clear and recreate) but add tile-new and tile-merged classes based on changes. We can compute changes by comparing old and new grid and apply classes to newly created tiles. Let's go back to simpler render but add animation classes. We'll keep the tileMap for detecting merges? Let's do a simpler solution: In move function, after computing newGrid, we can compute which cells are new (value changed from 0 to non-zero) and which cells merged (value increased). We'll store those coordinates, then in renderTiles we can add classes accordingly. We'll modify move function to store arrays newTiles and mergedTiles. Then in renderTiles, we can check if tile at (r,c) is in those arrays and add class. Let's implement that. First, revert to simpler renderTiles but keep tileMap? Actually we need to keep tile elements to add classes. Let's keep tileMap for easy lookup, but still recreate tiles each render? That's contradictory. Better: We'll keep tileMap but when we recreate tiles we can assign classes based on arrays. Let's change again. I'll edit the script again. Let's revert to simpler renderTiles but with tileMap for tracking existence? Let's just keep the current tileMap approach but fix the movement detection. We can improve detection: For each new grid cell, if there is a tile at that position with same value, keep it (maybe moved from elsewhere). Actually we need to map tiles from old positions to new positions. Since tiles can merge, it's nontrivial. Given the scope, I think we can accept the current tile movement without sliding animation. The game still has pop and appear animations for new/merged tiles. That's acceptable. Let's ensure tile-new and tile-merged classes are applied. We'll compute newTiles and mergedTiles in move function. We'll pass them to renderTiles. Let's adjust move function to compute changes and pass to renderTiles. We'll also keep tileMap for easy removal. Let's write a new version of move and renderTiles. I'll edit the script again but only replace relevant functions. Let's read the current script to see the exact lines again. Let's extract the move and renderTiles functions. I'll do a new edit with specific function replacements. Let's first find the move function lines. I'll search with bash but easier to read the file and locate. Let's open the file and look. I'll use read with offset. Let's read lines 350-450.
{"path":"index.html"}<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>2048 Game</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background: linear-gradient(135deg, #1a1a2e, #16213e); color: #fff; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; } .container { max-width: 500px; width: 100%; text-align: center; padding: 20px; } header { margin-bottom: 30px; } h1 { font-size: 3.5rem; margin-bottom: 10px; background: linear-gradient(to right, #ff9a00, #ff5e00); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 0 2px 4px rgba(0,0,0,0.3); } .game-info { display: flex; justify-content: space-between; margin-bottom: 20px; } .score-container, .best-container { background: rgba(255,255,255,0.1); border-radius: 6px; padding: 10px 20px; min-width: 120px; backdrop-filter: blur(5px); } .score-label, .best-label { font-size: 0.9rem; opacity: 0.8; text-transform: uppercase; letter-spacing: 1px; } .score, .best { font-size: 2rem; font-weight: bold; margin-top: 5px; } .instructions { margin: 20px 0; font-size: 1rem; opacity: 0.7; line-height: 1.5; } .grid-container { position: relative; background: rgba(255,255,255,0.05); border-radius: 10px; padding: 14px; margin: 20px auto; } .grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); gap: 12px; aspect-ratio: 1 / 1; position: relative; background: rgba(255,255,255,0.03); border-radius: 8px; padding: 12px; } .cell { background: rgba(255,255,255,0.08); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: bold; transition: background 0.2s; } .tile { position: absolute; width: calc((100% - 60px) / 4); height: calc((100% - 60px) / 4); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: bold; transition: all 0.15s ease-out; z-index: 2; user-select: none; } .tile-merged { animation: pop 0.3s ease-out; } @keyframes pop { 0% { transform: scale(1); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } } .tile-new { animation: appear 0.3s ease-out; } @keyframes appear { 0% { opacity: 0; transform: scale(0.5); } 100% { opacity: 1; transform: scale(1); } } .controls { margin-top: 30px; display: flex; gap: 10px; justify-content: center; } button { background: linear-gradient(to right, #00b4db, #0083b0); border: none; color: white; padding: 12px 24px; border-radius: 50px; font-size: 1rem; font-weight: bold; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; box-shadow: 0 4px 10px rgba(0,180,219,0.4); } button:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,180,219,0.5); } button:active { transform: translateY(0); } .game-over, .game-win { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 100; } .game-over.active, .game-win.active { display: flex; } .message-box { background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); border-radius: 15px; padding: 40px; max-width: 400px; text-align: center; } .message-title { font-size: 3rem; margin-bottom: 20px; color: #ff5e00; } .message-subtitle { font-size: 1.2rem; margin-bottom: 30px; opacity: 0.8; } .message-score { font-size: 2rem; margin-bottom: 30px; color: #ff9a00; } .message-buttons { display: flex; gap: 10px; justify-content: center; } .key-hint { margin-top: 20px; opacity: 0.6; font-size: 0.9rem; } /* Tile colors */ .tile[data-value="2"] { background: #2A9D8F; color: #fff; } .tile[data-value="4"] { background: #E9C46A; color: #333; } .tile[data-value="8"] { background: #F4A261; color: #fff; } .tile[data-value="16"] { background: #E76F51; color: #fff; } .tile[data-value="32"] { background: #9D4EDD; color: #fff; } .tile[data-value="64"] { background: #7209B7; color: #fff; } .tile[data-value="128"] { background: #3A86FF; color: #fff; font-size: 1.8rem; } .tile[data-value="256"] { background: #8338EC; color: #fff; font-size: 1.8rem; } .tile[data-value="512"] { background: #FF006E; color: #fff; font-size: 1.8rem; } .tile[data-value="1024"] { background: #FB5607; color: #fff; font-size: 1.6rem; } .tile[data-value="2048"] { background: #FFBE0B; color: #333; font-size: 1.6rem; box-shadow: 0 0 20px #FFBE0B; } /* Responsive */ @media (max-width: 520px) { .tile { font-size: 1.5rem; } h1 { font-size: 2.8rem; } .score, .best { font-size: 1.8rem; } } </style> </head> <body> <div class="container"> <header> <h1>2048</h1> <div class="instructions">Join the numbers and get to the <strong>2048 tile!</strong> Use arrow keys to move.</div> </header> <div class="game-info"> <div class="score-container"> <div class="score-label">Score</div> <div class="score">0</div> </div> <div class="best-container"> <div class="best-label">Best</div> <div class="best">0</div> </div> </div> <div class="grid-container"> <div class="grid" id="grid"> <!-- 16 empty cells --> <div class="cell"></div><div class="cell"></div><div class="cell"></div><div class="cell"></div> <div class="cell"></div><div class="cell"></div><div class="cell"></div><div class="cell"></div> <div class="cell"></div><div class="cell"></div><div class="cell"></div><div class="cell"></div> <div class="cell"></div><div class="cell"></div><div class="cell"></div><div class="cell"></div> </div> </div> <div class="controls"> <button id="new-game">New Game</button> <button id="undo-btn">Undo</button> </div> <div class="key-hint">Arrow keys ← ↑ → ↓ to move</div> </div> <!-- Game Over Overlay --> <div class="game-over" id="game-over"> <div class="message-box"> <div class="message-title">Game Over</div> <div class="message-subtitle">No more moves left.</div> <div class="message-score">Score: <span id="final-score">0</span></div> <div class="message-buttons"> <button id="restart-btn">Play Again</button> <button id="close-over">Close</button> </div> </div> </div> <!-- Game Win Overlay --> <div class="game-win" id="game-win"> <div class="message-box"> <div class="message-title">You Win!</div> <div class="message-subtitle">You made the 2048 tile!</div> <div class="message-score">Score: <span id="win-score">0</span></div> <div class="message-buttons"> <button id="continue-btn">Continue Playing</button> <button id="new-game-win">New Game</button> </div> </div> </div> <script> // Game state let grid = []; const size = 4; let score = 0; let best = localStorage.getItem('best2048') || 0; let won = false; let moved = false; let history = []; const MAX_HISTORY = 10; let tileMap = {}; // key: `${r}-${c}` -> tile element // DOM elements const gridEl = document.getElementById('grid'); const scoreEl = document.querySelector('.score'); const bestEl = document.querySelector('.best'); const newGameBtn = document.getElementById('new-game'); const undoBtn = document.getElementById('undo-btn'); const gameOverEl = document.getElementById('game-over'); const gameWinEl = document.getElementById('game-win'); const finalScoreEl = document.getElementById('final-score'); const winScoreEl = document.getElementById('win-score'); const restartBtn = document.getElementById('restart-btn'); const closeOverBtn = document.getElementById('close-over'); const continueBtn = document.getElementById('continue-btn'); const newGameWinBtn = document.getElementById('new-game-win'); // Initialize best score display bestEl.textContent = best; // Initialize empty grid function initGrid() { grid = Array(size).fill().map(() => Array(size).fill(0)); score = 0; won = false; history = []; tileMap = {}; updateScore(); clearTiles(); addRandomTile(); addRandomTile(); renderTiles(); } // Add a random tile (2 or 4) to an empty cell function addRandomTile() { const emptyCells = []; for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { if (grid[r][c] === 0) emptyCells.push({r, c}); } } if (emptyCells.length > 0) { const {r, c} = emptyCells[Math.floor(Math.random() * emptyCells.length)]; grid[r][c] = Math.random() < 0.9 ? 2 : 4; return {r, c, value: grid[r][c]}; } return null; } // Clear all tile elements from DOM function clearTiles() { Object.values(tileMap).forEach(tile => tile.remove()); tileMap = {}; } // Create or update a tile element function createOrUpdateTile(r, c, value, isNew = false, isMerged = false) { const key = `${r}-${c}`; let tile = tileMap[key]; if (!tile) { tile = document.createElement('div'); tile.className = 'tile'; tile.dataset.value = value; tile.textContent = value; gridEl.appendChild(tile); tileMap[key] = tile; } else { tile.dataset.value = value; tile.textContent = value; } // Position tile.style.left = `calc(${c} * (100% - 60px) / 4 + ${c * 12 + 12}px)`; tile.style.top = `calc(${r} * (100% - 60px) / 4 + ${r * 12 + 12}px)`; // Classes for animations tile.classList.remove('tile-new', 'tile-merged'); if (isNew) tile.classList.add('tile-new'); if (isMerged) tile.classList.add('tile-merged'); } // Remove tile at position function removeTile(r, c) { const key = `${r}-${c}`; const tile = tileMap[key]; if (tile) { tile.remove(); delete tileMap[key]; } } // Render tiles based on grid, with animation for changes function renderTiles(oldGrid = null) { if (!oldGrid) { // initial render for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { const value = grid[r][c]; if (value !== 0) { createOrUpdateTile(r, c, value, true, false); } } } return; } // Determine changes for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { const oldVal = oldGrid[r][c]; const newVal = grid[r][c]; if (oldVal === 0 && newVal !== 0) { // new tile createOrUpdateTile(r, c, newVal, true, false); } else if (oldVal !== 0 && newVal === 0) { // tile removed (should not happen without merge) removeTile(r, c); } else if (oldVal !== newVal && newVal !== 0) { // tile changed (merged) createOrUpdateTile(r, c, newVal, false, true); } else if (oldVal === newVal && newVal !== 0) { // tile moved (position may have changed) we need to update position // but we already update position in createOrUpdateTile const key = `${r}-${c}`; const tile = tileMap[key]; if (tile) { tile.style.left = `calc(${c} * (100% - 60px) / 4 + ${c * 12 + 12}px)`; tile.style.top = `calc(${r} * (100% - 60px) / 4 + ${r * 12 + 12}px)`; } } } } } // Update score display function updateScore() { scoreEl.textContent = score; if (score > best) { best = score; localStorage.setItem('best2048', best); bestEl.textContent = best; } } // Save current state to history function saveHistory() { const state = { grid: grid.map(row => [...row]), score: score, won: won }; history.push(state); if (history.length > MAX_HISTORY) history.shift(); } // Undo last move function undo() { if (history.length > 0) { const state = history.pop(); grid = state.grid.map(row => [...row]); score = state.score; won = state.won; updateScore(); renderTiles(); } } // Check for win function checkWin() { if (won) return; for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { if (grid[r][c] === 2048) { won = true; winScoreEl.textContent = score; gameWinEl.classList.add('active'); return; } } } } // Check for game over (no empty cells and no possible merges) function checkGameOver() { // If there's an empty cell, not over for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { if (grid[r][c] === 0) return false; } } // Check adjacent cells for possible merge for (let r = 0; r < size; r++) { for (let c = 0; c < size; c++) { const val = grid[r][c]; if (r < size - 1 && grid[r+1][c] === val) return false; if (c < size - 1 && grid[r][c+1] === val) return false; } } return true; } // Move tiles in a direction function move(direction) { saveHistory(); moved = false; const oldGrid = grid.map(row => [...row]); // Transpose or reverse grid to simplify movement logic // We'll handle each direction separately for clarity let newGrid = grid.map(row => [...row]); switch (direction) { case 'up': for (let c = 0; c < size; c++) { const column = newGrid.map(row => row[c]); const merged = moveLine(column); for (let r = 0; r < size; r++) newGrid[r][c] = merged[r]; } break; case 'down': for (let c = 0; c < size; c++) { const column = newGrid.map(row => row[c]).reverse(); const merged = moveLine(column); merged.reverse(); for (let r = 0; r < size; r++) newGrid[r][c] = merged[r]; } break; case 'left': for (let r = 0; r < size; r++) { const row = newGrid[r]; newGrid[r] = moveLine(row); } break; case 'right': for (let r = 0; r < size; r++) { const row = newGrid[r].reverse(); const merged = moveLine(row); merged.reverse(); newGrid[r] = merged; } break; } // Update grid if changed if (JSON.stringify(oldGrid) !== JSON.stringify(newGrid)) { grid = newGrid; moved = true; addRandomTile(); renderTiles(oldGrid); checkWin(); if (checkGameOver()) { finalScoreEl.textContent = score; gameOverEl.classList.add('active'); } } } // Move and merge a single line (array of values) function moveLine(line) { let filtered = line.filter(val => val !== 0); let merged = []; for (let i = 0; i < filtered.length; i++) { if (i < filtered.length - 1 && filtered[i] === filtered[i + 1]) { merged.push(filtered[i] * 2); score += filtered[i] * 2; i++; } else { merged.push(filtered[i]); } } while (merged.length < size) merged.push(0); return merged; } // Event Listeners document.addEventListener('keydown', e => { if (gameOverEl.classList.contains('active') || gameWinEl.classList.contains('active')) return; switch (e.key) { case 'ArrowUp': e.preventDefault(); move('up'); updateScore(); break; case 'ArrowDown': e.preventDefault(); move('down'); updateScore(); break; case 'ArrowLeft': e.preventDefault(); move('left'); updateScore(); break; case 'ArrowRight': e.preventDefault(); move('right'); updateScore(); break; } }); newGameBtn.addEventListener('click', initGrid); undoBtn.addEventListener('click', undo); restartBtn.addEventListener('click', () => { gameOverEl.classList.remove('active'); initGrid(); }); closeOverBtn.addEventListener('click', () => gameOverEl.classList.remove('active')); continueBtn.addEventListener('click', () => gameWinEl.classList.remove('active')); newGameWinBtn.addEventListener('click', () => { gameWinEl.classList.remove('active'); initGrid(); }); // Touch swipe for mobile let touchStartX, touchStartY; gridEl.addEventListener('touchstart', e => { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; }, {passive: true}); gridEl.addEventListener('touchend', e => { if (!touchStartX || !touchStartY) return; const touchEndX = e.changedTouches[0].clientX; const touchEndY = e.changedTouches[0].clientY; const dx = touchEndX - touchStartX; const dy = touchEndY - touchStartY; const minSwipe = 30; if (Math.abs(dx) > Math.abs(dy)) { if (Math.abs(dx) > minSwipe) { if (dx > 0) move('right'); else move('left'); updateScore(); } } else { if (Math.abs(dy) > minSwipe) { if (dy > 0) move('down'); else move('up'); updateScore(); } } touchStartX = null; touchStartY = null; }, {passive: true}); // Initialize game initGrid(); </script> </body> </html>
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.