| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var TILE_COLORS = { |
| 5 |
0: { bg: '#cdc1b4', text: '#776e65' }, |
| 6 |
2: { bg: '#eee4da', text: '#776e65' }, |
| 7 |
4: { bg: '#ede0c8', text: '#776e65' }, |
| 8 |
8: { bg: '#f2b179', text: '#f9f6f2' }, |
| 9 |
16: { bg: '#f59563', text: '#f9f6f2' }, |
| 10 |
32: { bg: '#f67c5f', text: '#f9f6f2' }, |
| 11 |
64: { bg: '#f65e3b', text: '#f9f6f2' }, |
| 12 |
128: { bg: '#edcf72', text: '#f9f6f2' }, |
| 13 |
256: { bg: '#edcc61', text: '#f9f6f2' }, |
| 14 |
512: { bg: '#edc850', text: '#f9f6f2' }, |
| 15 |
1024: { bg: '#edc53f', text: '#f9f6f2' }, |
| 16 |
2048: { bg: '#edc22e', text: '#f9f6f2' } |
| 17 |
}; |
| 18 |
|
| 19 |
var _typoKeys = { |
| 20 |
family:'font-family', weight:'font-weight', style:'font-style', |
| 21 |
transform:'text-transform', decoration:'text-decoration', |
| 22 |
sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', |
| 23 |
lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', |
| 24 |
letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', |
| 25 |
wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' |
| 26 |
}; |
| 27 |
var _typoUnits = { size:'sizeUnit', lineHeight:'lineHeightUnit', letterSpacing:'letterSpacingUnit', wordSpacing:'wordSpacingUnit' }; |
| 28 |
var _typoUnitDefaults = { size:'px', lineHeight:'', letterSpacing:'px', wordSpacing:'px' }; |
| 29 |
function typoCssVarsForEl(el, obj, prefix) { |
| 30 |
if (!obj || typeof obj !== 'object') return; |
| 31 |
Object.keys(_typoKeys).forEach(function (k) { |
| 32 |
var v = obj[k]; if (v === undefined || v === '') return; |
| 33 |
var prop = _typoKeys[k]; |
| 34 |
var base = k.replace(/Desktop|Tablet|Mobile/, ''); |
| 35 |
var uKey = _typoUnits[base]; |
| 36 |
if (uKey && typeof v === 'number') v = v + (obj[uKey] || _typoUnitDefaults[base] || ''); |
| 37 |
el.style.setProperty('--' + prefix + prop, v); |
| 38 |
}); |
| 39 |
} |
| 40 |
|
| 41 |
function getTileColor(val) { |
| 42 |
return TILE_COLORS[val] || { bg: '#3c3a32', text: '#f9f6f2' }; |
| 43 |
} |
| 44 |
|
| 45 |
function makeEmptyGrid() { |
| 46 |
return [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; |
| 47 |
} |
| 48 |
|
| 49 |
function cloneGrid(g) { return g.map(function (r) { return r.slice(); }); } |
| 50 |
|
| 51 |
function emptyPositions(g) { |
| 52 |
var pos = []; |
| 53 |
for (var r = 0; r < 4; r++) for (var c = 0; c < 4; c++) if (!g[r][c]) pos.push([r,c]); |
| 54 |
return pos; |
| 55 |
} |
| 56 |
|
| 57 |
function addRandom(g) { |
| 58 |
var empty = emptyPositions(g); |
| 59 |
if (!empty.length) return; |
| 60 |
var pick = empty[Math.floor(Math.random() * empty.length)]; |
| 61 |
g[pick[0]][pick[1]] = Math.random() < 0.9 ? 2 : 4; |
| 62 |
return [pick[0], pick[1]]; |
| 63 |
} |
| 64 |
|
| 65 |
function slideRow(row) { |
| 66 |
// Remove zeros |
| 67 |
var nums = row.filter(function (v) { return v !== 0; }); |
| 68 |
var merged = []; |
| 69 |
var i = 0; |
| 70 |
while (i < nums.length) { |
| 71 |
if (i + 1 < nums.length && nums[i] === nums[i+1]) { |
| 72 |
merged.push(nums[i] * 2); |
| 73 |
i += 2; |
| 74 |
} else { |
| 75 |
merged.push(nums[i]); |
| 76 |
i++; |
| 77 |
} |
| 78 |
} |
| 79 |
while (merged.length < 4) merged.push(0); |
| 80 |
return merged; |
| 81 |
} |
| 82 |
|
| 83 |
function moveLeft(g) { |
| 84 |
var next = makeEmptyGrid(); |
| 85 |
var score = 0; |
| 86 |
for (var r = 0; r < 4; r++) { |
| 87 |
var orig = g[r]; |
| 88 |
var slid = slideRow(orig.slice()); |
| 89 |
next[r] = slid; |
| 90 |
slid.forEach(function (v, c) { if (v > orig[c]) score += v; }); |
| 91 |
} |
| 92 |
return { grid: next, score: score }; |
| 93 |
} |
| 94 |
|
| 95 |
function rotateRight(g) { |
| 96 |
var n = makeEmptyGrid(); |
| 97 |
for (var r = 0; r < 4; r++) for (var c = 0; c < 4; c++) n[c][3-r] = g[r][c]; |
| 98 |
return n; |
| 99 |
} |
| 100 |
|
| 101 |
function rotateLeft(g) { |
| 102 |
var n = makeEmptyGrid(); |
| 103 |
for (var r = 0; r < 4; r++) for (var c = 0; c < 4; c++) n[3-c][r] = g[r][c]; |
| 104 |
return n; |
| 105 |
} |
| 106 |
|
| 107 |
function moveRight(g) { var res = moveLeft(rotateRight(rotateRight(g))); return { grid: rotateLeft(rotateLeft(res.grid)), score: res.score }; } |
| 108 |
function moveUp(g) { var res = moveLeft(rotateRight(g)); return { grid: rotateLeft(res.grid), score: res.score }; } |
| 109 |
function moveDown(g) { var res = moveLeft(rotateLeft(g)); return { grid: rotateRight(res.grid), score: res.score }; } |
| 110 |
|
| 111 |
function gridsEqual(a, b) { |
| 112 |
for (var r = 0; r < 4; r++) for (var c = 0; c < 4; c++) if (a[r][c] !== b[r][c]) return false; |
| 113 |
return true; |
| 114 |
} |
| 115 |
|
| 116 |
function hasMoves(g) { |
| 117 |
if (emptyPositions(g).length) return true; |
| 118 |
for (var r = 0; r < 4; r++) { |
| 119 |
for (var c = 0; c < 4; c++) { |
| 120 |
if (c < 3 && g[r][c] === g[r][c+1]) return true; |
| 121 |
if (r < 3 && g[r][c] === g[r+1][c]) return true; |
| 122 |
} |
| 123 |
} |
| 124 |
return false; |
| 125 |
} |
| 126 |
|
| 127 |
function hasWon(g) { |
| 128 |
for (var r = 0; r < 4; r++) for (var c = 0; c < 4; c++) if (g[r][c] >= 2048) return true; |
| 129 |
return false; |
| 130 |
} |
| 131 |
|
| 132 |
function initBlock(root) { |
| 133 |
var optsRaw = root.getAttribute('data-opts'); |
| 134 |
var opts; |
| 135 |
try { opts = JSON.parse(optsRaw); } catch (e) { opts = {}; } |
| 136 |
|
| 137 |
var uid = root.getAttribute('id') || ('g48_' + Date.now()); |
| 138 |
var boardSize = opts.boardSize || 380; |
| 139 |
var gridGap = opts.gridGap || 10; |
| 140 |
var tileRadius = opts.tileRadius || 8; |
| 141 |
var gridBg = opts.gridBg || '#bbada0'; |
| 142 |
var tileBg = opts.tileBg || '#cdc1b4'; |
| 143 |
var sectionBg = opts.sectionBg || ''; |
| 144 |
var titleColor = opts.titleColor || '#776e65'; |
| 145 |
var scoreHdrBg = opts.scoreHeaderBg|| '#bbada0'; |
| 146 |
var newGameBg = opts.newGameBg || '#8f7a66'; |
| 147 |
var animate = opts.animateTiles !== false; |
| 148 |
|
| 149 |
// Apply typography CSS vars |
| 150 |
typoCssVarsForEl(root, opts.titleTypo, 'bkg48-tt-'); |
| 151 |
typoCssVarsForEl(root, opts.subtitleTypo, 'bkg48-st-'); |
| 152 |
|
| 153 |
if (sectionBg) root.style.background = sectionBg; |
| 154 |
|
| 155 |
// Apply title styles |
| 156 |
var titleEl = root.querySelector('.bkbg-g48-title'); |
| 157 |
if (titleEl) { titleEl.style.color = titleColor; } |
| 158 |
var subEl = root.querySelector('.bkbg-g48-subtitle'); |
| 159 |
if (subEl) { subEl.style.color = titleColor; } |
| 160 |
|
| 161 |
// Game state |
| 162 |
var grid = makeEmptyGrid(); |
| 163 |
var score = 0; |
| 164 |
var best = parseInt(localStorage.getItem('bkbg_2048_best_' + uid)) || 0; |
| 165 |
var won = false; |
| 166 |
var over = false; |
| 167 |
var newTile = null; // [r, c] of recently added tile |
| 168 |
var mergedTiles = []; // [[r,c],...] that just merged |
| 169 |
|
| 170 |
addRandom(grid); |
| 171 |
addRandom(grid); |
| 172 |
|
| 173 |
// Header |
| 174 |
var header = document.createElement('div'); |
| 175 |
header.className = 'bkbg-g48-header'; |
| 176 |
|
| 177 |
var titleBlock = document.createElement('div'); |
| 178 |
var titleEl2 = root.querySelector('.bkbg-g48-title'); |
| 179 |
var subEl2 = root.querySelector('.bkbg-g48-subtitle'); |
| 180 |
if (titleEl2) titleBlock.appendChild(titleEl2.cloneNode(true)); |
| 181 |
if (subEl2) titleBlock.appendChild(subEl2.cloneNode(true)); |
| 182 |
header.appendChild(titleBlock); |
| 183 |
|
| 184 |
// Score section |
| 185 |
var scoresDiv = document.createElement('div'); |
| 186 |
scoresDiv.className = 'bkbg-g48-scores'; |
| 187 |
|
| 188 |
var scoreBox = null, scorVal = null, bestBox = null, bestVal = null; |
| 189 |
|
| 190 |
function makeScoreBox(label, val, bg) { |
| 191 |
var box = document.createElement('div'); |
| 192 |
box.className = 'bkbg-g48-score-box'; |
| 193 |
box.style.background = bg; |
| 194 |
var lbl = document.createElement('div'); |
| 195 |
lbl.className = 'bkbg-g48-score-label'; |
| 196 |
lbl.style.color = '#eee4da'; |
| 197 |
lbl.textContent = label; |
| 198 |
var vEl = document.createElement('div'); |
| 199 |
vEl.className = 'bkbg-g48-score-value'; |
| 200 |
vEl.style.color = '#fff'; |
| 201 |
vEl.textContent = val; |
| 202 |
box.appendChild(lbl); |
| 203 |
box.appendChild(vEl); |
| 204 |
return { box: box, valEl: vEl }; |
| 205 |
} |
| 206 |
|
| 207 |
if (opts.showScore !== false) { |
| 208 |
var sb = makeScoreBox('Score', '0', scoreHdrBg); |
| 209 |
scoreBox = sb.box; scorVal = sb.valEl; |
| 210 |
scoresDiv.appendChild(scoreBox); |
| 211 |
} |
| 212 |
if (opts.showBestScore !== false) { |
| 213 |
var bb = makeScoreBox('Best', best || '0', scoreHdrBg); |
| 214 |
bestBox = bb.box; bestVal = bb.valEl; |
| 215 |
scoresDiv.appendChild(bestBox); |
| 216 |
} |
| 217 |
|
| 218 |
var newBtn = document.createElement('button'); |
| 219 |
newBtn.className = 'bkbg-g48-new-btn'; |
| 220 |
newBtn.style.background = newGameBg; |
| 221 |
newBtn.textContent = 'New Game'; |
| 222 |
newBtn.addEventListener('click', newGame); |
| 223 |
scoresDiv.appendChild(newBtn); |
| 224 |
|
| 225 |
header.appendChild(scoresDiv); |
| 226 |
root.appendChild(header); |
| 227 |
|
| 228 |
// Board |
| 229 |
var boardWrap = document.createElement('div'); |
| 230 |
boardWrap.className = 'bkbg-g48-board-wrap'; |
| 231 |
|
| 232 |
var boardEl = document.createElement('div'); |
| 233 |
boardEl.className = 'bkbg-g48-grid'; |
| 234 |
boardEl.style.background = gridBg; |
| 235 |
boardEl.style.gap = gridGap + 'px'; |
| 236 |
boardEl.style.padding = gridGap + 'px'; |
| 237 |
boardEl.style.borderRadius = (tileRadius + gridGap) + 'px'; |
| 238 |
boardEl.style.width = boardSize + 'px'; |
| 239 |
boardEl.style.height = boardSize + 'px'; |
| 240 |
boardEl.style.boxSizing = 'border-box'; |
| 241 |
boardEl.style.touchAction = 'none'; |
| 242 |
boardEl.style.position = 'relative'; |
| 243 |
|
| 244 |
// 16 tile divs |
| 245 |
var tileDivs = []; |
| 246 |
for (var i = 0; i < 16; i++) { |
| 247 |
var td = document.createElement('div'); |
| 248 |
td.className = 'bkbg-g48-tile'; |
| 249 |
td.style.borderRadius = tileRadius + 'px'; |
| 250 |
td.style.fontSize = '36px'; |
| 251 |
boardEl.appendChild(td); |
| 252 |
tileDivs.push(td); |
| 253 |
} |
| 254 |
|
| 255 |
boardWrap.appendChild(boardEl); |
| 256 |
root.appendChild(boardWrap); |
| 257 |
|
| 258 |
// Hint |
| 259 |
var hint = document.createElement('p'); |
| 260 |
hint.className = 'bkbg-g48-hint'; |
| 261 |
hint.style.color = titleColor; |
| 262 |
hint.textContent = '\u2190 \u2192 \u2191 \u2193 arrow keys or swipe to play'; |
| 263 |
root.appendChild(hint); |
| 264 |
|
| 265 |
function updateScoreUI() { |
| 266 |
if (scorVal) scorVal.textContent = score; |
| 267 |
if (bestVal) bestVal.textContent = best; |
| 268 |
} |
| 269 |
|
| 270 |
function renderGrid() { |
| 271 |
for (var r = 0; r < 4; r++) { |
| 272 |
for (var c = 0; c < 4; c++) { |
| 273 |
var val = grid[r][c]; |
| 274 |
var td = tileDivs[r * 4 + c]; |
| 275 |
var tc = val ? getTileColor(val) : { bg: tileBg, text: '#776e65' }; |
| 276 |
td.style.background = tc.bg; |
| 277 |
td.style.color = tc.text; |
| 278 |
td.textContent = val || ''; |
| 279 |
var fs = val < 100 ? 30 : val < 1000 ? 22 : val < 10000 ? 17 : 13; |
| 280 |
td.style.fontSize = fs + 'px'; |
| 281 |
|
| 282 |
// Animations |
| 283 |
td.classList.remove('bkbg-g48-pop', 'bkbg-g48-new'); |
| 284 |
if (animate) { |
| 285 |
if (newTile && newTile[0] === r && newTile[1] === c) { |
| 286 |
td.classList.add('bkbg-g48-new'); |
| 287 |
} |
| 288 |
var isMerge = mergedTiles.some(function (mt) { return mt[0] === r && mt[1] === c; }); |
| 289 |
if (isMerge) { |
| 290 |
void td.offsetWidth; // reflow |
| 291 |
td.classList.add('bkbg-g48-pop'); |
| 292 |
} |
| 293 |
} |
| 294 |
} |
| 295 |
} |
| 296 |
} |
| 297 |
|
| 298 |
function showOverlay(msg, sub) { |
| 299 |
var existing = boardEl.querySelector('.bkbg-g48-overlay'); |
| 300 |
if (existing) existing.remove(); |
| 301 |
|
| 302 |
var ov = document.createElement('div'); |
| 303 |
ov.className = 'bkbg-g48-overlay'; |
| 304 |
ov.style.background = 'rgba(238,228,218,0.73)'; |
| 305 |
ov.style.borderRadius = (tileRadius + gridGap) + 'px'; |
| 306 |
ov.style.color = '#776e65'; |
| 307 |
|
| 308 |
var msgEl = document.createElement('div'); |
| 309 |
msgEl.className = 'bkbg-g48-overlay-msg'; |
| 310 |
msgEl.textContent = msg; |
| 311 |
|
| 312 |
var subEl = document.createElement('div'); |
| 313 |
subEl.className = 'bkbg-g48-overlay-sub'; |
| 314 |
subEl.textContent = sub; |
| 315 |
|
| 316 |
var btn = document.createElement('button'); |
| 317 |
btn.className = 'bkbg-g48-new-btn'; |
| 318 |
btn.style.background = newGameBg; |
| 319 |
btn.textContent = 'Try Again'; |
| 320 |
btn.addEventListener('click', newGame); |
| 321 |
|
| 322 |
ov.appendChild(msgEl); |
| 323 |
ov.appendChild(subEl); |
| 324 |
ov.appendChild(btn); |
| 325 |
boardEl.appendChild(ov); |
| 326 |
} |
| 327 |
|
| 328 |
function removeOverlay() { |
| 329 |
var ov = boardEl.querySelector('.bkbg-g48-overlay'); |
| 330 |
if (ov) ov.remove(); |
| 331 |
} |
| 332 |
|
| 333 |
function findMerged(before, after) { |
| 334 |
var res = []; |
| 335 |
for (var r = 0; r < 4; r++) { |
| 336 |
for (var c = 0; c < 4; c++) { |
| 337 |
if (after[r][c] && after[r][c] === before[r][c] * 2) res.push([r, c]); |
| 338 |
} |
| 339 |
} |
| 340 |
return res; |
| 341 |
} |
| 342 |
|
| 343 |
function doMove(moveFn) { |
| 344 |
if (over) return; |
| 345 |
var before = cloneGrid(grid); |
| 346 |
var res = moveFn(grid); |
| 347 |
if (gridsEqual(before, res.grid)) return; // no change |
| 348 |
grid = res.grid; |
| 349 |
score += res.score; |
| 350 |
if (score > best) { best = score; localStorage.setItem('bkbg_2048_best_' + uid, best); } |
| 351 |
mergedTiles = findMerged(before, grid); |
| 352 |
var pos = addRandom(grid); |
| 353 |
newTile = pos || null; |
| 354 |
renderGrid(); |
| 355 |
updateScoreUI(); |
| 356 |
if (!won && hasWon(grid)) { won = true; showOverlay('\uD83C\uDF89 You Win!', 'Score: ' + score); } |
| 357 |
else if (!hasMoves(grid)) { over = true; showOverlay('\uD83D\uDC80 Game Over', 'Score: ' + score); } |
| 358 |
} |
| 359 |
|
| 360 |
function newGame() { |
| 361 |
grid = makeEmptyGrid(); |
| 362 |
score = 0; |
| 363 |
won = false; |
| 364 |
over = false; |
| 365 |
newTile = null; |
| 366 |
mergedTiles = []; |
| 367 |
addRandom(grid); |
| 368 |
addRandom(grid); |
| 369 |
removeOverlay(); |
| 370 |
renderGrid(); |
| 371 |
updateScoreUI(); |
| 372 |
} |
| 373 |
|
| 374 |
renderGrid(); |
| 375 |
updateScoreUI(); |
| 376 |
|
| 377 |
// Keyboard |
| 378 |
document.addEventListener('keydown', function (e) { |
| 379 |
if (!boardEl.closest('body')) return; // guard if removed |
| 380 |
switch (e.key) { |
| 381 |
case 'ArrowLeft': e.preventDefault(); doMove(moveLeft); break; |
| 382 |
case 'ArrowRight': e.preventDefault(); doMove(moveRight); break; |
| 383 |
case 'ArrowUp': e.preventDefault(); doMove(moveUp); break; |
| 384 |
case 'ArrowDown': e.preventDefault(); doMove(moveDown); break; |
| 385 |
} |
| 386 |
}); |
| 387 |
|
| 388 |
// Touch / swipe |
| 389 |
var touchStart = null; |
| 390 |
boardEl.addEventListener('touchstart', function (e) { |
| 391 |
var t = e.touches[0]; |
| 392 |
touchStart = { x: t.clientX, y: t.clientY }; |
| 393 |
}, { passive: true }); |
| 394 |
|
| 395 |
boardEl.addEventListener('touchend', function (e) { |
| 396 |
if (!touchStart) return; |
| 397 |
var t = e.changedTouches[0]; |
| 398 |
var dx = t.clientX - touchStart.x; |
| 399 |
var dy = t.clientY - touchStart.y; |
| 400 |
touchStart = null; |
| 401 |
if (Math.abs(dx) < 20 && Math.abs(dy) < 20) return; |
| 402 |
if (Math.abs(dx) > Math.abs(dy)) { |
| 403 |
if (dx > 0) doMove(moveRight); else doMove(moveLeft); |
| 404 |
} else { |
| 405 |
if (dy > 0) doMove(moveDown); else doMove(moveUp); |
| 406 |
} |
| 407 |
}, { passive: true }); |
| 408 |
} |
| 409 |
|
| 410 |
document.querySelectorAll('.bkbg-g48-app').forEach(function (root) { |
| 411 |
initBlock(root); |
| 412 |
}); |
| 413 |
})(); |
| 414 |
|