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:18px 16px 32px; | |
| } | |
| @media (prefers-reduced-motion: reduce){ | |
| *{animation-duration:0.001ms ; transition-duration:0.001ms ;} | |
| } | |
| .wrap{max-width:1220px;margin:0 auto;} | |
| header.top{ | |
| display:flex; | |
| align-items:flex-end; | |
| justify-content:space-between; | |
| gap:16px; | |
| margin-bottom:16px; | |
| padding-bottom:12px; | |
| 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(300px, 400px) minmax(380px, 1fr); | |
| gap:16px; | |
| align-items:start; | |
| } | |
| @media (max-width:760px){ | |
| main.layout{grid-template-columns:1fr;} | |
| } | |
| .panel{ | |
| background:var(--panel); | |
| border:1px solid var(--grid-line-soft); | |
| border-radius:14px; | |
| padding:16px; | |
| } | |
| .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; | |
| touch-action:none; | |
| user-select:none; | |
| -webkit-user-select:none; | |
| } | |
| .cell{ | |
| aspect-ratio:1/1; | |
| border-radius:4px; | |
| background:var(--panel-2); | |
| cursor:pointer; | |
| border:1px solid transparent; | |
| position:relative; | |
| transition:background .1s; | |
| touch-action:none; | |
| } | |
| .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:10px; | |
| 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:14px; | |
| } | |
| .piece-head{ | |
| display:flex; align-items:flex-start; justify-content:space-between; | |
| gap:10px; | |
| margin-bottom:12px; | |
| } | |
| .piece-label{ | |
| font-family:'Space Grotesk',sans-serif; | |
| font-weight:600; | |
| font-size:13px; | |
| display:flex; flex-direction:column; gap:3px; | |
| } | |
| .hint-inline{ | |
| font-family:'Inter',sans-serif; | |
| font-weight:400; | |
| font-size:11px; | |
| color:var(--muted-2); | |
| } | |
| .piece-clear{ | |
| background:none; border:none; color:var(--muted-2); | |
| font-size:11px; cursor:pointer; font-family:'Inter',sans-serif; | |
| padding:2px 4px; | |
| white-space:nowrap; | |
| } | |
| .piece-clear:hover{color:var(--ink);} | |
| /* ---- slot row: pick which of A/B/C you're filling ---- */ | |
| .slot-row{ | |
| display:grid; | |
| grid-template-columns:repeat(3, 1fr); | |
| gap:8px; | |
| margin-bottom:14px; | |
| } | |
| .slot-chip{ | |
| background:var(--panel-2); | |
| border:1.5px solid var(--grid-line); | |
| border-radius:10px; | |
| padding:8px 6px; | |
| cursor:pointer; | |
| display:flex; | |
| flex-direction:column; | |
| align-items:center; | |
| gap:6px; | |
| transition:border-color .12s, box-shadow .12s, opacity .12s; | |
| opacity:0.72; | |
| } | |
| .slot-chip:hover{border-color:var(--muted-2);} | |
| .slot-chip:focus-visible{outline:2px solid var(--clear); outline-offset:2px;} | |
| .slot-chip.active{ | |
| opacity:1; | |
| border-color:var(--piece-color); | |
| box-shadow:0 0 0 1px var(--piece-color) inset, 0 0 14px -4px var(--piece-color); | |
| } | |
| .slot-thumb-wrap{ | |
| width:100%; | |
| height:34px; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| } | |
| .slot-empty{ | |
| font-family:'JetBrains Mono',monospace; | |
| font-size:11px; | |
| color:var(--muted-2); | |
| border:1.5px dashed var(--grid-line); | |
| border-radius:6px; | |
| width:26px; height:26px; | |
| display:flex; align-items:center; justify-content:center; | |
| } | |
| .slot-chip.active .slot-empty{border-color:var(--piece-color); color:var(--piece-color);} | |
| .slot-label{ | |
| font-family:'Space Grotesk',sans-serif; | |
| font-weight:700; | |
| font-size:12px; | |
| color:var(--piece-color); | |
| } | |
| /* ---- mode tabs (preset vs draw) ---- */ | |
| .mode-tabs{ | |
| display:flex; | |
| gap:4px; | |
| background:var(--panel-2); | |
| border-radius:8px; | |
| padding:3px; | |
| margin-bottom:12px; | |
| } | |
| .mode-tab{ | |
| flex:1; | |
| background:none; | |
| border:none; | |
| color:var(--muted); | |
| font-family:'Inter',sans-serif; | |
| font-size:12px; | |
| font-weight:500; | |
| padding:7px 4px; | |
| border-radius:6px; | |
| cursor:pointer; | |
| transition:background .12s, color .12s; | |
| } | |
| .mode-tab:hover{color:var(--ink);} | |
| .mode-tab.active{ | |
| background:var(--panel); | |
| color:var(--ink); | |
| font-weight:600; | |
| } | |
| .mode-tab:focus-visible{outline:2px solid var(--clear); outline-offset:1px;} | |
| /* ---- preset picker ---- */ | |
| /* Cell size is FIXED (--cell-px) across every shape, so a thumbnail's overall | |
| footprint scales with the piece's real size instead of every thumbnail | |
| being squeezed into an identical box. */ | |
| .preset-panel{ | |
| --cell-px:10px; | |
| max-height:64vh; | |
| overflow-y:auto; | |
| padding-right:4px; | |
| } | |
| .preset-group-label{ | |
| font-family:'JetBrains Mono',monospace; | |
| font-size:10px; | |
| letter-spacing:0.03em; | |
| text-transform:uppercase; | |
| color:var(--muted-2); | |
| margin:10px 0 6px; | |
| } | |
| .preset-group-label:first-child{margin-top:0;} | |
| .preset-group-grid{ | |
| display:flex; | |
| flex-wrap:wrap; | |
| align-items:center; | |
| gap:8px; | |
| } | |
| .preset-thumb-btn{ | |
| min-width:38px; | |
| min-height:38px; | |
| background:var(--panel-2); | |
| border:1.5px solid var(--grid-line); | |
| border-radius:8px; | |
| cursor:pointer; | |
| display:inline-flex; | |
| align-items:center; | |
| justify-content:center; | |
| padding:6px; | |
| transition:border-color .12s, background .12s; | |
| } | |
| .preset-thumb-btn:hover{border-color:var(--muted-2);} | |
| .preset-thumb-btn:focus-visible{outline:2px solid var(--clear); outline-offset:1px;} | |
| .preset-thumb-btn.selected{ | |
| border-color:var(--piece-color, var(--clear)); | |
| background:rgba(255,255,255,0.04); | |
| box-shadow:0 0 0 1px var(--piece-color, var(--clear)) inset; | |
| } | |
| .thumb-grid{ | |
| display:grid; | |
| gap:2px; | |
| } | |
| .thumb-cell{ | |
| width:var(--cell-px); | |
| height:var(--cell-px); | |
| background:transparent; | |
| border-radius:1.5px; | |
| } | |
| .thumb-cell.filled{ | |
| background:var(--piece-color, var(--piece-a)); | |
| } | |
| .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; | |
| touch-action:none; | |
| user-select:none; | |
| -webkit-user-select:none; | |
| } | |
| .pcell{ | |
| aspect-ratio:1/1; | |
| background:var(--panel-2); | |
| border-radius:3px; | |
| cursor:pointer; | |
| border:none; | |
| touch-action: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:16px;} | |
| .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;} | |
| .continue-btn{ | |
| display:none; | |
| width:100%; | |
| margin-top:16px; | |
| background:transparent; | |
| color:var(--clear); | |
| border:1.5px solid var(--clear); | |
| font-family:'Space Grotesk',sans-serif; | |
| font-weight:600; | |
| font-size:14px; | |
| padding:11px; | |
| border-radius:10px; | |
| cursor:pointer; | |
| transition:background .15s, transform .08s; | |
| } | |
| .continue-btn:hover{background:rgba(126,255,163,0.08);} | |
| .continue-btn:active{transform:scale(0.98);} | |
| .continue-btn:focus-visible{outline:2px solid var(--clear); outline-offset:2px;} | |
| .continue-btn.show{display:block;} | |
| .empty-note{ | |
| color:var(--muted); | |
| font-size:13px; | |
| line-height:1.7; | |
| text-align:center; | |
| padding:20px 10px; | |
| } | |
| footer.foot{ | |
| max-width:1220px; | |
| margin:22px 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> | |
| <button class="continue-btn" id="continueBtn" type="button"> | |
| μ΄ κ²°κ³Ό 보λλ‘ μ΄μ΄μ λ€μ μ‘°κ° λκΈ° β | |
| </button> | |
| </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="hint-inline">κ°μ νμμ 3λ² ν΄λ¦ β A, B, C μμλ‘ μ±μμ Έμ</span></div> | |
| <button class="piece-clear" id="clearActiveBtn">μ§κΈ μ‘°κ° μ§μ°κΈ°</button> | |
| </div> | |
| <div class="slot-row" id="slotRow"> | |
| <button class="slot-chip active" data-slot="a" style="--piece-color:var(--piece-a)" type="button"> | |
| <span class="slot-thumb-wrap" id="slotThumb-a"><span class="slot-empty">A</span></span> | |
| <span class="slot-label">A</span> | |
| </button> | |
| <button class="slot-chip" data-slot="b" style="--piece-color:var(--piece-b)" type="button"> | |
| <span class="slot-thumb-wrap" id="slotThumb-b"><span class="slot-empty">B</span></span> | |
| <span class="slot-label">B</span> | |
| </button> | |
| <button class="slot-chip" data-slot="c" style="--piece-color:var(--piece-c)" type="button"> | |
| <span class="slot-thumb-wrap" id="slotThumb-c"><span class="slot-empty">C</span></span> | |
| <span class="slot-label">C</span> | |
| </button> | |
| </div> | |
| <div class="mode-tabs" id="modeTabs"> | |
| <button class="mode-tab active" data-mode="preset" type="button">ν리μ μμ μ ν</button> | |
| <button class="mode-tab" data-mode="draw" type="button">μ§μ 그리기</button> | |
| </div> | |
| <div class="preset-panel" id="presetPanel"></div> | |
| <div class="piece-grid" id="pieceGridEditor" style="display:none;"></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'}; | |
| // ---- standard Block Blast piece shapes (fixed pool, no rotation in-game) ---- | |
| // Each shape's cells are offsets from its own top-left bounding box, matching | |
| // how they actually appear in the real game (already-rotated variants listed separately). | |
| const PRESET_SHAPES = [ | |
| // λΌμΈ / λ¨μΌ | |
| {id:'1x1', group:'ν μΉΈ / μ§μ ', cells:[[0,0]]}, | |
| {id:'1x2h', group:'ν μΉΈ / μ§μ ', cells:[[0,0],[0,1]]}, | |
| {id:'2x1v', group:'ν μΉΈ / μ§μ ', cells:[[0,0],[1,0]]}, | |
| {id:'1x3h', group:'ν μΉΈ / μ§μ ', cells:[[0,0],[0,1],[0,2]]}, | |
| {id:'3x1v', group:'ν μΉΈ / μ§μ ', cells:[[0,0],[1,0],[2,0]]}, | |
| {id:'1x4h', group:'ν μΉΈ / μ§μ ', cells:[[0,0],[0,1],[0,2],[0,3]]}, | |
| {id:'4x1v', group:'ν μΉΈ / μ§μ ', cells:[[0,0],[1,0],[2,0],[3,0]]}, | |
| {id:'1x5h', group:'ν μΉΈ / μ§μ ', cells:[[0,0],[0,1],[0,2],[0,3],[0,4]]}, | |
| {id:'5x1v', group:'ν μΉΈ / μ§μ ', cells:[[0,0],[1,0],[2,0],[3,0],[4,0]]}, | |
| // μ¬κ°ν | |
| {id:'2x2', group:'μ¬κ°ν', cells:[[0,0],[0,1],[1,0],[1,1]]}, | |
| {id:'3x3', group:'μ¬κ°ν', cells:[[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]]}, | |
| {id:'2x3', group:'μ¬κ°ν', cells:[[0,0],[0,1],[0,2],[1,0],[1,1],[1,2]]}, | |
| {id:'3x2', group:'μ¬κ°ν', cells:[[0,0],[0,1],[1,0],[1,1],[2,0],[2,1]]}, | |
| // κΊΎμΈ λͺ¨μ (3μΉΈ, μΈλͺ¨ μ½λ) | |
| {id:'tri-1', group:'κΊΎμΈ λͺ¨μ (3μΉΈ)', cells:[[0,0],[1,0],[1,1]]}, | |
| {id:'tri-2', group:'κΊΎμΈ λͺ¨μ (3μΉΈ)', cells:[[0,0],[0,1],[1,0]]}, | |
| {id:'tri-3', group:'κΊΎμΈ λͺ¨μ (3μΉΈ)', cells:[[0,0],[0,1],[1,1]]}, | |
| {id:'tri-4', group:'κΊΎμΈ λͺ¨μ (3μΉΈ)', cells:[[0,1],[1,0],[1,1]]}, | |
| // L / J λͺ¨μ (4μΉΈ) | |
| {id:'L1', group:'Lμ λͺ¨μ (4μΉΈ)', cells:[[0,0],[1,0],[2,0],[2,1]]}, | |
| {id:'L2', group:'Lμ λͺ¨μ (4μΉΈ)', cells:[[0,0],[0,1],[0,2],[1,0]]}, | |
| {id:'L3', group:'Lμ λͺ¨μ (4μΉΈ)', cells:[[0,0],[0,1],[1,1],[2,1]]}, | |
| {id:'L4', group:'Lμ λͺ¨μ (4μΉΈ)', cells:[[1,0],[1,1],[1,2],[0,2]]}, | |
| {id:'J1', group:'Lμ λͺ¨μ (4μΉΈ)', cells:[[0,1],[1,1],[2,0],[2,1]]}, | |
| {id:'J2', group:'Lμ λͺ¨μ (4μΉΈ)', cells:[[0,0],[1,0],[1,1],[1,2]]}, | |
| {id:'J3', group:'Lμ λͺ¨μ (4μΉΈ)', cells:[[0,0],[0,1],[1,0],[2,0]]}, | |
| {id:'J4', group:'Lμ λͺ¨μ (4μΉΈ)', cells:[[0,0],[0,1],[0,2],[1,2]]}, | |
| // T λͺ¨μ | |
| {id:'T-up', group:'Tμ λͺ¨μ', cells:[[0,0],[0,1],[0,2],[1,1]]}, | |
| {id:'T-down', group:'Tμ λͺ¨μ', cells:[[0,1],[1,0],[1,1],[1,2]]}, | |
| {id:'T-left', group:'Tμ λͺ¨μ', cells:[[0,1],[1,0],[1,1],[2,1]]}, | |
| {id:'T-right', group:'Tμ λͺ¨μ', cells:[[0,0],[1,0],[1,1],[2,0]]}, | |
| // S / Z λͺ¨μ | |
| {id:'S-h', group:'S / Z λͺ¨μ', cells:[[0,1],[0,2],[1,0],[1,1]]}, | |
| {id:'S-v', group:'S / Z λͺ¨μ', cells:[[0,0],[1,0],[1,1],[2,1]]}, | |
| {id:'Z-h', group:'S / Z λͺ¨μ', cells:[[0,0],[0,1],[1,1],[1,2]]}, | |
| {id:'Z-v', group:'S / Z λͺ¨μ', cells:[[0,1],[1,0],[1,1],[2,0]]}, | |
| // μμ / ν° μ½λ | |
| {id:'plus', group:'μμ / ν° μ½λ', cells:[[0,1],[1,0],[1,1],[1,2],[2,1]]}, | |
| {id:'bigcorner-1', group:'μμ / ν° μ½λ', cells:[[0,0],[1,0],[2,0],[2,1],[2,2]]}, | |
| {id:'bigcorner-2', group:'μμ / ν° μ½λ', cells:[[0,0],[0,1],[0,2],[1,0],[2,0]]}, | |
| {id:'bigcorner-3', group:'μμ / ν° μ½λ', cells:[[0,0],[0,1],[0,2],[1,2],[2,2]]}, | |
| {id:'bigcorner-4', group:'μμ / ν° μ½λ', cells:[[0,2],[1,2],[2,0],[2,1],[2,2]]}, | |
| ]; | |
| // ---- 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 pieceMode = {a:'preset', b:'preset', c:'preset'}; // 'preset' | 'draw' | |
| let selectedPreset = {a:null, b:null, c:null}; | |
| let activeSlot = 'a'; // which piece (a/b/c) the shared picker is currently filling | |
| 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'); | |
| const continueBtn = document.getElementById('continueBtn'); | |
| // ---- 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.dataset.role='board'; | |
| cell.dataset.key='board'; | |
| cell.dataset.r=r; | |
| cell.dataset.c=c; | |
| boardEl.appendChild(cell); | |
| row.push(cell); | |
| } | |
| boardCells.push(row); | |
| } | |
| // ---- build the single shared 5x5 piece draw grid (used by whichever slot is active) ---- | |
| const pieceCellEls = []; | |
| { | |
| const el = document.getElementById('pieceGridEditor'); | |
| 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.dataset.role='piece'; | |
| b.dataset.key='a'; // updated live to whichever slot is active | |
| b.dataset.r=r; | |
| b.dataset.c=c; | |
| el.appendChild(b); | |
| rowEls.push(b); | |
| } | |
| pieceCellEls.push(rowEls); | |
| } | |
| } | |
| // ---- shared cell get/set (used by both click and drag-paint) ---- | |
| function getCellValue(role, key, r, c){ | |
| return role==='board' ? board[r][c] : pieceGrids[key][r][c]; | |
| } | |
| function setCellValue(role, key, r, c, val){ | |
| if(getCellValue(role,key,r,c)===val) return false; // no-op, skip redundant render | |
| if(role==='board'){ board[r][c]=val; } else { pieceGrids[key][r][c]=val; } | |
| clearSolution(); | |
| if(role==='board'){ | |
| renderBoard(); | |
| } else { | |
| selectedPreset[key] = null; // manual edit no longer matches any preset | |
| renderPiece(key); | |
| renderPresetSelection(); | |
| } | |
| return true; | |
| } | |
| function toggleCellValue(role, key, r, c){ | |
| const nv = getCellValue(role,key,r,c) ? 0 : 1; | |
| setCellValue(role,key,r,c,nv); | |
| return nv; | |
| } | |
| // ---- drag painting: click-and-drag (mouse) or touch-drag to paint many cells at once ---- | |
| let dragActive = false; | |
| let dragRole = null, dragKey = null, dragValue = 0; | |
| function targetFromPoint(x, y){ | |
| const el = document.elementFromPoint(x, y); | |
| return el ? el.closest('[data-role]') : null; | |
| } | |
| function startDrag(target){ | |
| if(!target) return; | |
| dragActive = true; | |
| dragRole = target.dataset.role; | |
| dragKey = target.dataset.key; | |
| const r = +target.dataset.r, c = +target.dataset.c; | |
| dragValue = toggleCellValue(dragRole, dragKey, r, c); | |
| } | |
| function continueDrag(target){ | |
| if(!dragActive || !target) return; | |
| if(target.dataset.role !== dragRole || target.dataset.key !== dragKey) return; | |
| const r = +target.dataset.r, c = +target.dataset.c; | |
| setCellValue(dragRole, dragKey, r, c, dragValue); | |
| } | |
| function endDrag(){ dragActive = false; dragRole = null; dragKey = null; } | |
| // mouse: paint by holding button and moving over cells | |
| document.addEventListener('mousedown', (e)=>{ | |
| const target = e.target.closest('[data-role]'); | |
| if(!target) return; | |
| e.preventDefault(); // avoid text-selection while dragging | |
| startDrag(target); | |
| }); | |
| document.addEventListener('mouseover', (e)=>{ | |
| continueDrag(e.target.closest('[data-role]')); | |
| }); | |
| document.addEventListener('mouseup', endDrag); | |
| // touch: paint by dragging a finger across cells | |
| document.addEventListener('touchstart', (e)=>{ | |
| const t = e.touches[0]; | |
| const target = targetFromPoint(t.clientX, t.clientY); | |
| if(!target) return; | |
| e.preventDefault(); // avoid page scroll while painting | |
| startDrag(target); | |
| }, {passive:false}); | |
| document.addEventListener('touchmove', (e)=>{ | |
| if(!dragActive) return; | |
| e.preventDefault(); | |
| const t = e.touches[0]; | |
| continueDrag(targetFromPoint(t.clientX, t.clientY)); | |
| }, {passive:false}); | |
| document.addEventListener('touchend', endDrag); | |
| document.addEventListener('touchcancel', endDrag); | |
| // keyboard accessibility: Enter/Space on a focused cell still toggles it. | |
| // (real mouse clicks have event.detail >= 1 and are already handled by mousedown above; | |
| // keyboard-triggered clicks have event.detail === 0, so this only fires for keyboard use.) | |
| document.addEventListener('click', (e)=>{ | |
| if(e.detail !== 0) return; | |
| const target = e.target.closest('[data-role]'); | |
| if(!target) return; | |
| toggleCellValue(target.dataset.role, target.dataset.key, +target.dataset.r, +target.dataset.c); | |
| }); | |
| // ---- preset piece picker ---- | |
| function shapeBBox(cells){ | |
| let maxR=0, maxC=0; | |
| cells.forEach(([r,c])=>{ if(r>maxR)maxR=r; if(c>maxC)maxC=c; }); | |
| return {h:maxR+1, w:maxC+1}; | |
| } | |
| // Cell size is fixed (CSS var --cell-px on .preset-panel), so a shape's thumbnail | |
| // grows/shrinks with its real footprint instead of every shape sharing one box size. | |
| function buildThumb(cells){ | |
| const {h,w} = shapeBBox(cells); | |
| const filled = new Set(cells.map(([r,c])=>r+','+c)); | |
| const grid = document.createElement('div'); | |
| grid.className = 'thumb-grid'; | |
| grid.style.gridTemplateColumns = `repeat(${w}, var(--cell-px))`; | |
| grid.style.gridTemplateRows = `repeat(${h}, var(--cell-px))`; | |
| for(let r=0;r<h;r++){ | |
| for(let c=0;c<w;c++){ | |
| const cell = document.createElement('div'); | |
| cell.className = 'thumb-cell' + (filled.has(r+','+c) ? ' filled' : ''); | |
| grid.appendChild(cell); | |
| } | |
| } | |
| return grid; | |
| } | |
| const SLOT_ORDER = ['a','b','c']; | |
| // apply a chosen preset shape to whichever slot is currently active, then | |
| // auto-advance to the next slot (a -> b -> c) so 3 clicks in this one panel | |
| // fill all three pieces in order. | |
| function applyPresetToActiveSlot(shape){ | |
| const key = activeSlot; | |
| pieceGrids[key] = Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0)); | |
| shape.cells.forEach(([r,c])=>{ pieceGrids[key][r][c] = 1; }); | |
| selectedPreset[key] = shape.id; | |
| clearSolution(); | |
| renderSlotChip(key); | |
| renderPresetSelection(); | |
| const idx = SLOT_ORDER.indexOf(key); | |
| if(idx < SLOT_ORDER.length - 1){ | |
| setActiveSlot(SLOT_ORDER[idx+1]); | |
| } | |
| } | |
| function renderPresetSelection(){ | |
| document.querySelectorAll('#presetPanel .preset-thumb-btn').forEach(btn=>{ | |
| btn.classList.toggle('selected', btn.dataset.presetId === selectedPreset[activeSlot]); | |
| }); | |
| } | |
| function buildPresetPanel(){ | |
| const container = document.getElementById('presetPanel'); | |
| container.innerHTML = ''; | |
| const groups = {}; | |
| const groupOrder = []; | |
| PRESET_SHAPES.forEach(shape=>{ | |
| if(!groups[shape.group]){ groups[shape.group]=[]; groupOrder.push(shape.group); } | |
| groups[shape.group].push(shape); | |
| }); | |
| groupOrder.forEach(groupName=>{ | |
| const label = document.createElement('div'); | |
| label.className = 'preset-group-label'; | |
| label.textContent = groupName; | |
| container.appendChild(label); | |
| const row = document.createElement('div'); | |
| row.className = 'preset-group-grid'; | |
| groups[groupName].forEach(shape=>{ | |
| const btn = document.createElement('button'); | |
| btn.type = 'button'; | |
| btn.className = 'preset-thumb-btn'; | |
| btn.dataset.presetId = shape.id; | |
| btn.setAttribute('aria-label', `${groupName} λͺ¨μ μ ν`); | |
| btn.appendChild(buildThumb(shape.cells)); | |
| btn.addEventListener('click', ()=> applyPresetToActiveSlot(shape)); | |
| row.appendChild(btn); | |
| }); | |
| container.appendChild(row); | |
| }); | |
| } | |
| buildPresetPanel(); | |
| // ---- slot chip preview (small thumbnail of whatever's currently in that slot) ---- | |
| function renderSlotChip(key){ | |
| const wrap = document.getElementById('slotThumb-'+key); | |
| wrap.innerHTML = ''; | |
| const trimmed = trimShape(pieceGrids[key]); | |
| if(trimmed){ | |
| wrap.appendChild(buildThumb(trimmed.offsets)); | |
| } else { | |
| const span = document.createElement('span'); | |
| span.className = 'slot-empty'; | |
| span.textContent = pieceNameKo[key]; | |
| wrap.appendChild(span); | |
| } | |
| } | |
| // ---- switch which slot (A/B/C) the shared picker + draw grid apply to ---- | |
| function setActiveSlot(key){ | |
| activeSlot = key; | |
| document.querySelectorAll('.slot-chip').forEach(chip=>{ | |
| chip.classList.toggle('active', chip.dataset.slot===key); | |
| }); | |
| document.querySelectorAll('#modeTabs .mode-tab').forEach(tabBtn=>{ | |
| tabBtn.classList.toggle('active', tabBtn.dataset.mode===pieceMode[key]); | |
| }); | |
| document.getElementById('presetPanel').style.display = pieceMode[key]==='preset' ? 'block' : 'none'; | |
| const editor = document.getElementById('pieceGridEditor'); | |
| editor.style.display = pieceMode[key]==='draw' ? 'grid' : 'none'; | |
| editor.style.setProperty('--piece-color', pieceColorVar[key]); | |
| document.getElementById('presetPanel').style.setProperty('--piece-color', pieceColorVar[key]); | |
| renderPresetSelection(); | |
| renderDrawGrid(); | |
| } | |
| document.querySelectorAll('.slot-chip').forEach(chip=>{ | |
| chip.addEventListener('click', ()=> setActiveSlot(chip.dataset.slot)); | |
| }); | |
| // ---- mode tabs: ν리μ μμ μ ν (default) vs μ§μ 그리기, applies to the active slot ---- | |
| document.querySelectorAll('#modeTabs .mode-tab').forEach(tabBtn=>{ | |
| tabBtn.addEventListener('click', ()=>{ | |
| const mode = tabBtn.dataset.mode; | |
| if(pieceMode[activeSlot] === mode) return; | |
| pieceMode[activeSlot] = mode; | |
| document.querySelectorAll('#modeTabs .mode-tab').forEach(b=> b.classList.toggle('active', b===tabBtn)); | |
| document.getElementById('presetPanel').style.display = mode==='preset' ? 'block' : 'none'; | |
| const editor = document.getElementById('pieceGridEditor'); | |
| editor.style.display = mode==='draw' ? 'grid' : 'none'; | |
| if(mode==='draw') renderDrawGrid(); | |
| }); | |
| }); | |
| document.getElementById('clearActiveBtn').addEventListener('click', ()=>{ | |
| const key = activeSlot; | |
| pieceGrids[key] = Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0)); | |
| selectedPreset[key] = null; | |
| clearSolution(); | |
| renderSlotChip(key); | |
| renderPresetSelection(); | |
| renderDrawGrid(); | |
| }); | |
| 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)); | |
| selectedPreset[k] = null; | |
| pieceMode[k] = 'preset'; | |
| }); | |
| clearSolution(); | |
| renderBoard(); | |
| pieceKeys.forEach(renderSlotChip); | |
| setActiveSlot('a'); | |
| 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'); | |
| } | |
| } | |
| } | |
| } | |
| // refresh the single shared draw grid to show the active slot's shape | |
| function renderDrawGrid(){ | |
| for(let r=0;r<PIECE_GRID;r++){ | |
| for(let c=0;c<PIECE_GRID;c++){ | |
| const el = pieceCellEls[r][c]; | |
| el.dataset.key = activeSlot; | |
| el.setAttribute('aria-label', `${pieceNameKo[activeSlot]} μ‘°κ° ${r+1},${c+1}`); | |
| el.classList.toggle('on', !!pieceGrids[activeSlot][r][c]); | |
| } | |
| } | |
| } | |
| // called after a manual (draw-mode) edit changes pieceGrids[key] | |
| function renderPiece(key){ | |
| renderDrawGrid(); | |
| renderSlotChip(key); | |
| } | |
| function clearSolution(){ | |
| lastSolution = null; | |
| resultsBlock.style.display='none'; | |
| emptyNote.style.display='block'; | |
| continueBtn.classList.remove('show'); | |
| } | |
| renderBoard(); | |
| pieceKeys.forEach(renderSlotChip); | |
| setActiveSlot('a'); | |
| // ---- 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); | |
| } | |
| if(sol.steps.length > 0){ | |
| continueBtn.classList.add('show'); | |
| continueBtn.onclick = ()=> continueWithResult(sol); | |
| } else { | |
| continueBtn.classList.remove('show'); | |
| } | |
| setActiveStep(0); | |
| } | |
| // ---- take the solved final board as the new starting board, and clear piece editors | |
| // so the next 3 pieces can be drawn and solved right away ---- | |
| function continueWithResult(sol){ | |
| const last = sol.steps[sol.steps.length - 1]; | |
| board = last.boardAfterClear.map(row => row.slice()); | |
| pieceKeys.forEach(k=>{ | |
| pieceGrids[k] = Array.from({length:PIECE_GRID}, ()=>Array(PIECE_GRID).fill(0)); | |
| selectedPreset[k] = null; | |
| pieceMode[k] = 'preset'; | |
| }); | |
| clearSolution(); | |
| renderBoard(); | |
| pieceKeys.forEach(renderSlotChip); | |
| setActiveSlot('a'); | |
| statusLine.textContent = 'μ΄μ΄μ λ€μ μ‘°κ° 3κ°λ₯Ό μ νν΄μ£ΌμΈμ.'; | |
| emptyNote.textContent = 'μλμμ λ€μ μΈ μ‘°κ°μ κ³ λ₯΄κ³ ν΄λ΅ μ°ΎκΈ°λ₯Ό λλ₯΄λ©΄ μ¬κΈ° κ²°κ³Όκ° νμλ©λλ€.'; | |
| } | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |