| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var THEMES = { |
| 5 |
animals: ['🐶','🐱','🐭','🐹','🐰','🦊','🐻','🐼','🐨','🐯','🦁','🐸'], |
| 6 |
food: ['🍕','🍔','🍟','🌮','🌯','🍜','🍣','🍩','🍪','🎂','🍦','🍫'], |
| 7 |
nature: ['🌸','🌺','🌻','🌹','🌷','🍀','🍁','🍂','🌿','🎋','🌵','🌴'], |
| 8 |
space: ['🚀','🌍','🌙','⭐','☀️','🪐','🌟','💫','🌌','🔭','👨🚀','🛸'], |
| 9 |
sports: ['⚽','🏀','🏈','⚾','🎾','🏐','🏉','🎱','🏓','🏸','🥊','🎯'], |
| 10 |
symbols: ['❤️','⚡','🔥','💎','🎵','🌈','💡','🔑','🎭','🎨','🎪','🎲'] |
| 11 |
}; |
| 12 |
|
| 13 |
var DIFFICULTIES = { |
| 14 |
easy: { pairs: 6, cols: 3 }, |
| 15 |
medium: { pairs: 8, cols: 4 }, |
| 16 |
hard: { pairs: 12, cols: 6 } |
| 17 |
}; |
| 18 |
|
| 19 |
var STORAGE_KEY = 'bkbg_memory_best'; |
| 20 |
|
| 21 |
function getBest(uid, diff) { |
| 22 |
try { |
| 23 |
var raw = localStorage.getItem(STORAGE_KEY + '_' + uid + '_' + diff); |
| 24 |
return raw ? parseInt(raw) : null; |
| 25 |
} catch (e) { return null; } |
| 26 |
} |
| 27 |
|
| 28 |
function setBest(uid, diff, moves) { |
| 29 |
try { localStorage.setItem(STORAGE_KEY + '_' + uid + '_' + diff, moves); } catch (e) {} |
| 30 |
} |
| 31 |
|
| 32 |
function shuffle(arr) { |
| 33 |
var a = arr.slice(); |
| 34 |
for (var i = a.length - 1; i > 0; i--) { |
| 35 |
var j = Math.floor(Math.random() * (i + 1)); |
| 36 |
var tmp = a[i]; a[i] = a[j]; a[j] = tmp; |
| 37 |
} |
| 38 |
return a; |
| 39 |
} |
| 40 |
|
| 41 |
function formatTime(secs) { |
| 42 |
var m = Math.floor(secs / 60), s = secs % 60; |
| 43 |
return m + ':' + (s < 10 ? '0' : '') + s; |
| 44 |
} |
| 45 |
|
| 46 |
function initBlock(root) { |
| 47 |
var opts; |
| 48 |
try { opts = JSON.parse(root.getAttribute('data-opts')); } catch (e) { return; } |
| 49 |
var a = opts; |
| 50 |
|
| 51 |
var _typoKeys = { family:'font-family', weight:'font-weight', style:'font-style', decoration:'text-decoration', transform:'text-transform', sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' }; |
| 52 |
function typoCssVarsForEl(el, typo, prefix) { |
| 53 |
if (!typo || typeof typo !== 'object') return; |
| 54 |
Object.keys(_typoKeys).forEach(function (k) { |
| 55 |
if (typo[k] !== undefined && typo[k] !== '') el.style.setProperty(prefix + _typoKeys[k], typo[k]); |
| 56 |
}); |
| 57 |
} |
| 58 |
var uid = root.getAttribute('data-uid') || ('mem' + Math.random().toString(36).slice(2)); |
| 59 |
root.setAttribute('data-uid', uid); |
| 60 |
|
| 61 |
typoCssVarsForEl(root, a.titleTypo, '--bkbg-mem-tt-'); |
| 62 |
typoCssVarsForEl(root, a.subtitleTypo, '--bkbg-mem-st-'); |
| 63 |
|
| 64 |
var difficulty = a.defaultDifficulty || 'medium'; |
| 65 |
var theme = a.defaultTheme || 'animals'; |
| 66 |
var flipDelay = parseInt(a.flipDelay) || 900; |
| 67 |
|
| 68 |
/* State */ |
| 69 |
var cards = []; |
| 70 |
var flipped = []; |
| 71 |
var matched = []; |
| 72 |
var moves = 0; |
| 73 |
var seconds = 0; |
| 74 |
var timerInterval = null; |
| 75 |
var locked = false; |
| 76 |
var gameStarted = false; |
| 77 |
|
| 78 |
root.innerHTML = ''; |
| 79 |
|
| 80 |
var wrap = document.createElement('div'); |
| 81 |
wrap.className = 'bkbg-mem-wrap'; |
| 82 |
wrap.style.cssText = 'background:' + a.sectionBg + ';max-width:' + a.contentMaxWidth + 'px;margin:0 auto;'; |
| 83 |
root.appendChild(wrap); |
| 84 |
|
| 85 |
/* Title */ |
| 86 |
if (a.showTitle) { |
| 87 |
var h = document.createElement('div'); |
| 88 |
h.className = 'bkbg-mem-title'; |
| 89 |
h.style.color = a.titleColor; |
| 90 |
h.textContent = a.title; |
| 91 |
wrap.appendChild(h); |
| 92 |
} |
| 93 |
if (a.showSubtitle) { |
| 94 |
var sub = document.createElement('div'); |
| 95 |
sub.className = 'bkbg-mem-subtitle'; |
| 96 |
sub.style.color = a.subtitleColor; |
| 97 |
sub.textContent = a.subtitle; |
| 98 |
wrap.appendChild(sub); |
| 99 |
} |
| 100 |
|
| 101 |
/* Win banner */ |
| 102 |
var winBanner = document.createElement('div'); |
| 103 |
winBanner.className = 'bkbg-mem-win'; |
| 104 |
winBanner.style.background = a.cardMatched; |
| 105 |
winBanner.innerHTML = '<span class="bkbg-mem-win-icon">🎉</span>' + |
| 106 |
'<div class="bkbg-mem-win-title">You Win!</div>' + |
| 107 |
'<div class="bkbg-mem-win-stats" id="bkbg-mem-winstats-' + uid + '"></div>'; |
| 108 |
wrap.appendChild(winBanner); |
| 109 |
|
| 110 |
/* Stats */ |
| 111 |
var statsRow = document.createElement('div'); |
| 112 |
statsRow.className = 'bkbg-mem-stats'; |
| 113 |
wrap.appendChild(statsRow); |
| 114 |
|
| 115 |
function makeStat(id, label) { |
| 116 |
var box = document.createElement('div'); |
| 117 |
box.className = 'bkbg-mem-stat'; |
| 118 |
box.style.background = a.cardBg; |
| 119 |
var val = document.createElement('div'); |
| 120 |
val.className = 'bkbg-mem-stat-val'; |
| 121 |
val.id = 'bkbg-mem-' + id + '-' + uid; |
| 122 |
val.style.color = a.accentColor; |
| 123 |
val.textContent = id === 'time' ? '0:00' : (id === 'best' ? '—' : '0'); |
| 124 |
var lbl = document.createElement('div'); |
| 125 |
lbl.className = 'bkbg-mem-stat-lbl'; |
| 126 |
lbl.style.color = a.subtitleColor; |
| 127 |
lbl.textContent = label; |
| 128 |
box.appendChild(val); |
| 129 |
box.appendChild(lbl); |
| 130 |
statsRow.appendChild(box); |
| 131 |
return val; |
| 132 |
} |
| 133 |
|
| 134 |
var timeEl = a.showTimer ? makeStat('time', 'Time') : null; |
| 135 |
var movesEl = a.showMoves ? makeStat('moves', 'Moves') : null; |
| 136 |
var bestEl = a.showBestScore ? makeStat('best', 'Best') : null; |
| 137 |
|
| 138 |
function updateBestDisplay() { |
| 139 |
if (!bestEl) return; |
| 140 |
var b = getBest(uid, difficulty); |
| 141 |
bestEl.textContent = b !== null ? b + ' moves' : '—'; |
| 142 |
} |
| 143 |
updateBestDisplay(); |
| 144 |
|
| 145 |
/* Difficulty selectors */ |
| 146 |
var diffRow = document.createElement('div'); |
| 147 |
diffRow.className = 'bkbg-mem-selectors'; |
| 148 |
wrap.appendChild(diffRow); |
| 149 |
|
| 150 |
['easy','medium','hard'].forEach(function (d) { |
| 151 |
var btn = document.createElement('button'); |
| 152 |
btn.className = 'bkbg-mem-sel-btn'; |
| 153 |
btn.textContent = d.charAt(0).toUpperCase() + d.slice(1); |
| 154 |
setDiffBtnStyle(btn, d === difficulty); |
| 155 |
btn.addEventListener('click', function () { |
| 156 |
if (gameStarted && matched.length < cards.length && !confirm('Start new game?')) return; |
| 157 |
difficulty = d; |
| 158 |
updateDiffButtons(); |
| 159 |
resetGame(); |
| 160 |
}); |
| 161 |
diffRow.appendChild(btn); |
| 162 |
}); |
| 163 |
|
| 164 |
/* Theme selectors */ |
| 165 |
var themeNames = Object.keys(THEMES); |
| 166 |
var themeRow = document.createElement('div'); |
| 167 |
themeRow.className = 'bkbg-mem-selectors'; |
| 168 |
themeRow.style.marginTop = '-4px'; |
| 169 |
wrap.appendChild(themeRow); |
| 170 |
|
| 171 |
themeNames.forEach(function (t) { |
| 172 |
var btn = document.createElement('button'); |
| 173 |
btn.className = 'bkbg-mem-sel-btn'; |
| 174 |
btn.style.fontSize = '11px'; |
| 175 |
btn.textContent = t.charAt(0).toUpperCase() + t.slice(1) + ' ' + THEMES[t][0]; |
| 176 |
setThemeBtnStyle(btn, t === theme); |
| 177 |
btn.addEventListener('click', function () { |
| 178 |
if (gameStarted && matched.length < cards.length && !confirm('Change theme?')) return; |
| 179 |
theme = t; |
| 180 |
updateThemeButtons(); |
| 181 |
resetGame(); |
| 182 |
}); |
| 183 |
themeRow.appendChild(btn); |
| 184 |
}); |
| 185 |
|
| 186 |
function setDiffBtnStyle(btn, active) { |
| 187 |
btn.style.cssText = 'border-color:' + a.accentColor + ';background:' + (active ? a.accentColor : 'transparent') + ';color:' + (active ? '#fff' : a.accentColor) + ';padding:5px 14px;border-radius:20px;border:2px solid;font-size:13px;font-weight:600;cursor:pointer;'; |
| 188 |
} |
| 189 |
function setThemeBtnStyle(btn, active) { |
| 190 |
btn.style.cssText = 'border-color:' + a.accentColor + ';background:' + (active ? a.accentColor + '22' : 'transparent') + ';color:' + a.accentColor + ';padding:4px 10px;border-radius:20px;border:2px solid;font-size:11px;font-weight:600;cursor:pointer;' + (active ? 'outline:2px solid ' + a.accentColor + ';' : ''); |
| 191 |
} |
| 192 |
|
| 193 |
function updateDiffButtons() { |
| 194 |
diffRow.querySelectorAll('.bkbg-mem-sel-btn').forEach(function (btn, i) { |
| 195 |
setDiffBtnStyle(btn, ['easy','medium','hard'][i] === difficulty); |
| 196 |
}); |
| 197 |
} |
| 198 |
function updateThemeButtons() { |
| 199 |
themeRow.querySelectorAll('.bkbg-mem-sel-btn').forEach(function (btn, i) { |
| 200 |
setThemeBtnStyle(btn, themeNames[i] === theme); |
| 201 |
}); |
| 202 |
} |
| 203 |
|
| 204 |
/* Card grid */ |
| 205 |
var grid = document.createElement('div'); |
| 206 |
grid.className = 'bkbg-mem-grid'; |
| 207 |
wrap.appendChild(grid); |
| 208 |
|
| 209 |
/* Actions */ |
| 210 |
var actionsEl = document.createElement('div'); |
| 211 |
actionsEl.className = 'bkbg-mem-actions'; |
| 212 |
wrap.appendChild(actionsEl); |
| 213 |
|
| 214 |
var startBtn = document.createElement('button'); |
| 215 |
startBtn.className = 'bkbg-mem-btn'; |
| 216 |
startBtn.style.background = a.accentColor; |
| 217 |
startBtn.textContent = '▶ New Game'; |
| 218 |
startBtn.addEventListener('click', function () { resetGame(); }); |
| 219 |
actionsEl.appendChild(startBtn); |
| 220 |
|
| 221 |
function resetGame() { |
| 222 |
clearInterval(timerInterval); |
| 223 |
var cfg = DIFFICULTIES[difficulty] || DIFFICULTIES.medium; |
| 224 |
var pool = (THEMES[theme] || THEMES.animals).slice(0, cfg.pairs); |
| 225 |
var all = shuffle(pool.concat(pool)); |
| 226 |
|
| 227 |
cards = []; |
| 228 |
flipped = []; |
| 229 |
matched = []; |
| 230 |
moves = 0; |
| 231 |
seconds = 0; |
| 232 |
locked = false; |
| 233 |
gameStarted = false; |
| 234 |
|
| 235 |
if (timeEl) timeEl.textContent = '0:00'; |
| 236 |
if (movesEl) movesEl.textContent = '0'; |
| 237 |
updateBestDisplay(); |
| 238 |
winBanner.classList.remove('bkbg-mem-win-show'); |
| 239 |
|
| 240 |
grid.style.gridTemplateColumns = 'repeat(' + cfg.cols + ', 1fr)'; |
| 241 |
grid.innerHTML = ''; |
| 242 |
|
| 243 |
all.forEach(function (emoji, idx) { |
| 244 |
var card = document.createElement('div'); |
| 245 |
card.className = 'bkbg-mem-card'; |
| 246 |
card.dataset.idx = idx; |
| 247 |
card.dataset.emoji = emoji; |
| 248 |
|
| 249 |
var inner = document.createElement('div'); |
| 250 |
inner.className = 'bkbg-mem-card-inner'; |
| 251 |
|
| 252 |
var back = document.createElement('div'); |
| 253 |
back.className = 'bkbg-mem-card-back'; |
| 254 |
back.style.background = a.cardBack; |
| 255 |
back.textContent = '✦'; |
| 256 |
|
| 257 |
var front = document.createElement('div'); |
| 258 |
front.className = 'bkbg-mem-card-front'; |
| 259 |
front.style.background = a.cardFront; |
| 260 |
front.textContent = emoji; |
| 261 |
|
| 262 |
inner.appendChild(back); |
| 263 |
inner.appendChild(front); |
| 264 |
card.appendChild(inner); |
| 265 |
|
| 266 |
card.addEventListener('click', function () { onCardClick(card); }); |
| 267 |
grid.appendChild(card); |
| 268 |
cards.push(card); |
| 269 |
}); |
| 270 |
} |
| 271 |
|
| 272 |
function startTimer() { |
| 273 |
if (timerInterval) return; |
| 274 |
timerInterval = setInterval(function () { |
| 275 |
seconds++; |
| 276 |
if (timeEl) timeEl.textContent = formatTime(seconds); |
| 277 |
}, 1000); |
| 278 |
} |
| 279 |
|
| 280 |
function onCardClick(card) { |
| 281 |
if (locked) return; |
| 282 |
if (card.classList.contains('bkbg-mem-flipped')) return; |
| 283 |
if (card.classList.contains('bkbg-mem-matched')) return; |
| 284 |
if (flipped.length >= 2) return; |
| 285 |
|
| 286 |
if (!gameStarted) { gameStarted = true; startTimer(); } |
| 287 |
|
| 288 |
card.classList.add('bkbg-mem-flipped'); |
| 289 |
flipped.push(card); |
| 290 |
|
| 291 |
if (flipped.length === 2) { |
| 292 |
moves++; |
| 293 |
if (movesEl) movesEl.textContent = moves; |
| 294 |
checkMatch(); |
| 295 |
} |
| 296 |
} |
| 297 |
|
| 298 |
function checkMatch() { |
| 299 |
var a2 = flipped[0].dataset.emoji; |
| 300 |
var b2 = flipped[1].dataset.emoji; |
| 301 |
if (a2 === b2) { |
| 302 |
/* Match */ |
| 303 |
flipped.forEach(function (c) { |
| 304 |
c.classList.add('bkbg-mem-matched'); |
| 305 |
c.querySelector('.bkbg-mem-card-front').style.background = a.cardMatched; |
| 306 |
matched.push(c); |
| 307 |
}); |
| 308 |
flipped = []; |
| 309 |
if (matched.length === cards.length) { onWin(); } |
| 310 |
} else { |
| 311 |
/* No match — shake and flip back */ |
| 312 |
locked = true; |
| 313 |
flipped.forEach(function (c) { c.classList.add('bkbg-mem-wrong'); }); |
| 314 |
setTimeout(function () { |
| 315 |
flipped.forEach(function (c) { |
| 316 |
c.classList.remove('bkbg-mem-flipped', 'bkbg-mem-wrong'); |
| 317 |
}); |
| 318 |
flipped = []; |
| 319 |
locked = false; |
| 320 |
}, flipDelay); |
| 321 |
} |
| 322 |
} |
| 323 |
|
| 324 |
function onWin() { |
| 325 |
clearInterval(timerInterval); |
| 326 |
timerInterval = null; |
| 327 |
/* Best score */ |
| 328 |
var prev = getBest(uid, difficulty); |
| 329 |
if (prev === null || moves < prev) { setBest(uid, difficulty, moves); } |
| 330 |
updateBestDisplay(); |
| 331 |
var statsNode = root.querySelector('#bkbg-mem-winstats-' + uid); |
| 332 |
if (statsNode) statsNode.textContent = moves + ' moves · ' + formatTime(seconds); |
| 333 |
winBanner.classList.add('bkbg-mem-win-show'); |
| 334 |
/* Scroll to banner */ |
| 335 |
winBanner.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); |
| 336 |
} |
| 337 |
|
| 338 |
resetGame(); |
| 339 |
} |
| 340 |
|
| 341 |
function init() { |
| 342 |
document.querySelectorAll('.bkbg-mem-app').forEach(initBlock); |
| 343 |
} |
| 344 |
|
| 345 |
document.readyState === 'loading' |
| 346 |
? document.addEventListener('DOMContentLoaded', init) |
| 347 |
: init(); |
| 348 |
})(); |
| 349 |
|