Spaces:
Running
Running
| <html lang="ja"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Block Blast Solver</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| .grid-cell { | |
| aspect-ratio: 1 / 1; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: bold; | |
| color: white; | |
| font-size: 1.2rem; | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.5); | |
| } | |
| .cell-empty { background-color: #e5e7eb; border: 1px solid #d1d5db; } | |
| .cell-filled { background-color: #4b5563; border: 1px solid #374151; } | |
| .cell-preview-1 { background-color: #fbbf24; border: 2px solid #d97706; } | |
| .cell-preview-2 { background-color: #f59e0b; border: 2px solid #b45309; } | |
| .cell-preview-3 { background-color: #d97706; border: 2px solid #92400e; } | |
| .piece-editor-grid { | |
| display: grid; | |
| grid-template-columns: repeat(5, 1fr); | |
| gap: 2px; | |
| width: 120px; | |
| background-color: #f3f4f6; | |
| padding: 4px; | |
| border-radius: 8px; | |
| } | |
| .mini-cell { | |
| aspect-ratio: 1 / 1; | |
| border-radius: 2px; | |
| cursor: pointer; | |
| } | |
| .mini-empty { background-color: #ffffff; border: 1px solid #e5e7eb; } | |
| .mini-filled { background-color: #6366f1; border: 1px solid #4338ca; } | |
| </style> | |
| </head> | |
| <body class="bg-slate-100 min-h-screen p-4 md:p-8 font-sans"> | |
| <div class="max-w-6xl mx-auto bg-white rounded-2xl shadow-xl overflow-hidden"> | |
| <div class="bg-indigo-600 p-6 text-white text-center"> | |
| <h1 class="text-2xl font-bold italic uppercase tracking-wider">BLOCK BLAST SOLVER</h1> | |
| </div> | |
| <div class="p-6 grid grid-cols-1 lg:grid-cols-12 gap-8"> | |
| <div class="lg:col-span-7"> | |
| <h2 class="text-lg font-semibold mb-4 flex items-center"> | |
| <span class="bg-indigo-100 text-indigo-600 w-6 h-6 rounded-full flex items-center justify-center mr-2 text-sm">1</span> | |
| ็พๅจใฎ็ค้ขใๅ ฅๅ | |
| </h2> | |
| <div id="game-grid" class="grid grid-cols-8 gap-1 bg-gray-300 p-2 rounded-xl shadow-inner border-4 border-gray-400"> | |
| <!-- JS generates 64 cells --> | |
| </div> | |
| <div class="flex justify-between mt-4 items-center"> | |
| <button id="clear-grid" class="px-4 py-2 bg-white border border-gray-300 rounded-lg text-sm font-medium text-gray-600 hover:bg-gray-50"> | |
| ็ค้ขใใชใปใใ | |
| </button> | |
| </div> | |
| </div> | |
| <div class="lg:col-span-5 space-y-4"> | |
| <h2 class="text-lg font-semibold mb-2 flex items-center"> | |
| <span class="bg-indigo-100 text-indigo-600 w-6 h-6 rounded-full flex items-center justify-center mr-2 text-sm">2</span> | |
| ๆใก้งใๅ ฅๅ | |
| </h2> | |
| <div class="grid grid-cols-3 lg:grid-cols-1 gap-4"> | |
| <div class="bg-slate-50 p-3 rounded-xl border border-slate-200 flex flex-col items-center"> | |
| <span class="text-[10px] font-bold text-slate-400 mb-1 uppercase">Piece A</span> | |
| <div id="piece-editor-0" class="piece-editor-grid"></div> | |
| <button onclick="clearPiece(0)" class="mt-1 text-[10px] text-indigo-500">CLEAR</button> | |
| </div> | |
| <div class="bg-slate-50 p-3 rounded-xl border border-slate-200 flex flex-col items-center"> | |
| <span class="text-[10px] font-bold text-slate-400 mb-1 uppercase">Piece B</span> | |
| <div id="piece-editor-1" class="piece-editor-grid"></div> | |
| <button onclick="clearPiece(1)" class="mt-1 text-[10px] text-indigo-500">CLEAR</button> | |
| </div> | |
| <div class="bg-slate-50 p-3 rounded-xl border border-slate-200 flex flex-col items-center"> | |
| <span class="text-[10px] font-bold text-slate-400 mb-1 uppercase">Piece C</span> | |
| <div id="piece-editor-2" class="piece-editor-grid"></div> | |
| <button onclick="clearPiece(2)" class="mt-1 text-[10px] text-indigo-500">CLEAR</button> | |
| </div> | |
| </div> | |
| <div class="pt-4 border-t space-y-3"> | |
| <button id="solve-btn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-4 rounded-2xl shadow-lg transition-all transform active:scale-95 text-lg"> | |
| ๆ้ฉใซใผใใ็ฎๅบ | |
| </button> | |
| <button id="apply-btn" disabled class="w-full bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white font-bold py-4 rounded-2xl shadow-lg transition-all transform active:scale-95 text-lg"> | |
| ้ ็ฝฎใ็ขบๅฎใใฆๆฌกใธ | |
| </button> | |
| <div id="status-message" class="p-4 rounded-xl text-center text-sm font-medium border min-h-[80px] flex items-center justify-center leading-relaxed"> | |
| ็ค้ขใจๅฐใชใใจใ1ใคใฎ้งใๆใใฆใใ ใใ | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const GRID_SIZE = 8; | |
| const MINI_SIZE = 5; | |
| let grid = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(0)); | |
| let piecesRaw = Array(3).fill().map(() => Array(MINI_SIZE).fill().map(() => Array(MINI_SIZE).fill(0))); | |
| let currentBestSequence = null; | |
| // --- UI --- | |
| const gridEl = document.getElementById('game-grid'); | |
| function initBoard() { | |
| gridEl.innerHTML = ''; | |
| for (let r = 0; r < GRID_SIZE; r++) { | |
| for (let c = 0; c < GRID_SIZE; c++) { | |
| const cell = document.createElement('div'); | |
| cell.className = `grid-cell rounded cursor-pointer ${grid[r][c] ? 'cell-filled' : 'cell-empty'}`; | |
| cell.dataset.r = r; cell.dataset.c = c; | |
| cell.onclick = () => { grid[r][c] = grid[r][c] ? 0 : 1; renderBoard(); }; | |
| gridEl.appendChild(cell); | |
| } | |
| } | |
| } | |
| function renderBoard(previews = []) { | |
| const cells = gridEl.children; | |
| for (let i = 0; i < cells.length; i++) { | |
| const r = parseInt(cells[i].dataset.r); | |
| const c = parseInt(cells[i].dataset.c); | |
| cells[i].className = 'grid-cell rounded cursor-pointer'; | |
| cells[i].innerText = ''; | |
| if (grid[r][c]) { | |
| cells[i].classList.add('cell-filled'); | |
| } else { | |
| let pIdx = previews.findIndex(p => p.coords.some(coord => coord[0] === r && coord[1] === c)); | |
| if (pIdx !== -1) { | |
| cells[i].classList.add(`cell-preview-${pIdx + 1}`); | |
| cells[i].innerText = (pIdx + 1); | |
| } else { | |
| cells[i].classList.add('cell-empty'); | |
| } | |
| } | |
| } | |
| } | |
| function initPieceEditors() { | |
| for (let i = 0; i < 3; i++) { | |
| const container = document.getElementById(`piece-editor-${i}`); | |
| container.innerHTML = ''; | |
| for (let r = 0; r < MINI_SIZE; r++) { | |
| for (let c = 0; c < MINI_SIZE; c++) { | |
| const cell = document.createElement('div'); | |
| cell.className = `mini-cell ${piecesRaw[i][r][c] ? 'mini-filled' : 'mini-empty'}`; | |
| cell.onclick = () => { piecesRaw[i][r][c] = piecesRaw[i][r][c] ? 0 : 1; renderPieceEditor(i); }; | |
| container.appendChild(cell); | |
| } | |
| } | |
| } | |
| } | |
| function renderPieceEditor(index) { | |
| const container = document.getElementById(`piece-editor-${index}`); | |
| const cells = container.children; | |
| for (let i = 0; i < 25; i++) { | |
| const r = Math.floor(i / 5); const c = i % 5; | |
| cells[i].className = `mini-cell ${piecesRaw[index][r][c] ? 'mini-filled' : 'mini-empty'}`; | |
| } | |
| } | |
| function clearPiece(index) { | |
| piecesRaw[index] = Array(MINI_SIZE).fill().map(() => Array(MINI_SIZE).fill(0)); | |
| renderPieceEditor(index); | |
| } | |
| function getPiecePoints(index) { | |
| const pts = []; let minR = 5, minC = 5; | |
| for (let r = 0; r < 5; r++) { | |
| for (let c = 0; c < 5; c++) { | |
| if (piecesRaw[index][r][c]) { | |
| pts.push([r, c]); minR = Math.min(minR, r); minC = Math.min(minC, c); | |
| } | |
| } | |
| } | |
| if (pts.length === 0) return null; | |
| return pts.map(p => [p[0] - minR, p[1] - minC]); | |
| } | |
| function canPlace(board, piece, r, c) { | |
| for (let [pr, pc] of piece) { | |
| const nr = r + pr, nc = c + pc; | |
| if (nr < 0 || nr >= 8 || nc < 0 || nc >= 8 || board[nr][nc]) return false; | |
| } | |
| return true; | |
| } | |
| function applyPlacement(board, piece, r, c) { | |
| const newBoard = board.map(row => [...row]); | |
| piece.forEach(([pr, pc]) => newBoard[r + pr][c + pc] = 1); | |
| let rows = [], cols = []; | |
| for (let i = 0; i < 8; i++) { | |
| if (newBoard[i].every(v => v === 1)) rows.push(i); | |
| let fullCol = true; | |
| for(let j=0; j<8; j++) if(newBoard[j][i] === 0) { fullCol = false; break; } | |
| if (fullCol) cols.push(i); | |
| } | |
| rows.forEach(ri => newBoard[ri].fill(0)); | |
| cols.forEach(ci => { for(let i=0; i<8; i++) newBoard[i][ci] = 0; }); | |
| return { board: newBoard, lines: rows.length + cols.length }; | |
| } | |
| function evaluate(board, linesCleared) { | |
| let score = 0; | |
| if (linesCleared > 0) { | |
| score += linesCleared * 1000; | |
| } else { | |
| for (let i = 0; i < 8; i++) { | |
| let rCount = 0, cCount = 0; | |
| for (let j = 0; j < 8; j++) { | |
| if (board[i][j]) rCount++; | |
| if (board[j][i]) cCount++; | |
| } | |
| if (rCount === 7) score += 150; | |
| if (cCount === 7) score += 150; | |
| } | |
| } | |
| const giantPiece = [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]]; | |
| let canFitGiantCenter = false; | |
| for (let r=1; r<=4; r++) { | |
| for (let c=1; c<=4; c++) { | |
| if (canPlace(board, giantPiece, r, c)) { canFitGiantCenter = true; break; } | |
| } | |
| if (canFitGiantCenter) break; | |
| } | |
| if (canFitGiantCenter) score += 500; else score -= 2000; | |
| for (let i=0; i<8; i++) { | |
| if (board[0][i]) score += 5; if (board[7][i]) score += 5; | |
| if (board[i][0]) score += 5; if (board[i][7]) score += 5; | |
| } | |
| let empty = 0; | |
| board.forEach(row => row.forEach(v => { if(!v) empty++; })); | |
| score += empty * 10; | |
| return score; | |
| } | |
| // --- Solver Engine (Supports 1-3 pieces) --- | |
| async function solve() { | |
| const status = document.getElementById('status-message'); | |
| const applyBtn = document.getElementById('apply-btn'); | |
| applyBtn.disabled = true; | |
| currentBestSequence = null; | |
| const available = []; | |
| for (let i=0; i<3; i++) { | |
| const p = getPiecePoints(i); | |
| if (p) available.push({ id: i, shape: p }); | |
| } | |
| if (available.length === 0) { | |
| status.innerText = "ๅฐใชใใจใ1ใคใฎ้งใๆใใฆใใ ใใใ"; | |
| status.className = "p-4 rounded-xl text-center text-sm font-medium border bg-yellow-50 text-yellow-700 border-yellow-200"; | |
| return; | |
| } | |
| status.innerText = "่จ็ฎไธญ..."; | |
| await new Promise(r => setTimeout(r, 50)); | |
| let maxTotalScore = -Infinity; | |
| // Generate all permutations of indices 0..n-1 | |
| function getPermutations(arr) { | |
| if (arr.length <= 1) return [arr]; | |
| let perms = []; | |
| for (let i = 0; i < arr.length; i++) { | |
| let rest = getPermutations(arr.slice(0, i).concat(arr.slice(i + 1))); | |
| for (let r of rest) perms.push([arr[i]].concat(r)); | |
| } | |
| return perms; | |
| } | |
| const perms = getPermutations(available.map((_, i) => i)); | |
| // Recursive simulation for n pieces | |
| function simulate(currentBoard, availableIdxs, currentPath, currentScore) { | |
| if (availableIdxs.length === 0) { | |
| if (currentScore > maxTotalScore) { | |
| maxTotalScore = currentScore; | |
| currentBestSequence = [...currentPath]; | |
| } | |
| return; | |
| } | |
| const piece = available[availableIdxs[0]]; | |
| const nextIdxs = availableIdxs.slice(1); | |
| for (let r = 0; r < 8; r++) { | |
| for (let c = 0; c < 8; c++) { | |
| if (canPlace(currentBoard, piece.shape, r, c)) { | |
| const result = applyPlacement(currentBoard, piece.shape, r, c); | |
| const stepScore = evaluate(result.board, result.lines); | |
| currentPath.push({ | |
| shape: piece.shape, | |
| r: r, c: c, | |
| coords: piece.shape.map(pt => [r + pt[0], c + pt[1]]) | |
| }); | |
| simulate(result.board, nextIdxs, currentPath, currentScore + stepScore); | |
| currentPath.pop(); | |
| } | |
| } | |
| } | |
| } | |
| for (let p of perms) { | |
| simulate(grid, p, [], 0); | |
| } | |
| if (currentBestSequence) { | |
| renderBoard(currentBestSequence); | |
| applyBtn.disabled = false; | |
| status.innerHTML = `<b>ๆ้ฉใซใผใ(${currentBestSequence.length}ๆ)ใ็ฎๅบ๏ผ</b><br>็ขบๅฎใใฟใณใง็ค้ขใๆดๆฐใใพใใ`; | |
| status.className = "p-4 rounded-xl text-center text-sm font-medium border bg-green-50 text-green-700 border-green-200"; | |
| } else { | |
| status.innerText = "ๆๅฎใใใ้งใใในใฆ็ฝฎใใใซใผใใ่ฆใคใใใพใใใงใใใ"; | |
| status.className = "p-4 rounded-xl text-center text-sm font-medium border bg-red-50 text-red-700 border-red-200"; | |
| } | |
| } | |
| function applyBestSequence() { | |
| if (!currentBestSequence) return; | |
| let tempBoard = grid; | |
| for (const step of currentBestSequence) { | |
| const res = applyPlacement(tempBoard, step.shape, step.r, step.c); | |
| tempBoard = res.board; | |
| } | |
| grid = tempBoard; | |
| currentBestSequence = null; | |
| renderBoard(); | |
| for (let i=0; i<3; i++) clearPiece(i); | |
| document.getElementById('apply-btn').disabled = true; | |
| document.getElementById('status-message').innerText = "้ ็ฝฎใ้ฉ็จใใพใใใ"; | |
| document.getElementById('status-message').className = "p-4 rounded-xl text-center text-sm font-medium border bg-indigo-50 text-indigo-700 border-indigo-200"; | |
| } | |
| document.getElementById('solve-btn').onclick = solve; | |
| document.getElementById('apply-btn').onclick = applyBestSequence; | |
| document.getElementById('clear-grid').onclick = () => { | |
| grid = Array(GRID_SIZE).fill().map(() => Array(GRID_SIZE).fill(0)); | |
| currentBestSequence = null; | |
| document.getElementById('apply-btn').disabled = true; | |
| renderBoard(); | |
| document.getElementById('status-message').innerText = "็ค้ขใใชใปใใใใพใใ"; | |
| }; | |
| initBoard(); | |
| initPieceEditors(); | |
| </script> | |
| </body> | |
| </html> |