smoothretro82

2048 tiles game (w.i.p)

Jan 15th, 2026
42
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.49 KB | None | 0 0
  1. (() => {
  2. const BOARD_SIZE_KEY = "2048BoardSize";
  3. let SIZE = parseInt(localStorage.getItem(BOARD_SIZE_KEY)) || 4;
  4. let tileSize = Math.min(60, window.innerWidth / (SIZE + 2));
  5. const tileGap = 5;
  6. const hudOpacity = 0.95;
  7. let aiEnabled = false;
  8. let numberDisplayMode = "normal";
  9.  
  10. let board = [];
  11. let score = 0;
  12. let highScore = 0;
  13. const historyStack = [];
  14. const UNDO_LIMIT = 20;
  15.  
  16. // ---------------- HUD ----------------
  17. const hud = document.createElement("div");
  18. hud.style.cssText = `
  19. position: fixed; top: 50px; left: 50px;
  20. width: ${SIZE*tileSize + (SIZE-1)*tileGap + 20}px;
  21. background: rgba(187,173,160,${hudOpacity});
  22. border-radius: 10px; padding: 10px;
  23. z-index: 9999; user-select: none; cursor: move;
  24. display: flex; flex-direction: column; gap: 10px;
  25. max-width: 90vw;
  26. `;
  27. document.body.appendChild(hud);
  28.  
  29. // Dragging HUD
  30. let isDragging=false, offsetX=0, offsetY=0;
  31. hud.addEventListener("mousedown", e=>{
  32. isDragging=true; offsetX=e.clientX-hud.getBoundingClientRect().left;
  33. offsetY=e.clientY-hud.getBoundingClientRect().top;
  34. });
  35. document.addEventListener("mousemove", e=>{
  36. if(!isDragging) return;
  37. let newX = e.clientX - offsetX;
  38. let newY = e.clientY - offsetY;
  39. newX=Math.min(window.innerWidth-hud.offsetWidth, Math.max(0,newX));
  40. newY=Math.min(window.innerHeight-hud.offsetHeight, Math.max(0,newY));
  41. hud.style.left=newX+"px"; hud.style.top=newY+"px";
  42. });
  43. document.addEventListener("mouseup", ()=>isDragging=false);
  44.  
  45. // Score display
  46. const scoreDiv = document.createElement("div");
  47. scoreDiv.style.color="#f9f6f2"; scoreDiv.style.fontWeight="bold"; scoreDiv.style.fontSize="16px";
  48. hud.appendChild(scoreDiv);
  49.  
  50. const highScoreDiv = document.createElement("div");
  51. highScoreDiv.style.color="#f9f6f2"; highScoreDiv.style.fontSize="14px";
  52. hud.appendChild(highScoreDiv);
  53.  
  54. // Board container
  55. const boardDiv = document.createElement("div");
  56. boardDiv.style.position="relative"; boardDiv.style.background="#bbada0";
  57. boardDiv.style.display="grid"; boardDiv.style.gap=`${tileGap}px`;
  58. hud.appendChild(boardDiv);
  59.  
  60. // Arrow controls
  61. const arrows = document.createElement("div");
  62. arrows.style.cssText = `
  63. position: absolute; right: -55px; top: 50%;
  64. transform: translateY(-50%); display: grid;
  65. grid-template-rows: repeat(4,1fr); gap: 6px;
  66. `;
  67. hud.appendChild(arrows);
  68.  
  69. function createArrow(label, key){
  70. const btn=document.createElement("button");
  71. btn.textContent=label;
  72. btn.style.cssText="width:45px;height:40px;font-size:18px;border-radius:6px;border:none;background:#8f7a66;color:#f9f6f2;cursor:pointer";
  73. btn.addEventListener("mousedown",()=>btn.style.transform="scale(0.9)");
  74. btn.addEventListener("mouseup",()=>btn.style.transform="scale(1)");
  75. btn.addEventListener("mouseleave",()=>btn.style.transform="scale(1)");
  76. btn.addEventListener("click",()=>handleMove(key));
  77. arrows.appendChild(btn);
  78. }
  79. ["↑","←","→","↓"].forEach((label,i)=>{
  80. const keys=["ArrowUp","ArrowLeft","ArrowRight","ArrowDown"];
  81. createArrow(label, keys[i]);
  82. });
  83.  
  84. // Buttons container
  85. const btnContainer=document.createElement("div");
  86. btnContainer.style.display="flex"; btnContainer.style.gap="5px";
  87. hud.appendChild(btnContainer);
  88.  
  89. function createButton(label, callback){
  90. const btn=document.createElement("button");
  91. btn.textContent=label;
  92. btn.style.cssText="padding:5px;border-radius:5px;border:none;background:#8f7a66;color:#f9f6f2;cursor:pointer";
  93. btn.addEventListener("click", callback);
  94. btnContainer.appendChild(btn);
  95. }
  96.  
  97. // Options
  98. const options=document.createElement("div");
  99. options.style.cssText="display:flex;flex-direction:column;gap:5px;margin-top:5px";
  100. hud.appendChild(options);
  101.  
  102. // Number style selector
  103. const numberStyleLabel=document.createElement("div");
  104. numberStyleLabel.textContent="Number Style"; numberStyleLabel.style.cssText="color:#000;font-size:13px";
  105. options.appendChild(numberStyleLabel);
  106.  
  107. const numberStyleSelect=document.createElement("select");
  108. numberStyleSelect.style.cssText="padding:5px;border-radius:5px;border:none;background:#eee;cursor:pointer";
  109. numberStyleSelect.innerHTML=`
  110. <option value="normal">Normal</option>
  111. <option value="commas">Commas</option>
  112. <option value="power">Powers (2ⁿ)</option>
  113. <option value="letters">Letters (A,B,C)</option>
  114. <option value="emoji">Emojis</option>
  115. `;
  116. numberStyleSelect.value=numberDisplayMode;
  117. numberStyleSelect.addEventListener("change", e=>{
  118. numberDisplayMode=e.target.value;
  119. drawBoard();
  120. });
  121. options.appendChild(numberStyleSelect);
  122.  
  123. // Board size selector
  124. const sizeLabel=document.createElement("div");
  125. sizeLabel.textContent="Board Size"; sizeLabel.style.cssText="color:#f9f6f2;font-size:13px";
  126. options.appendChild(sizeLabel);
  127.  
  128. const sizeSelect=document.createElement("select");
  129. sizeSelect.style.cssText="padding:5px;border-radius:5px;border:none;cursor:pointer";
  130. sizeSelect.innerHTML=`
  131. <option value="3">3 × 3</option>
  132. <option value="4">4 × 4</option>
  133. <option value="5">5 × 5</option>
  134. <option value="6">6 × 6</option>
  135. `;
  136. sizeSelect.value=SIZE;
  137. sizeSelect.addEventListener("change", e=>{
  138. SIZE=parseInt(e.target.value);
  139. localStorage.setItem(BOARD_SIZE_KEY,SIZE);
  140. highScore=parseInt(localStorage.getItem(highScoreKey(SIZE)))||0;
  141. initBoard(); updateHighScoreTable();
  142. });
  143. options.appendChild(sizeSelect);
  144.  
  145. // Tile size selector
  146. const tileSizeLabel = document.createElement("div");
  147. tileSizeLabel.textContent = "Tile Size (px)"; tileSizeLabel.style.cssText="color:#f9f6f2;font-size:13px";
  148. options.appendChild(tileSizeLabel);
  149.  
  150. const tileSizeSelect = document.createElement("select");
  151. tileSizeSelect.style.cssText="padding:5px;border-radius:5px;border:none;cursor:pointer;background:#eee";
  152. [40, 50, 60, 70, 80].forEach(sz=>{
  153. const opt = document.createElement("option");
  154. opt.value = sz;
  155. opt.textContent = sz;
  156. tileSizeSelect.appendChild(opt);
  157. });
  158. tileSizeSelect.value = tileSize;
  159. tileSizeSelect.addEventListener("change", e=>{
  160. tileSize = parseInt(e.target.value);
  161. rebuildBoardUI();
  162. drawBoard();
  163. });
  164. options.appendChild(tileSizeSelect);
  165.  
  166. createButton("Restart", initBoard);
  167. createButton("Undo", undo);
  168. createButton("AI Toggle", ()=>{ aiEnabled=!aiEnabled; if(aiEnabled) aiMove(); });
  169.  
  170. // ---------------- Board Logic ----------------
  171. let tileMap=[];
  172.  
  173. function createTileElement(){
  174. const tile=document.createElement("div");
  175. tile.style.position="absolute"; tile.style.borderRadius="5px";
  176. tile.style.display="flex"; tile.style.alignItems="center"; tile.style.justifyContent="center";
  177. tile.style.fontWeight="bold"; tile.style.transition="all 0.2s ease";
  178. boardDiv.appendChild(tile); return tile;
  179. }
  180.  
  181. function getTileColor(val){
  182. if(val===0) return "#cdc1b4";
  183. const hue=Math.min(60 + Math.log2(val)*8,360);
  184. return `hsl(${hue},70%,60%)`;
  185. }
  186.  
  187. function formatTileValue(val){
  188. if(val===0) return "";
  189. switch(numberDisplayMode){
  190. case "commas": return val.toLocaleString();
  191. case "power": return `2^${Math.log2(val)}`;
  192. case "letters": return String.fromCharCode(64 + Math.log2(val));
  193. case "emoji":
  194. const emojiMap={2:"🟦",4:"🟩",8:"🟨",16:"🟧",32:"🟥",64:"🟪",128:"⬛",256:"⭐",512:"🔥",1024:"💎",2048:"👑"};
  195. return emojiMap[val]||"✨";
  196. default: return val;
  197. }
  198. }
  199.  
  200. function drawBoard(mergePositions=[]){
  201. for(let r=0;r<SIZE;r++){
  202. for(let c=0;c<SIZE;c++){
  203. const val=board[r][c];
  204. let tile=tileMap[r]?.[c];
  205. if(val===0){
  206. if(tile){ boardDiv.removeChild(tile); tileMap[r][c]=null;}
  207. }else{
  208. if(!tile){ tile=createTileElement(); if(!tileMap[r]) tileMap[r]=[]; tileMap[r][c]=tile;}
  209. tile.textContent=formatTileValue(val);
  210. tile.style.background=getTileColor(val);
  211. tile.style.color=val>4?"#f9f6f2":"#776e65";
  212. tile.style.fontSize=Math.max(tileSize/3,10)+"px";
  213. tile.style.width=tile.style.height=tileSize+"px";
  214. tile.style.left=`${c*tileSize + c*tileGap}px`;
  215. tile.style.top=`${r*tileSize + r*tileGap}px`;
  216. tile.style.transform=mergePositions.some(([mr,mc])=>mr===r && mc===c)?"scale(1.2)":"scale(1)";
  217. if(mergePositions.some(([mr,mc])=>mr===r && mc===c)){
  218. setTimeout(()=>{tile.style.transform="scale(1)";},100);
  219. createParticles(r,c);
  220. }
  221. }
  222. }
  223. }
  224. scoreDiv.textContent=`Score: ${score}`;
  225. highScoreDiv.textContent=`High Score: ${highScore}`;
  226. }
  227.  
  228. function createParticles(r,c){
  229. const count=10;
  230. for(let i=0;i<count;i++){
  231. const p=document.createElement("div");
  232. p.style.cssText=`position:absolute;width:5px;height:5px;background:yellow;border-radius:50%;left:${c*tileSize + c*tileGap + tileSize/2}px;top:${r*tileSize + r*tileGap + tileSize/2}px;pointer-events:none;opacity:1`;
  233. boardDiv.appendChild(p);
  234. const dx=(Math.random()-0.5)*60, dy=(Math.random()-0.5)*60;
  235. p.animate([{transform:"translate(0,0)",opacity:1},{transform:`translate(${dx}px,${dy}px)`,opacity:0}],{duration:500});
  236. setTimeout(()=>boardDiv.removeChild(p),500);
  237. }
  238. }
  239.  
  240. function getEmpty(){
  241. const empty=[];
  242. for(let r=0;r<SIZE;r++) for(let c=0;c<SIZE;c++) if(board[r][c]===0) empty.push([r,c]);
  243. return empty;
  244. }
  245.  
  246. function addRandom(){
  247. const empty=getEmpty();
  248. if(empty.length===0) return;
  249. const [r,c]=empty[Math.floor(Math.random()*empty.length)];
  250. board[r][c]=Math.random()<0.9?2:4;
  251. }
  252.  
  253. function slide(row){
  254. const arr=row.filter(v=>v!==0);
  255. const mergePositions=[];
  256. for(let i=0;i<arr.length-1;i++){
  257. if(arr[i]===arr[i+1]){
  258. arr[i]*=2; score+=arr[i]; arr[i+1]=0;
  259. mergePositions.push(i);
  260. }
  261. }
  262. const newRow=arr.filter(v=>v!==0).concat(Array(SIZE-arr.filter(v=>v!==0).length).fill(0));
  263. return [newRow, mergePositions];
  264. }
  265.  
  266. function moveLeft(){const merges=[]; for(let r=0;r<SIZE;r++){const [row,idx]=slide(board[r]); board[r]=row; idx.forEach(c=>merges.push([r,c]));} return merges;}
  267. function moveRight(){const merges=[]; for(let r=0;r<SIZE;r++){const [row,idx]=slide([...board[r]].reverse()); board[r]=row.reverse(); idx.forEach(c=>merges.push([r,SIZE-1-c]));} return merges;}
  268. function moveUp(){const merges=[]; for(let c=0;c<SIZE;c++){const col=[]; for(let r=0;r<SIZE;r++) col.push(board[r][c]); const [newCol,idx]=slide(col); for(let r=0;r<SIZE;r++) board[r][c]=newCol[r]; idx.forEach(r=>merges.push([r,c]));} return merges;}
  269. function moveDown(){const merges=[]; for(let c=0;c<SIZE;c++){const col=[]; for(let r=0;r<SIZE;r++) col.push(board[r][c]); const [newCol,idx]=slide(col.reverse()); const newColRev=newCol.reverse(); for(let r=0;r<SIZE;r++) board[r][c]=newColRev[r]; idx.forEach(r=>merges.push([SIZE-1-r,c]));} return merges;}
  270.  
  271. function isOver(){
  272. if(getEmpty().length>0) return false;
  273. for(let r=0;r<SIZE;r++) for(let c=0;c<SIZE;c++){
  274. const val=board[r][c];
  275. if((c<SIZE-1 && board[r][c+1]===val)||(r<SIZE-1 && board[r+1][c]===val)) return false;
  276. }
  277. return true;
  278. }
  279.  
  280. function undo(){
  281. if(historyStack.length>0){
  282. const last=historyStack.pop();
  283. board=last.board.map(r=>[...r]);
  284. score=last.score;
  285. drawBoard();
  286. }
  287. }
  288.  
  289. function rebuildBoardUI(){
  290. boardDiv.innerHTML="";
  291. boardDiv.style.width=`${SIZE*tileSize + (SIZE-1)*tileGap}px`;
  292. boardDiv.style.height=`${SIZE*tileSize + (SIZE-1)*tileGap}px`;
  293. boardDiv.style.gridTemplateRows=`repeat(${SIZE},1fr)`;
  294. boardDiv.style.gridTemplateColumns=`repeat(${SIZE},1fr)`;
  295. hud.style.width=`${SIZE*tileSize + (SIZE-1)*tileGap + 20}px`;
  296. tileMap=Array.from({length:SIZE},()=>Array(SIZE).fill(null));
  297. for(let i=0;i<SIZE*SIZE;i++){
  298. const cell=document.createElement("div");
  299. cell.style.background="#cdc1b4"; cell.style.borderRadius="5px";
  300. boardDiv.appendChild(cell);
  301. }
  302. }
  303.  
  304. function initBoard(){
  305. board=Array.from({length:SIZE},()=>Array(SIZE).fill(0));
  306. score=0; historyStack.length=0;
  307. rebuildBoardUI(); addRandom(); addRandom(); drawBoard();
  308. }
  309.  
  310. function handleMove(key){
  311. if(aiEnabled) return;
  312. const copy=board.map(r=>[...r]);
  313. let merges=[];
  314. switch(key){
  315. case "ArrowLeft": merges=moveLeft(); break;
  316. case "ArrowRight": merges=moveRight(); break;
  317. case "ArrowUp": merges=moveUp(); break;
  318. case "ArrowDown": merges=moveDown(); break;
  319. default: return;
  320. }
  321. if(JSON.stringify(copy)!==JSON.stringify(board)){
  322. historyStack.push({board:copy.map(r=>[...r]), score:score});
  323. if(historyStack.length>UNDO_LIMIT) historyStack.shift();
  324.  
  325. if(score>highScore){
  326. highScore=score;
  327. localStorage.setItem(highScoreKey(SIZE),highScore);
  328. updateHighScoreTable();
  329. }
  330.  
  331. addRandom(); drawBoard(merges);
  332. if(isOver()) setTimeout(()=>alert(`Game Over! Final Score: ${score}`),100);
  333. }
  334. }
  335.  
  336. // ---------------- AI ----------------
  337. function aiMove(){
  338. if(!aiEnabled) return;
  339. const moves = ["ArrowUp","ArrowLeft","ArrowRight","ArrowDown"];
  340. let bestMove = null;
  341. let bestScore = -Infinity;
  342.  
  343. for(let m of moves){
  344. const newBoard = simulateMove(m, board);
  345. if(boardsEqual(newBoard, board)) continue;
  346. const score = evaluateBoard(newBoard);
  347. if(score > bestScore){ bestScore = score; bestMove = m; }
  348. }
  349.  
  350. if(bestMove){ handleMove(bestMove); setTimeout(aiMove, 120); }
  351. }
  352.  
  353. function cloneBoard(b){ return b.map(r => r.slice()); }
  354. function boardsEqual(a,b){ return JSON.stringify(a) === JSON.stringify(b); }
  355. function simulateMove(key, b){
  356. const savedBoard = board; const savedScore = score;
  357. board = cloneBoard(b); score = 0;
  358. switch(key){ case "ArrowLeft": moveLeft(); break; case "ArrowRight": moveRight(); break; case "ArrowUp": moveUp(); break; case "ArrowDown": moveDown(); break; }
  359. const result = cloneBoard(board);
  360. board = savedBoard; score = savedScore;
  361. return result;
  362. }
  363.  
  364. function countEmpty(b){ let n=0; for(let r=0;r<SIZE;r++) for(let c=0;c<SIZE;c++) if(b[r][c]===0)n++; return n; }
  365. function maxTile(b){ return Math.max(...b.flat()); }
  366. function isMaxInCorner(b){ const m=maxTile(b); return [b[0][0],b[0][SIZE-1],b[SIZE-1][0],b[SIZE-1][SIZE-1]].includes(m); }
  367. function smoothness(b){
  368. let s=0;
  369. for(let r=0;r<SIZE;r++) for(let c=0;c<SIZE;c++){ if(b[r][c]===0) continue; const v=Math.log2(b[r][c]); if(c+1<SIZE && b[r][c+1]) s-=Math.abs(v-Math.log2(b[r][c+1])); if(r+1<SIZE && b[r+1][c]) s-=Math.abs(v-Math.log2(b[r+1][c])); }
  370. return s;
  371. }
  372. function monotonicity(b){
  373. let score=0;
  374. for(let r=0;r<SIZE;r++) for(let c=0;c<SIZE-1;c++){ if(b[r][c] && b[r][c+1]) score+=Math.log2(b[r][c])>=Math.log2(b[r][c+1])?1:-1; }
  375. for(let c=0;c<SIZE;c++) for(let r=0;r<SIZE-1;r++){ if(b[r][c] && b[r+1][c]) score+=Math.log2(b[r][c])>=Math.log2(b[r+1][c])?1:-1; }
  376. return score;
  377. }
  378. function evaluateBoard(b){ return countEmpty(b)*100 + smoothness(b)*3 + monotonicity(b)*10 + (isMaxInCorner(b)?500:0) + Math.log2(maxTile(b))*20; }
  379.  
  380. // ---------------- High Scores ----------------
  381. const scoreTableContainer=document.createElement("div");
  382. scoreTableContainer.style.cssText="display:flex;flex-direction:column;gap:3px;margin-top:5px";
  383. hud.appendChild(scoreTableContainer);
  384.  
  385. const tableTitle=document.createElement("div");
  386. tableTitle.textContent="High Scores by Board Size"; tableTitle.style.cssText="color:#f9f6f2;font-size:13px";
  387. scoreTableContainer.appendChild(tableTitle);
  388.  
  389. const scoreTable=document.createElement("div");
  390. scoreTableContainer.appendChild(scoreTable);
  391.  
  392. function highScoreKey(size){ return `2048HighScore_${size}x${size}`; }
  393. function updateHighScoreTable(){
  394. scoreTable.innerHTML="";
  395. [3,4,5,6].forEach(size=>{
  396. const hs=parseInt(localStorage.getItem(highScoreKey(size)))||0;
  397. const row=document.createElement("div");
  398. row.style.cssText="display:flex;justify-content:space-between;font-size:13px;color:#f9f6f2";
  399. row.textContent=`${size} × ${size} : ${hs}`;
  400. scoreTable.appendChild(row);
  401. });
  402. }
  403.  
  404. window.addEventListener("keydown", e=>handleMove(e.key));
  405.  
  406. // Init
  407. highScore=parseInt(localStorage.getItem(highScoreKey(SIZE)))||0;
  408. initBoard(); updateHighScoreTable();
  409. console.log("%c2048 Loaded!", "color:green;font-weight:bold;");
  410. })();
  411.  
Advertisement
Comments
Add Comment
Please, Sign In to add comment