Download block-blast.html from Azivandor/Games: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/Azivandor/Games/resolve/main/block-blast.html
- Command line
-
hf download hf://spaces/Azivandor/Games/block-blast.html
-
curl -L -o block-blast.html https://huggingface.co/spaces/Azivandor/Games/resolve/main/block-blast.html
15.6 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Block Blast</title> | |
| <style> | |
| * { -webkit-tap-highlight-color: transparent; user-select: none; touch-action: none; } | |
| html, body { | |
| margin: 0; padding: 0; min-height: 100%; | |
| background: radial-gradient(circle at 50% 10%, #eaf6ff, #bfe0fb 70%); | |
| display: flex; align-items: center; justify-content: center; | |
| font-family: 'Segoe UI', Arial, sans-serif; | |
| } | |
| #wrap { text-align: center; padding: 12px 0; } | |
| canvas { | |
| background: #eaf6ff; | |
| border: 3px solid #2a8fd8; | |
| box-shadow: 0 0 24px #2a8fd855, inset 0 0 20px #ffffffaa; | |
| border-radius: 10px; | |
| } | |
| #hud { | |
| display: flex; justify-content: space-between; align-items: center; | |
| color: #16406b; font-size: 16px; letter-spacing: 1px; | |
| width: 360px; margin: 0 auto 8px auto; | |
| text-shadow: none; | |
| } | |
| #hud .score { | |
| font-family: 'Courier New', monospace; | |
| font-size: 26px; font-weight: bold; color: #ff8a3d; text-shadow: 0 0 6px #ffcaa3; | |
| } | |
| #best { font-size: 13px; color: #3d76ad; } | |
| #msg { | |
| position: absolute; color: #16406b; font-size: 28px; font-weight: bold; | |
| text-shadow: none; | |
| background: rgba(255,255,255,0.85); | |
| padding: 16px 24px; border-radius: 12px; | |
| box-shadow: 0 0 20px #2a8fd855; | |
| display: none; text-align: center; pointer-events: none; | |
| } | |
| #msg span { display:block; font-size: 15px; font-weight: normal; margin-top: 10px; color: #3d76ad; } | |
| </style> | |
| </head> | |
| <body> | |
| <a href="index.html" style="position:fixed; top:10px; left:10px; z-index:999; display:inline-flex; align-items:center; gap:5px; padding:7px 12px; border-radius:20px; background:rgba(10,10,20,0.75); color:#fff; font-family:'Segoe UI', Arial, sans-serif; font-size:13px; font-weight:bold; text-decoration:none; border:1px solid rgba(255,255,255,0.25); backdrop-filter: blur(3px);">← Home</a> | |
| <div id="wrap"> | |
| <div id="hud"> | |
| <div> | |
| <div class="score" id="score">0</div> | |
| <div id="best">best: <span id="bestVal">0</span></div> | |
| </div> | |
| <div id="comboLabel" style="font-size:13px; color:#1e9e4f; min-height:16px; font-weight:bold;"></div> | |
| </div> | |
| <canvas id="game" width="360" height="560"></canvas> | |
| </div> | |
| <div id="msg"></div> | |
| <script> | |
| const canvas = document.getElementById('game'); | |
| const ctx = canvas.getContext('2d'); | |
| const W = canvas.width, H = canvas.height; | |
| const scoreEl = document.getElementById('score'); | |
| const bestEl = document.getElementById('bestVal'); | |
| const comboLabelEl = document.getElementById('comboLabel'); | |
| const msgEl = document.getElementById('msg'); | |
| const GRID = 8; | |
| const CELL = 34; | |
| const boardX = (W - GRID*CELL) / 2; | |
| const boardY = 56; | |
| const COLORS = ['#ff4ecb', '#4ecbff', '#ffe14e', '#63ff8f', '#ff8a3d', '#a273ff', '#ff5a5a']; | |
| 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]], [[0,0],[1,0],[1,1]], [[0,1],[1,1],[1,0]], [[0,0],[0,1],[1,1]], | |
| [[0,0],[1,0],[0,1],[1,1]], | |
| [[0,0],[1,0],[2,0],[3,0]], [[0,0],[0,1],[0,2],[0,3]], | |
| [[0,0],[0,1],[0,2],[1,2]], [[0,0],[1,0],[2,0],[0,1]], [[0,0],[1,0],[1,1],[1,2]], [[2,0],[0,1],[1,1],[2,1]], | |
| [[1,0],[1,1],[1,2],[0,2]], [[0,0],[0,1],[1,1],[2,1]], [[0,0],[1,0],[0,1],[0,2]], [[0,0],[1,0],[2,0],[2,1]], | |
| [[0,0],[1,0],[2,0],[1,1]], [[1,0],[0,1],[1,1],[1,2]], [[1,0],[0,1],[1,1],[2,1]], [[0,0],[0,1],[0,2],[1,1]], | |
| [[1,0],[2,0],[0,1],[1,1]], [[0,0],[1,0],[1,1],[2,1]], | |
| [[1,0],[0,1],[1,1],[2,1],[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],[2,0],[0,1],[1,1],[2,1]], | |
| [[0,0],[0,1],[0,2],[1,2],[2,2]], | |
| ]; | |
| let score = 0, best = 0, displayScore = 0; | |
| let board = Array.from({length: GRID}, () => Array(GRID).fill(null)); | |
| let tray = [null, null, null]; | |
| let dragging = null; // { trayIndex, shape, color, px, py } | |
| let flashCells = []; // legacy, unused now but kept safe | |
| let clearingCells = []; // { r, c, color, timer } — shrink/fade animation for cleared cells | |
| let placingCells = []; // { r, c, timer } — pop-in bounce for freshly placed cells | |
| let particles = []; // { x, y, vx, vy, life, color, size } | |
| let shakeTimer = 0, shakeMag = 0; | |
| let gameOver = false; | |
| function randShape(){ | |
| const shape = SHAPES[Math.floor(Math.random()*SHAPES.length)]; | |
| const color = COLORS[Math.floor(Math.random()*COLORS.length)]; | |
| return { shape, color }; | |
| } | |
| function refillTrayIfEmpty(){ | |
| if (tray.every(t => t === null)){ | |
| tray = [randShape(), randShape(), randShape()]; | |
| if (checkGameOver()) triggerGameOver(); | |
| } | |
| } | |
| refillTrayIfEmpty(); | |
| function setScore(v){ | |
| score = v; | |
| if (score > best) best = score; | |
| } | |
| function updateScoreDisplay(){ | |
| if (displayScore < score){ | |
| const step = Math.max(1, Math.ceil((score - displayScore) / 10)); | |
| displayScore = Math.min(score, displayScore + step); | |
| } | |
| scoreEl.textContent = displayScore; | |
| bestEl.textContent = best; | |
| } | |
| function bbox(shape){ | |
| let maxX = 0, maxY = 0; | |
| for (const [x,y] of shape){ maxX = Math.max(maxX,x); maxY = Math.max(maxY,y); } | |
| return { w: maxX+1, h: maxY+1 }; | |
| } | |
| function canPlace(shape, col, row){ | |
| for (const [dx,dy] of shape){ | |
| const c = col+dx, r = row+dy; | |
| if (c < 0 || c >= GRID || r < 0 || r >= GRID) return false; | |
| if (board[r][c]) return false; | |
| } | |
| return true; | |
| } | |
| function anyPlacement(shape){ | |
| for (let r = 0; r < GRID; r++) | |
| for (let c = 0; c < GRID; c++) | |
| if (canPlace(shape, c, r)) return true; | |
| return false; | |
| } | |
| function checkGameOver(){ | |
| return tray.every(t => t === null || !anyPlacement(t.shape)); | |
| } | |
| function triggerGameOver(){ | |
| gameOver = true; | |
| msgEl.style.display = 'block'; | |
| msgEl.innerHTML = `GAME OVER<span>Score: ${score} — click to try again</span>`; | |
| } | |
| function newGame(){ | |
| score = 0; displayScore = 0; setScore(0); | |
| board = Array.from({length: GRID}, () => Array(GRID).fill(null)); | |
| tray = [null, null, null]; | |
| gameOver = false; | |
| clearingCells = []; placingCells = []; particles = []; shakeTimer = 0; | |
| msgEl.style.display = 'none'; | |
| comboLabelEl.textContent = ''; | |
| refillTrayIfEmpty(); | |
| } | |
| function spawnParticles(x, y, color, count){ | |
| for (let i = 0; i < count; i++){ | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = 1.5 + Math.random() * 3.5; | |
| particles.push({ | |
| x, y, | |
| vx: Math.cos(angle) * speed, | |
| vy: Math.sin(angle) * speed - 1, | |
| life: 1, | |
| color, | |
| size: 3 + Math.random() * 3 | |
| }); | |
| } | |
| } | |
| function placePiece(trayIndex, shape, color, col, row){ | |
| for (const [dx,dy] of shape){ | |
| board[row+dy][col+dx] = color; | |
| placingCells.push({ r: row+dy, c: col+dx, timer: 1 }); | |
| } | |
| setScore(score + shape.length * 2); | |
| tray[trayIndex] = null; | |
| const fullRows = [], fullCols = []; | |
| for (let r = 0; r < GRID; r++) if (board[r].every(v => v)) fullRows.push(r); | |
| for (let c = 0; c < GRID; c++) if (board.every(row => row[c])) fullCols.push(c); | |
| const linesCleared = fullRows.length + fullCols.length; | |
| if (linesCleared > 0){ | |
| const clearedCoords = new Set(); | |
| for (const r of fullRows) for (let c = 0; c < GRID; c++) clearedCoords.add(r + ',' + c); | |
| for (const c of fullCols) for (let r = 0; r < GRID; r++) clearedCoords.add(r + ',' + c); | |
| clearedCoords.forEach(key => { | |
| const [r, c] = key.split(',').map(Number); | |
| const cellColor = board[r][c] || '#ffffff'; | |
| clearingCells.push({ r, c, color: cellColor, timer: 1 }); | |
| const cx = boardX + c*CELL + CELL/2, cy = boardY + r*CELL + CELL/2; | |
| spawnParticles(cx, cy, cellColor, 5); | |
| board[r][c] = null; | |
| }); | |
| shakeTimer = 10 + linesCleared * 5; | |
| shakeMag = 3 + linesCleared * 2.5; | |
| const bonus = linesCleared * 80 + (linesCleared > 1 ? (linesCleared-1) * 60 : 0); | |
| setScore(score + bonus); | |
| comboLabelEl.textContent = linesCleared > 1 ? `${linesCleared}x COMBO! +${bonus}` : `LINE! +${bonus}`; | |
| setTimeout(() => { if (comboLabelEl.textContent.includes(String(bonus))) comboLabelEl.textContent = ''; }, 1200); | |
| } | |
| refillTrayIfEmpty(); | |
| if (!gameOver && checkGameOver()) triggerGameOver(); | |
| } | |
| // ---- Input ---- | |
| function getPos(e){ | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleX = W/rect.width, scaleY = H/rect.height; | |
| const t = e.touches ? e.touches[0] : e; | |
| return { x: (t.clientX-rect.left)*scaleX, y: (t.clientY-rect.top)*scaleY }; | |
| } | |
| function trayImgSlots(){ | |
| const slotW = 108, gap = 10; | |
| const totalW = slotW*3 + gap*2; | |
| const startX = (W - totalW)/2; | |
| const y = boardY + GRID*CELL + 20; | |
| return [0,1,2].map(i => ({ x: startX + i*(slotW+gap), y, w: slotW, h: 100 })); | |
| } | |
| function pointerDown(e){ | |
| if (gameOver){ newGame(); return; } | |
| const pos = getPos(e); | |
| const slots = trayImgSlots(); | |
| for (let i = 0; i < 3; i++){ | |
| const piece = tray[i]; | |
| if (!piece) continue; | |
| const s = slots[i]; | |
| if (pos.x >= s.x && pos.x <= s.x+s.w && pos.y >= s.y && pos.y <= s.y+s.h){ | |
| dragging = { trayIndex: i, shape: piece.shape, color: piece.color, px: pos.x, py: pos.y }; | |
| e.preventDefault && e.preventDefault(); | |
| return; | |
| } | |
| } | |
| } | |
| function pointerMove(e){ | |
| if (!dragging) return; | |
| const pos = getPos(e); | |
| dragging.px = pos.x; dragging.py = pos.y; | |
| e.preventDefault && e.preventDefault(); | |
| } | |
| function currentPreview(){ | |
| if (!dragging) return null; | |
| const { w, h } = bbox(dragging.shape); | |
| const anchorX = dragging.px, anchorY = dragging.py - 46; // lift above finger/cursor | |
| const centerCol = Math.round((anchorX - boardX) / CELL - (w-1)/2); | |
| const centerRow = Math.round((anchorY - boardY) / CELL - (h-1)/2); | |
| return { col: centerCol, row: centerRow }; | |
| } | |
| function pointerUp(e){ | |
| if (!dragging) return; | |
| const preview = currentPreview(); | |
| const valid = preview && canPlace(dragging.shape, preview.col, preview.row); | |
| if (valid) placePiece(dragging.trayIndex, dragging.shape, dragging.color, preview.col, preview.row); | |
| dragging = null; | |
| } | |
| canvas.addEventListener('mousedown', pointerDown); | |
| canvas.addEventListener('mousemove', pointerMove); | |
| canvas.addEventListener('mouseup', pointerUp); | |
| canvas.addEventListener('mouseleave', () => { dragging = null; }); | |
| canvas.addEventListener('touchstart', pointerDown, { passive: false }); | |
| canvas.addEventListener('touchmove', pointerMove, { passive: false }); | |
| canvas.addEventListener('touchend', pointerUp); | |
| // ---- Draw ---- | |
| function drawBoard(){ | |
| ctx.save(); | |
| ctx.shadowColor = '#2a8fd8'; | |
| ctx.shadowBlur = 6; | |
| ctx.fillStyle = '#d5ecfb'; | |
| ctx.fillRect(boardX-4, boardY-4, GRID*CELL+8, GRID*CELL+8); | |
| ctx.shadowBlur = 0; | |
| ctx.restore(); | |
| for (let r = 0; r < GRID; r++){ | |
| for (let c = 0; c < GRID; c++){ | |
| const x = boardX + c*CELL, y = boardY + r*CELL; | |
| const val = board[r][c]; | |
| ctx.fillStyle = val ? val : ((r+c)%2===0 ? '#eef8ff' : '#e2f0fc'); | |
| if (val){ ctx.shadowColor = val; ctx.shadowBlur = 6; } | |
| const placing = placingCells.find(p => p.r === r && p.c === c); | |
| if (placing){ | |
| const t = placing.timer; // 1 -> 0 | |
| const scale = 1 + 0.35 * Math.sin(t * Math.PI); // bounce out then settle | |
| const size = (CELL-2) * scale; | |
| const off = (CELL-2 - size) / 2; | |
| ctx.fillRect(x+1+off, y+1+off, size, size); | |
| } else { | |
| ctx.fillRect(x+1, y+1, CELL-2, CELL-2); | |
| } | |
| ctx.shadowBlur = 0; | |
| if (val && !placing){ | |
| ctx.fillStyle = 'rgba(255,255,255,0.25)'; | |
| ctx.fillRect(x+2, y+2, CELL-4, (CELL-4)*0.35); | |
| } | |
| } | |
| } | |
| placingCells.forEach(p => p.timer -= 0.12); | |
| placingCells = placingCells.filter(p => p.timer > 0); | |
| // shrinking / fading cells for lines that just cleared | |
| clearingCells.forEach(f => { | |
| const x = boardX + f.c*CELL + CELL/2, y = boardY + f.r*CELL + CELL/2; | |
| const t = f.timer; // 1 -> 0 | |
| const size = (CELL-2) * t; | |
| ctx.save(); | |
| ctx.globalAlpha = t; | |
| ctx.fillStyle = f.color; | |
| ctx.shadowColor = f.color; ctx.shadowBlur = 10; | |
| ctx.fillRect(x - size/2, y - size/2, size, size); | |
| ctx.restore(); | |
| f.timer -= 0.1; | |
| }); | |
| clearingCells = clearingCells.filter(f => f.timer > 0); | |
| // particle burst | |
| particles.forEach(p => { | |
| p.x += p.vx; p.y += p.vy; p.vy += 0.15; p.life -= 0.035; | |
| ctx.save(); | |
| ctx.globalAlpha = Math.max(0, p.life); | |
| ctx.fillStyle = p.color; | |
| ctx.shadowColor = p.color; ctx.shadowBlur = 6; | |
| ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size); | |
| ctx.restore(); | |
| }); | |
| particles = particles.filter(p => p.life > 0); | |
| ctx.strokeStyle = '#2a8fd855'; | |
| ctx.lineWidth = 1; | |
| for (let i = 0; i <= GRID; i++){ | |
| ctx.beginPath(); ctx.moveTo(boardX, boardY+i*CELL); ctx.lineTo(boardX+GRID*CELL, boardY+i*CELL); ctx.stroke(); | |
| ctx.beginPath(); ctx.moveTo(boardX+i*CELL, boardY); ctx.lineTo(boardX+i*CELL, boardY+GRID*CELL); ctx.stroke(); | |
| } | |
| } | |
| function drawPreview(){ | |
| if (!dragging) return; | |
| const preview = currentPreview(); | |
| if (!preview) return; | |
| const valid = canPlace(dragging.shape, preview.col, preview.row); | |
| for (const [dx,dy] of dragging.shape){ | |
| const c = preview.col+dx, r = preview.row+dy; | |
| if (c<0 || c>=GRID || r<0 || r>=GRID) continue; | |
| const x = boardX + c*CELL, y = boardY + r*CELL; | |
| ctx.fillStyle = valid ? 'rgba(99,255,143,0.45)' : 'rgba(255,90,90,0.45)'; | |
| ctx.fillRect(x+2, y+2, CELL-4, CELL-4); | |
| ctx.strokeStyle = valid ? '#63ff8f' : '#ff5a5a'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(x+2, y+2, CELL-4, CELL-4); | |
| } | |
| } | |
| function drawPieceInBox(shape, color, box, blockSize){ | |
| const { w, h } = bbox(shape); | |
| const offX = box.x + (box.w - w*blockSize)/2; | |
| const offY = box.y + (box.h - h*blockSize)/2; | |
| for (const [dx,dy] of shape){ | |
| const x = offX + dx*blockSize, y = offY + dy*blockSize; | |
| ctx.fillStyle = color; | |
| ctx.shadowColor = color; ctx.shadowBlur = 6; | |
| ctx.fillRect(x+1, y+1, blockSize-2, blockSize-2); | |
| ctx.shadowBlur = 0; | |
| ctx.fillStyle = 'rgba(255,255,255,0.25)'; | |
| ctx.fillRect(x+2, y+2, blockSize-4, (blockSize-4)*0.35); | |
| } | |
| } | |
| function drawTray(){ | |
| const slots = trayImgSlots(); | |
| for (let i = 0; i < 3; i++){ | |
| const s = slots[i]; | |
| ctx.strokeStyle = '#2a8fd855'; | |
| ctx.lineWidth = 1.5; | |
| ctx.strokeRect(s.x, s.y, s.w, s.h); | |
| const piece = tray[i]; | |
| if (piece && !(dragging && dragging.trayIndex === i)){ | |
| drawPieceInBox(piece.shape, piece.color, s, 18); | |
| } | |
| } | |
| } | |
| function drawDraggedPiece(){ | |
| if (!dragging) return; | |
| const { w, h } = bbox(dragging.shape); | |
| const blockSize = CELL; | |
| const anchorX = dragging.px, anchorY = dragging.py - 46; | |
| const offX = anchorX - (w*blockSize)/2; | |
| const offY = anchorY - (h*blockSize)/2; | |
| ctx.save(); | |
| ctx.globalAlpha = 0.9; | |
| for (const [dx,dy] of dragging.shape){ | |
| const x = offX + dx*blockSize, y = offY + dy*blockSize; | |
| ctx.fillStyle = dragging.color; | |
| ctx.shadowColor = dragging.color; ctx.shadowBlur = 10; | |
| ctx.fillRect(x+2, y+2, blockSize-4, blockSize-4); | |
| } | |
| ctx.restore(); | |
| ctx.shadowBlur = 0; | |
| } | |
| function drawTitle(){ | |
| ctx.save(); | |
| ctx.font = 'bold 18px Segoe UI'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillStyle = '#1565c0'; | |
| ctx.shadowColor = '#7fc4f0'; ctx.shadowBlur = 8; | |
| ctx.fillText('B L O C K B L A S T', W/2, 30); | |
| ctx.restore(); | |
| } | |
| function render(){ | |
| ctx.clearRect(0,0,W,H); | |
| const bg = ctx.createLinearGradient(0,0,0,H); | |
| bg.addColorStop(0, '#f2faff'); bg.addColorStop(1, '#cfe8fb'); | |
| ctx.fillStyle = bg; ctx.fillRect(0,0,W,H); | |
| ctx.save(); | |
| if (shakeTimer > 0){ | |
| const amt = shakeMag * (shakeTimer / 25); | |
| ctx.translate((Math.random()-0.5)*amt, (Math.random()-0.5)*amt); | |
| shakeTimer--; | |
| } | |
| drawTitle(); | |
| drawBoard(); | |
| drawPreview(); | |
| drawTray(); | |
| drawDraggedPiece(); | |
| ctx.restore(); | |
| } | |
| function loop(){ | |
| updateScoreDisplay(); | |
| render(); | |
| requestAnimationFrame(loop); | |
| } | |
| loop(); | |
| </script> | |
| </body> | |
| </html> |