Block-Blast-Solver / index.html
ek15072809's picture
Update index.html
58a896d verified
<!DOCTYPE html>
<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>