홈 › 읽을거리 › 개발노트

스도쿠 힌트 뒤 되돌리기로 칸이 잠기던 버그

개발노트 · 2026-10-08 · GamePool 편집팀

스도쿠에서 힌트 버튼을 누르고 곧바로 되돌리기를 누르면 그 칸이 비어 있는데도 아무 숫자도 입력되지 않는 문제가 있었습니다. 칸에는 처음부터 주어진 숫자 칸과 같은 given 스타일 클래스가 붙은 채 값만 비어 있었고, 그 판은 끝낼 수 없었습니다. 이 글은 그 버그가 어떻게 드러났는지, 소스에서 어떤 경로로 일어났는지, 어떻게 고쳤는지, 비슷한 상태 버그를 어떻게 찾는지를 저장소 이력에서 확인되는 만큼만 적은 개발 노트입니다.

재현 순서

재현은 네 단계면 충분합니다. 새 게임에서 빈 칸 하나를 고르고, 힌트 버튼을 눌러 정답이 채워지게 한 다음, 되돌리기 버튼을 누릅니다. 이때 칸의 값은 비워지지만 칸은 그대로 선택된 상태로 남습니다. 숫자 패드를 눌러도 반응이 없고, 지우기 버튼도, 키보드 숫자도 소용이 없습니다. 새 게임을 시작하는 것 외에는 이 판을 풀 방법이 없었습니다.

힌트를 쓴 칸이 아니라 틀린 숫자를 넣어 둔 칸에서 힌트를 쓰고 되돌려도 똑같이 잠깁니다. 이 경우 되돌아오는 값은 이전에 입력했던 틀린 숫자이고, 칸은 잠긴 채 '틀림' 표시(.bad)만 남습니다. 이전 코드의 되돌리기는 칸의 값과 메모만 옛 상태로 돌렸을 뿐, 잠금 상태는 건드리지 않았습니다.

잠긴 판에서 남은 길은 둘이었습니다. 새 게임 버튼을 누르거나, 다른 칸에서 실수를 세 번 채워 판을 지는 것입니다. 타이머는 tick() 이 done 이 아닐 때 1초마다 올리므로 완성할 수 없는 판에서도 시간만 계속 흘렀고, 완성 시간이 난이도별 최고 기록과 랭킹에 올라가는 게임이라 이 상태는 단순한 불편 이상이었습니다.

소스에서 원인을 따라간 경로

스도쿠 파일에는 칸마다 상태를 나눠 담는 병렬 배열이 있습니다. 현재 값 values, 후보 메모 notes, 문제의 처음 숫자 여부 given, 그리고 입력할 때마다 쌓는 history 입니다. input() 의 첫 줄에는 if (done || sel < 0 || given[sel]) return; 이 있어, given 이 참인 칸에는 어떤 입력도 들어가지 않습니다. 이 줄이 문제 자체를 막는 것은 아니고, 잠금의 근거가 되는 given 을 힌트가 거짓에서 참으로 바꾸는 데서 시작합니다.

hint() 는 먼저 선택된 칸(없으면 처음 틀린 칸)의 정답을 input() 으로 넣고, 그다음 줄에서 given[i] = true 로 칸을 잠급니다. 힌트로 채운 칸을 사용자가 마음대로 지우거나 바꾸지 못하게 하려는 의도입니다. 이때 input() 은 입력 직전에 칸의 이전 값과 메모를 history 에 한 개 저장합니다. 즉 힌트 한 번이 history 항목 하나를 만든다는 점은 일반 입력과 같습니다.

undo() 는 history 에서 항목을 꺼내 values[h.i] 와 notes[h.i] 만 되돌리고 선택 칸을 옮깁니다. given 은 되돌리지 않았습니다. 그래서 힌트로 만든 항목을 되돌리면 값은 이전 상태(빈칸이거나 틀린 숫자)로 돌아가는데 given 은 참으로 남아 칸이 잠긴 채가 됩니다. 판 완료 판정은 input() 끝에서만 실행되는 values.every(...정답과 같음) 이라 잠긴 칸이 하나만 있어도 영원히 참이 될 수 없습니다. 소스를 따라가면 힌트 버튼도 쓸모가 없어질 수 있습니다. hint() 가 '처음 틀린 칸'을 고르는데 그 칸이 바로 잠긴 칸이면, input() 이 그 칸에서 즉시 반환하기 때문입니다. 다만 이 마지막 부분은 소스를 읽고 따라간 추론이고 화면에서 따로 눌러 본 것은 아닙니다.

한 줄 수정

수정은 undo() 의 한 줄입니다. values[h.i] = h.v; notes[h.i] = h.notes; 뒤에 given[h.i] = false; 를 덧붙이고, 같은 줄 끝에 '힌트로 고정된 칸도 되돌리면 다시 입력 가능'이라는 주석을 남겼습니다. 이 수정이 안전한 이유는 history 에 들어가는 칸이 반드시 given 이 아니었던 칸이라는 점입니다. input() 은 given 칸에서 첫 줄에 반환하므로 처음 주어진 숫자 칸이 history 에 들어갈 길이 없고, 따라서 undo() 가 given 을 거짓으로 돌려도 원래의 문제 숫자를 푸는 일은 일어나지 않습니다. 이 가정은 소스에서 input() 과 hint() 두 곳만 history 에 쓴다는 것을 확인해서 세운 것입니다.

수정 뒤 흐름과 건드리지 않은 부분

수정 뒤의 흐름을 소스대로 따라가면 이렇습니다. 힌트가 칸을 채우고 given 을 참으로 만들고, 되돌리기가 값과 메모를 옛 상태로 돌리면서 given 을 거짓으로 되돌리며 그 칸을 선택합니다. 이제 input() 의 첫 줄 검사를 통과하므로 숫자 패드와 지우기가 다시 동작하고, 같은 칸에 힌트를 다시 눌러도 정상적으로 채워집니다. 힌트 한 번과 되돌리기 한 번이 서로 상쇄되어 칸이 힌트를 쓰기 전과 같은 상태가 됩니다.

일부러 건드리지 않은 부분도 있습니다. hint() 자체의 로직(선택한 칸이 비었거나 틀렸으면 그 칸, 아니면 앞에서부터 처음 틀린 칸)은 그대로이고, 힌트에는 횟수 제한이 없으며 사용 횟수를 세는 변수도 없습니다. 난이도별로 비우는 칸 수 REMOVE 는 쉬움 38, 보통 48, 어려움 55 로 그대로여서, 힌트를 쓸 수 있는 칸의 수도 달라지지 않았습니다. 기록은 완성 시간만 남기고 힌트 사용 여부는 기록하지 않는 점도 이 수정과 무관하게 같습니다.

같은 유형의 상태 버그를 점검하는 방법

원인이 '상태가 여러 곳에 나뉘어 있는데 되돌리기가 일부만 되돌린다'였으므로 같은 점검을 다른 곳에 적용했습니다. 방법은 단순합니다. 첫째, 게임이 들고 있는 상태 변수를 모두 나열합니다. 둘째, 되돌리기(undo)가 그중 무엇을 복원하는지 표시합니다. 셋째, 변수를 바꾸는 모든 함수에서 그 변수가 undo 에서 복원되는지 대조합니다. 스도쿠에서는 이 대조표에서 given 한 칸이 비어 있었던 셈입니다.

대조표를 만들면 아직 복원하지 않는 상태도 드러납니다. 현재 스도쿠의 undo() 는 mistakes(실수 횟수)를 되돌리지 않고, 정답을 맞게 넣을 때 같은 행·열·상자 칸들의 메모를 자동으로 지우는 동작도 되돌리지 않습니다. history 는 선택한 칸 하나의 값과 메모만 저장하기 때문입니다. 이것을 버그로 볼지 의도로 볼지는 이력에 근거가 없어서 이 글에서는 판정하지 않고 사실만 적습니다.

오목과 장기의 무르기도 소스를 읽어 같은 대조를 했습니다. 오목의 undo() 는 마지막 수를 꺼내 판을 비우고 차례를 흑으로 돌립니다. 내 수를 두기 전에 컴퓨터가 둔 수였다면 두 수를 함께 되돌립니다. 장기의 undo() 는 두 수를 되돌리면서 움직인 말, 잡힌 말 목록, 마지막 수 표시, 선택 상태, 차례를 모두 복원합니다. 읽은 범위에서는 스도쿠의 given 에 해당하는 '복원하지 않는 잠금 플래그'를 찾지 못했습니다. 다만 이것은 코드를 읽은 결과이고 모든 조합을 눌러 본 시험은 아닙니다.

이력으로 확인되는 사실

  • 수정 커밋은 (2026-10-08 06:25 UTC)이며, 스도쿠 index.html 은 이 커밋에서 1줄만 바뀌었습니다(추가 1줄·삭제 1줄).
  • 같은 커밋에 '게임 페이지 보강 1차'로 장기·알까기·오목·스도쿠·지뢰찾기·카드 짝맞추기의 플레이 화면과 설명 글(scripts/content/pages-c.js 423줄 추가 등)이 함께 들어 있어, 설명 글을 쓰기 위해 스도쿠의 동작을 소스로 확인하는 과정이 있었습니다.
  • 커밋 제목에 '스도쿠 힌트 되돌리기 버그'가 포함되어 있습니다. 같은 커밋의 설명 글은 수정 후 동작을 힌트로 채운 칸도 되돌리기로 비울 수 있고 비운 뒤에는 다시 직접 입력할 수 있다고 적습니다.
  • 스도쿠 index.html 의 이 저장소 이력은 초기 기준 커밋과 이 수정 커밋 두 개뿐이라, 버그가 처음 들어간 시점은 이력으로 특정되지 않습니다.

이 글에 적은 재현 순서와 원인은 현재 소스와 수정 전후의 diff 를 읽고 정리한 것입니다. 수정 전 코드로 되돌려 실제로 눌러 보는 재현은 이 글을 쓰며 새로 하지 않았고, 그 부분은 앞에서 밝힌 대로 추론이라고 표시했습니다. 앞으로 되돌리기나 새 잠금 상태를 추가할 때는 위의 대조표를 먼저 만들어 복원 대상에서 빠진 변수가 없는지 확인하는 것을 수정 절차로 삼겠습니다.

관련 게임 플레이하기

다른 읽을거리

개발노트알까기에서 승리 소리가 끝없이 울린 이유게임이 끝난 뒤에도 승패 효과음이 반복되고 승리 횟수가 계속 오르던 알까기 버그를 settle() 흐름과 한 줄 수정, 같은 유형 점검 결과로 정리했습니다.입문스도쿠 입문: 봇이 푼 난이도별 필요 기법이 사이트 스도쿠가 난이도별로 주는 단서 수와, 같은 규칙으로 만든 판을 논리 풀이 봇으로 풀어 본 결과, 힌트·되돌리기의 실제 동작을 정리한 입문 글입니다.공략지뢰찾기 추리법: 봇 1,000판으로 본 추측 횟수이 사이트 지뢰찾기의 지뢰 배치 규칙과 난이도별 지뢰 밀도, 봇으로 1,000판씩 돌려 본 추측 횟수와 승률을 바탕으로 숫자 읽기와 클릭 요령을 설명합니다.입문오목 입문: 컴퓨터 점수표를 읽고 삼삼으로 이기기이 사이트 오목 컴퓨터의 점수표와 난이도별 차이를 소스에서 읽고, 삼삼 준비 모양과 봇 대전 결과로 초반 배치·수비 우선순위·무르기 활용을 정리했습니다.입문장기 입문 가이드: 말 움직임부터 외통 몰기까지장기의 말 움직임, 포와 차의 쓰임, 궁성 대각선, 장군과 외통의 원리, 초보가 자주 하는 실수를 GamePool 장기 기준으로 정리했습니다.입문알까기 입문: 힘 조절, 각도, 충돌 물리 활용법알까기를 이기려면 얼마나 세게, 어느 방향으로 튕겨야 할까요? 당김 길이와 이동 거리, 충돌 때 돌이 움직이는 방식과 전술을 정리했습니다.