const canvas = document.getElementById("gameCanvas"); const ctx = canvas.getContext("2d"); let isAnimating = false; const GRID_SIZE = 6; const TILE = 64; const GEM_TYPES = 5; const SPEED = 8; let dragStart = null; let dragActive = false; const DRAG_THRESHOLD = 20; const MATERIALS = [ { name: "Sand", value: 5 }, { name: "Rock", value: 10 }, { name: "Iron", value: 20 }, { name: "Gold", value: 40 }, { name: "Diamond", value: 100 } ]; const dpr = window.devicePixelRatio || 1; const cssSize = GRID_SIZE * TILE; canvas.style.width = cssSize + "px"; canvas.style.height = cssSize + "px"; canvas.width = cssSize * dpr; canvas.height = cssSize * dpr; ctx.scale(dpr, dpr); let board = []; let selected = null; let score = 0; let moves = 20; const targetScore = 5000; const scoreEl = document.getElementById("score"); const movesEl = document.getElementById("moves"); const targetEl = document.getElementById("target"); targetEl.textContent = targetScore; let level = 1; const levelEl = document.getElementById("level"); const progressBar = document.getElementById("progressBar"); levelEl.textContent = level; function updateProgress() { const percent = Math.min(score / targetScore, 1) * 100; progressBar.style.width = percent + "%"; } const particles = []; class Particle { constructor(x, y, color) { this.x = x; this.y = y; this.vx = (Math.random() - 0.5) * 6; this.vy = (Math.random() - 0.5) * 6; this.life = 1; this.color = color; } update() { this.x += this.vx; this.y += this.vy; this.vy += 0.2; this.life -= 0.03; } draw() { if (this.life <= 0) return; ctx.globalAlpha = this.life; ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x, this.y, 3, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; } } class Gem { constructor(type, row, col) { this.type = type; this.row = row; this.col = col; this.x = col * TILE; this.y = row * TILE; this.targetX = this.x; this.targetY = this.y; this.scale = 1; this.alpha = 1; this.popping = false; } update() { this.x += (this.targetX - this.x) / SPEED; this.y += (this.targetY - this.y) / SPEED; if (this.popping) { this.scale += 0.1; this.alpha -= 0.15; } } draw() { if (this.alpha <= 0) return; ctx.save(); ctx.globalAlpha = this.alpha; ctx.translate(this.x + TILE / 2, this.y + TILE / 2); ctx.scale(this.scale, this.scale); ctx.beginPath(); ctx.moveTo(-18, -12); ctx.lineTo(12, -18); ctx.lineTo(20, 0); ctx.lineTo(10, 18); ctx.lineTo(-14, 14); ctx.lineTo(-20, 0); ctx.closePath(); ctx.fillStyle = gemColor(this.type); ctx.fill(); ctx.strokeStyle = "rgba(255,255,255,0.25)"; ctx.stroke(); ctx.restore(); } } function resizeCanvasToContainer() { const frame = document.getElementById("game-frame"); const rect = frame.getBoundingClientRect(); // Available width inside frame (minus padding) const size = rect.width - 20; const dpr = window.devicePixelRatio || 1; canvas.width = size * dpr; canvas.height = size * dpr; canvas.style.width = size + "px"; canvas.style.height = size + "px"; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } function pointerDown(e) { if (isAnimating || moves <= 0) return; const pos = getPointerPos(e); dragStart = pos; selected = getCellFromPos(pos); dragActive = true; } function getPointerPos(e) { const rect = canvas.getBoundingClientRect(); const point = e.touches ? e.touches[0] : e; return { x: point.clientX - rect.left, y: point.clientY - rect.top }; } function getCellFromPos(pos) { return { row: Math.floor(pos.y / TILE), col: Math.floor(pos.x / TILE) }; } function pointerMove(e) { if (!dragActive || !selected || isAnimating) return; const pos = getPointerPos(e); const dx = pos.x - dragStart.x; const dy = pos.y - dragStart.y; if (Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) return; let target = null; if (Math.abs(dx) > Math.abs(dy)) { target = { row: selected.row, col: selected.col + (dx > 0 ? 1 : -1) }; } else { target = { row: selected.row + (dy > 0 ? 1 : -1), col: selected.col }; } if ( target.row < 0 || target.row >= GRID_SIZE || target.col < 0 || target.col >= GRID_SIZE ) { dragActive = false; selected = null; return; } dragActive = false; animatedSwap(selected, target, () => { if (!removeMatches()) { animatedSwap(target, selected, () => { }); } else { moves--; movesEl.textContent = moves; } }); selected = null; } function pointerUp() { dragActive = false; selected = null; } function gemColor(type) { return [ "#fff5cbff", // Sand "#6f6157ff", // Rock "#6f747dff", // Iron "#876c00ff", // Gold "#acd1ffff" // Diamond ][type]; } function randomGem(row, col) { return new Gem(Math.floor(Math.random() * GEM_TYPES), row, col); } function initBoard() { board = []; for (let r = 0; r < GRID_SIZE; r++) { board[r] = []; for (let c = 0; c < GRID_SIZE; c++) { board[r][c] = randomGem(r, c); } } removeMatches(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let row of board) { for (let gem of row) { if (gem) gem.draw(); } } if (selected) { ctx.strokeStyle = "white"; ctx.lineWidth = 3; ctx.strokeRect( selected.col * TILE + 2, selected.row * TILE + 2, TILE - 4, TILE - 4 ); } } function update() { // Update gems (movement + pop animation) for (let row of board) { for (let gem of row) { if (gem) gem.update(); } } // Update particles for (let i = particles.length - 1; i >= 0; i--) { particles[i].update(); if (particles[i].life <= 0) { particles.splice(i, 1); } } } function animatedSwap(a, b, onComplete) { const g1 = board[a.row][a.col]; const g2 = board[b.row][b.col]; isAnimating = true; g1.targetX = b.col * TILE; g1.targetY = b.row * TILE; g2.targetX = a.col * TILE; g2.targetY = a.row * TILE; const check = () => { if (!gemsAreMoving()) { board[a.row][a.col] = g2; board[b.row][b.col] = g1; g1.row = b.row; g1.col = b.col; g2.row = a.row; g2.col = a.col; isAnimating = false; onComplete(); } else { requestAnimationFrame(check); } }; check(); } function areAdjacent(a, b) { return Math.abs(a.row - b.row) + Math.abs(a.col - b.col) === 1; } function findMatches() { const matches = new Set(); for (let r = 0; r < GRID_SIZE; r++) { let count = 1; for (let c = 1; c <= GRID_SIZE; c++) { if ( c < GRID_SIZE && board[r][c] && board[r][c].type === board[r][c - 1].type ) count++; else { if (count >= 3) for (let i = 0; i < count; i++) matches.add(`${r},${c - 1 - i}`); count = 1; } } } for (let c = 0; c < GRID_SIZE; c++) { let count = 1; for (let r = 1; r <= GRID_SIZE; r++) { if ( r < GRID_SIZE && board[r][c] && board[r][c].type === board[r - 1][c].type ) count++; else { if (count >= 3) for (let i = 0; i < count; i++) matches.add(`${r - 1 - i},${c}`); count = 1; } } } return matches; } function removeMatches() { const matches = findMatches(); if (matches.size === 0) return false; isAnimating = true; // start pop animation matches.forEach(key => { const [r, c] = key.split(",").map(Number); const gem = board[r][c]; if (gem) { gem.popping = true; score += MATERIALS[gem.type].value; for (let i = 0; i < 8; i++) { particles.push( new Particle( gem.x + TILE / 2, gem.y + TILE / 2, gemColor(gem.type) ) ); } } }); scoreEl.textContent = score; // wait for pop animation to finish const waitForPop = () => { let stillPopping = false; matches.forEach(key => { const [r, c] = key.split(",").map(Number); const gem = board[r][c]; if (gem && gem.alpha > 0) stillPopping = true; }); if (!stillPopping) { matches.forEach(key => { const [r, c] = key.split(",").map(Number); board[r][c] = null; }); isAnimating = false; collapse(); } else { requestAnimationFrame(waitForPop); } }; waitForPop(); return true; } function collapse() { for (let c = 0; c < GRID_SIZE; c++) { for (let r = GRID_SIZE - 1; r >= 0; r--) { if (!board[r][c]) { for (let k = r - 1; k >= 0; k--) { if (board[k][c]) { board[r][c] = board[k][c]; board[k][c] = null; board[r][c].row = r; board[r][c].targetY = r * TILE; break; } } } } } refill(); } function refill() { for (let r = 0; r < GRID_SIZE; r++) { for (let c = 0; c < GRID_SIZE; c++) { if (!board[r][c]) { const gem = randomGem(r, c); gem.y = -TILE; gem.targetY = r * TILE; board[r][c] = gem; } } } const wait = () => { if (!gemsAreMoving()) { removeMatches(); } else { requestAnimationFrame(wait); } }; wait(); } function gemsAreMoving() { for (let row of board) { for (let gem of row) { if (!gem) continue; if ( Math.abs(gem.x - gem.targetX) > 0.5 || Math.abs(gem.y - gem.targetY) > 0.5 ) { return true; } } } return false; } function getCell(e) { const rect = canvas.getBoundingClientRect(); const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left; const y = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top; return { row: Math.floor(y / TILE), col: Math.floor(x / TILE) }; } function input(e) { if (moves <= 0 || isAnimating) return; const cell = getCell(e); if (!selected) { selected = cell; return; } if (areAdjacent(selected, cell)) { animatedSwap(selected, cell, () => { if (!removeMatches()) { animatedSwap(cell, selected, () => { }); } else { moves--; movesEl.textContent = moves; } }); } selected = null; } canvas.addEventListener("mousedown", pointerDown); canvas.addEventListener("mousemove", pointerMove); canvas.addEventListener("mouseup", pointerUp); canvas.addEventListener("touchstart", pointerDown, { passive: true }); canvas.addEventListener("touchmove", pointerMove, { passive: true }); canvas.addEventListener("touchend", pointerUp); function loop() { update(); draw(); requestAnimationFrame(loop); } initBoard(); loop();