(() => {
const size = 8;
const boardElement = document.getElementById('board');
const piecesElement = document.getElementById('pieces');
const scoreElement = document.getElementById('score');
const bestScoreElement = document.getElementById('best-score');
const newGameBtn = document.getElementById('new-game');
const startBtn = document.getElementById('start-game');
const startOverlay = document.getElementById('start-overlay');
const floatingScore = document.getElementById('floating-score');
const bestKey = 'blossomBlocksBest';
const SHAPES = [
[[0, 0]],
[[0, 0], [1, 0]],
[[0, 0], [0, 1]],
[[0, 0], [1, 0], [2, 0]],
[[0, 0], [0, 1], [0, 2]],
[[0, 0], [1, 0], [0, 1], [1, 1]],
[[0, 0], [1, 0], [0, 1]],
[[0, 0], [0, 1], [1, 1]],
[[0, 0], [1, 0], [2, 0], [3, 0]],
[[0, 0], [0, 1], [0, 2], [0, 3]],
[[0, 0], [1, 0], [2, 0], [2, 1]],
[[0, 0], [0, 1], [1, 1], [2, 1]],
[[0, 0], [1, 0], [1, 1], [1, 2]],
[[0, 0], [0, 1], [0, 2], [1, 2]],
[[0, 0], [1, 0], [2, 0], [3, 0], [4, 0]],
[[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]],
[[0, 0], [1, 0], [0, 1], [0, 2]],
[[0, 0], [0, 1], [1, 1], [2, 1]],
[[0, 0], [1, 0], [1, 1], [2, 1], [2, 2]],
];
const EMOJIS = ['🌸', '🌷', '🌺', '💐', '💮', '💗'];
let board = [];
let score = 0;
let bestScore = Number(localStorage.getItem(bestKey)) || 0;
let availablePieces = [];
let selectedPieceIndex = null;
let isActive = false;
const cells = [];
let draggingPieceIndex = null;
let dragGhost = null;
let dragTarget = null;
let dragValid = false;
let dragMoved = false;
let suppressClick = false;
const highlightedCells = [];
let dragAnchor = { row: 0, col: 0 };
let ghostSnapped = false;
let boardMetrics = null;
const audio = createAudio();
function parsePixels(value) {
if (!value) return 0;
const number = parseFloat(value);
return Number.isNaN(number) ? 0 : number;
}
function measureBoard() {
const firstCell = cells[0]?.[0];
if (!firstCell) return null;
const boardRect = boardElement.getBoundingClientRect();
const cellRect = firstCell.getBoundingClientRect();
const styles = window.getComputedStyle(boardElement);
const gapX = parsePixels(styles.columnGap || styles.gap || '0');
const gapY = parsePixels(styles.rowGap || styles.gap || '0');
boardMetrics = {
boardRect,
cellWidth: cellRect.width,
cellHeight: cellRect.height,
offsetX: cellRect.left - boardRect.left,
offsetY: cellRect.top - boardRect.top,
gapX,
gapY,
};
return boardMetrics;
}
function pointerToCell(clientX, clientY) {
if (!boardMetrics) measureBoard();
if (!boardMetrics) return null;
const {
boardRect, offsetX, offsetY, cellWidth, cellHeight, gapX, gapY,
} = boardMetrics;
const stepX = cellWidth + gapX;
const stepY = cellHeight + gapY;
const localX = clientX - boardRect.left - offsetX;
const localY = clientY - boardRect.top - offsetY;
if (localX < -gapX / 2 || localY < -gapY / 2) return null;
const col = Math.floor((localX + gapX / 2) / stepX);
const row = Math.floor((localY + gapY / 2) / stepY);
if (row < 0 || row >= size || col < 0 || col >= size) return null;
return { row, col };
}
function initBoard() {
boardElement.innerHTML = '';
cells.length = 0;
boardElement.style.gridTemplateColumns = `repeat(${size}, var(--cell-size))`;
boardElement.style.gridTemplateRows = `repeat(${size}, var(--cell-size))`;
for (let r = 0; r < size; r += 1) {
const row = [];
for (let c = 0; c < size; c += 1) {
const cell = document.createElement('button');
cell.type = 'button';
cell.className = 'cell';
cell.dataset.row = r;
cell.dataset.col = c;
cell.addEventListener('click', () => handleBoardClick(r, c));
boardElement.appendChild(cell);
row.push(cell);
}
cells.push(row);
}
}
function createEmptyBoard() {
return Array.from({ length: size }, () => Array(size).fill(null));
}
function randomItem(list) {
return list[Math.floor(Math.random() * list.length)];
}
const createId = () => {
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
return window.crypto.randomUUID();
}
return `piece-${Date.now()}-${Math.random().toString(16).slice(2)}`;
};
function createPiece() {
const shape = randomItem(SHAPES);
const emoji = randomItem(EMOJIS);
return {
id: createId(),
cells: shape.map(([r, c]) => ({ r, c })),
emoji,
};
}
function refillPieces() {
availablePieces = [createPiece(), createPiece(), createPiece()];
selectedPieceIndex = null;
renderPieces();
}
function renderPieces() {
piecesElement.innerHTML = '';
availablePieces.forEach((piece, index) => {
if (!piece) return;
const maxRow = Math.max(...piece.cells.map((cell) => cell.r));
const maxCol = Math.max(...piece.cells.map((cell) => cell.c));
const pieceEl = document.createElement('div');
pieceEl.className = 'piece';
pieceEl.style.gridTemplateRows = `repeat(${maxRow + 1}, 28px)`;
pieceEl.style.gridTemplateColumns = `repeat(${maxCol + 1}, 28px)`;
pieceEl.addEventListener('click', () => {
if (suppressClick) return;
selectPiece(index);
});
pieceEl.addEventListener('pointerdown', (event) => startDrag(event, index));
if (selectedPieceIndex === index) {
pieceEl.classList.add('selected');
}
const grid = Array.from({ length: maxRow + 1 }, () => Array(maxCol + 1).fill(false));
piece.cells.forEach(({ r, c }) => {
grid[r][c] = true;
});
grid.forEach((row, rowIndex) => {
row.forEach((isFilled, colIndex) => {
if (isFilled) {
const mini = document.createElement('div');
mini.className = 'piece-cell';
mini.textContent = piece.emoji;
mini.style.gridRow = rowIndex + 1;
mini.style.gridColumn = colIndex + 1;
mini.dataset.row = rowIndex;
mini.dataset.col = colIndex;
pieceEl.appendChild(mini);
}
});
});
piecesElement.appendChild(pieceEl);
});
}
function clearHighlights() {
while (highlightedCells.length) {
const cell = highlightedCells.pop();
cell.classList.remove('cell--highlight');
}
}
function highlightPlacement(piece, baseRow, baseCol) {
clearHighlights();
piece.cells.forEach(({ r, c }) => {
const row = baseRow + r;
const col = baseCol + c;
if (row >= 0 && row < size && col >= 0 && col < size) {
const cell = cells[row][col];
if (cell) {
cell.classList.add('cell--highlight');
highlightedCells.push(cell);
}
}
});
}
function startDrag(event, index) {
if (!isActive) return;
if (event.button !== undefined && event.button !== 0) return;
const piece = availablePieces[index];
if (!piece) return;
measureBoard();
const targetCell = event.target.closest('.piece-cell');
if (targetCell) {
const cellRow = Number(targetCell.dataset.row);
const cellCol = Number(targetCell.dataset.col);
dragAnchor = {
row: Number.isNaN(cellRow) ? 0 : cellRow,
col: Number.isNaN(cellCol) ? 0 : cellCol,
};
} else {
dragAnchor = { row: 0, col: 0 };
}
draggingPieceIndex = index;
dragTarget = null;
dragValid = false;
dragMoved = false;
ghostSnapped = false;
selectedPieceIndex = index;
renderPieces();
createDragGhost(piece);
followPointer(event);
document.addEventListener('pointermove', handleDragMove);
document.addEventListener('pointerup', handleDragEnd);
event.preventDefault();
}
function createDragGhost(piece) {
if (dragGhost) {
dragGhost.remove();
}
const ghost = document.createElement('div');
ghost.className = 'drag-ghost';
const metrics = boardMetrics || measureBoard();
const cellWidth = metrics ? metrics.cellWidth : 32;
const cellHeight = metrics ? metrics.cellHeight : 32;
if (metrics) {
ghost.style.columnGap = `${metrics.gapX}px`;
ghost.style.rowGap = `${metrics.gapY}px`;
} else {
ghost.style.gap = '6px';
}
const maxRow = Math.max(...piece.cells.map((cell) => cell.r));
const maxCol = Math.max(...piece.cells.map((cell) => cell.c));
ghost.style.gridTemplateRows = `repeat(${maxRow + 1}, ${cellHeight}px)`;
ghost.style.gridTemplateColumns = `repeat(${maxCol + 1}, ${cellWidth}px)`;
const grid = Array.from({ length: maxRow + 1 }, () => Array(maxCol + 1).fill(false));
piece.cells.forEach(({ r, c }) => {
grid[r][c] = true;
});
grid.forEach((row, rowIndex) => {
row.forEach((isFilled, colIndex) => {
if (isFilled) {
const mini = document.createElement('div');
mini.className = 'piece-cell';
mini.textContent = piece.emoji;
mini.style.gridRow = rowIndex + 1;
mini.style.gridColumn = colIndex + 1;
mini.style.fontSize = `${Math.max(18, cellWidth * 0.6)}px`;
ghost.appendChild(mini);
}
});
});
document.body.appendChild(ghost);
dragGhost = ghost;
}
function followPointer(event) {
if (!dragGhost) return;
ghostSnapped = false;
dragGhost.classList.remove('drag-ghost--snapped');
dragGhost.style.left = `${event.clientX}px`;
dragGhost.style.top = `${event.clientY}px`;
}
function snapGhostToCell(baseRow, baseCol) {
if (!dragGhost) return;
if (!boardMetrics) measureBoard();
if (!boardMetrics) return;
const cell = cells[baseRow]?.[baseCol];
if (!cell) return;
const rect = cell.getBoundingClientRect();
ghostSnapped = true;
dragGhost.classList.add('drag-ghost--snapped');
dragGhost.style.left = `${rect.left}px`;
dragGhost.style.top = `${rect.top}px`;
}
function handleDragMove(event) {
if (!dragGhost || draggingPieceIndex === null) return;
dragMoved = true;
measureBoard();
const piece = availablePieces[draggingPieceIndex];
if (!piece) return;
const cell = pointerToCell(event.clientX, event.clientY);
if (cell) {
const baseRow = cell.row - dragAnchor.row;
const baseCol = cell.col - dragAnchor.col;
if (canPlacePiece(piece, baseRow, baseCol)) {
dragValid = true;
dragTarget = { row: baseRow, col: baseCol };
highlightPlacement(piece, baseRow, baseCol);
snapGhostToCell(baseRow, baseCol);
return;
}
}
dragValid = false;
dragTarget = null;
clearHighlights();
followPointer(event);
}
function handleDragEnd() {
document.removeEventListener('pointermove', handleDragMove);
document.removeEventListener('pointerup', handleDragEnd);
const moved = dragMoved;
if (dragGhost) {
dragGhost.remove();
dragGhost = null;
}
if (dragValid && dragTarget && draggingPieceIndex !== null) {
selectedPieceIndex = draggingPieceIndex;
handleBoardClick(dragTarget.row, dragTarget.col);
}
clearHighlights();
draggingPieceIndex = null;
dragTarget = null;
dragValid = false;
dragMoved = false;
ghostSnapped = false;
if (moved) {
suppressClick = true;
requestAnimationFrame(() => {
suppressClick = false;
});
}
dragAnchor = { row: 0, col: 0 };
}
function selectPiece(index) {
if (!isActive) return;
if (selectedPieceIndex === index) {
selectedPieceIndex = null;
} else {
selectedPieceIndex = index;
}
renderPieces();
}
function handleBoardClick(row, col) {
if (!isActive) return;
clearHighlights();
if (selectedPieceIndex === null) return;
const piece = availablePieces[selectedPieceIndex];
if (!piece) return;
if (!canPlacePiece(piece, row, col)) return;
placePiece(piece, row, col);
availablePieces[selectedPieceIndex] = null;
selectedPieceIndex = null;
if (availablePieces.every((p) => !p)) {
refillPieces();
} else {
renderPieces();
}
if (!hasAvailableMoves()) {
endGame(false);
}
}
function canPlacePiece(piece, baseRow, baseCol) {
return piece.cells.every(({ r, c }) => {
const targetRow = baseRow + r;
const targetCol = baseCol + c;
return (
targetRow >= 0 &&
targetRow < size &&
targetCol >= 0 &&
targetCol < size &&
!board[targetRow][targetCol]
);
});
}
function placePiece(piece, baseRow, baseCol) {
let placedCells = 0;
piece.cells.forEach(({ r, c }) => {
const targetRow = baseRow + r;
const targetCol = baseCol + c;
board[targetRow][targetCol] = { emoji: piece.emoji };
placedCells += 1;
});
playSound('place');
updateBoardUI();
const clearedLines = clearLines();
const gained = placedCells * 5 + clearedLines * 15;
updateScore(score + gained, clearedLines);
}
function updateBoardUI() {
for (let r = 0; r < size; r += 1) {
for (let c = 0; c < size; c += 1) {
const cellData = board[r][c];
const cellEl = cells[r][c];
cellEl.classList.toggle('cell--filled', Boolean(cellData));
if (cellData) {
cellEl.dataset.emoji = cellData.emoji;
} else {
cellEl.removeAttribute('data-emoji');
}
}
}
}
function clearLines() {
const rowsToClear = [];
const colsToClear = [];
for (let r = 0; r < size; r += 1) {
if (board[r].every(Boolean)) {
rowsToClear.push(r);
}
}
for (let c = 0; c < size; c += 1) {
let full = true;
for (let r = 0; r < size; r += 1) {
if (!board[r][c]) {
full = false;
break;
}
}
if (full) colsToClear.push(c);
}
rowsToClear.forEach((row) => {
for (let c = 0; c < size; c += 1) {
addClearingEffect(row, c);
board[row][c] = null;
}
});
colsToClear.forEach((col) => {
for (let r = 0; r < size; r += 1) {
addClearingEffect(r, col);
board[r][col] = null;
}
});
if (rowsToClear.length || colsToClear.length) {
setTimeout(updateBoardUI, 200);
showFloatingBonus(rowsToClear.length + colsToClear.length);
playSound('clear');
}
return rowsToClear.length + colsToClear.length;
}
function addClearingEffect(row, col) {
const cell = cells[row][col];
cell.classList.add('cell--clearing');
setTimeout(() => cell.classList.remove('cell--clearing'), 400);
}
function showFloatingBonus(lines) {
const bonus = `+${lines * 15} ✨`;
floatingScore.textContent = bonus;
floatingScore.style.opacity = '1';
const rect = boardElement.getBoundingClientRect();
floatingScore.style.left = `${rect.left + rect.width / 2 + window.scrollX}px`;
floatingScore.style.top = `${rect.top + window.scrollY + 30}px`;
floatingScore.animate(
[
{ transform: 'translate(-50%, 0)', opacity: 1 },
{ transform: 'translate(-50%, -30px)', opacity: 0 }
],
{ duration: 900 }
);
playSound('bonus');
setTimeout(() => {
floatingScore.style.opacity = '0';
}, 900);
}
function updateScore(newScore) {
score = newScore;
scoreElement.textContent = score;
if (score > bestScore) {
bestScore = score;
bestScoreElement.textContent = bestScore;
localStorage.setItem(bestKey, String(bestScore));
}
}
function hasAvailableMoves() {
return availablePieces.some((piece) => {
if (!piece) return false;
for (let r = 0; r < size; r += 1) {
for (let c = 0; c < size; c += 1) {
if (canPlacePiece(piece, r, c)) {
return true;
}
}
}
return false;
});
}
function startGame() {
audio.resume();
isActive = true;
score = 0;
board = createEmptyBoard();
updateScore(0);
updateBoardUI();
refillPieces();
startOverlay.classList.add('hidden');
}
function endGame(fromNewGame) {
isActive = false;
const message = fromNewGame ? 'Ready for a fresh bouquet?' : `Garden full! Final score: ${score}`;
startOverlay.querySelector('p').textContent = message;
startOverlay.querySelector('h2').textContent = 'Blossom Blocks';
startOverlay.classList.remove('hidden');
if (!fromNewGame) {
playSound('over');
}
}
newGameBtn.addEventListener('click', () => {
endGame(true);
});
startBtn.addEventListener('click', startGame);
bestScoreElement.textContent = bestScore;
initBoard();
board = createEmptyBoard();
updateBoardUI();
function playSound(type) {
if (audio) audio.play(type);
}
function createAudio() {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (!AudioCtx) return null;
const ctx = new AudioCtx();
const master = ctx.createGain();
master.gain.value = 0.14;
master.connect(ctx.destination);
ctx.suspend();
const tones = {
place: 820,
clear: 560,
bonus: 960,
over: 280,
};
function chirp(freq, duration = 0.2) {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'triangle';
osc.frequency.value = freq;
osc.connect(gain);
gain.connect(master);
const start = ctx.currentTime;
gain.gain.setValueAtTime(0.001, start);
gain.gain.linearRampToValueAtTime(0.22, start + 0.03);
gain.gain.exponentialRampToValueAtTime(0.001, start + duration);
osc.start(start);
osc.stop(start + duration + 0.02);
}
return {
resume() {
if (ctx.state === 'suspended') ctx.resume();
},
play(event) {
const freq = tones[event];
if (freq) chirp(freq);
},
};
}
})();