Spaces:
Running
Running
| <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 ; transition-duration:0.001ms ;} | |
| } | |
| .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> | |