block-blast-solver / index.html
chanmin0723's picture
upload index.html
f27c169 verified
Raw
History Blame
24.5 kB
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Block Blast Solver β€” λ‹€μŒ 수 계산기</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#12141b;
--panel:#1b1e28;
--panel-2:#20232f;
--grid-line:#2c3040;
--grid-line-soft:#242733;
--ink:#eef0f6;
--muted:#8b90a4;
--muted-2:#5d6175;
--piece-a:#ff6f61;
--piece-a-dim:#5a2c28;
--piece-b:#3fd1c4;
--piece-b-dim:#1f4744;
--piece-c:#ffcb4d;
--piece-c-dim:#4d411c;
--filled:#3b3f52;
--clear:#7effa3;
--radius:10px;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;}
body{
background:
linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0) 300px),
var(--bg);
color:var(--ink);
font-family:'Inter',sans-serif;
min-height:100vh;
padding:28px 18px 60px;
}
@media (prefers-reduced-motion: reduce){
*{animation-duration:0.001ms !important; transition-duration:0.001ms !important;}
}
.wrap{max-width:1080px;margin:0 auto;}
header.top{
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:16px;
margin-bottom:26px;
padding-bottom:18px;
border-bottom:1px solid var(--grid-line-soft);
}
.brand{
font-family:'Space Grotesk',sans-serif;
font-weight:700;
font-size:22px;
letter-spacing:-0.01em;
display:flex;
align-items:baseline;
gap:10px;
}
.brand .mono-tag{
font-family:'JetBrains Mono',monospace;
font-size:11px;
font-weight:500;
color:var(--bg);
background:var(--clear);
padding:2px 7px;
border-radius:5px;
letter-spacing:0.02em;
}
.brand-sub{
color:var(--muted);
font-size:13px;
margin-top:4px;
}
.reset-btn{
background:none;
border:1px solid var(--grid-line);
color:var(--muted);
font-family:'Inter',sans-serif;
font-size:13px;
padding:8px 14px;
border-radius:8px;
cursor:pointer;
transition:border-color .15s, color .15s;
}
.reset-btn:hover{border-color:var(--muted-2); color:var(--ink);}
.reset-btn:focus-visible{outline:2px solid var(--clear); outline-offset:2px;}
main.layout{
display:grid;
grid-template-columns:minmax(0,1fr) 300px;
gap:22px;
align-items:start;
}
@media (max-width:820px){
main.layout{grid-template-columns:1fr;}
}
.panel{
background:var(--panel);
border:1px solid var(--grid-line-soft);
border-radius:14px;
padding:20px;
}
.panel-title{
font-family:'Space Grotesk',sans-serif;
font-weight:600;
font-size:14px;
letter-spacing:0.02em;
text-transform:uppercase;
color:var(--muted);
margin:0 0 14px;
display:flex;
align-items:center;
justify-content:space-between;
}
.panel-title .hint{
font-family:'Inter',sans-serif;
text-transform:none;
font-weight:400;
font-size:12px;
color:var(--muted-2);
letter-spacing:0;
}
/* ---- 8x8 board ---- */
.board8{
display:grid;
grid-template-columns:repeat(8, 1fr);
gap:3px;
aspect-ratio:1/1;
max-width:460px;
margin:0 auto;
background:var(--grid-line-soft);
padding:3px;
border-radius:8px;
}
.cell{
aspect-ratio:1/1;
border-radius:4px;
background:var(--panel-2);
cursor:pointer;
border:1px solid transparent;
position:relative;
transition:background .1s;
}
.cell:hover{background:#282c3a;}
.cell:focus-visible{outline:2px solid var(--clear); outline-offset:-2px;}
.cell.filled{background:var(--filled);}
.cell.piece-a{background:var(--piece-a);}
.cell.piece-b{background:var(--piece-b);}
.cell.piece-c{background:var(--piece-c);}
.cell.will-clear::after{
content:"";
position:absolute; inset:0;
border:2px solid var(--clear);
border-radius:4px;
box-shadow:0 0 8px rgba(126,255,163,0.55);
}
.cell .num{
position:absolute; top:1px; right:2px;
font-family:'JetBrains Mono',monospace;
font-size:9px;
color:rgba(0,0,0,0.55);
}
.board-legend{
display:flex;
gap:16px;
justify-content:center;
margin-top:12px;
font-size:11px;
color:var(--muted);
flex-wrap:wrap;
}
.board-legend span{display:inline-flex; align-items:center; gap:5px;}
.swatch{width:10px;height:10px;border-radius:3px;display:inline-block;}
/* ---- piece editors ---- */
.pieces{display:flex; flex-direction:column; gap:14px;}
.piece-card{
background:var(--panel);
border:1px solid var(--grid-line-soft);
border-radius:14px;
padding:16px;
}
.piece-head{
display:flex; align-items:center; justify-content:space-between;
margin-bottom:10px;
}
.piece-label{
font-family:'Space Grotesk',sans-serif;
font-weight:600;
font-size:13px;
display:flex; align-items:center; gap:8px;
}
.piece-dot{width:9px;height:9px;border-radius:50%;}
.piece-clear{
background:none; border:none; color:var(--muted-2);
font-size:11px; cursor:pointer; font-family:'Inter',sans-serif;
padding:2px 4px;
}
.piece-clear:hover{color:var(--ink);}
.piece-grid{
display:grid;
grid-template-columns:repeat(5, 1fr);
gap:2px;
width:120px;
margin:0 auto;
background:var(--grid-line-soft);
padding:2px;
border-radius:6px;
}
.pcell{
aspect-ratio:1/1;
background:var(--panel-2);
border-radius:3px;
cursor:pointer;
border:none;
}
.pcell:hover{background:#282c3a;}
.pcell:focus-visible{outline:2px solid var(--clear); outline-offset:-1px;}
.pcell.on{background:var(--piece-color, var(--piece-a));}
.solve-row{
margin-top:6px;
}
.solve-btn{
width:100%;
background:var(--clear);
color:#0b1710;
border:none;
font-family:'Space Grotesk',sans-serif;
font-weight:700;
font-size:15px;
padding:13px;
border-radius:10px;
cursor:pointer;
transition:transform .08s, box-shadow .15s;
box-shadow:0 0 0 rgba(126,255,163,0);
}
.solve-btn:hover{box-shadow:0 4px 18px rgba(126,255,163,0.25);}
.solve-btn:active{transform:scale(0.98);}
.solve-btn:focus-visible{outline:2px solid var(--ink); outline-offset:2px;}
.solve-btn:disabled{background:var(--grid-line); color:var(--muted-2); cursor:default; box-shadow:none;}
.status-line{
font-family:'JetBrains Mono',monospace;
font-size:12px;
color:var(--muted);
margin-top:10px;
min-height:16px;
text-align:center;
}
/* ---- results ---- */
.results{margin-top:22px;}
.steps-row{
display:flex; gap:8px; flex-wrap:wrap;
margin-bottom:16px;
}
.step-chip{
font-family:'JetBrains Mono',monospace;
font-size:13px;
background:var(--panel-2);
border:1px solid var(--grid-line);
color:var(--muted);
padding:8px 14px;
border-radius:8px;
cursor:pointer;
transition:all .12s;
}
.step-chip:hover{color:var(--ink); border-color:var(--muted-2);}
.step-chip.active{
background:var(--clear);
color:#0b1710;
border-color:var(--clear);
font-weight:700;
}
.step-chip:focus-visible{outline:2px solid var(--clear); outline-offset:2px;}
.step-desc{
font-size:14px;
line-height:1.6;
color:var(--ink);
background:var(--panel-2);
border-left:3px solid var(--clear);
padding:12px 16px;
border-radius:0 8px 8px 0;
margin-bottom:16px;
}
.step-desc .coord{
font-family:'JetBrains Mono',monospace;
color:var(--clear);
}
.score-strip{
display:flex;
gap:22px;
flex-wrap:wrap;
font-family:'JetBrains Mono',monospace;
font-size:12px;
color:var(--muted);
padding-top:4px;
border-top:1px solid var(--grid-line-soft);
}
.score-strip b{color:var(--ink); font-weight:700;}
.empty-note{
color:var(--muted);
font-size:13px;
line-height:1.7;
text-align:center;
padding:20px 10px;
}
footer.foot{
max-width:1080px;
margin:34px auto 0;
color:var(--muted-2);
font-size:12px;
text-align:center;
line-height:1.6;
}
</style>
</head>
<body>
<div class="wrap">
<header class="top">
<div>
<div class="brand">Block Blast Solver <span class="mono-tag">SEARCH</span></div>
<div class="brand-sub">ν˜„μž¬ λ³΄λ“œμ™€ 3개 쑰각을 그리면, μ™„μ „νƒμƒ‰μœΌλ‘œ κ°€μž₯ 쀄을 많이 μ§€μš°λŠ” 배치 μˆœμ„œλ₯Ό μ°Ύμ•„λ“œλ €μš”.</div>
</div>
<button class="reset-btn" id="resetAllBtn">전체 μ΄ˆκΈ°ν™”</button>
</header>
<main class="layout">
<section class="panel">
<div class="panel-title">λ³΄λ“œ μƒνƒœ <span class="hint">칸을 클릭해 μ±„μ›Œμ§„ 블둝을 ν‘œμ‹œν•˜μ„Έμš”</span></div>
<div class="board8" id="board"></div>
<div class="board-legend">
<span><span class="swatch" style="background:var(--filled)"></span>μ±„μ›Œμ§„ μΉΈ</span>
<span><span class="swatch" style="background:var(--piece-a)"></span>쑰각 A</span>
<span><span class="swatch" style="background:var(--piece-b)"></span>쑰각 B</span>
<span><span class="swatch" style="background:var(--piece-c)"></span>쑰각 C</span>
</div>
<div class="results" id="resultsBlock" style="display:none;">
<div class="steps-row" id="stepsRow"></div>
<div class="step-desc" id="stepDesc"></div>
<div class="score-strip" id="scoreStrip"></div>
</div>
<div class="empty-note" id="emptyNote">
μ•„λž˜μ—μ„œ μ„Έ 쑰각 λͺ¨μ–‘을 그리고 <b style="color:var(--ink)">ν•΄λ‹΅ μ°ΎκΈ°</b>λ₯Ό λˆ„λ₯΄λ©΄ μ—¬κΈ° κ²°κ³Όκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.
</div>
</section>
<aside class="pieces">
<div class="piece-card">
<div class="piece-head">
<div class="piece-label"><span class="piece-dot" style="background:var(--piece-a)"></span>쑰각 A</div>
<button class="piece-clear" data-clear="a">μ§€μš°κΈ°</button>
</div>
<div class="piece-grid" id="piece-a" style="--piece-color:var(--piece-a)"></div>
</div>
<div class="piece-card">
<div class="piece-head">
<div class="piece-label"><span class="piece-dot" style="background:var(--piece-b)"></span>쑰각 B</div>
<button class="piece-clear" data-clear="b">μ§€μš°κΈ°</button>
</div>
<div class="piece-grid" id="piece-b" style="--piece-color:var(--piece-b)"></div>
</div>
<div class="piece-card">
<div class="piece-head">
<div class="piece-label"><span class="piece-dot" style="background:var(--piece-c)"></span>쑰각 C</div>
<button class="piece-clear" data-clear="c">μ§€μš°κΈ°</button>
</div>
<div class="piece-grid" id="piece-c" style="--piece-color:var(--piece-c)"></div>
</div>
<div class="solve-row">
<button class="solve-btn" id="solveBtn">ν•΄λ‹΅ μ°ΎκΈ°</button>
<div class="status-line" id="statusLine"></div>
</div>
</aside>
</main>
<footer class="foot">
λ³΄λ“œ 클릭 = μ±„μ›Œμ§„ μΉΈ ν† κΈ€ Β· 쑰각 νŽΈμ§‘κΈ° 클릭 = λͺ¨μ–‘ 그리기 Β· 탐색은 λΈŒλΌμš°μ €μ—μ„œλ§Œ μ‹€ν–‰λ˜λ©° μ„œλ²„λ‘œ μ „μ†‘λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
</footer>
</div>
<script>
(function(){
const N = 8;
const PIECE_GRID = 5;
const pieceKeys = ['a','b','c'];
const pieceColorVar = {a:'var(--piece-a)', b:'var(--piece-b)', c:'var(--piece-c)'};
const pieceNameKo = {a:'A', b:'B', c:'C'};
// ---- state ----
let board = Array.from({length:N}, ()=>Array(N).fill(0));
let pieceGrids = {
a: Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0)),
b: Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0)),
c: Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0)),
};
let lastSolution = null; // {steps:[{piece,r,c,offsets,boardBeforeClear,clearRows,clearCols,boardAfterClear}], totalLines, finalEmpty}
// ---- DOM refs ----
const boardEl = document.getElementById('board');
const statusLine = document.getElementById('statusLine');
const resultsBlock = document.getElementById('resultsBlock');
const emptyNote = document.getElementById('emptyNote');
const stepsRow = document.getElementById('stepsRow');
const stepDesc = document.getElementById('stepDesc');
const scoreStrip = document.getElementById('scoreStrip');
// ---- build board grid ----
const boardCells = [];
for(let r=0;r<N;r++){
const row=[];
for(let c=0;c<N;c++){
const cell = document.createElement('button');
cell.className='cell';
cell.type='button';
cell.setAttribute('aria-label', `${r+1}ν–‰ ${c+1}μ—΄`);
cell.addEventListener('click', ()=>{
board[r][c] = board[r][c] ? 0 : 1;
clearSolution();
renderBoard();
});
boardEl.appendChild(cell);
row.push(cell);
}
boardCells.push(row);
}
// ---- build piece editors ----
const pieceCellEls = {a:[], b:[], c:[]};
pieceKeys.forEach(k=>{
const el = document.getElementById('piece-'+k);
for(let r=0;r<PIECE_GRID;r++){
const rowEls=[];
for(let c=0;c<PIECE_GRID;c++){
const b = document.createElement('button');
b.type='button';
b.className='pcell';
b.setAttribute('aria-label', `${pieceNameKo[k]} 쑰각 ${r+1},${c+1}`);
b.addEventListener('click', ()=>{
pieceGrids[k][r][c] = pieceGrids[k][r][c] ? 0 : 1;
clearSolution();
renderPiece(k);
});
el.appendChild(b);
rowEls.push(b);
}
pieceCellEls[k].push(rowEls);
}
});
document.querySelectorAll('.piece-clear').forEach(btn=>{
btn.addEventListener('click', ()=>{
const k = btn.dataset.clear;
pieceGrids[k] = Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0));
clearSolution();
renderPiece(k);
});
});
document.getElementById('resetAllBtn').addEventListener('click', ()=>{
board = Array.from({length:N}, ()=>Array(N).fill(0));
pieceKeys.forEach(k=> pieceGrids[k] = Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0)));
clearSolution();
renderBoard();
pieceKeys.forEach(renderPiece);
statusLine.textContent='';
});
document.getElementById('solveBtn').addEventListener('click', runSolve);
// ---- rendering ----
function renderBoard(overlay){
// overlay: optional {board: 8x8 with 0/'a'/'b'/'c', willClear:Set of "r,c"}
for(let r=0;r<N;r++){
for(let c=0;c<N;c++){
const cell = boardCells[r][c];
cell.className='cell';
if(overlay){
const v = overlay.board[r][c];
if(v==='a'||v==='b'||v==='c') cell.classList.add('piece-'+v);
else if(v===1) cell.classList.add('filled');
if(overlay.willClear && overlay.willClear.has(r+','+c)) cell.classList.add('will-clear');
} else {
if(board[r][c]) cell.classList.add('filled');
}
}
}
}
function renderPiece(k){
for(let r=0;r<PIECE_GRID;r++){
for(let c=0;c<PIECE_GRID;c++){
const el = pieceCellEls[k][r][c];
el.classList.toggle('on', !!pieceGrids[k][r][c]);
}
}
}
function clearSolution(){
lastSolution = null;
resultsBlock.style.display='none';
emptyNote.style.display='block';
}
renderBoard();
pieceKeys.forEach(renderPiece);
// ---- shape trimming ----
function trimShape(grid){
let minR=99,maxR=-1,minC=99,maxC=-1;
for(let r=0;r<grid.length;r++){
for(let c=0;c<grid[0].length;c++){
if(grid[r][c]){
if(r<minR)minR=r; if(r>maxR)maxR=r;
if(c<minC)minC=c; if(c>maxC)maxC=c;
}
}
}
if(maxR===-1) return null; // empty piece
const offsets=[];
for(let r=minR;r<=maxR;r++){
for(let c=minC;c<=maxC;c++){
if(grid[r][c]) offsets.push([r-minR, c-minC]);
}
}
return {offsets, h:maxR-minR+1, w:maxC-minC+1};
}
// ---- board ops ----
function canPlace(bd, offsets, r, c){
for(const [dr,dc] of offsets){
const rr=r+dr, cc=c+dc;
if(rr<0||rr>=N||cc<0||cc>=N) return false;
if(bd[rr][cc]) return false;
}
return true;
}
function cloneBoard(bd){
return bd.map(row=>row.slice());
}
function place(bd, offsets, r, c){
for(const [dr,dc] of offsets){
bd[r+dr][c+dc]=1;
}
}
function findClears(bd){
const rows=[], cols=[];
for(let r=0;r<N;r++){
let full=true;
for(let c=0;c<N;c++) if(!bd[r][c]){full=false;break;}
if(full) rows.push(r);
}
for(let c=0;c<N;c++){
let full=true;
for(let r=0;r<N;r++) if(!bd[r][c]){full=false;break;}
if(full) cols.push(c);
}
return {rows, cols};
}
function applyClear(bd, rows, cols){
for(const r of rows) for(let c=0;c<N;c++) bd[r][c]=0;
for(const c of cols) for(let r=0;r<N;r++) bd[r][c]=0;
}
function countEmpty(bd){
let n=0;
for(let r=0;r<N;r++) for(let c=0;c<N;c++) if(!bd[r][c]) n++;
return n;
}
// ---- solver (backtracking over all orderings x all positions) ----
function solve(initialBoard, pieces){
// pieces: [{key, offsets, h, w}]
let best = null; // {steps, totalLines, finalEmpty, score}
let bestPartial = null; // fallback if no full solution: {steps, placedCount, totalLines, score}
function evaluate(steps, totalLines, finalBoard){
const finalEmpty = countEmpty(finalBoard);
const score = totalLines*1000 + finalEmpty;
return {steps: steps.slice(), totalLines, finalEmpty, score};
}
function recurse(bd, remaining, steps, totalLines){
if(remaining.length===0){
const result = evaluate(steps, totalLines, bd);
if(!best || result.score > best.score) best = result;
return;
}
// track partial best in case nothing completes
{
const partialScore = totalLines*1000 + countEmpty(bd) + steps.length*2000; // prioritize placing more pieces
if(!bestPartial || partialScore > bestPartial.score){
bestPartial = {steps: steps.slice(), placedCount: steps.length, totalLines, score: partialScore};
}
}
for(let i=0;i<remaining.length;i++){
const piece = remaining[i];
const restPieces = remaining.slice(0,i).concat(remaining.slice(i+1));
const maxR = N - piece.h, maxC = N - piece.w;
for(let r=0;r<=maxR;r++){
for(let c=0;c<=maxC;c++){
if(canPlace(bd, piece.offsets, r, c)){
const nb = cloneBoard(bd);
place(nb, piece.offsets, r, c);
const beforeClear = cloneBoard(nb);
const {rows, cols} = findClears(nb);
applyClear(nb, rows, cols);
const linesThisStep = rows.length + cols.length;
steps.push({
pieceKey: piece.key,
r, c,
offsets: piece.offsets,
boardBeforeClear: beforeClear,
clearRows: rows,
clearCols: cols,
boardAfterClear: cloneBoard(nb),
linesThisStep
});
recurse(nb, restPieces, steps, totalLines + linesThisStep);
steps.pop();
}
}
}
}
}
recurse(cloneBoard(initialBoard), pieces, [], 0);
return {best, bestPartial};
}
function runSolve(){
// prepare pieces
const pieces = [];
let anyEmpty = false;
for(const k of pieceKeys){
const trimmed = trimShape(pieceGrids[k]);
if(!trimmed){ anyEmpty = true; continue; }
pieces.push({key:k, offsets:trimmed.offsets, h:trimmed.h, w:trimmed.w});
}
if(pieces.length===0){
statusLine.textContent = '쑰각을 μ΅œμ†Œ 1개 이상 κ·Έλ €μ£Όμ„Έμš”.';
return;
}
if(anyEmpty){
statusLine.textContent = 'λΉ„μ–΄ μžˆλŠ” 쑰각은 이번 νƒμƒ‰μ—μ„œ μ œμ™ΈλΌμš”. (ν•„μš”ν•˜λ©΄ μ„Έ 쑰각을 λͺ¨λ‘ μ±„μ›Œμ£Όμ„Έμš”)';
} else {
statusLine.textContent = '';
}
statusLine.textContent += (statusLine.textContent?' Β· ':'') + '탐색 쀑...';
document.getElementById('solveBtn').disabled = true;
setTimeout(()=>{
const t0 = performance.now();
const {best, bestPartial} = solve(board, pieces);
const t1 = performance.now();
document.getElementById('solveBtn').disabled = false;
if(best){
lastSolution = best;
statusLine.textContent = `탐색 μ™„λ£Œ (${Math.round(t1-t0)}ms) Β· 전체 배치 κ°€λŠ₯`;
showSolution(best, pieces.length);
} else if(bestPartial && bestPartial.steps.length>0){
lastSolution = bestPartial;
statusLine.textContent = `탐색 μ™„λ£Œ (${Math.round(t1-t0)}ms) Β· 쑰각 ${pieces.length}개λ₯Ό λͺ¨λ‘ 놓을 방법은 μ—†μ–΄μš”`;
showSolution(bestPartial, pieces.length, true);
} else {
lastSolution = null;
statusLine.textContent = `탐색 μ™„λ£Œ (${Math.round(t1-t0)}ms) Β· 놓을 수 μžˆλŠ” 쑰각이 μ—†μ–΄μš”`;
resultsBlock.style.display='none';
emptyNote.style.display='block';
emptyNote.textContent = 'μ§€κΈˆ λ³΄λ“œ μƒνƒœλ‘œλŠ” κ·Έλ €μ£Όμ‹  쑰각을 ν•˜λ‚˜λ„ 놓을 수 μ—†μ–΄μš”. λ³΄λ“œλ‚˜ 쑰각 λͺ¨μ–‘을 λ‹€μ‹œ ν™•μΈν•΄μ£Όμ„Έμš”.';
}
}, 20);
}
// ---- display solution ----
let activeStepIdx = 0;
function showSolution(sol, totalPiecesRequested, isPartial){
resultsBlock.style.display='block';
emptyNote.style.display='none';
stepsRow.innerHTML='';
sol.steps.forEach((step, idx)=>{
const chip = document.createElement('button');
chip.type='button';
chip.className='step-chip';
chip.textContent = `${idx+1}단계 Β· ${pieceNameKo[step.pieceKey]}`;
chip.addEventListener('click', ()=> setActiveStep(idx));
stepsRow.appendChild(chip);
});
const finalChip = document.createElement('button');
finalChip.type='button';
finalChip.className='step-chip';
finalChip.textContent = 'μ΅œμ’… κ²°κ³Ό';
finalChip.addEventListener('click', ()=> setActiveStep(sol.steps.length));
stepsRow.appendChild(finalChip);
function setActiveStep(idx){
activeStepIdx = idx;
Array.from(stepsRow.children).forEach((el,i)=> el.classList.toggle('active', i===idx));
if(idx < sol.steps.length){
const step = sol.steps[idx];
const overlayBoard = step.boardBeforeClear.map(row=>row.slice());
// mark piece cells with its color letter for rendering
for(const [dr,dc] of step.offsets){
overlayBoard[step.r+dr][step.c+dc] = step.pieceKey;
}
const willClear = new Set();
for(const r of step.clearRows) for(let c=0;c<N;c++) willClear.add(r+','+c);
for(const c of step.clearCols) for(let r=0;r<N;r++) willClear.add(r+','+c);
renderBoard({board:overlayBoard, willClear});
const lineTxt = step.linesThisStep>0
? `이 배치둜 <b class="coord">${step.linesThisStep}쀄</b>이 μ§€μ›Œμ Έμš”.`
: `이 λ°°μΉ˜λ‘œλŠ” 아직 쀄이 μ§€μ›Œμ§€μ§€ μ•Šμ•„μš”.`;
stepDesc.innerHTML = `<b>${idx+1}단계</b> β€” 쑰각 <b class="coord">${pieceNameKo[step.pieceKey]}</b>λ₯Ό `
+ `<span class="coord">${step.r+1}ν–‰ ${step.c+1}μ—΄</span>(μ™Όμͺ½ μœ„ κΈ°μ€€)에 λ†“μœΌμ„Έμš”. ${lineTxt}`;
} else {
// final state
const last = sol.steps[sol.steps.length-1];
renderBoard({board: last.boardAfterClear});
stepDesc.innerHTML = `<b>μ΅œμ’… λ³΄λ“œ</b> β€” λͺ¨λ“  λ°°μΉ˜μ™€ 쀄 μ§€μš°κΈ°κ°€ λλ‚œ λ’€μ˜ μƒνƒœμ˜ˆμš”.`;
}
}
const totalLines = sol.totalLines;
const placedCount = sol.steps.length;
scoreStrip.innerHTML = `
<span>배치된 쑰각: <b>${placedCount} / ${totalPiecesRequested}</b></span>
<span>총 μ§€μ›Œμ§„ 쀄: <b>${totalLines}</b></span>
<span>탐색 방식: <b>완전탐색 (λ°±νŠΈλž˜ν‚Ή)</b></span>
`;
if(isPartial){
const warn = document.createElement('div');
warn.style.color = 'var(--piece-a)';
warn.style.marginTop = '6px';
warn.textContent = '⚠ 3개λ₯Ό λͺ¨λ‘ λ†“μœΌλ©΄ λ³΄λ“œκ°€ λ§‰ν˜€μš”. ν‘œμ‹œλœ 것은 μ΅œλŒ€ν•œ 많이 놓을 수 μžˆλŠ” λ°°μΉ˜μ˜ˆμš”.';
scoreStrip.appendChild(warn);
}
setActiveStep(0);
}
})();
</script>
</body>
</html>