<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>암산 on Ted Factory</title><link>https://tedfactory.com/tags/%EC%95%94%EC%82%B0/</link><description>Recent content in 암산 on Ted Factory</description><generator>Hugo</generator><language>ko</language><lastBuildDate>Tue, 21 Apr 2026 13:14:49 +0900</lastBuildDate><atom:link href="https://tedfactory.com/tags/%EC%95%94%EC%82%B0/index.xml" rel="self" type="application/rss+xml"/><item><title>암산 트랙</title><link>https://tedfactory.com/widgets/math-track/</link><pubDate>Tue, 21 Apr 2026 00:00:00 +0900</pubDate><guid>https://tedfactory.com/widgets/math-track/</guid><description>&lt;h1 id="암산-트랙"&gt;암산 트랙&lt;a class="anchor" href="#%ec%95%94%ec%82%b0-%ed%8a%b8%eb%9e%99"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;&lt;img src="https://tedfactory.com/images/widgets/math-track-cover.png" alt="사각 트랙 보드 위에 다양한 연산 칸과 빨간 폰, 흰색 주사위가 놓인 암산 트랙 게임 커버 이미지" /&gt;&lt;/p&gt;
&lt;p&gt;주사위를 굴려 사각 트랙 위의 연산 칸들을 돌며 암산을 누적하는 싱글플레이 게임입니다. &lt;strong&gt;도착한 칸의 연산만&lt;/strong&gt; 현재 값에 적용되고, 말이 출발지로 돌아왔을 때 최종 값을 입력합니다. 정답을 맞히면 다음 단계(Level)로 올라가며, 단계가 올라갈수록 연산 종류와 피연산자 범위가 넓어집니다.&lt;/p&gt;

&lt;style&gt;
 .mt-wrap {
 max-width: 640px;
 margin: 24px auto;
 font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
 color: #e8ecf4;
 user-select: none;
 }
 .mt-wrap *, .mt-wrap *::before, .mt-wrap *::after { box-sizing: border-box; }

 /* HUD */
 .mt-hud {
 display: flex;
 justify-content: space-around;
 align-items: center;
 padding: 10px 16px;
 background: rgba(30, 40, 70, 0.85);
 border-radius: 12px 12px 0 0;
 gap: 10px;
 flex-wrap: wrap;
 }
 .mt-hud-item { display: flex; align-items: center; gap: 6px; }
 .mt-hud-label { opacity: 0.7; font-size: 12px; letter-spacing: 1px; }
 .mt-hud-value { font-weight: 700; font-size: 18px; min-width: 28px; text-align: center; }

 /* Board */
 .mt-board-wrap {
 position: relative;
 background: rgba(20, 28, 52, 0.9);
 padding: 18px;
 }
 .mt-board {
 position: relative;
 width: 100%;
 aspect-ratio: 1 / 1;
 background: rgba(15, 22, 42, 0.8);
 border: 2px solid rgba(120, 160, 220, 0.3);
 border-radius: 8px;
 overflow: hidden;
 }
 .mt-lines {
 position: absolute;
 inset: 0;
 width: 100%;
 height: 100%;
 pointer-events: none;
 }
 .mt-line {
 stroke: rgba(120, 160, 220, 0.28);
 stroke-width: 0.35;
 fill: none;
 }
 .mt-line.diag {
 stroke: rgba(255, 220, 100, 0.22);
 stroke-width: 0.3;
 stroke-dasharray: 1.2 1.4;
 }

 .mt-cell {
 position: absolute;
 width: 13%;
 height: 13%;
 transform: translate(-50%, -50%);
 border-radius: 10px;
 background: rgba(50, 70, 110, 0.9);
 border: 2px solid rgba(120, 160, 220, 0.4);
 display: flex;
 align-items: center;
 justify-content: center;
 font-weight: 700;
 font-size: clamp(10px, 2.1vw, 15px);
 color: #eef3ff;
 transition: background 0.2s, box-shadow 0.2s, filter 0.2s;
 text-align: center;
 line-height: 1.1;
 letter-spacing: 0.3px;
 }
 .mt-cell.corner {
 background: rgba(80, 100, 150, 0.95);
 border-color: rgba(255, 220, 100, 0.55);
 width: 15%;
 height: 15%;
 }
 .mt-cell.start {
 background: linear-gradient(135deg, #3a7cc9, #5fa3d9);
 border-color: #9ad2ff;
 color: #fff;
 font-size: clamp(10px, 2.0vw, 13px);
 }
 .mt-cell.center {
 background: rgba(100, 120, 180, 0.95);
 border-color: rgba(255, 220, 100, 0.55);
 width: 15%;
 height: 15%;
 }
 .mt-cell.diag {
 background: rgba(65, 85, 130, 0.9);
 border-style: dashed;
 border-color: rgba(255, 220, 100, 0.4);
 }
 .mt-cell.visited {
 opacity: 0.55;
 filter: grayscale(0.25);
 }
 .mt-cell.current {
 box-shadow: 0 0 0 3px #ffe066, 0 0 18px rgba(255,224,102,0.65);
 z-index: 3;
 }
 .mt-cell.just-applied {
 animation: mtPulse 0.55s ease-out;
 }
 @keyframes mtPulse {
 0% { transform: translate(-50%, -50%) scale(1); }
 40% { transform: translate(-50%, -50%) scale(1.22); background: rgba(255, 224, 102, 0.85); color: #111; }
 100% { transform: translate(-50%, -50%) scale(1); }
 }

 /* Pawn — placed at the top-right corner of a cell so the centered
 operation text below remains fully visible. */
 .mt-pawn {
 position: absolute;
 width: 5%;
 height: 5%;
 border-radius: 50%;
 background: radial-gradient(circle at 30% 30%, #ffb3bb, #d13b4e 70%, #7a1d2c);
 border: 2px solid #fff;
 box-shadow: 0 2px 6px rgba(0,0,0,0.55), inset 0 0 6px rgba(255,255,255,0.35);
 transform: translate(-50%, -50%);
 transition: left 0.22s ease-in-out, top 0.22s ease-in-out;
 z-index: 10;
 pointer-events: none;
 }

 /* Controls */
 .mt-controls {
 display: flex;
 gap: 10px;
 padding: 12px 16px;
 background: rgba(30, 40, 70, 0.85);
 flex-wrap: wrap;
 align-items: center;
 }
 .mt-btn {
 padding: 10px 18px;
 border: none;
 border-radius: 8px;
 font-size: 14px;
 font-weight: 700;
 cursor: pointer;
 transition: background 0.15s, transform 0.1s, opacity 0.15s, filter 0.15s;
 font-family: inherit;
 }
 .mt-btn:active:not(:disabled) { transform: scale(0.97); }
 .mt-btn:disabled { opacity: 0.45; cursor: not-allowed; }
 .mt-btn-primary {
 background: linear-gradient(135deg, #4facfe, #00d2d6);
 color: #fff;
 flex: 1;
 min-width: 160px;
 }
 .mt-btn-primary:hover:not(:disabled) { filter: brightness(1.08); }
 .mt-btn-ghost {
 background: rgba(255,255,255,0.12);
 color: #e8ecf4;
 }
 .mt-btn-ghost:hover:not(:disabled) { background: rgba(255,255,255,0.22); }

 /* Dice (graphical SVG) */
 .mt-dice {
 width: 60px;
 height: 60px;
 flex: 0 0 60px;
 border-radius: 12px;
 cursor: pointer;
 background: transparent;
 border: none;
 padding: 0;
 filter: drop-shadow(0 3px 8px rgba(0,0,0,0.5));
 transition: transform 0.15s, filter 0.15s;
 }
 .mt-dice:hover:not(:disabled) { filter: drop-shadow(0 4px 12px rgba(255,224,102,0.5)); }
 .mt-dice:active:not(:disabled) { transform: scale(0.94); }
 .mt-dice:disabled { cursor: not-allowed; opacity: 0.85; }
 .mt-dice svg { width: 100%; height: 100%; display: block; }
 .mt-dice-bg {
 fill: url(#mtDiceGrad);
 stroke: #b8c5dc;
 stroke-width: 1.6;
 }
 .mt-dice-pip { fill: #1a1f2e; }
 .mt-dice-q {
 fill: #6b7a95;
 font-size: 34px;
 font-weight: 700;
 font-family: 'Segoe UI', system-ui, sans-serif;
 }
 .mt-dice.rolling {
 animation: mtDiceRoll 0.45s linear infinite;
 }
 .mt-dice.settle {
 animation: mtDiceSettle 0.32s ease-out;
 }
 @keyframes mtDiceRoll {
 0% { transform: rotate(0deg) scale(1); }
 50% { transform: rotate(180deg) scale(1.06); }
 100% { transform: rotate(360deg) scale(1); }
 }
 @keyframes mtDiceSettle {
 0% { transform: scale(1.18); }
 60% { transform: scale(0.95); }
 100% { transform: scale(1); }
 }

 /* Path log */
 .mt-path-log {
 background: rgba(30, 40, 70, 0.85);
 padding: 0;
 border-top: 1px solid rgba(255,255,255,0.06);
 }
 .mt-log-toggle {
 display: flex;
 width: 100%;
 align-items: center;
 gap: 8px;
 padding: 9px 16px;
 background: transparent;
 border: none;
 cursor: pointer;
 color: inherit;
 font-family: inherit;
 font-size: 11px;
 letter-spacing: 2px;
 opacity: 0.7;
 transition: opacity 0.15s, background 0.15s;
 }
 .mt-log-toggle:hover { opacity: 1; background: rgba(255,255,255,0.04); }
 .mt-log-arrow {
 display: inline-block;
 font-size: 9px;
 transition: transform 0.2s;
 width: 10px;
 text-align: center;
 }
 .mt-path-log:not(.collapsed) .mt-log-arrow { transform: rotate(90deg); }
 .mt-log-count {
 margin-left: auto;
 font-size: 11px;
 opacity: 0.6;
 letter-spacing: 0.5px;
 font-variant-numeric: tabular-nums;
 }
 .mt-log-items-wrap {
 max-height: 128px;
 overflow-y: auto;
 padding: 0 16px 12px;
 }
 .mt-path-log.collapsed .mt-log-items-wrap { display: none; }
 .mt-log-items {
 display: flex;
 flex-wrap: wrap;
 gap: 6px;
 align-items: center;
 font-size: 14px;
 font-family: 'Consolas', 'Menlo', monospace;
 }
 .mt-log-item {
 padding: 2px 8px;
 background: rgba(255,255,255,0.08);
 border-radius: 4px;
 white-space: nowrap;
 }
 .mt-log-item.start-tag { background: rgba(100, 170, 255, 0.25); color: #9ad2ff; }
 .mt-log-item.op-plus { color: #7fff7f; }
 .mt-log-item.op-minus { color: #ff9a9a; }
 .mt-log-item.op-times { color: #ffd27f; }
 .mt-log-item.op-div { color: #b79cff; }
 .mt-log-item.shortcut { background: rgba(255, 220, 100, 0.18); color: #ffe066; }

 /* Input panel */
 .mt-input-panel {
 background: rgba(30, 40, 70, 0.95);
 padding: 16px;
 border-radius: 0 0 12px 12px;
 text-align: center;
 border-top: 1px solid rgba(255,255,255,0.08);
 }
 .mt-input-label {
 font-size: 14px;
 opacity: 0.9;
 margin-bottom: 10px;
 }
 .mt-input-row {
 display: flex;
 gap: 8px;
 justify-content: center;
 align-items: center;
 flex-wrap: wrap;
 }
 .mt-input-row input {
 width: 160px;
 padding: 10px 14px;
 font-size: 20px;
 font-weight: 700;
 border: 2px solid rgba(255,255,255,0.25);
 border-radius: 8px;
 background: rgba(255,255,255,0.1);
 color: #fff;
 text-align: center;
 outline: none;
 font-family: 'Consolas', 'Menlo', monospace;
 }
 .mt-input-row input:focus { border-color: rgba(100,200,255,0.7); }
 .mt-result {
 margin-top: 10px;
 font-size: 15px;
 font-weight: 700;
 min-height: 22px;
 }
 .mt-result.correct { color: #7fff7f; }
 .mt-result.wrong { color: #ff9a9a; }

 /* Toast */
 .mt-toast {
 position: absolute;
 top: 10px;
 left: 50%;
 transform: translateX(-50%);
 background: rgba(0,0,0,0.78);
 color: #ffe066;
 padding: 6px 14px;
 border-radius: 20px;
 font-size: 13px;
 font-weight: 700;
 pointer-events: none;
 opacity: 0;
 transition: opacity 0.3s;
 z-index: 20;
 white-space: nowrap;
 }
 .mt-toast.show { opacity: 1; }

 /* Rounded bottom when input panel hidden */
 .mt-wrap.no-input .mt-path-log { border-radius: 0 0 12px 12px; }

 @media (max-width: 540px) {
 .mt-hud-value { font-size: 16px; }
 .mt-cell { font-size: clamp(9px, 2.6vw, 13px); }
 .mt-btn { padding: 9px 12px; font-size: 13px; }
 .mt-btn-primary { min-width: 120px; }
 .mt-input-row input { width: 120px; font-size: 18px; }
 }
&lt;/style&gt;

&lt;div class="mt-wrap no-input" id="mathTrack"&gt;
 &lt;div class="mt-hud"&gt;
 &lt;div class="mt-hud-item"&gt;
 &lt;span class="mt-hud-label"&gt;LV&lt;/span&gt;
 &lt;span class="mt-hud-value" id="mt-level"&gt;1&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="mt-hud-item"&gt;
 &lt;span class="mt-hud-label"&gt;ROLLS&lt;/span&gt;
 &lt;span class="mt-hud-value" id="mt-rolls"&gt;0&lt;/span&gt;
 &lt;/div&gt;
 &lt;/div&gt;

 &lt;div class="mt-board-wrap"&gt;
 &lt;div class="mt-board" id="mt-board"&gt;
 &lt;svg class="mt-lines" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"&gt;
 &lt;line class="mt-line" x1="92" y1="92" x2="92" y2="8"&gt;&lt;/line&gt;
 &lt;line class="mt-line" x1="92" y1="8" x2="8" y2="8"&gt;&lt;/line&gt;
 &lt;line class="mt-line" x1="8" y1="8" x2="8" y2="92"&gt;&lt;/line&gt;
 &lt;line class="mt-line" x1="8" y1="92" x2="92" y2="92"&gt;&lt;/line&gt;
 &lt;line class="mt-line diag" x1="92" y1="8" x2="8" y2="92"&gt;&lt;/line&gt;
 &lt;line class="mt-line diag" x1="8" y1="8" x2="92" y2="92"&gt;&lt;/line&gt;
 &lt;/svg&gt;
 &lt;div class="mt-pawn" id="mt-pawn"&gt;&lt;/div&gt;
 &lt;div class="mt-toast" id="mt-toast"&gt;&lt;/div&gt;
 &lt;/div&gt;
 &lt;/div&gt;

 &lt;div class="mt-controls"&gt;
 &lt;button type="button" class="mt-dice" id="mt-dice" aria-label="주사위"&gt;
 &lt;svg viewBox="0 0 60 60" preserveAspectRatio="xMidYMid meet" aria-hidden="true"&gt;
 &lt;defs&gt;
 &lt;linearGradient id="mtDiceGrad" x1="0" y1="0" x2="0" y2="1"&gt;
 &lt;stop offset="0%" stop-color="#ffffff"/&gt;
 &lt;stop offset="100%" stop-color="#dde6f3"/&gt;
 &lt;/linearGradient&gt;
 &lt;/defs&gt;
 &lt;rect class="mt-dice-bg" x="3" y="3" width="54" height="54" rx="11"&gt;&lt;/rect&gt;
 &lt;g id="mt-dice-pips"&gt;&lt;/g&gt;
 &lt;/svg&gt;
 &lt;/button&gt;
 &lt;button class="mt-btn mt-btn-primary" id="mt-roll-btn"&gt;주사위 굴리기&lt;/button&gt;
 &lt;button class="mt-btn mt-btn-ghost" id="mt-new-btn"&gt;새 보드&lt;/button&gt;
 &lt;/div&gt;

 &lt;div class="mt-path-log collapsed" id="mt-log-root"&gt;
 &lt;button type="button" class="mt-log-toggle" id="mt-log-toggle" aria-expanded="false" aria-controls="mt-log-items-wrap"&gt;
 &lt;span class="mt-log-arrow"&gt;▶&lt;/span&gt;
 &lt;span class="mt-log-label"&gt;PATH LOG&lt;/span&gt;
 &lt;span class="mt-log-count" id="mt-log-count"&gt;0&lt;/span&gt;
 &lt;/button&gt;
 &lt;div class="mt-log-items-wrap" id="mt-log-items-wrap"&gt;
 &lt;div class="mt-log-items" id="mt-log-items"&gt;&lt;/div&gt;
 &lt;/div&gt;
 &lt;/div&gt;

 &lt;div class="mt-input-panel" id="mt-input-panel" style="display:none;"&gt;
 &lt;div class="mt-input-label"&gt;출발지로 돌아왔습니다. 최종 값을 입력하세요.&lt;/div&gt;
 &lt;div class="mt-input-row"&gt;
 &lt;input type="number" id="mt-answer" placeholder="정수" autocomplete="off" inputmode="numeric" /&gt;
 &lt;button class="mt-btn mt-btn-primary" id="mt-submit-btn" style="flex:0 0 auto; min-width:80px;"&gt;제출&lt;/button&gt;
 &lt;/div&gt;
 &lt;div class="mt-result" id="mt-result"&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/div&gt;

&lt;script&gt;
(function() {
 const COORDS = {
 0: [92, 92],
 1: [92, 75.2], 2: [92, 58.4], 3: [92, 41.6], 4: [92, 24.8],
 5: [92, 8],
 6: [75.2, 8], 7: [58.4, 8], 8: [41.6, 8], 9: [24.8, 8],
 10: [8, 8],
 11: [8, 24.8], 12: [8, 41.6], 13: [8, 58.4], 14: [8, 75.2],
 15: [8, 92],
 16: [24.8, 92], 17: [41.6, 92], 18: [58.4, 92], 19: [75.2, 92],
 20: [78, 22], 21: [64, 36],
 22: [50, 50],
 23: [64, 64], 24: [78, 78],
 25: [22, 22], 26: [36, 36],
 27: [36, 64], 28: [22, 78]
 };

 const N_CELLS = 29;
 const START = 0, CORNER_A = 5, CORNER_B = 10, CORNER_C = 15, CENTER = 22;
 const CORNERS = new Set([CORNER_A, CORNER_B, CORNER_C]);
 const DIAG_CELLS = new Set([20, 21, 23, 24, 25, 26, 27, 28]);

 function levelConfig(lv) {
 if (lv &lt;= 1) return { ops: ['+', '-'], addMax: 9 };
 if (lv === 2) return { ops: ['+', '-'], addMax: 20 };
 if (lv === 3) return { ops: ['+', '-', '+', '-', '×'], addMax: 20, mulMax: 5 };
 if (lv === 4) return { ops: ['+', '-', '+', '-', '×', '÷'], addMax: 30, mulMax: 9, divMax: 5 };
 return { ops: ['+', '-', '+', '-', '×', '÷'], addMax: 50, mulMax: 12, divMax: 9 };
 }

 function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }

 function genOp(cfg) {
 const t = cfg.ops[randInt(0, cfg.ops.length - 1)];
 let n;
 if (t === '+' || t === '-') n = randInt(1, cfg.addMax);
 else if (t === '×') n = randInt(2, cfg.mulMax);
 else n = randInt(2, cfg.divMax);
 return { type: t, n: n };
 }

 function applyOp(v, op) {
 switch (op.type) {
 case '+': return v + op.n;
 case '-': return v - op.n;
 case '×': return v * op.n;
 case '÷': return Math.trunc(v / op.n);
 }
 return v;
 }

 function opText(op) { return op.type + op.n; }
 function opClass(op) {
 return { '+': 'op-plus', '-': 'op-minus', '×': 'op-times', '÷': 'op-div' }[op.type];
 }

 const SVG_NS = 'http://www.w3.org/2000/svg';
 const PIP_POS = {
 tl: [16, 16], tr: [44, 16],
 ml: [16, 30], mr: [44, 30],
 c: [30, 30],
 bl: [16, 44], br: [44, 44]
 };
 const PIP_SETS = {
 1: ['c'],
 2: ['tl', 'br'],
 3: ['tl', 'c', 'br'],
 4: ['tl', 'tr', 'bl', 'br'],
 5: ['tl', 'tr', 'c', 'bl', 'br'],
 6: ['tl', 'tr', 'ml', 'mr', 'bl', 'br']
 };

 let level = 1;
 let pos = START;
 let value = 0;
 let rolls = 0;
 let shortcutPending = null; // 'A' | 'B' | null
 let cells = [];
 let phase = 'idle'; // 'idle' | 'rolling' | 'moving' | 'input'
 let pathLog = [];

 let wrap, board, pawn, rollBtn, newBtn;
 let levelEl, diceEl, dicePipsEl, rollsEl, logEl, logScroll, logRoot, logToggleBtn, logCountEl;
 let inputPanel, answerInput, submitBtn, resultEl, toastEl;

 function init() {
 wrap = document.getElementById('mathTrack');
 board = document.getElementById('mt-board');
 pawn = document.getElementById('mt-pawn');
 rollBtn = document.getElementById('mt-roll-btn');
 newBtn = document.getElementById('mt-new-btn');
 levelEl = document.getElementById('mt-level');
 diceEl = document.getElementById('mt-dice');
 dicePipsEl = document.getElementById('mt-dice-pips');
 rollsEl = document.getElementById('mt-rolls');
 logEl = document.getElementById('mt-log-items');
 logScroll = document.getElementById('mt-log-items-wrap');
 logRoot = document.getElementById('mt-log-root');
 logToggleBtn = document.getElementById('mt-log-toggle');
 logCountEl = document.getElementById('mt-log-count');
 inputPanel = document.getElementById('mt-input-panel');
 answerInput = document.getElementById('mt-answer');
 submitBtn = document.getElementById('mt-submit-btn');
 resultEl = document.getElementById('mt-result');
 toastEl = document.getElementById('mt-toast');

 buildBoard();
 resetRun(false);

 rollBtn.addEventListener('click', onRoll);
 diceEl.addEventListener('click', onRoll);
 newBtn.addEventListener('click', onNewBoard);
 submitBtn.addEventListener('click', onSubmit);
 logToggleBtn.addEventListener('click', toggleLog);
 answerInput.addEventListener('keydown', function(e) {
 if (e.key === 'Enter') { e.preventDefault(); onSubmit(); }
 });
 }

 function clearDicePips() {
 while (dicePipsEl.firstChild) dicePipsEl.removeChild(dicePipsEl.firstChild);
 }

 function renderDiceFace(n) {
 clearDicePips();
 const positions = PIP_SETS[n] || [];
 positions.forEach(function(key) {
 const xy = PIP_POS[key];
 const pip = document.createElementNS(SVG_NS, 'circle');
 pip.setAttribute('cx', xy[0]);
 pip.setAttribute('cy', xy[1]);
 pip.setAttribute('r', 4.2);
 pip.setAttribute('class', 'mt-dice-pip');
 dicePipsEl.appendChild(pip);
 });
 }

 function renderDiceQuestion() {
 clearDicePips();
 const t = document.createElementNS(SVG_NS, 'text');
 t.setAttribute('x', 30);
 t.setAttribute('y', 41);
 t.setAttribute('text-anchor', 'middle');
 t.setAttribute('class', 'mt-dice-q');
 t.textContent = '?';
 dicePipsEl.appendChild(t);
 }

 function buildBoard() {
 cells.forEach(function(c) { if (c.el) c.el.remove(); });
 cells = [];
 const cfg = levelConfig(level);

 for (let i = 0; i &lt; N_CELLS; i++) {
 const coord = COORDS[i];
 const el = document.createElement('div');
 el.className = 'mt-cell';
 if (i === START) el.classList.add('start');
 else if (CORNERS.has(i)) el.classList.add('corner');
 else if (i === CENTER) el.classList.add('center');
 else if (DIAG_CELLS.has(i)) el.classList.add('diag');

 el.style.left = coord[0] + '%';
 el.style.top = coord[1] + '%';

 let op = null;
 if (i === START) {
 el.textContent = 'START';
 } else {
 op = genOp(cfg);
 el.textContent = opText(op);
 }
 board.appendChild(el);
 cells.push({ idx: i, op: op, el: el });
 }
 }

 function resetRun(regenerateBoard) {
 phase = 'idle';
 pos = START;
 value = 0;
 rolls = 0;
 shortcutPending = null;
 pathLog = [];
 diceEl.classList.remove('rolling', 'settle');
 diceEl.disabled = false;
 renderDiceQuestion();
 rollsEl.textContent = '0';
 levelEl.textContent = level;
 resultEl.textContent = '';
 resultEl.className = 'mt-result';
 inputPanel.style.display = 'none';
 wrap.classList.add('no-input');
 answerInput.value = '';
 rollBtn.disabled = false;
 rollBtn.textContent = '주사위 굴리기';

 if (regenerateBoard) buildBoard();
 clearVisitedHighlights();
 updatePawn(false);
 pushLog({ type: 'start' });
 }

 function clearVisitedHighlights() {
 cells.forEach(function(c) {
 c.el.classList.remove('current', 'visited', 'just-applied');
 });
 if (cells[START]) cells[START].el.classList.add('current');
 }

 const PAWN_OFFSET_X = 4.2;
 const PAWN_OFFSET_Y = -4.2;

 function updatePawn(animate) {
 const coord = COORDS[pos];
 if (!animate) pawn.style.transition = 'none';
 pawn.style.left = (coord[0] + PAWN_OFFSET_X) + '%';
 pawn.style.top = (coord[1] + PAWN_OFFSET_Y) + '%';
 if (!animate) {
 void pawn.offsetWidth;
 pawn.style.transition = '';
 }
 }

 function standardNext(p) {
 if (p === 20) return 21;
 if (p === 21) return CENTER;
 if (p === 25) return 26;
 if (p === 26) return CENTER;
 if (p === CENTER) return 23;
 if (p === 23) return 24;
 if (p === 24) return START;
 if (p === 27) return 28;
 if (p === 28) return CORNER_C;
 if (p &gt;= 0 &amp;&amp; p &lt; 19) return p + 1;
 if (p === 19) return START;
 return START;
 }

 function nextStep(p) {
 if (p === CORNER_A &amp;&amp; shortcutPending === 'A') { shortcutPending = null; return 20; }
 if (p === CORNER_B &amp;&amp; shortcutPending === 'B') { shortcutPending = null; return 25; }
 return standardNext(p);
 }

 function pushLog(entry) {
 pathLog.push(entry);
 renderLog();
 }

 function renderLog() {
 logEl.innerHTML = '';
 pathLog.forEach(function(e) {
 const span = document.createElement('span');
 span.className = 'mt-log-item';
 if (e.type === 'start') {
 span.classList.add('start-tag');
 span.textContent = 'START · 0';
 } else if (e.type === 'op') {
 span.classList.add(opClass(e.op));
 span.textContent = opText(e.op);
 } else if (e.type === 'shortcut') {
 span.classList.add('shortcut');
 span.textContent = '↘ shortcut';
 } else if (e.type === 'center-pass') {
 span.classList.add('shortcut');
 span.textContent = '✦ CENTER';
 } else if (e.type === 'end') {
 span.classList.add('start-tag');
 span.textContent = '⟲ BACK';
 }
 logEl.appendChild(span);
 });
 logCountEl.textContent = pathLog.length;
 logScroll.scrollTop = logScroll.scrollHeight;
 }

 function toggleLog() {
 const willCollapse = !logRoot.classList.contains('collapsed');
 logRoot.classList.toggle('collapsed', willCollapse);
 logToggleBtn.setAttribute('aria-expanded', String(!willCollapse));
 if (!willCollapse) {
 logScroll.scrollTop = logScroll.scrollHeight;
 }
 }

 function showToast(text, ms) {
 toastEl.textContent = text;
 toastEl.classList.add('show');
 clearTimeout(showToast._t);
 showToast._t = setTimeout(function() { toastEl.classList.remove('show'); }, ms || 1200);
 }

 function onRoll() {
 if (phase !== 'idle') return;
 phase = 'rolling';
 rollBtn.disabled = true;
 diceEl.disabled = true;

 diceEl.classList.remove('settle');
 diceEl.classList.add('rolling');
 let ticks = 0;
 const maxTicks = 9;
 const tick = setInterval(function() {
 renderDiceFace(randInt(1, 6));
 ticks++;
 if (ticks &gt;= maxTicks) {
 clearInterval(tick);
 const d = randInt(1, 6);
 renderDiceFace(d);
 diceEl.classList.remove('rolling');
 diceEl.classList.add('settle');
 setTimeout(function() { diceEl.classList.remove('settle'); }, 320);
 rolls++;
 rollsEl.textContent = rolls;
 movePawn(d);
 }
 }, 70);
 }

 function movePawn(steps) {
 phase = 'moving';
 let remaining = steps;
 let reachedStart = false;
 // True only while a single dice move is passing through CENTER mid-stride.
 // When set, the very next step from CENTER diverts onto the BL diagonal
 // (27 → 28 → CORNER_C) instead of the standard BR diagonal (23 → 24).
 let centerPassthrough = false;

 function stepOnce() {
 const prev = pos;
 let nxt;
 if (prev === CENTER &amp;&amp; centerPassthrough) {
 nxt = 27;
 centerPassthrough = false;
 } else {
 nxt = nextStep(prev);
 }
 cells[prev].el.classList.remove('current');
 if (prev !== START) cells[prev].el.classList.add('visited');
 pos = nxt;
 cells[nxt].el.classList.add('current');
 updatePawn(true);

 if (nxt === START) {
 reachedStart = true;
 setTimeout(finishMove, 260);
 return;
 }

 remaining--;

 if (nxt === CENTER &amp;&amp; remaining &gt; 0) {
 centerPassthrough = true;
 pushLog({ type: 'center-pass' });
 showToast('중앙 통과 → 좌하단 경로!');
 }

 if (remaining &gt; 0) setTimeout(stepOnce, 220);
 else setTimeout(finishMove, 260);
 }

 function finishMove() {
 if (reachedStart) {
 pushLog({ type: 'end' });
 enterInputPhase();
 return;
 }
 const cell = cells[pos];
 if (cell.op) {
 value = applyOp(value, cell.op);
 pushLog({ type: 'op', op: cell.op });
 cell.el.classList.add('just-applied');
 setTimeout(function() { cell.el.classList.remove('just-applied'); }, 550);
 }
 if (pos === CORNER_A) {
 shortcutPending = 'A';
 pushLog({ type: 'shortcut' });
 showToast('지름길 진입!');
 } else if (pos === CORNER_B) {
 shortcutPending = 'B';
 pushLog({ type: 'shortcut' });
 showToast('지름길 진입!');
 }
 phase = 'idle';
 rollBtn.disabled = false;
 diceEl.disabled = false;
 }

 stepOnce();
 }

 function enterInputPhase() {
 phase = 'input';
 rollBtn.disabled = true;
 diceEl.disabled = true;
 inputPanel.style.display = 'block';
 wrap.classList.remove('no-input');
 answerInput.value = '';
 resultEl.textContent = '';
 resultEl.className = 'mt-result';
 setTimeout(function() { answerInput.focus(); }, 60);
 }

 function onSubmit() {
 if (phase !== 'input') return;
 const raw = answerInput.value.trim();
 if (raw === '' || !/^-?\d+$/.test(raw)) {
 resultEl.textContent = '정수를 입력하세요.';
 resultEl.className = 'mt-result wrong';
 return;
 }
 const user = parseInt(raw, 10);
 if (user === value) {
 resultEl.textContent = '정답! 최종 값 = ' + value + ' · Lv ' + (level + 1) + '(으)로 진행합니다.';
 resultEl.className = 'mt-result correct';
 level++;
 setTimeout(function() { resetRun(true); }, 1700);
 } else {
 resultEl.textContent = '오답. 정답은 ' + value + ' 입니다. 같은 보드로 다시 도전합니다.';
 resultEl.className = 'mt-result wrong';
 setTimeout(function() { resetRun(false); }, 2400);
 }
 }

 function onNewBoard() {
 if (phase === 'moving' || phase === 'rolling') return;
 resetRun(true);
 }

 if (document.readyState === 'loading') {
 document.addEventListener('DOMContentLoaded', init);
 } else {
 init();
 }
})();
&lt;/script&gt;

&lt;hr&gt;
&lt;h2 id="규칙"&gt;규칙&lt;a class="anchor" href="#%ea%b7%9c%ec%b9%99"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;시작 시 값은 &lt;code&gt;0&lt;/code&gt; 이고 말은 &lt;strong&gt;출발지(START)&lt;/strong&gt; 에 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;주사위(1~6)&lt;/strong&gt; 를 굴려 나온 눈만큼 한 칸씩 이동합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도착한 칸의 연산만&lt;/strong&gt; 현재 값에 적용됩니다. 지나친 칸은 적용되지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;상단 좌우 코너&lt;/strong&gt;(우상단 / 좌상단)에 정확히 도착하면, 다음 굴림부터 자동으로 대각선 &lt;strong&gt;지름길&lt;/strong&gt;을 탑니다 (항상 출발지까지 더 짧은 경로).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;중앙(CENTER)&lt;/strong&gt; 통과 규칙:
&lt;ul&gt;
&lt;li&gt;중앙에 &lt;strong&gt;정확히 도착해서 멈추면&lt;/strong&gt;, 다음 굴림부터 우하단 대각선(&lt;code&gt;23 → 24 → START&lt;/code&gt;)을 따라 &lt;strong&gt;출발지로 직행&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;중앙을 &lt;strong&gt;그냥 지나치면&lt;/strong&gt;(이번 굴림에 남은 칸이 있다면), 좌하단 대각선(&lt;code&gt;27 → 28 → 좌하단 코너&lt;/code&gt;)으로 빠져나가 외곽을 돌아 출발지로 갑니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;말이 출발지로 돌아오면(또는 넘어서면) 최종 값 입력 모드로 전환됩니다.&lt;/li&gt;
&lt;li&gt;정답을 맞히면 다음 단계(Level)로, 틀리면 같은 보드에서 다시 도전합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="계산-규칙"&gt;계산 규칙&lt;a class="anchor" href="#%ea%b3%84%ec%82%b0-%ea%b7%9c%ec%b9%99"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;누적값은 화면에 표시되지 않습니다.&lt;/strong&gt; 경로 로그의 연산 순서만 보고 암산으로 추적하세요.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;÷&lt;/code&gt; 연산은 &lt;strong&gt;정수 몫(소수점 버림)&lt;/strong&gt; 입니다. 예: &lt;code&gt;7 ÷ 3 = 2&lt;/code&gt;, &lt;code&gt;-7 ÷ 3 = -2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;뺄셈과 곱셈으로 &lt;strong&gt;음수&lt;/strong&gt; 가 자연스럽게 나올 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="단계level"&gt;단계(Level)&lt;a class="anchor" href="#%eb%8b%a8%ea%b3%84level"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Lv&lt;/th&gt;
 &lt;th&gt;연산&lt;/th&gt;
 &lt;th&gt;피연산자 범위&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;1&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1 ~ 9&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1 ~ 20&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;3&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;×&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1 ~ 20 (×는 2 ~ 5)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;4&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;×&lt;/code&gt;, &lt;code&gt;÷&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1 ~ 30 (×는 2 ~ 9, ÷는 2 ~ 5)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;5+&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;×&lt;/code&gt;, &lt;code&gt;÷&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1 ~ 50 (×는 2 ~ 12, ÷는 2 ~ 9)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="팁"&gt;팁&lt;a class="anchor" href="#%ed%8c%81"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;경로 로그에는 지나온 연산과 지름길 진입 기록이 차례로 쌓입니다. 중간에 감을 잃었을 때 되짚어 보세요.&lt;/li&gt;
&lt;li&gt;너무 복잡한 연산 배치가 걸렸다 싶으면 &lt;strong&gt;&amp;ldquo;새 보드&amp;rdquo;&lt;/strong&gt; 로 연산 배치를 다시 뽑을 수 있습니다 (단계는 유지).&lt;/li&gt;
&lt;li&gt;곱셈·나눗셈이 섞이면 작은 수부터 빠르게 계산하는 습관을 들이세요. 예를 들어 &lt;code&gt;× 7&lt;/code&gt; 뒤에 &lt;code&gt;÷ 2&lt;/code&gt; 가 오면, 가능하면 먼저 간단히 만들 수 있는 묶음을 찾습니다.&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>