Honeybound Tiles is a cozy Mahjong Solitaire puzzle game set inside a warm honeylit library, where every tile feels like a tiny piece of an old golden archive. Books, honey jars, teacups, bookmarks, reading glasses, keys, quills, candles, bees, and honeycomb pages are stacked across the board, waiting to be uncovered and matched.
Your goal is simple: find two matching open tiles and clear them from the shelf. A tile can only be selected when it is not covered by another tile above it and has at least one free side. Clear the stack layer by layer, protect your time, and restore every shelf in the Honey Library.
A Cozy Mahjong Puzzle in a Honeylit Library
Honeybound Tiles brings the classic Mahjong Solitaire experience into a softer and more magical world. Instead of rushing through a cold board, you enter a quiet reading room filled with amber light, warm wooden shelves, parchment colors, floating golden dust, and charming library objects.
Each level gives you a layered tile stack to solve. Some tiles are open and ready to match, while others stay hidden beneath the upper layers. The challenge is to think carefully, remove the right pairs, and slowly reveal the pieces underneath.
LEVEL 1
SCORE 0
BEST 0
MOVES 0
TIME 00:00
HINT
SHUFFLE TILES
' +
'
'
);
bindOverlayButton("start",function(){
sound("button");
startNewGame();
});
bindOverlayButton("continue",function(){
sound("button");
continueGame();
});
}
function showPausePopup(){
if(state.mode !== "playing") return;
state.mode = "paused";
saveProgress();
showOverlay(
'
' +
'
READING BREAK ' +
'' +
'
The honeylit shelf is resting. Save your quiet puzzle and return anytime.
' +
'
'
);
bindOverlayButton("resume",function(){
sound("button");
hideOverlay();
state.mode = "playing";
});
bindOverlayButton("saveGame",function(){
sound("button");
saveProgress();
clearSelection();
state.mode = "menu";
showStartPopup();
});
bindOverlayButton("confirmNew",function(){
sound("button");
showNewGameConfirm();
});
}
function showNewGameConfirm(){
showOverlay(
'
' +
'
NEW ARCHIVE? ' +
'' +
'
A fresh archive will open, and the current honeybound shelf will be closed.
' +
'
'
);
bindOverlayButton("cancelNew",function(){
sound("button");
if(state.mode === "paused") showPausePopup();
else showStartPopup();
});
bindOverlayButton("newGame",function(){
sound("button");
startNewGame();
});
}
function showNoMovesPopup(){
if(state.mode !== "playing") return;
if(!canOfferShuffle()){
showGameOverPopup();
return;
}
state.mode = "nomoves";
saveProgress();
showOverlay(
'
' +
'
NO OPEN SHELF ' +
'' +
'
The golden shelf has no open pair. Shuffle the archive and let the honeylight guide another move.
' +
'
'
);
bindOverlayButton("shuffleNow",function(){
sound("button");
hideOverlay();
state.mode = "playing";
shuffleTiles(true);
});
bindOverlayButton("confirmNew",function(){
sound("button");
showNewGameConfirm();
});
}
function showGameOverPopup(){
if(state.mode !== "playing") return;
state.mode = "gameover";
saveProgress();
showOverlay(
'
' +
'
ARCHIVE SEALED ' +
'' +
'
No golden pair can be opened further. Begin a fresh archive.
' +
'
'
);
bindOverlayButton("newGame",function(){
sound("button");
startNewGame();
});
}
function showTimeUpPopup(){
state.mode = "timeup";
saveProgress();
showOverlay(
'
' +
'
THE LAMP HAS DIMMED ' +
'' +
'
The reading lamp faded before the shelf was cleared. Try this level again.
' +
'
'
);
bindOverlayButton("retry",function(){
sound("button");
startLevel(state.level,true);
});
bindOverlayButton("confirmNew",function(){
sound("button");
showNewGameConfirm();
});
}
function showLevelClearPopup(){
state.mode = "levelclear";
saveProgress();
const isLast = state.level >= 50;
const title = isLast ? "ARCHIVE COMPLETE" : "SHELF CLEARED";
const action = isLast ? "NEW ARCHIVE" : "NEXT SHELF";
const note = isLast
? "Every honeybound tile has found its pair beneath the golden library light."
: "Another golden shelf is restored. A new honeybound shelf is waiting.";
showOverlay(
'
' +
'
' + title + ' ' +
'' +
'
' + note + '
' +
'
'
);
bindOverlayButton("next",function(){
sound("button");
if(isLast){
startNewGame();
}else{
state.level += 1;
startLevel(state.level,false);
}
});
}
function startNewGame(){
storageRemove(KEY_SAVE);
state.level = 1;
state.score = 0;
state.moves = 0;
state.rngSeed = Date.now() + Math.floor(Math.random() * 999999);
hideOverlay();
startLevel(1,false);
}
function continueGame(){
try{
const save = JSON.parse(storageGet(KEY_SAVE) || "{}");
if(!save || !Array.isArray(save.tiles)){
showStartPopup();
return;
}
state.level = clamp(save.level || 1,1,50);
state.score = Number(save.score) || 0;
state.moves = Number(save.moves) || 0;
state.timeLeft = Number(save.timeLeft) || 180;
state.timeLimit = Number(save.timeLimit) || state.timeLeft;
state.usedHint = !!save.usedHint;
state.usedShuffle = !!save.usedShuffle;
state.combo = 0;
state.comboTimer = 0;
state.hintIds = [];
state.hintTimer = 0;
state.particles = [];
state.floats = [];
state.pendingClear = false;
state.levelIntro = 1.2;
state.selectedId = null;
state.lastTickSecond = -1;
state.tiles = save.tiles.map(function(t,i){
return {
id:i + 1,
x:t.x,
y:t.y,
z:t.z,
symbol:t.symbol,
removed:!!t.removed,
selected:false,
removing:0,
deny:0,
wrong:0,
unlockGlow:0,
sx:0,
sy:0,
w:0,
h:0
};
});
state.bounds = save.bounds || makeBounds(state.tiles);
hideOverlay();
state.mode = "playing";
updateHud();
}catch(e){
showStartPopup();
}
}
function startLevel(level,retry){
state.mode = "playing";
state.level = level;
state.selectedId = null;
state.hintIds = [];
state.hintTimer = 0;
state.particles = [];
state.floats = [];
state.combo = 0;
state.comboTimer = 0;
state.usedHint = false;
state.usedShuffle = false;
state.pendingClear = false;
state.levelIntro = 1.35;
state.lastTickSecond = -1;
if(retry) state.moves = Math.max(0,state.moves);
const tileCount = LEVELS[level - 1];
state.timeLimit = Math.round(120 + tileCount * 4 + level * 2);
state.timeLeft = state.timeLimit;
generateLevel(level);
hideOverlay();
updateHud();
saveProgress();
}
function makeBounds(list){
const usable = list.filter(Boolean);
if(!usable.length) return {minX:-1,maxX:1,minY:-1,maxY:1,maxZ:2};
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
let maxZ = 0;
usable.forEach(function(t){
minX = Math.min(minX,t.x);
maxX = Math.max(maxX,t.x);
minY = Math.min(minY,t.y);
maxY = Math.max(maxY,t.y);
maxZ = Math.max(maxZ,t.z);
});
return {minX:minX,maxX:maxX,minY:minY,maxY:maxY,maxZ:maxZ};
}
function layerCounts(total){
let l1 = Math.max(8,Math.round(total * .52));
let l2 = Math.max(6,Math.round(total * .30));
let l3 = total - l1 - l2;
if(l3 < 4){
const need = 4 - l3;
l1 -= Math.ceil(need / 2);
l2 -= Math.floor(need / 2);
l3 = 4;
}
let diff = total - (l1 + l2 + l3);
l1 += diff;
return [l1,l2,l3];
}
function generateLayerPositions(count,z,level,rng,used){
const positions = [];
const maxCols = z === 0
? Math.min(17,10 + Math.floor(level / 5))
: z === 1
? Math.min(13,7 + Math.floor(level / 7))
: Math.min(9,4 + Math.floor(level / 10));
const rows = Math.max(2,Math.ceil(count / maxCols));
const cols = Math.ceil(count / rows);
const candidates = [];
for(let y = 0; y < rows; y++){
const rowExtra = z === 0 ? 2 : 1;
const rowCols = cols + rowExtra;
for(let x = 0; x < rowCols; x++){
const px = x - Math.floor(rowCols / 2);
const py = y - Math.floor(rows / 2);
const centerWeight = Math.abs(px) + Math.abs(py);
candidates.push({
x:px,
y:py,
score:z === 0 ? rng() * 5 + Math.abs(py) * .15 : centerWeight + rng() * .7
});
}
}
if(z === 0){
shuffleArray(candidates,rng);
}else{
candidates.sort(function(a,b){return a.score - b.score;});
}
for(let i = 0; ((__hbtAndValue) => __hbtAndValue ? (positions.length < count) : __hbtAndValue)(i < candidates.length); i++){
const c = candidates[i];
const key = c.x + "," + c.y + "," + z;
if(!used.has(key)){
used.add(key);
positions.push({x:c.x,y:c.y,z:z});
}
}
let guard = 0;
while(((__hbtAndValue) => __hbtAndValue ? (guard < 1000) : __hbtAndValue)(positions.length < count)){
const x = Math.floor(rng() * (cols + 2)) - Math.floor((cols + 2) / 2);
const y = Math.floor(rng() * rows) - Math.floor(rows / 2);
const key = x + "," + y + "," + z;
if(!used.has(key)){
used.add(key);
positions.push({x:x,y:y,z:z});
}
guard++;
}
return positions;
}
function createThreeLayerPositions(total,level,rng){
const counts = layerCounts(total);
const used = new Set();
let positions = [];
for(let z = 0; z < FIXED_LAYERS; z++){
positions = positions.concat(generateLayerPositions(counts[z],z,level,rng,used));
}
positions = positions.slice(0,total);
if(positions.length % 2 === 1) positions.pop();
return positions;
}
function createSafeThreeLayerPositions(total,rng){
const counts = layerCounts(total);
let positions = [];
counts.forEach(function(count,z){
const rows = z === 0 ? Math.max(2,Math.ceil(count / 16)) : z === 1 ? Math.max(2,Math.ceil(count / 12)) : Math.max(2,Math.ceil(count / 8));
const cols = Math.ceil(count / rows);
for(let i = 0; i < count; i++){
const row = Math.floor(i / cols);
const col = i % cols;
const items = Math.min(cols,count - row * cols);
positions.push({
x:col - Math.floor(items / 2),
y:row - Math.floor(rows / 2),
z:z
});
}
});
return shuffleArray(positions,rng).slice(0,total);
}
function generateLevel(level){
const tileCount = LEVELS[level - 1];
const baseSeed = (state.rngSeed + level * 99173 + Math.floor(Date.now() % 100000)) >>> 0;
for(let attempt = 0; attempt < 120; attempt++){
const rng = seeded((baseSeed + attempt * 17239) >>> 0);
const positions = attempt < 100
? createThreeLayerPositions(tileCount,level,rng)
: createSafeThreeLayerPositions(tileCount,rng);
state.tiles = positions.map(function(p,i){
return {
id:i + 1,
x:p.x,
y:p.y,
z:p.z,
symbol:"moon",
removed:false,
selected:false,
removing:0,
deny:0,
wrong:0,
unlockGlow:0,
sx:0,
sy:0,
w:0,
h:0
};
});
state.bounds = makeBounds(state.tiles);
computeTileRects();
if(((__hbtAndValue) => __hbtAndValue ? (hasValidMove()) : __hbtAndValue)(assignSolvableSymbols(rng,level))){
return;
}
}
const fallbackRng = seeded((baseSeed + 999999) >>> 0);
const fallback = createSafeThreeLayerPositions(tileCount,fallbackRng);
state.tiles = fallback.map(function(p,i){
return {
id:i + 1,
x:p.x,
y:p.y,
z:p.z,
symbol:"moon",
removed:false,
selected:false,
removing:0,
deny:0,
wrong:0,
unlockGlow:0,
sx:0,
sy:0,
w:0,
h:0
};
});
state.bounds = makeBounds(state.tiles);
computeTileRects();
assignSolvableSymbols(fallbackRng,level);
}
function computeTileRects(){
if(!state.bounds) return;
const b = state.bounds;
const area = {x:88,y:108,w:1104,h:486};
const cols = Math.max(1,b.maxX - b.minX + 1);
const rows = Math.max(1,b.maxY - b.minY + 1);
let tileW = Math.min(58,area.w / (cols * 1.08 + .8));
let tileH = tileW * 1.5;
let stepX = tileW * 1.045;
let stepY = tileH * 1.025;
let depthX = tileW * .11;
let depthY = tileH * .085;
let totalW = (cols - 1) * stepX + tileW + 2 * depthX;
let totalH = (rows - 1) * stepY + tileH + 2 * depthY;
const sc = Math.min(1,area.w / totalW,area.h / totalH);
tileW *= sc;
tileH *= sc;
stepX *= sc;
stepY *= sc;
depthX *= sc;
depthY *= sc;
totalW = (cols - 1) * stepX + tileW + 2 * depthX;
totalH = (rows - 1) * stepY + tileH + 2 * depthY;
const startX = area.x + (area.w - totalW) / 2;
const startY = area.y + (area.h - totalH) / 2 + 2 * depthY;
state.tiles.forEach(function(t){
t.w = tileW;
t.h = tileH;
t.sx = startX + (t.x - b.minX) * stepX + t.z * depthX;
t.sy = startY + (t.y - b.minY) * stepY - t.z * depthY;
});
}
function assignSolvableSymbols(rng,level){
const originalRemoved = {};
const originalRemoving = {};
const assigned = {};
const symbolCount = Math.min(18,6 + Math.floor((level + 1) / 4));
let pairIndex = 0;
let guard = 0;
state.tiles.forEach(function(t){
originalRemoved[t.id] = !!t.removed;
originalRemoving[t.id] = t.removing || 0;
t.removing = 0;
});
while(true){
const remaining = state.tiles.filter(function(t){return !t.removed;});
if(!remaining.length) break;
if(remaining.length < 2 || guard++ > 500){
restore();
return false;
}
computeTileRects();
const free = remaining.filter(function(t){
return isFree(t);
});
if(free.length < 2){
restore();
return false;
}
shuffleArray(free,rng);
const a = free[0];
const b = free[1];
const symbol = SYMBOLS[(pairIndex + Math.floor(rng() * symbolCount)) % symbolCount];
assigned[a.id] = symbol;
assigned[b.id] = symbol;
a.removed = true;
b.removed = true;
pairIndex++;
}
state.tiles.forEach(function(t){
t.removed = originalRemoved[t.id];
t.removing = originalRemoving[t.id];
if(((__hbtAndValue) => __hbtAndValue ? (assigned[t.id]) : __hbtAndValue)(!t.removed)){
t.symbol = assigned[t.id];
t.selected = false;
t.deny = 0;
t.wrong = 0;
}
});
return true;
function restore(){
state.tiles.forEach(function(t){
t.removed = originalRemoved[t.id];
t.removing = originalRemoving[t.id];
});
}
}
function isActive(t){
return ((__hbtAndValue) => __hbtAndValue ? (!t.removing) : __hbtAndValue)(((__hbtAndValue) => __hbtAndValue ? (!t.removed) : __hbtAndValue)(t));
}
function hasRemovingTiles(){
return state.tiles.some(function(t){return t.removing > 0;});
}
function rectOverlapAmount(a,b){
const left = Math.max(a.x,b.x);
const right = Math.min(a.x + a.w,b.x + b.w);
const top = Math.max(a.y,b.y);
const bottom = Math.min(a.y + a.h,b.y + b.h);
const ow = right - left;
const oh = bottom - top;
if(ow <= 0 || oh <= 0) return 0;
return ow * oh;
}
function getTileCoverRect(t){
return {
x:t.sx + t.w * .04,
y:t.sy + t.h * .04,
w:t.w * .92,
h:t.h * .92
};
}
function isCovered(t){
if(!isActive(t)) return true;
if(!t.w || !t.h) computeTileRects();
const target = getTileCoverRect(t);
const targetArea = target.w * target.h;
return state.tiles.some(function(u){
if(!isActive(u)) return false;
if(u.z <= t.z) return false;
const upper = getTileCoverRect(u);
const overlap = rectOverlapAmount(target,upper);
return overlap > targetArea * .002;
});
}
function sideBlocked(t,dir){
if(!isActive(t)) return true;
return state.tiles.some(function(u){
if(!isActive(u)) return false;
if(u.z !== t.z) return false;
const sameRow = Math.abs(u.y - t.y) < .8;
const side = Math.abs(u.x - (t.x + dir)) < .45;
return ((__hbtAndValue) => __hbtAndValue ? (side) : __hbtAndValue)(sameRow);
});
}
function isFree(t){
if(!isActive(t)) return false;
if(isCovered(t)) return false;
const leftOpen = !sideBlocked(t,-1);
const rightOpen = !sideBlocked(t,1);
return leftOpen || rightOpen;
}
function activeTiles(){
return state.tiles.filter(isActive);
}
function getFreeTiles(){
computeTileRects();
return activeTiles().filter(isFree);
}
function canOfferShuffle(){
return getFreeTiles().length >= 2;
}
function findValidPairs(){
const free = getFreeTiles();
const groups = {};
const pairs = [];
free.forEach(function(t){
if(!groups[t.symbol]) groups[t.symbol] = [];
groups[t.symbol].push(t);
});
Object.keys(groups).forEach(function(k){
const g = groups[k];
if(g.length >= 2){
for(let i = 0; i < g.length - 1; i++){
pairs.push([g[i],g[i + 1]]);
}
}
});
return pairs;
}
function hasValidMove(){
return findValidPairs().length > 0;
}
function canvasPoint(ev){
const rect = canvas.getBoundingClientRect();
const px = ev.clientX - rect.left;
const py = ev.clientY - rect.top;
if(((__hbtAndValue) => __hbtAndValue ? (document.fullscreenElement === shell) : __hbtAndValue)(shell.classList.contains("clm01-portrait-full"))){
return {
x:(1 - py / rect.height) * W,
y:(px / rect.width) * H
};
}
return {
x:px * W / rect.width,
y:py * H / rect.height
};
}
function pointHitsTileRect(x,y,t){
return ((__hbtAndValue) => __hbtAndValue ? (y <= t.sy + t.h) : __hbtAndValue)(((__hbtAndValue) => __hbtAndValue ? (y >= t.sy) : __hbtAndValue)(((__hbtAndValue) => __hbtAndValue ? (x <= t.sx + t.w) : __hbtAndValue)(x >= t.sx)));
}
function tileAt(x,y){
computeTileRects();
const topMost = state.tiles.slice().sort(function(a,b){
if(a.z !== b.z) return b.z - a.z;
if(a.y !== b.y) return b.y - a.y;
return b.x - a.x;
});
for(let i = 0; i < topMost.length; i++){
const t = topMost[i];
if(t.removed) continue;
if(pointHitsTileRect(x,y,t)) return t;
}
return null;
}
function clearSelection(){
state.tiles.forEach(function(t){t.selected = false;});
state.selectedId = null;
}
function freeMap(){
const map = {};
state.tiles.forEach(function(t){
if(((__hbtAndValue) => __hbtAndValue ? (isFree(t)) : __hbtAndValue)(isActive(t))){
map[t.id] = true;
}
});
return map;
}
function addMatchEffects(a,b,gain){
[a,b].forEach(function(t){
const cx = t.sx + t.w / 2;
const cy = t.sy + t.h / 2;
for(let i = 0; i < 12; i++){
const colorPick = Math.random();
state.particles.push({
x:cx,
y:cy,
vx:(Math.random() - .5) * 72,
vy:-24 - Math.random() * 70,
life:.55 + Math.random() * .25,
max:.8,
size:3 + Math.random() * 4,
color:colorPick > .72 ? "#D9953F" : colorPick > .40 ? "#F4C542" : "#FFF8DF"
});
}
});
state.floats.push({
text:"+" + gain + (state.combo >= 2 ? " COMBO x" + state.combo : ""),
x:(a.sx + b.sx) / 2 + a.w / 2,
y:(a.sy + b.sy) / 2,
vy:-38,
life:1,
max:1,
color:"#5A321F"
});
}
function addUnlockSpark(t){
const cx = t.sx + t.w / 2;
const cy = t.sy + t.h / 2;
for(let i = 0; i < 5; i++){
state.particles.push({
x:cx + (Math.random() - .5) * t.w * .5,
y:cy + (Math.random() - .5) * t.h * .5,
vx:(Math.random() - .5) * 28,
vy:-20 - Math.random() * 35,
life:.55,
max:.55,
size:2 + Math.random() * 3,
color:Math.random() > .45 ? "#F4C542" : "#FFF8DF"
});
}
}
function matchTiles(a,b){
const before = freeMap();
clearSelection();
a.removing = 1;
b.removing = 1;
state.combo = Math.min(9,state.comboTimer > 0 ? state.combo + 1 : 1);
state.comboTimer = 2.4;
const gain = 20 + state.level + state.combo * 5;
state.score += gain;
state.moves += 1;
state.timeLeft = Math.min(state.timeLimit + 60,state.timeLeft + 1 + (state.combo >= 2 ? 2 : 0));
if(state.score > state.best){
state.best = state.score;
storageSet(KEY_BEST,String(state.best));
}
addMatchEffects(a,b,gain);
const after = freeMap();
state.tiles.forEach(function(t){
if(((__hbtAndValue) => __hbtAndValue ? (!before[t.id]) : __hbtAndValue)(after[t.id])){
t.unlockGlow = 1;
addUnlockSpark(t);
}
});
sound("match",state.combo);
updateHud();
if(activeTiles().length === 0){
state.pendingClear = true;
}else{
setTimeout(function(){
if(((__hbtAndValue) => __hbtAndValue ? (!hasValidMove()) : __hbtAndValue)(((__hbtAndValue) => __hbtAndValue ? (!hasRemovingTiles()) : __hbtAndValue)(((__hbtAndValue) => __hbtAndValue ? (!state.pendingClear) : __hbtAndValue)(state.mode === "playing")))){
if(canOfferShuffle()) showNoMovesPopup();
else showGameOverPopup();
}
},420);
}
saveProgress();
}
function useHint(){
if(state.mode !== "playing" || state.levelIntro > 0 || hasRemovingTiles()) return;
const pairs = findValidPairs();
if(!pairs.length){
if(canOfferShuffle()) showNoMovesPopup();
else showGameOverPopup();
return;
}
clearSelection();
const pair = pairs[Math.floor(Math.random() * pairs.length)];
state.hintIds = [pair[0].id,pair[1].id];
state.hintTimer = 1.8;
state.usedHint = true;
state.score = Math.max(0,state.score - 30);
state.timeLeft = Math.max(0,state.timeLeft - 8);
sound("hint");
updateHud();
saveProgress();
}
function shuffleTiles(fromNoMove){
if(state.mode !== "playing" || hasRemovingTiles()) return;
if(!canOfferShuffle()){
showGameOverPopup();
return;
}
const rng = seeded(Date.now() + state.level * 77 + Math.floor(Math.random() * 999999));
clearSelection();
let success = assignSolvableSymbols(rng,state.level);
if(!success){
success = relayoutRemainingTiles(rng);
}
state.usedShuffle = true;
if(!fromNoMove){
state.score = Math.max(0,state.score - 80);
state.timeLeft = Math.max(0,state.timeLeft - 15);
}
state.hintIds = [];
state.hintTimer = 0;
activeTiles().forEach(function(t){
t.unlockGlow = Math.max(t.unlockGlow,.55);
});
for(let i = 0; i < 42; i++){
state.particles.push({
x:W / 2 + (Math.random() - .5) * 560,
y:H / 2 + (Math.random() - .5) * 300,
vx:(Math.random() - .5) * 115,
vy:(Math.random() - .5) * 80,
life:.75,
max:.75,
size:2 + Math.random() * 4,
color:Math.random() > .66 ? "#FFF8DF" : Math.random() > .5 ? "#F4C542" : "#D9953F"
});
}
sound("shuffle");
updateHud();
saveProgress();
if(!success || !hasValidMove()){
setTimeout(function(){
if(state.mode === "playing"){
if(canOfferShuffle()) showNoMovesPopup();
else showGameOverPopup();
}
},140);
}
}
function relayoutRemainingTiles(rng){
const active = activeTiles();
if(active.length < 2) return false;
const positions = createThreeLayerPositions(active.length,state.level,rng);
active.forEach(function(t,i){
t.x = positions[i].x;
t.y = positions[i].y;
t.z = positions[i].z;
t.selected = false;
t.deny = 0;
t.wrong = 0;
t.unlockGlow = Math.max(t.unlockGlow,.55);
});
state.bounds = makeBounds(state.tiles);
computeTileRects();
return ((__hbtAndValue) => __hbtAndValue ? (hasValidMove()) : __hbtAndValue)(assignSolvableSymbols(rng,state.level));
}
function completeLevelIfReady(){
if(!state.pendingClear) return;
if(hasRemovingTiles()) return;
state.pendingClear = false;
const clearBonus = state.level * 100;
const timeBonus = Math.ceil(state.timeLeft) * 2;
const noHint = state.usedHint ? 0 : 150;
const noShuffle = state.usedShuffle ? 0 : 200;
const perfect = (((__hbtAndValue) => __hbtAndValue ? (state.timeLeft > state.timeLimit * .28) : __hbtAndValue)(((__hbtAndValue) => __hbtAndValue ? (!state.usedShuffle) : __hbtAndValue)(!state.usedHint))) ? 300 : 0;
state.score += clearBonus + timeBonus + noHint + noShuffle + perfect;
if(state.score > state.best){
state.best = state.score;
storageSet(KEY_BEST,String(state.best));
}
updateHud();
sound(state.level >= 50 ? "complete" : "clear");
showLevelClearPopup();
}
function saveProgress(){
try{
if(!state.tiles.length) return;
const save = {
level:state.level,
score:state.score,
moves:state.moves,
timeLeft:state.timeLeft,
timeLimit:state.timeLimit,
usedHint:state.usedHint,
usedShuffle:state.usedShuffle,
bounds:state.bounds,
tiles:state.tiles.map(function(t){
return {
x:t.x,
y:t.y,
z:t.z,
symbol:t.symbol,
removed:!!t.removed || t.removing > 0
};
})
};
storageSet(KEY_SAVE,JSON.stringify(save));
}catch(e){}
}
function sound(type,combo){
if(state.muted) return;
try{
if(!audioCtx){
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
const now = audioCtx.currentTime;
const gain = audioCtx.createGain();
gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(.0001,now);
let freq = 420;
let dur = .08;
let vol = .048;
let wave = "sine";
if(type === "button"){freq = 330; dur = .060; vol = .030; wave = "triangle";}
if(type === "tap"){freq = 420; dur = .050; vol = .030; wave = "triangle";}
if(type === "match"){freq = 560 + Math.min(6,combo || 1) * 38; dur = .155; vol = .058;}
if(type === "wrong"){freq = 190; dur = .12; vol = .042; wave = "triangle";}
if(type === "deny"){freq = 155; dur = .085; vol = .034; wave = "triangle";}
if(type === "hint"){freq = 860; dur = .17; vol = .044;}
if(type === "shuffle"){freq = 360; dur = .22; vol = .046; wave = "triangle";}
if(type === "clear"){freq = 720; dur = .25; vol = .064;}
if(type === "complete"){freq = 920; dur = .34; vol = .072;}
if(type === "tick"){freq = 250; dur = .04; vol = .016;}
const osc = audioCtx.createOscillator();
osc.type = wave;
osc.frequency.setValueAtTime(freq,now);
gain.gain.exponentialRampToValueAtTime(vol,now + .012);
gain.gain.exponentialRampToValueAtTime(.0001,now + dur);
osc.connect(gain);
osc.start(now);
osc.stop(now + dur + .02);
}catch(e){}
}
function onCanvasDown(ev){
if(state.mode !== "playing") return;
if(state.levelIntro > 0) return;
if(hasRemovingTiles()) return;
const p = canvasPoint(ev);
const t = tileAt(p.x,p.y);
if(!t) return;
sound("tap");
if(!isFree(t)){
t.deny = 1;
sound("deny");
return;
}
if(state.selectedId === t.id){
t.selected = false;
state.selectedId = null;
return;
}
if(state.selectedId === null){
clearSelection();
t.selected = true;
state.selectedId = t.id;
return;
}
const first = state.tiles.find(function(tile){
return tile.id === state.selectedId;
});
if(!first || !isActive(first) || !isFree(first)){
clearSelection();
t.selected = true;
state.selectedId = t.id;
return;
}
if(((__hbtAndValue) => __hbtAndValue ? (isFree(t)) : __hbtAndValue)(((__hbtAndValue) => __hbtAndValue ? (isFree(first)) : __hbtAndValue)(first.symbol === t.symbol))){
matchTiles(first,t);
}else{
first.wrong = 1;
t.wrong = 1;
clearSelection();
sound("wrong");
}
}
function update(dt){
if(state.mode === "playing"){
if(state.levelIntro > 0){
state.levelIntro = Math.max(0,state.levelIntro - dt);
}else{
state.timeLeft -= dt;
const sec = Math.ceil(state.timeLeft);
if(((__hbtAndValue) => __hbtAndValue ? (sec >= 0) : __hbtAndValue)(((__hbtAndValue) => __hbtAndValue ? (sec !== state.lastTickSecond) : __hbtAndValue)(state.timeLeft <= 30))){
state.lastTickSecond = sec;
sound("tick");
}
if(state.timeLeft <= 0){
state.timeLeft = 0;
updateHud();
showTimeUpPopup();
}
}
}
if(state.comboTimer > 0){
state.comboTimer = Math.max(0,state.comboTimer - dt);
if(state.comboTimer <= 0) state.combo = 0;
}
if(state.hintTimer > 0){
state.hintTimer = Math.max(0,state.hintTimer - dt);
if(state.hintTimer <= 0) state.hintIds = [];
}
state.tiles.forEach(function(t){
if(t.removing > 0){
t.removing = Math.max(0,t.removing - dt * 3.2);
if(t.removing <= 0){
t.removed = true;
t.removing = 0;
}
}
if(t.deny > 0) t.deny = Math.max(0,t.deny - dt * 4.5);
if(t.wrong > 0) t.wrong = Math.max(0,t.wrong - dt * 4.5);
if(t.unlockGlow > 0) t.unlockGlow = Math.max(0,t.unlockGlow - dt * 1.8);
});
state.particles = state.particles.filter(function(p){
p.life -= dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 40 * dt;
return p.life > 0;
});
state.floats = state.floats.filter(function(f){
f.life -= dt;
f.y += f.vy * dt;
return f.life > 0;
});
completeLevelIfReady();
updateHud();
}
function roundRect(c,x,y,w,h,r){
r = Math.min(r,w / 2,h / 2);
c.beginPath();
c.moveTo(x + r,y);
c.arcTo(x + w,y,x + w,y + h,r);
c.arcTo(x + w,y + h,x,y + h,r);
c.arcTo(x,y + h,x,y,r);
c.arcTo(x,y,x + w,y,r);
c.closePath();
}
function drawBackground(){
const g = ctx.createLinearGradient(0,0,0,H);
g.addColorStop(0,"#2B1710");
g.addColorStop(.32,"#5A321F");
g.addColorStop(.70,"#9B6426");
g.addColorStop(1,"#FFF1C9");
ctx.fillStyle = g;
ctx.fillRect(0,0,W,H);
ctx.save();
const lampGlow = ctx.createRadialGradient(1000,82,8,1000,82,240);
lampGlow.addColorStop(0,"rgba(255,248,223,.88)");
lampGlow.addColorStop(.38,"rgba(244,197,66,.34)");
lampGlow.addColorStop(1,"rgba(244,197,66,0)");
ctx.fillStyle = lampGlow;
ctx.beginPath();
ctx.arc(1000,82,240,0,Math.PI * 2);
ctx.fill();
function spine(x,y,w,h,color,lean){
ctx.save();
ctx.translate(x + w / 2,y + h / 2);
ctx.rotate(lean || 0);
roundRect(ctx,-w / 2,-h / 2,w,h,4);
ctx.fillStyle = color;
ctx.fill();
ctx.globalAlpha = .42;
ctx.strokeStyle = "#FFF1C9";
ctx.lineWidth = 1.1;
ctx.beginPath();
ctx.moveTo(-w * .22,-h * .36);
ctx.lineTo(-w * .22,h * .34);
ctx.moveTo(w * .22,-h * .28);
ctx.lineTo(w * .22,h * .26);
ctx.stroke();
ctx.restore();
}
const shelfColors = ["#5A321F","#6F4128","#8F6431","#B87333","#C69032","#3F2418","#6F7F4F"];
for(let row = 0; row < 4; row++){
const sy = 86 + row * 84;
ctx.globalAlpha = .34;
ctx.fillStyle = "rgba(63,36,24,.70)";
roundRect(ctx,38,sy + 52,356,10,4);
ctx.fill();
roundRect(ctx,886,sy + 52,356,10,4);
ctx.fill();
ctx.globalAlpha = .58;
for(let i = 0; i < 17; i++){
const bw = 11 + (i % 4) * 3;
const bh = 34 + ((i + row) % 6) * 5;
const lx = 46 + i * 20 + (row % 2) * 7;
const rx = 894 + i * 20 + (row % 2) * 7;
spine(lx,sy + 52 - bh,bw,bh,shelfColors[(i + row) % shelfColors.length],(i % 3 - 1) * .025);
spine(rx,sy + 52 - bh,bw,bh,shelfColors[(i + row + 2) % shelfColors.length],(1 - i % 3) * .022);
}
}
ctx.globalAlpha = 1;
// honeycomb window glow
ctx.save();
ctx.globalAlpha = .23;
roundRect(ctx,520,50,240,118,28);
ctx.fillStyle = "rgba(255,248,223,.30)";
ctx.fill();
ctx.strokeStyle = "rgba(255,241,201,.36)";
ctx.lineWidth = 2;
ctx.stroke();
for(let i = 0; i < 7; i++){
for(let j = 0; j < 3; j++){
const hx = 548 + i * 30 + (j % 2) * 15;
const hy = 76 + j * 26;
ctx.beginPath();
for(let k = 0; k < 6; k++){
const a = Math.PI / 6 + k * Math.PI / 3;
const px = hx + Math.cos(a) * 12;
const py = hy + Math.sin(a) * 12;
if(k === 0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
}
ctx.closePath();
ctx.stroke();
}
}
ctx.restore();
// floating honey dust and tiny page flecks
for(let i = 0; i < 70; i++){
const x = (i * 137 + 49) % W;
const y = 34 + ((i * 71) % 470);
const r = 1.2 + (i % 5) * .55;
ctx.globalAlpha = .08 + (i % 4) * .035;
ctx.fillStyle = i % 3 === 0 ? "#FFF8DF" : (i % 3 === 1 ? "#F4C542" : "#FFF1C9");
if(i % 7 === 0){
ctx.save();
ctx.translate(x,y);
ctx.rotate(i * .42);
roundRect(ctx,-r * 2,-r,r * 4,r * 2,1.5);
ctx.fill();
ctx.restore();
}else{
ctx.beginPath();
ctx.arc(x,y,r,0,Math.PI * 2);
ctx.fill();
}
}
ctx.globalAlpha = 1;
// desk and foreground warmth
const desk = ctx.createLinearGradient(0,560,0,H);
desk.addColorStop(0,"rgba(111,65,40,.88)");
desk.addColorStop(.48,"rgba(90,50,31,.96)");
desk.addColorStop(1,"rgba(63,36,24,1)");
ctx.fillStyle = desk;
ctx.beginPath();
ctx.moveTo(0,566);
ctx.bezierCurveTo(160,538,310,580,470,550);
ctx.bezierCurveTo(650,518,820,580,1000,548);
ctx.bezierCurveTo(1120,528,1190,554,W,538);
ctx.lineTo(W,H);
ctx.lineTo(0,H);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = .18;
ctx.strokeStyle = "#FFF1C9";
ctx.lineWidth = 1.4;
for(let k = 0; k < 6; k++){
ctx.beginPath();
ctx.moveTo(42,592 + k * 14);
ctx.quadraticCurveTo(610,558 + k * 7,1238,592 + k * 14);
ctx.stroke();
}
ctx.globalAlpha = 1;
// decorative open book and honey jar silhouettes
ctx.save();
ctx.globalAlpha = .25;
roundRect(ctx,82,594,96,42,12);
ctx.fillStyle = "rgba(255,248,223,.52)";
ctx.fill();
ctx.strokeStyle = "rgba(63,36,24,.42)";
ctx.stroke();
ctx.beginPath();
ctx.moveTo(130,598);
ctx.lineTo(130,634);
ctx.stroke();
roundRect(ctx,1104,584,60,70,14);
ctx.fillStyle = "rgba(244,197,66,.42)";
ctx.fill();
roundRect(ctx,1116,572,36,16,6);
ctx.fillStyle = "rgba(255,241,201,.52)";
ctx.fill();
ctx.restore();
ctx.restore();
}
function drawBoardPanel(){
ctx.save();
const x = 76;
const y = 92;
const w = 1128;
const h = 530;
ctx.fillStyle = "rgba(30,17,11,.30)";
roundRect(ctx,x + 16,y + 22,w - 22,h - 4,36);
ctx.fill();
const outer = ctx.createLinearGradient(0,y,0,y + h);
outer.addColorStop(0,"rgba(255,248,223,.28)");
outer.addColorStop(.40,"rgba(184,115,51,.20)");
outer.addColorStop(1,"rgba(63,36,24,.26)");
ctx.fillStyle = outer;
roundRect(ctx,x,y,w,h,34);
ctx.fill();
const inner = ctx.createRadialGradient(x + w / 2,y + h * .18,10,x + w / 2,y + h * .18,620);
inner.addColorStop(0,"rgba(255,248,223,.20)");
inner.addColorStop(.45,"rgba(244,197,66,.08)");
inner.addColorStop(1,"rgba(63,36,24,.18)");
ctx.fillStyle = inner;
roundRect(ctx,x + 10,y + 10,w - 20,h - 20,28);
ctx.fill();
ctx.strokeStyle = "rgba(244,197,66,.42)";
ctx.lineWidth = 2.2;
roundRect(ctx,x + 1.5,y + 1.5,w - 3,h - 3,33);
ctx.stroke();
ctx.strokeStyle = "rgba(255,248,223,.34)";
ctx.lineWidth = 1.4;
roundRect(ctx,x + 13,y + 13,w - 26,h - 26,25);
ctx.stroke();
// subtle archive grid texture
ctx.globalAlpha = .09;
ctx.strokeStyle = "#FFF1C9";
ctx.lineWidth = 1;
for(let i = 0; i < 18; i++){
const gx = x + 44 + i * 58;
ctx.beginPath();
ctx.moveTo(gx,y + 34);
ctx.lineTo(gx,y + h - 34);
ctx.stroke();
}
for(let j = 0; j < 7; j++){
const gy = y + 50 + j * 60;
ctx.beginPath();
ctx.moveTo(x + 34,gy);
ctx.lineTo(x + w - 34,gy);
ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.restore();
}
function drawStar(x,y,outer,inner,points,color){
ctx.fillStyle = color;
ctx.beginPath();
for(let i = 0; i < points * 2; i++){
const r = i % 2 === 0 ? outer : inner;
const a = -Math.PI / 2 + i * Math.PI / points;
const px = x + Math.cos(a) * r;
const py = y + Math.sin(a) * r;
if(i === 0) ctx.moveTo(px,py);
else ctx.lineTo(px,py);
}
ctx.closePath();
ctx.fill();
}
function drawSymbol(symbol,x,y,s){
ctx.save();
ctx.lineCap = "round";
ctx.lineJoin = "round";
const cream = "#FFF8DF";
const parchment = "#FFF1C9";
const honey = "#F4C542";
const amber = "#D9953F";
const brown = "#5A321F";
const cocoa = "#3F2418";
const sage = "#6F7F4F";
const rose = "#B8735A";
if(symbol === "book"){
ctx.fillStyle = brown;
roundRect(ctx,x - s * .34,y - s * .44,s * .68,s * .88,s * .08);
ctx.fill();
ctx.fillStyle = "rgba(255,241,201,.32)";
roundRect(ctx,x - s * .24,y - s * .36,s * .14,s * .72,s * .03);
ctx.fill();
ctx.strokeStyle = honey;
ctx.lineWidth = s * .055;
roundRect(ctx,x - s * .34,y - s * .44,s * .68,s * .88,s * .08);
ctx.stroke();
ctx.strokeStyle = parchment;
ctx.lineWidth = s * .035;
ctx.beginPath();
ctx.moveTo(x - s * .02,y - s * .22);
ctx.lineTo(x + s * .20,y - s * .22);
ctx.moveTo(x - s * .02,y - s * .03);
ctx.lineTo(x + s * .18,y - s * .03);
ctx.moveTo(x - s * .02,y + s * .16);
ctx.lineTo(x + s * .16,y + s * .16);
ctx.stroke();
}
if(symbol === "honeyjar"){
const jar = ctx.createLinearGradient(x - s * .32,y - s * .30,x + s * .32,y + s * .38);
jar.addColorStop(0,"#FFF8DF");
jar.addColorStop(.38,"#F4C542");
jar.addColorStop(1,"#B87333");
ctx.fillStyle = jar;
roundRect(ctx,x - s * .33,y - s * .28,s * .66,s * .64,s * .13);
ctx.fill();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .052;
ctx.stroke();
ctx.fillStyle = parchment;
roundRect(ctx,x - s * .21,y - s * .47,s * .42,s * .18,s * .06);
ctx.fill();
ctx.strokeStyle = brown;
ctx.stroke();
ctx.fillStyle = "rgba(255,248,223,.70)";
roundRect(ctx,x - s * .21,y - s * .03,s * .42,s * .18,s * .06);
ctx.fill();
ctx.fillStyle = amber;
ctx.beginPath();
ctx.arc(x - s * .10,y - s * .13,s * .045,0,Math.PI * 2);
ctx.arc(x + s * .14,y + s * .20,s * .040,0,Math.PI * 2);
ctx.fill();
}
if(symbol === "teacup"){
ctx.fillStyle = cream;
ctx.beginPath();
ctx.moveTo(x - s * .34,y - s * .12);
ctx.quadraticCurveTo(x - s * .28,y + s * .34,x + s * .18,y + s * .28);
ctx.quadraticCurveTo(x + s * .34,y + s * .10,x + s * .30,y - s * .12);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .052;
ctx.stroke();
ctx.beginPath();
ctx.arc(x + s * .33,y + s * .04,s * .13,-Math.PI / 2,Math.PI / 2);
ctx.stroke();
ctx.strokeStyle = honey;
ctx.lineWidth = s * .04;
ctx.beginPath();
ctx.moveTo(x - s * .18,y - s * .34);
ctx.quadraticCurveTo(x - s * .08,y - s * .49,x,y - s * .32);
ctx.moveTo(x + s * .08,y - s * .34);
ctx.quadraticCurveTo(x + s * .18,y - s * .49,x + s * .25,y - s * .32);
ctx.stroke();
ctx.strokeStyle = amber;
ctx.beginPath();
ctx.moveTo(x - s * .36,y + s * .38);
ctx.lineTo(x + s * .38,y + s * .38);
ctx.stroke();
}
if(symbol === "parchment"){
ctx.fillStyle = parchment;
roundRect(ctx,x - s * .40,y - s * .35,s * .80,s * .70,s * .10);
ctx.fill();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .045;
ctx.stroke();
ctx.fillStyle = "rgba(184,115,51,.22)";
roundRect(ctx,x - s * .35,y - s * .30,s * .18,s * .60,s * .07);
ctx.fill();
ctx.strokeStyle = amber;
ctx.lineWidth = s * .035;
for(let i = 0; i < 4; i++){
ctx.beginPath();
ctx.moveTo(x - s * .08,y - s * .20 + i * s * .12);
ctx.lineTo(x + s * .27,y - s * .20 + i * s * .12);
ctx.stroke();
}
}
if(symbol === "bookmark"){
ctx.fillStyle = honey;
ctx.beginPath();
ctx.moveTo(x - s * .18,y - s * .48);
ctx.lineTo(x + s * .18,y - s * .48);
ctx.lineTo(x + s * .18,y + s * .44);
ctx.lineTo(x,y + s * .25);
ctx.lineTo(x - s * .18,y + s * .44);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .052;
ctx.stroke();
ctx.strokeStyle = cream;
ctx.lineWidth = s * .04;
ctx.beginPath();
ctx.moveTo(x - s * .08,y - s * .18);
ctx.lineTo(x + s * .08,y - s * .18);
ctx.moveTo(x - s * .08,y + s * .02);
ctx.lineTo(x + s * .08,y + s * .02);
ctx.stroke();
}
if(symbol === "glasses"){
ctx.strokeStyle = brown;
ctx.lineWidth = s * .07;
ctx.beginPath();
ctx.arc(x - s * .18,y + s * .02,s * .18,0,Math.PI * 2);
ctx.arc(x + s * .18,y + s * .02,s * .18,0,Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x - s * .01,y + s * .02);
ctx.lineTo(x + s * .01,y + s * .02);
ctx.moveTo(x - s * .35,y - s * .02);
ctx.lineTo(x - s * .48,y - s * .14);
ctx.moveTo(x + s * .35,y - s * .02);
ctx.lineTo(x + s * .48,y - s * .14);
ctx.stroke();
ctx.strokeStyle = honey;
ctx.lineWidth = s * .03;
ctx.beginPath();
ctx.arc(x - s * .18,y + s * .02,s * .11,0,Math.PI * 2);
ctx.arc(x + s * .18,y + s * .02,s * .11,0,Math.PI * 2);
ctx.stroke();
}
if(symbol === "key"){
ctx.strokeStyle = honey;
ctx.lineWidth = s * .085;
ctx.beginPath();
ctx.arc(x - s * .20,y - s * .08,s * .16,0,Math.PI * 2);
ctx.moveTo(x - s * .04,y - s * .08);
ctx.lineTo(x + s * .42,y + s * .28);
ctx.moveTo(x + s * .22,y + s * .12);
ctx.lineTo(x + s * .10,y + s * .26);
ctx.moveTo(x + s * .32,y + s * .20);
ctx.lineTo(x + s * .22,y + s * .34);
ctx.stroke();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .035;
ctx.stroke();
}
if(symbol === "bookstack"){
const colors = [sage,amber,brown];
for(let i = 0; i < 3; i++){
ctx.fillStyle = colors[i];
roundRect(ctx,x - s * .42 + i * s * .05,y - s * .30 + i * s * .24,s * .78,s * .20,s * .05);
ctx.fill();
ctx.strokeStyle = cream;
ctx.lineWidth = s * .028;
ctx.beginPath();
ctx.moveTo(x - s * .25 + i * s * .05,y - s * .20 + i * s * .24);
ctx.lineTo(x + s * .22 + i * s * .05,y - s * .20 + i * s * .24);
ctx.stroke();
}
}
if(symbol === "quill"){
ctx.fillStyle = cream;
ctx.beginPath();
ctx.moveTo(x + s * .32,y - s * .46);
ctx.bezierCurveTo(x - s * .20,y - s * .38,x - s * .38,y + s * .04,x - s * .08,y + s * .22);
ctx.bezierCurveTo(x + s * .06,y + s * .02,x + s * .18,y - s * .22,x + s * .32,y - s * .46);
ctx.fill();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .04;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + s * .24,y - s * .36);
ctx.lineTo(x - s * .28,y + s * .44);
ctx.stroke();
ctx.strokeStyle = honey;
ctx.beginPath();
ctx.moveTo(x - s * .12,y - s * .10);
ctx.lineTo(x + s * .13,y - s * .19);
ctx.moveTo(x - s * .20,y + s * .04);
ctx.lineTo(x + s * .05,y - s * .03);
ctx.stroke();
}
if(symbol === "honeydrop"){
ctx.fillStyle = honey;
ctx.beginPath();
ctx.moveTo(x,y - s * .48);
ctx.quadraticCurveTo(x + s * .38,y - s * .02,x + s * .20,y + s * .30);
ctx.quadraticCurveTo(x,y + s * .58,x - s * .20,y + s * .30);
ctx.quadraticCurveTo(x - s * .38,y - s * .02,x,y - s * .48);
ctx.fill();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .05;
ctx.stroke();
ctx.fillStyle = "rgba(255,248,223,.55)";
ctx.beginPath();
ctx.ellipse(x - s * .10,y - s * .08,s * .07,s * .16,-.4,0,Math.PI * 2);
ctx.fill();
}
if(symbol === "ink"){
ctx.fillStyle = cocoa;
roundRect(ctx,x - s * .28,y - s * .22,s * .56,s * .58,s * .09);
ctx.fill();
ctx.strokeStyle = honey;
ctx.lineWidth = s * .045;
ctx.stroke();
ctx.fillStyle = amber;
roundRect(ctx,x - s * .16,y - s * .42,s * .32,s * .18,s * .05);
ctx.fill();
ctx.fillStyle = cream;
roundRect(ctx,x - s * .18,y - s * .02,s * .36,s * .16,s * .05);
ctx.fill();
ctx.fillStyle = honey;
ctx.beginPath();
ctx.arc(x,y + s * .06,s * .035,0,Math.PI * 2);
ctx.fill();
}
if(symbol === "lamp"){
ctx.fillStyle = honey;
ctx.beginPath();
ctx.moveTo(x - s * .32,y - s * .14);
ctx.lineTo(x + s * .32,y - s * .14);
ctx.lineTo(x + s * .18,y + s * .10);
ctx.lineTo(x - s * .18,y + s * .10);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .05;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x,y + s * .10);
ctx.lineTo(x,y + s * .42);
ctx.moveTo(x - s * .28,y + s * .42);
ctx.lineTo(x + s * .28,y + s * .42);
ctx.stroke();
ctx.fillStyle = "rgba(255,248,223,.40)";
ctx.beginPath();
ctx.ellipse(x,y + s * .03,s * .34,s * .22,0,0,Math.PI * 2);
ctx.fill();
}
if(symbol === "pageswirl"){
ctx.strokeStyle = amber;
ctx.lineWidth = s * .075;
ctx.beginPath();
for(let a = 0; a < Math.PI * 2.55; a += .08){
const r = s * .06 + a * s * .055;
const px = x + Math.cos(a) * r;
const py = y + Math.sin(a) * r;
if(a === 0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
}
ctx.stroke();
ctx.strokeStyle = cream;
ctx.lineWidth = s * .038;
ctx.beginPath();
ctx.arc(x,y,s * .44,0,Math.PI * 2);
ctx.stroke();
}
if(symbol === "bee"){
ctx.save();
ctx.rotate(-.12);
ctx.fillStyle = honey;
ctx.beginPath();
ctx.ellipse(x,y,s * .30,s * .22,0,0,Math.PI * 2);
ctx.fill();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .045;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x - s * .12,y - s * .20);
ctx.lineTo(x - s * .12,y + s * .20);
ctx.moveTo(x + s * .04,y - s * .20);
ctx.lineTo(x + s * .04,y + s * .20);
ctx.stroke();
ctx.fillStyle = "rgba(255,248,223,.70)";
ctx.beginPath();
ctx.ellipse(x - s * .10,y - s * .25,s * .15,s * .09,-.4,0,Math.PI * 2);
ctx.ellipse(x + s * .10,y - s * .25,s * .15,s * .09,.4,0,Math.PI * 2);
ctx.fill();
ctx.fillStyle = brown;
ctx.beginPath();
ctx.arc(x + s * .30,y - s * .02,s * .06,0,Math.PI * 2);
ctx.fill();
ctx.restore();
}
if(symbol === "waxribbon"){
ctx.fillStyle = rose;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.lineTo(x - s * .46,y - s * .28);
ctx.lineTo(x - s * .40,y + s * .30);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(x,y);
ctx.lineTo(x + s * .46,y - s * .28);
ctx.lineTo(x + s * .40,y + s * .30);
ctx.closePath();
ctx.fill();
ctx.fillStyle = amber;
ctx.beginPath();
ctx.arc(x,y,s * .18,0,Math.PI * 2);
ctx.fill();
ctx.strokeStyle = cream;
ctx.lineWidth = s * .03;
ctx.beginPath();
ctx.arc(x,y,s * .10,0,Math.PI * 2);
ctx.stroke();
}
if(symbol === "openbook"){
ctx.fillStyle = parchment;
ctx.beginPath();
ctx.moveTo(x,y - s * .30);
ctx.quadraticCurveTo(x - s * .26,y - s * .42,x - s * .46,y - s * .24);
ctx.lineTo(x - s * .42,y + s * .34);
ctx.quadraticCurveTo(x - s * .18,y + s * .22,x,y + s * .34);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(x,y - s * .30);
ctx.quadraticCurveTo(x + s * .26,y - s * .42,x + s * .46,y - s * .24);
ctx.lineTo(x + s * .42,y + s * .34);
ctx.quadraticCurveTo(x + s * .18,y + s * .22,x,y + s * .34);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .04;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x,y - s * .30);
ctx.lineTo(x,y + s * .34);
ctx.stroke();
ctx.strokeStyle = amber;
ctx.lineWidth = s * .027;
for(let i = 0; i < 3; i++){
ctx.beginPath();
ctx.moveTo(x - s * .34,y - s * .14 + i * s * .13);
ctx.lineTo(x - s * .10,y - s * .08 + i * s * .10);
ctx.moveTo(x + s * .10,y - s * .08 + i * s * .10);
ctx.lineTo(x + s * .34,y - s * .14 + i * s * .13);
ctx.stroke();
}
}
if(symbol === "honeycomb"){
ctx.strokeStyle = amber;
ctx.lineWidth = s * .052;
ctx.fillStyle = "rgba(244,197,66,.20)";
function hex(cx2,cy2,r){
ctx.beginPath();
for(let i = 0; i < 6; i++){
const a = Math.PI / 6 + i * Math.PI / 3;
const px = cx2 + Math.cos(a) * r;
const py = cy2 + Math.sin(a) * r;
if(i === 0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
}
hex(x,y,s * .15);
hex(x - s * .20,y,s * .14);
hex(x + s * .20,y,s * .14);
hex(x - s * .10,y - s * .18,s * .13);
hex(x + s * .10,y + s * .18,s * .13);
}
if(symbol === "candle"){
ctx.fillStyle = parchment;
roundRect(ctx,x - s * .15,y - s * .05,s * .30,s * .48,s * .06);
ctx.fill();
ctx.strokeStyle = brown;
ctx.lineWidth = s * .04;
ctx.stroke();
const flame = ctx.createRadialGradient(x,y - s * .24,1,x,y - s * .24,s * .22);
flame.addColorStop(0,"#FFF8DF");
flame.addColorStop(.45,"#F4C542");
flame.addColorStop(1,"rgba(244,197,66,0)");
ctx.fillStyle = flame;
ctx.beginPath();
ctx.moveTo(x,y - s * .52);
ctx.quadraticCurveTo(x + s * .18,y - s * .30,x,y - s * .16);
ctx.quadraticCurveTo(x - s * .16,y - s * .30,x,y - s * .52);
ctx.fill();
ctx.strokeStyle = amber;
ctx.lineWidth = s * .04;
ctx.beginPath();
ctx.moveTo(x - s * .28,y + s * .43);
ctx.lineTo(x + s * .28,y + s * .43);
ctx.stroke();
}
ctx.restore();
}
function drawTile(t){
if(t.removed) return;
const alpha = t.removing > 0 ? clamp(t.removing,0,1) : 1;
const cx = t.sx + t.w / 2;
const cy = t.sy + t.h / 2;
const scale = t.removing > 0 ? .82 + alpha * .18 : 1;
const wiggle = t.deny > 0 ? Math.sin(t.deny * 28) * 5 : 0;
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(cx + wiggle,cy);
ctx.scale(scale,scale);
ctx.translate(-cx,-cy);
const free = isFree(t);
const selected = t.selected;
const hint = ((__hbtAndValue) => __hbtAndValue ? (state.hintTimer > 0) : __hbtAndValue)(state.hintIds.indexOf(t.id) !== -1);
const glow = Math.max(t.unlockGlow,hint ? .7 : 0,selected ? 1 : 0);
if(glow > 0){
ctx.save();
ctx.globalAlpha = .20 + glow * .25;
ctx.fillStyle = selected ? "#F4C542" : "#FFF1C9";
roundRect(ctx,t.sx - 9,t.sy - 9,t.w + 18,t.h + 18,18);
ctx.fill();
ctx.restore();
}
if(t.wrong > 0 || t.deny > 0){
ctx.save();
ctx.globalAlpha = Math.max(t.wrong,t.deny) * .28;
ctx.fillStyle = "#C2412D";
roundRect(ctx,t.sx - 7,t.sy - 7,t.w + 14,t.h + 14,16);
ctx.fill();
ctx.restore();
}
ctx.save();
ctx.fillStyle = "rgba(63,36,24,.25)";
roundRect(ctx,t.sx + t.w * .10,t.sy + t.h * .11,t.w,t.h,15);
ctx.fill();
ctx.restore();
ctx.save();
ctx.fillStyle = free ? "#B87333" : "#7A6A5D";
roundRect(ctx,t.sx + t.w * .075,t.sy + t.h * .075,t.w,t.h,15);
ctx.fill();
ctx.fillStyle = free ? "rgba(90,50,31,.13)" : "rgba(63,36,24,.30)";
roundRect(ctx,t.sx + t.w * .075,t.sy + t.h * .075,t.w,t.h,15);
ctx.fill();
ctx.restore();
const bodyGrad = ctx.createLinearGradient(0,t.sy,0,t.sy + t.h);
if(free){
bodyGrad.addColorStop(0,"#FFF8DF");
bodyGrad.addColorStop(.58,"#FFF1C9");
bodyGrad.addColorStop(1,"#D9B06F");
}else{
bodyGrad.addColorStop(0,"#CBBCA5");
bodyGrad.addColorStop(.60,"#A99579");
bodyGrad.addColorStop(1,"#84715E");
}
ctx.fillStyle = bodyGrad;
roundRect(ctx,t.sx,t.sy,t.w,t.h,15);
ctx.fill();
ctx.save();
ctx.globalAlpha = free ? .45 : .22;
ctx.fillStyle = "#FFF8DF";
roundRect(ctx,t.sx + t.w * .10,t.sy + t.h * .08,t.w * .80,t.h * .13,8);
ctx.fill();
ctx.restore();
ctx.strokeStyle = selected ? "#F4C542" : (free ? "#5A321F" : "#6B5A4A");
ctx.lineWidth = selected ? 4 : 3;
roundRect(ctx,t.sx + 1.5,t.sy + 1.5,t.w - 3,t.h - 3,14);
ctx.stroke();
ctx.strokeStyle = free ? "#D9953F" : "#8F806F";
ctx.lineWidth = 1.5;
roundRect(ctx,t.sx + 6,t.sy + 7,t.w - 12,t.h - 14,9);
ctx.stroke();
ctx.save();
ctx.globalAlpha = free ? .42 : .22;
ctx.strokeStyle = "#5A321F";
ctx.lineWidth = 1;
ctx.setLineDash([3,5]);
roundRect(ctx,t.sx + 10,t.sy + 11,t.w - 20,t.h - 22,7);
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
ctx.save();
ctx.globalAlpha = free ? 1 : .60;
drawSymbol(t.symbol,t.sx + t.w / 2,t.sy + t.h / 2,Math.min(t.w,t.h) * .58);
ctx.restore();
ctx.restore();
}
function drawParticles(){
state.particles.forEach(function(p){
const a = clamp(p.life / p.max,0,1);
ctx.save();
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x,p.y,p.size * (.4 + a * .8),0,Math.PI * 2);
ctx.fill();
ctx.restore();
});
}
function drawFloats(){
state.floats.forEach(function(f){
const a = clamp(f.life / f.max,0,1);
ctx.save();
ctx.globalAlpha = a;
ctx.fillStyle = f.color;
ctx.font = "900 24px Arial, Helvetica, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.shadowColor = "rgba(10,12,30,.34)";
ctx.shadowBlur = 8;
ctx.fillText(f.text,f.x,f.y);
ctx.restore();
});
}
function drawLevelIntro(){
const a = Math.min(1,state.levelIntro);
ctx.save();
ctx.globalAlpha = clamp(a,0,1) * .92;
ctx.fillStyle = "rgba(142,201,230,.24)";
ctx.fillRect(0,0,W,H);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#fff4dc";
ctx.font = "900 42px Arial, Helvetica, sans-serif";
ctx.shadowColor = "rgba(72,106,134,.24)";
ctx.shadowBlur = 12;
ctx.fillText("LEVEL " + state.level,W / 2,H / 2 - 24);
ctx.font = "800 25px Arial, Helvetica, sans-serif";
ctx.fillStyle = "rgba(72,106,134,.86)";
ctx.shadowBlur = 0;
ctx.fillText("MATCH OPEN TILES ONLY",W / 2,H / 2 + 24);
ctx.restore();
}
function draw(){
computeTileRects();
drawBackground();
drawBoardPanel();
const drawList = state.tiles.slice().sort(function(a,b){
if(a.z !== b.z) return a.z - b.z;
if(a.y !== b.y) return a.y - b.y;
return a.x - b.x;
});
drawList.forEach(drawTile);
drawParticles();
drawFloats();
if(((__hbtAndValue) => __hbtAndValue ? (state.mode === "playing") : __hbtAndValue)(state.levelIntro > 0)){
drawLevelIntro();
}
}
let hbtVisible = true;
if (typeof IntersectionObserver !== "undefined") {
const hbtVisibilityObserver = new IntersectionObserver(function(entries){
hbtVisible = entries[0].isIntersecting;
},{threshold:0.01});
hbtVisibilityObserver.observe(shell);
}
function loop(now){
const dt = Math.min(.05,(now - lastTime) / 1000);
lastTime = now;
if(document.hidden || !hbtVisible){
requestAnimationFrame(loop);
return;
}
update(dt);
draw();
requestAnimationFrame(loop);
}
soundBtn.addEventListener("click",function(){
state.muted = !state.muted;
storageSet(KEY_MUTE,state.muted ? "1" : "0");
sound("button");
updateHud();
});
pauseBtn.addEventListener("click",function(){
sound("button");
if(state.mode === "playing") showPausePopup();
else if(state.mode === "paused"){
hideOverlay();
state.mode = "playing";
}
});
fullBtn.addEventListener("click",function(){
sound("button");
if(document.fullscreenElement === shell){
document.exitFullscreen().catch(function(){});
unlockOrientation();
}else{
shell.requestFullscreen().then(function(){
tryOrientationLock();
updateFullscreenClass();
}).catch(function(){});
}
});
hintBtn.addEventListener("click",function(){
sound("button");
useHint();
});
shuffleBtn.addEventListener("click",function(){
sound("button");
if(state.mode === "playing") shuffleTiles(false);
});
canvas.addEventListener("pointerdown",onCanvasDown);
document.addEventListener("fullscreenchange",updateFullscreenClass);
window.addEventListener("resize",updateFullscreenClass);
window.addEventListener("orientationchange",function(){
setTimeout(updateFullscreenClass,250);
});
window.addEventListener("beforeunload",saveProgress);
let hbtKeyboardActive = false;
shell.addEventListener("pointerdown",function(){hbtKeyboardActive = true;});
shell.addEventListener("focusin",function(){hbtKeyboardActive = true;});
document.addEventListener("pointerdown",function(ev){
if(!shell.contains(ev.target)) hbtKeyboardActive = false;
});
document.addEventListener("keydown",function(ev){
if(!hbtKeyboardActive){
if(document.fullscreenElement !== shell) return;
}
const target = ev.target;
if(target){
if(target.closest){
if(target.closest('input,textarea,select,[contenteditable="true"]')) return;
}
}
if(ev.key === "Escape"){
if(state.mode === "playing") showPausePopup();
}
if(ev.key === "p" || ev.key === "P"){
if(state.mode === "playing") showPausePopup();
}
if(ev.key === "m" || ev.key === "M"){
state.muted = !state.muted;
storageSet(KEY_MUTE,state.muted ? "1" : "0");
updateHud();
}
});
resizeCanvas();
updateHud();
showStartPopup();
requestAnimationFrame(loop);
}
if(document.readyState === "loading"){
document.addEventListener("DOMContentLoaded",initializeHoneyboundTiles,{once:true});
}else{
initializeHoneyboundTiles();
}
})();
Match Open Tiles and Clear the Golden Shelf
The gameplay is easy to understand but rewarding to master. Tap one open tile, then tap another matching open tile. If both are free, the pair disappears in a gentle honey glow, giving you more space and revealing new possible matches.
As you progress, the layouts become larger and more complex. The game includes 50 levels, a timer, score tracking, best score saving, move counter, hint button, shuffle button, pause mode, sound toggle, fullscreen support, and save-and-continue progress.
Why You Will Love Honeybound Tiles
Honeybound Tiles is perfect for players who enjoy calm puzzle games with a thoughtful pace. It is relaxing enough for a short break, but still challenging enough to make every move feel important.
Step into the golden reading room, follow the honeylight across the stacked tiles, and see how many shelves you can clear before the library lamp fades.