GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

스도쿠 9종 — 후보 소거부터 해가 하나뿐인 판 만들기까지

스도쿠는 정말 찍어서 푸는 게임이 아니다 — 01번은 칸마다 남은 후보를 비트 하나씩으로 지우고, 03번은 후보가 가장 적은 칸부터 되짚어 풀고, 04번은 구멍을 뚫을 때마다 풀이가 하나뿐인지 세어 본다.

스도쿠는 빈칸에 숫자를 찍어 보는 게임이 아니라, 줄·세로줄·3×3 상자에서 이미 나온 숫자를 지워 남는 후보로 푸는 계산이다 — 스도쿠 게임을 만들려면 후보를 어떤 값으로 들고 있을지부터 정한다

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 장면은 같은 판 모델 하나를 쓰고, 장면마다 그 판에서 읽는 값만 다르다. 후보를 칸마다 따로 적어 두지 않고 판 배열에서 그때그때 계산하면, 숫자가 바뀌는 순간 후보도 같이 바뀐다.

후보는 9비트 한 수다

판은 81칸 숫자 배열(0 은 빈칸)이고, 숫자 d 는 (d−1) 번째 비트로 적는다. 전체는 ALL 511 이라 후보 집합이 곧 정수 하나이고, 남은 후보 수는 켜진 비트를 세는 popcount 가 알려 준다.

난수 없이 같은 판이 다시 나온다

판은 정수 해시 hash2(a, b) 로 섞은 완성판에서 구멍을 뚫어 만든다. 같은 씨앗이면 같은 판이 나오고, 시간은 장면 시계 하나를 고정 1/60초 걸음으로 나눠 쓴다.

    const ALL = 511;
    function bit(d) { return 1 << (d - 1); }
    function popcount(m) {
        let c = 0;
        let v = m;
        while (v) {
            v &= v - 1;
            c += 1;
        }
        return c;
    }

스도쿠 9종 — 후보 소거 지도·하나뿐인 후보 연쇄 채우기·후보 적은 칸부터 되짚기·해가 하나인 판 만들기·겹친 숫자 표시·이유를 보여 주는 힌트·숫자별 남은 개수·완성 물결·난이도 점수

아홉 장면을 렌더해 잰 면적과 프레임

장면 누적면적 움직인 프레임 강도 평균
01 후보 소거 지도 21.3% 23/23 46.1
02 연쇄 채우기 24.6% 23/23 40.3
03 후보 적은 칸 되짚기 12.4% 21/23 96.7
04 구멍 파기 20.9% 21/23 29.7
05 겹침 표시 17.1% 14/23 42.8
06 소거 근거 힌트 8.2% 9/23 58.9
07 남은 개수 링 20.8% 18/23 53.9
08 완성 물결 31.9% 17/23 92.5
09 난이도 점수 20.4% 21/23 123.8

누적면적이 가장 넓은 것은 08 의 31.9% 이고 가장 좁은 것은 06 의 8.2% 다. 움직인 프레임은 01·02 가 23/23 이고 06 이 9/23 으로 가장 적으며, 강도 평균은 09 가 123.8 로 가장 높고 04 가 29.7 로 가장 낮다.

01칸마다 남은 후보를 비트로 지우는 소거 지도

칸마다 1~9 를 9비트 하나로 들고 같은 줄·세로줄·상자에 놓인 숫자의 비트를 OR 로 모아 빼므로, 메모 후보를 자동으로 채우고 지우는 일에 쓴다. 칸 색은 후보가 하나면 민트, 일곱 개 이상이면 바탕색이다.

칸마다 남은 후보를 비트로 지우는 소거 지도
새 창에서 열기

01 · 칸마다 남은 후보를 비트로 지우는 소거 지도 — 측정 누적면적 21.349% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

candMaskPEERSpopcount
    // 칸 i 가 속한 kind 묶음에서 i 를 뺀 칸들이 이미 쓴 숫자의 비트 합(OR)
    function usedMask(b, i, kind) {
        const u = UNITS[unitIndex(kind, i)];
        let m = 0;
        for (let k = 0; k < 9; k += 1) {
            if (u[k] !== i && b[u[k]] !== 0) {
                m |= bit(b[u[k]]);
            }
        }
        return m;
    }
    // 후보 = 9비트 전체에서 세 묶음이 쓴 비트를 뺀 것. 채워진 칸의 후보는 0
    function candMask(b, i) {
        if (b[i] !== 0) {
            return 0;
        }
        return ALL & ~(usedMask(b, i, 0) | usedMask(b, i, 1) | usedMask(b, i, 2));
    }

네 판의 빈칸 188칸 전부에서 이 비트 계산이 집합으로 센 후보와 일치했고, 첫 판은 시연이 놓는 8칸을 채우면 빈칸이 47 에서 39 로, 칸당 평균 후보가 3.15 에서 2.28 로 줄었다.

02하나뿐인 후보를 채우면 이어서 풀리는 연쇄

후보가 정확히 하나인 칸을 채우고 후보를 다시 세는 일을 되풀이하며, 사람이 막혔을 때 다음 한 칸을 짚어 주는 화면에 쓴다.

하나뿐인 후보를 채우면 이어서 풀리는 연쇄
새 창에서 열기

02 · 하나뿐인 후보를 채우면 이어서 풀리는 연쇄 — 측정 누적면적 24.627% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

singlesFillorderisStuck
    function singlesFill(start) {
        const b = start.slice();
        const order = [];
        const digits = [];
        const avail = [];
        for (;;) {
            let first = -1;
            let n = 0;
            for (let i = 0; i < 81; i += 1) {
                if (b[i] === 0 && popcount(candMask(b, i)) === 1) {
                    if (first < 0) {
                        first = i;
                    }
                    n += 1;
                }
            }
            if (first < 0) {
                break;
            }
            const d = lowDigit(candMask(b, first));
            b[first] = d;
            order.push(first);
            digits.push(d);
            avail.push(n);
        }

단서 32 인 네 판에서 채운 칸은 27·20·19·17 개로 모두 정답과 같았지만, 네 판 모두 하나뿐인 후보만으로는 끝까지 풀리지 않고 멈춰 후보가 가장 적은 칸에 붉은 테가 남는다.

03후보가 가장 적은 칸부터 되짚는 탐색

하나뿐인 후보로 풀리지 않으면 후보가 가장 적은 칸에 숫자를 걸고 막히면 되돌려, 풀이가 안 보이는 어려운 판을 끝까지 푸는 데 쓴다. 걸린 숫자 수는 NODE_CAP 400 에서 끊는다.

후보가 가장 적은 칸부터 되짚는 탐색
새 창에서 열기

03 · 후보가 가장 적은 칸부터 되짚는 탐색 — 측정 누적면적 12.417% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

NODE_CAPsearchdead
            let mm = bm;
            while (mm) {
                const lo = mm & -mm;
                mm ^= lo;
                const d = 32 - Math.clz32(lo);
                r.nodes += 1;
                if (r.nodes > cap) {
                    r.capHit = true;
                    return;
                }
                b[best] = d;
                rm[rowOf(best)] |= lo;
                cm[colOf(best)] |= lo;
                xm[boxOf(best)] |= lo;
                if (wantTrace) {
                    r.trace.push((best << 4) | d);
                }
                rec();

단서 24~25 인 네 판은 후보 적은 칸 먼저로 노드 150·167·173·244 개에 풀렸고 앞쪽 빈칸부터 푸는 순서는 네 판 모두 한도 20,000 에 닿아 20,001 노드에서 멈췄다.

04구멍을 뚫을 때마다 풀이가 하나인지 세는 판 만들기

완성판에서 한 칸씩 비우고 풀이 개수를 2 까지만 세어, 하나면 비운 채로 두고 둘이면 숫자를 되돌리는 방식이라 풀이가 하나뿐인 새 판을 계속 내보낼 때 쓴다.

구멍을 뚫을 때마다 풀이가 하나인지 세는 판 만들기
새 창에서 열기

04 · 구멍을 뚫을 때마다 풀이가 하나인지 세는 판 만들기 — 측정 누적면적 20.873% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CLUESdigBoardsearch
    function digBoard(sol, seed, target, singlesOnly) {
        const puz = sol.slice();
        const order = perm(81, seed * 31 + 5);
        const steps = [];
        let clues = 81;
        for (let k = 0; k < 81; k += 1) {
            if (clues <= target) {
                break;
            }
            const c = order[k];
            const d = puz[c];
            puz[c] = 0;
            const s = search(puz, 2, 6000, false, false);
            const cnt = s.capHit ? 2 : Math.min(2, s.count);
            const ok = cnt === 1 && (!singlesOnly || singlesFill(puz).solved);
            steps.push({ cell: c, cnt: cnt, ok: ok, nodes: s.nodes });
            if (ok) {
                clues -= 1;
            }
            else {
                puz[c] = d;
            }
        }
        return { puz: puz, steps: steps, clues: clues };
    }

CLUES 36 으로 판 네 개를 파면 시도가 47·46·45·45 번이고 되돌린 칸은 2·1·0·0 이며, 네 판 모두 단서 36 에 풀이 개수 1 로 끝났다.

05겹친 숫자를 같은 줄·상자에 표시하는 충돌 표시

놓은 숫자가 같은 줄·세로줄·상자에 이미 있으면 겹친 두 칸을 장미색으로 칠해 틀린 입력을 바로 짚어 준다. 표시하는 것은 규칙을 어긴 입력뿐이고 풀 수 없는 판인지는 03번 탐색이 따로 가린다.

겹친 숫자를 같은 줄·상자에 표시하는 충돌 막대
새 창에서 열기

05 · 겹친 숫자를 같은 줄·상자에 표시하는 충돌 막대 — 측정 누적면적 17.101% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

conflictssharedKindtintUnit
    // 놓은 숫자가 겹치는 쌍 — [칸, 칸, 묶음 종류]
    function conflicts(b) {
        const out = [];
        for (let i = 0; i < 81; i += 1) {
            if (b[i] === 0) {
                continue;
            }
            for (let k = 0; k < 20; k += 1) {
                const p = PEERS[i][k];
                if (p > i && b[p] === b[i]) {
                    out.push([i, p, sharedKind(i, p)]);
                }
            }
        }
        return out;
    }

판마다 입력 8번 중 규칙 위반은 4번이고, 틀린 입력은 0.42초 뒤 지워진다.

06이유까지 보여 주는 힌트 — 어느 줄이 후보를 지웠나

힌트가 정답 숫자만 알리지 않고 그 칸의 후보를 지운 줄·세로줄·상자를 차례로 밝혀, 왜 그 숫자인지 배우게 하는 가르치는 화면에 쓴다.

이유까지 보여 주는 힌트 — 어느 줄이 후보를 지웠나
새 창에서 열기

06 · 이유까지 보여 주는 힌트 — 어느 줄이 후보를 지웠나 — 측정 누적면적 8.234% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

computeHintwhysleftMask
    function computeHint(c) {
        hintCell = c;
        whys = [];
        if (g[c] !== 0) {
            leftMask = 0;
            hintCell = -1;
            return;
        }
        const cand = candMask(g, c);
        leftMask = cand;
        let n = 0;
        for (let d = 1; d <= 9; d += 1) {
            if ((cand & bit(d)) !== 0) {
                continue;
            }
            const srcs = [-1, -1, -1];
            for (let kind = 0; kind < 3; kind += 1) {
                const u = UNITS[unitIndex(kind, c)];
                for (let k = 0; k < 9; k += 1) {
                    if (u[k] !== c && g[u[k]] === d) {
                        srcs[kind] = u[k];
                    }
                }
            }
            let pick = -1;

네 판의 목표 칸은 59·41·20·48 번이고 후보에서 빠진 숫자 근거가 칸마다 8개씩, 줄·세로줄·상자 세 종류가 모두 나왔으며 마지막에 한 비트가 남아 그 숫자가 칸에 나타난다.

07숫자별 남은 개수를 링으로 줄이는 키패드

1~9 키마다 아직 놓지 않은 개수를 링 호로 그리고 9개를 다 놓으면 키가 흐려져, 어떤 숫자가 몇 개 남았는지 한눈에 보는 입력 패널을 만든다.

숫자별 남은 개수를 링으로 줄이는 키패드
새 창에서 열기

07 · 숫자별 남은 개수를 링으로 줄이는 키패드 — 측정 누적면적 20.815% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

leftOfselDigitARC_RATE
    function leftOf(d) {
        let c = 0;
        for (let i = 0; i < 81; i += 1) {
            if (g[i] === d) {
                c += 1;
            }
        }
        return 9 - c;
    }

호는 한 번에 줄지 않고 ARC_RATE 9 로 목표 길이를 따라가며, 첫 판의 시작 남은 개수는 [1,4,2,5,4,7,5,5,4] 로 합이 37 이다. 시연은 남은 개수가 가장 적은 숫자 셋의 빈칸을 채워 세 판에서 7·9·7 칸을 놓는다.

08완성하면 칸마다 시간차로 번지는 물결

판이 다 차고 겹침이 없으면 마지막 칸에서 맨해튼 거리가 먼 칸일수록 늦게 밝아지는 물결이 번져 풀었을 때 보상을 주는 마무리 화면이 되고, 시간차는 WAVE_MS 곱하기 거리다.

완성하면 칸마다 시간차로 번지는 물결
새 창에서 열기

08 · 완성하면 칸마다 시간차로 번지는 물결 — 측정 누적면적 31.885% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

WAVE_MSdistOfwaveP
    function waveP(i) {
        if (solvedT < 0) {
            return -1;
        }
        return (sceneT - solvedT - distOf(i) * WAVE_MS / 1000) / PULSE_S;
    }

세 판에서 마지막 칸으로부터 가장 먼 칸의 거리는 12·12·11 이고, WAVE_MS 40 이면 그 지연이 480·480·440ms 다. 물결 한 번은 0.55초이니 마지막 칸의 지연까지 합치면 약 1.03초면 끝난다.

09필요한 기법으로 재는 난이도 점수 막대

하나뿐인 후보만으로 풀리면 쉬움, 걸어 보아야 하면 보통, 막다른 길이 3번 넘으면 어려움으로 점수를 매겨 판마다 난이도 이름을 붙인다.

필요한 기법으로 재는 난이도 점수 막대
새 창에서 열기

09 · 필요한 기법으로 재는 난이도 점수 막대 — 측정 누적면적 20.439% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rateOfbranchclueScore
    // 점수 — 하나뿐인 후보만으로 풀리면 0~39, 걸어 보아야 하면 40~59, 되짚기 막다른 길이 3번 넘으면 60 이상
    function rateOf(puz, solution) {
        const chain = singlesFill(puz);
        const clues = filledOf(puz);
        let score = 0;
        let dead = 0;
        let branch = 0;
        if (chain.solved) {
            score = Math.min(39, 8 + Math.floor((81 - clues) / 1.6));
        }
        else {
            const s = search(puz, 1, 3000, false, false);
            dead = s.dead;
            branch = s.branch;
            score = dead <= 2 ? 40 + Math.min(19, branch * 2 + dead * 4) : 60 + Math.min(39, dead);
        }
        // 단서 수만 본 점수 — 단서가 적을수록 어렵다고 매긴다
        const clueScore = clamp(Math.round((46 - clues) * 5), 0, 99);
        return { puz: puz, sol: solution, clues: clues, chain: chain, level: levelOf(score), score: score, clueScore: clueScore, clueLevel: levelOf(clueScore), dead: dead, branch: branch };
    }

단서 28·40·32·24 인 네 판의 기법 점수는 39·33·46·85 이고 단서만 본 점수는 90·30·70·99 다. 판 1 은 단서로는 어려움인데 기법으로는 쉬움이고 판 3 은 단서로는 어려움인데 기법으로는 보통이라 두 판정이 어긋난다.

스도쿠 규칙을 코드로 옮기면 칸 하나의 후보는 어떤 값 몇 개로 정해질까?

판 배열과 묶음 27개가 전부다

칸 하나의 후보는 그 칸이 속한 줄·세로줄·상자 세 묶음이 쓴 숫자의 비트 합으로 정해진다. 묶음은 줄 9·세로줄 9·상자 9 로 27개이고, 한 칸의 이웃은 같은 묶음의 다른 칸 20개다.

값 뜻 바꾸는 곳
g 판 상태(0 = 빈칸) putDigit
PEERS[i] 칸 i 의 이웃 20칸 소거·겹침 검사
candMask 칸의 후보 비트 01·02·03·06

하나의 판에서 여러 계산이 파생된다

같은 판 배열을 놓고 singlesFill 은 연쇄를, search 는 되짚기를, conflicts 는 겹침을 계산한다. 장면이 아홉 개여도 후보를 만드는 규칙은 하나라 어느 장면의 판이든 다른 장면의 함수로 검사할 수 있다.

판을 81개 숫자 배열로 두고, 줄·세로줄·상자의 사용 숫자를 모아 후보를 지우고, 하나만 남은 칸을 채워 다시 지우는 — 스도쿠 한 수가 풀리는 순서

배열이 먼저, 화면은 뒤따른다

숫자가 들어오면 판 배열이 먼저 바뀌고 후보와 겹침은 그 배열에서 다시 계산된다. 화면의 색과 막대는 계산된 값을 읽어 그릴 뿐이라 보간 도중에 다시 입력해도 판 상태는 어긋나지 않는다.

막히면 걸어 본다

하나뿐인 후보가 바닥나면 후보가 가장 적은 칸에 숫자를 걸고 이어 가다 후보가 0 인 칸이 나오면 한 단계 되돌린다. 걸어 본 숫자 수는 nodes, 막다른 길은 dead 로 세므로 장면마다 얼마나 어려운 판이었는지 숫자로 읽힌다.

스도쿠 사이트처럼 실제 화면에 붙일 때 — 라이브러리 없이 만들기·터치와 키보드 입력·판을 만드는 시점

라이브러리 없이 만들기

아홉 장면은 라이브러리·WebGL·CDN 을 쓰지 않고 소스 한 파일에 계산 함수와 시연을 같이 담았다.

터치와 키보드 입력

무대 상자는 role="application" 에 tabindex 0 이라 포커스를 받고, 방향키로 칸을 옮기며 1~9 가 숫자, Delete·Backspace·0 이 지우기, H 가 힌트다. 포인터는 pointerdown·pointermove·pointerup·pointercancel 과 setPointerCapture 로 받고 무대에 touch-action: none 을 걸어 누르기 중 페이지가 같이 스크롤되지 않게 한다.

판을 만드는 시점

사용자가 판을 받기 전에 04 의 방식으로 구멍을 뚫고 풀이 개수가 1 인지 확인해 두는 편이 안전하다. 판을 만드는 일이 무거우면 상한을 두거나 웹 워커로 옮긴다.

지뢰찾기·슬라이딩 퍼즐 편과 다른 점

슬라이딩 퍼즐 편은 풀 수 있는 판을 가리는 홀짝 판정과 맨해튼 거리 탐색을 다루고, 이 편은 칸마다 후보 집합을 지우고 풀이가 하나뿐인지 세는 일을 다룬다. 격자 위에서 이웃 칸으로 퍼지는 계산이 궁금하면 지뢰찾기 편도 같은 틀을 쓴다.

스도쿠가 깨지는 함정 — 풀이가 둘 이상인 판이 나오는 일·되짚기가 너무 깊어져 화면이 멈추는 일·메모 후보가 입력 뒤에 낡은 채 남는 일

풀이가 둘 이상인 판이 나온다

구멍을 아무 칸에나 뚫으면 풀이가 둘 이상인 판이 나와 어느 숫자가 정답인지 가려지지 않는다. 04 는 구멍마다 풀이 개수를 2 까지 세어 둘이면 되돌리므로, 네 판 모두 CLUES 36 에서도 26 에서도 풀이 개수 1 로 끝났다.

되짚기가 너무 깊어지면 화면이 멈춘다

앞쪽 빈칸부터 푸는 순서는 단서 24~25 인 네 판 모두 20,000 노드 한도에 닿았지만, 후보 적은 칸 먼저는 150~244 노드로 풀렸다. 그래도 어려운 판이 들어올 수 있으니 03 의 NODE_CAP 400 같은 상한을 두고, 닿으면 풀이를 못 찾았다고 표시하고 그만둔다.

메모 후보가 입력 뒤에 낡은 채 남는다

메모로 적어 둔 후보를 따로 저장하면 숫자를 놓은 뒤에도 지워진 후보가 남는다. 01 처럼 후보를 판 배열에서 candMask 로 그때그때 계산하면 같은 줄·세로줄·상자의 후보가 입력 직후에 맞춰진다.

아홉 장면의 소스 전체는 아래 상자의 zip 으로 받을 수 있고, 압축을 여는 비밀번호는 6r5snyxx 이다.

단서 수와 되짚기 한도와 완성 물결 속도를 바꾸면 느낌은 얼마나 달라질까 — CLUES·NODE_CAP·WAVE_MS 를 바꿔 본 차이

이름 바꾼 값 느낌
단서를 더 적게 04 CLUES 36 → 26 CLUES 를 36 에서 26 으로 줄이면 씨앗 4 판은 비운 칸이 45 에서 55 로 늘고, 하나뿐인 후보로 채워지는 칸이 45 에서 4 로 줄며 풀이 탐색 노드가 45 에서 653 으로 늘어난다(렌더 측정이 아니라 소스 함수를 직접 돌려 센 값이다) — 단서를 늘리면 판이 쉬워진다
되짚기 한도를 낮게 03 NODE_CAP 400 → 60 NODE_CAP 을 400 에서 60 으로 낮추면 네 판 모두(필요한 노드 150·167·173·244) 상한에 닿아 풀이를 못 찾고 멈추고, 400 에서는 네 판 모두 풀이를 찾는다(소스 함수를 직접 돌려 센 값)
완성 물결을 느리게 08 WAVE_MS 40 → 120 WAVE_MS 를 40 에서 120 으로 늘리면 마지막 칸에서 가장 먼 칸(거리 12)의 지연이 480ms 에서 1,440ms 로 벌어져 물결이 느리게 번지고, 줄이면 칸 사이 간격이 좁아져 거의 한꺼번에 켜진다

세 값은 서로 다른 단계에 걸려 있다. CLUES 는 판의 난이도, NODE_CAP 은 탐색이 멈추는 지점, WAVE_MS 는 완성 뒤 화면만 바꾸니 하나씩 바꿔 어느 값이 느낌을 바꿨는지 보는 것이 좋다.

움직임 줄이기에서는 숫자가 바로 채워지는 판 한 장 — 장면 이름과 채운 칸 수를 문장으로 읽어 주는 접근성

방향키와 숫자키로 직접 풀 수 있다

무대에서 손을 대면 시간표가 멈추고 계산만 이어진다. N 은 새 판, R 은 처음부터, Enter·Space·P 는 일시정지다.

낭독은 손댄 직후와 모드가 바뀔 때만

화면 상태 줄은 aria-live="off" 라 시연 중에는 읽히지 않고, role="status" 숨김 문단에 장면 이름과 채운 칸 수를 문장으로 쓰되 손댄 직후와 모드가 바뀔 때만 쓴다. 자동 시연 중에는 낭독이 흐르지 않는다.

움직임 줄이기와 숨은 탭

prefers-reduced-motion 이 켜져 있으면 루프 없이 시간표의 정지 시각까지 한 번에 치른 판 한 장만 그리고 숫자 등장 보간은 없다. 탭이 숨으면 루프를 멈췄다가 돌아오면 쌓인 시간을 버리고 이어 간다. 설정 이름과 동작은 prefers-reduced-motion 문서에 정리되어 있다.

    // 탭이 숨으면 멈추고, 돌아오면 시계를 다시 맞춰 시작
    document.addEventListener('visibilitychange', function () {
        if (document.hidden) {
            stopLoop();
        }
        else if (!paused && !still) {
            startLoop();
        }
    }, { signal: off.signal });
    // 설정이 바뀌면 — 켜지면 판 한 장, 꺼지면 다시 움직인다
    reduceQuery.addEventListener('change', function () {
        still = reduceQuery.matches;
        if (still) {
            showRest();
        }
        else {
            lap = 0;
            play();
        }
    }, { signal: off.signal });

스도쿠 FAQ — 스도쿠 고급 전략·풀이가 하나뿐인 판의 조건·웹에 올릴 때 필요한 것

스도쿠 고급 전략은 코드로 어떻게 시작하나요

02 의 하나뿐인 후보 채우기가 가장 기본이고, 그것만으로 막히면 03 처럼 후보가 가장 적은 칸을 골라 걸어 보는 것이 이 편이 다루는 한 걸음 더다. 06 의 소거 근거를 같이 보면 어느 줄이 후보를 지웠는지 읽을 수 있다.

풀이가 하나뿐인 판의 조건은 무엇인가요

구멍을 뚫고 풀이 개수를 센 값이 정확히 1 이어야 한다. 04 는 2 까지만 세어 둘 이상이면 되돌려 이 조건을 지킨다.

웹에 올릴 때는 무엇이 필요한가요

서버 없이 HTML 파일 한 벌만 올려도 돈다. 화면 배율은 devicePixelRatio 를 2 로 잘라 쓰고 외부 글꼴이나 서버 호출이 없으니 파일을 둘 곳만 있으면 된다.

출처 문서는 MDN 의 Pointer events 항목이다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.