はじめに
数独(ナンプレ)ゲームを作るとき、数百行にわたるコードの中でわずかなミスや設計の不備が、思わぬ動作不具合(bug)を引き起こします。
初心者が「何が問題なのかわからない」「何を修正すればよいのかわからない」という壁に直面しがちです。そこで今回は、数独プレイヤーの基本的な構造から、発生しやすいバグの原因、そしてそれらをどのように発見・修正できるかを、コード例とともに分かりやすく解説します。
数独プログラムの基本構造
数独を実装するときは、主に3つの要素に分けて考えると管理しやすいです。
| 要素 | 主な役割 | 典型的なコード例 |
|---|---|---|
| 1. データ構造 | 9×9 の盤面、固定セルかどうか、候補リストなどを保持 | int[][] board = new int[9][9]; boolean[][] fixed = new boolean[9][9]; |
| 2. ルールチェック | 入力がルールに従っているか(同じ行・列・3×3ブロックに重複がないか)を検証 | boolean isValid(int r, int c, int val) |
| 3. UI/UX | ユーザーインタフェース、入力・出力ロジック | Scanner sc = new Scanner(System.in); |
この3要素が相互に依存し合うため、どこかに不整合が生じると全体が崩れます。
よくあるバグとその原因
1. 値の範囲チェックミス
- 原因:0〜9 の範囲(または1〜9)を意識していない。
-
典型的事例:
if(value < 1 || value > 9) { /* error */ } // ここに value == 0 のケースが入ってきてしまう -
対策:
- すべての入力操作で範囲チェックを必ず実装する。
- 可能なら、配列のインデックスを直接使用せず、専用メソッド(
isValidValue)でチェックする。
2. 3×3 ブロックのインデックス計算ミス
-
原因:
(row / 3) * 3 + col / 3と書くのを忘れたり、余計に余計な計算をしたり。 -
典型的事例:
int block = (row / 3) * 3 + (col / 3); // 正しい int wrongBlock = (row / 3) + (col / 3); // 1×1 つのブロックに見えてしまう -
対策:
- テストケース:各ブロックのコーナーとなるセルを入力して確認。
- 計算ロジックを ユーティリティ へ分離し、複数の場所から呼び出せるようにする。
3. 固定セルの変更を許可してしまう
-
原因:
fixed配列の更新漏れ、もしくは入力時のチェック不足。 -
典型的事例:
if(infix[r][c]) return false; // けど infix ではなく fixed と誤記 -
対策:
- 入力前に必ず
isFixed(r, c)を呼び、固定セルなら 再入力不可 と返す。 - 変更可否を関数でカプセル化し、単一責任化。
- 入力前に必ず
4. ループの境界条件ミス
-
原因:
for(int i=0;i<9;i++)と正しく書くのに、i<=9としてしまう。 -
典型的事例:
for(int row=0; row<9; row++) { for(int col=0; col<9; col++) { // 正常な処理 } }→ 修正前:
col<=9だと 10 列目にアクセスしてArrayIndexOutOfBoundsException -
対策:
-
IDE の自動補完 を利用して境界条件を必ず
0 <= idx < SIZEに統一。 -
Javadoc で
@paramにサイズ制限を明記し、ドキュメント化する。
-
IDE の自動補完 を利用して境界条件を必ず
5. 盤面のコピーミス(シャローコピー)
-
原因:
int[][]のコピー時に参照コピーをしてしまい、元盤面に影響が出る。 -
典型的事例:
int[][] clone = board; // 参照コピー -
対策:
-
Arrays.copyOfやcloneを使うときは、2 次元配列なので各行もコピー。
int[][] copy = board.clone(); for (int i = 0; i < copy.length; i++) { copy[i] = board[i].clone(); } -
バグを見つけるための実践的手順
| ステップ | やること | ツール / コード例 |
|---|---|---|
| 1. ユニットテスト | それぞれの機能(isValidValue, isFixed, isBlockValid など)を単体でテスト |
JUnit 5, assertTrue, assertFalse |
| 2. ログ出力 | 主要処理箇所で変数の状態を出力 | System.out.println("row="+row+",col="+col+",val="+val); |
| 3. デバッグ | IDE のブレークポイントを活用 | Debugger のステップ実行 |
| 4. 入力妥当性確認 | すべての入力パスで必ず null や invalid チェック |
`if (value == null |
| 5. テストケースの網羅性 | 9×9 の盤面をランダムに生成し、ルール違反を検知 | Random board generator, for loops |
例:JUnit での簡単なテスト
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
public class SudokuTest {
@Test
void testValidRow() {
Sudoku su = new Sudoku();
su.setCell(0, 0, 5);
su.setCell(0, 1, 3);
su.setCell(0, 2, 5); // 重複
assertFalse(su.isRowValid(0),
"Row 0 should be invalid due to duplicate 5");
}
@Test
void testFixedCell() {
Sudoku su = new Sudoku();
su.setFixed(4, 4, 7);
su.setCell(4, 4, 9); // 変更しようとする
assertEquals(7, su.getCell(4, 4),
"Fixed cell should remain unchanged");
}
}
よくある修正ポイント(箇条書き)
| 問題 | 修正内容 | 具体例 |
|---|---|---|
| 固定セルを変更できてしまう | 入力前に isFixed() で判定 |
if(isFixed(r,c)) { System.out.println("変更不可"); } |
| 3×3 ブロックチェックの誤算 | getBlock(row, col) を作成 |
private int getBlock(int r, int c){ return (r/3)*3 + c/3; } |
| 盤面のコピーでシャローコピー | 深いコピー を実装 | 先ほどのコードスニペット |
ルート計算で >=9 の条件ミス |
ループ境界を統一 | for(int i=0; i<9; i++) |
| 値の範囲チェックで 0 を忘れる | 1〜9 のチェックを必ず | `if(val < 1 |
デバッグ時に役立つヒント
-
小さくテストケースを作成
- 1 行だけ入力し、正しく判定されるか確認。
- 特定のブロックで重複をテスト。
-
エラーメッセージに詳細情報を含める
- 何が原因かを即座に把握できる。
- 例:
Error: row 3, col 5 already has value 2.
-
ステップ実行で状態を確認
-
board[r][c]の値、fixed[r][c]の状態を確認。 - ブレークポイントで変数をフォーカス。
-
-
コードレビューを行う
- 他者の目でバグが発見されやすい。
- 特にインデックス計算や境界チェックは見逃しやすい。
-
ログレベルを変更
- 本番では
INFO、デバッグ時はDEBUG。 - コンソールやファイルに出力される内容を制御。
- 本番では
まとめ:成功へのチェックリスト
-
入力検証
- 範囲:1〜9
- 固定セルチェック
-
ルールチェック(行・列・ブロック)
-
isRowValid,isColumnValid,isBlockValidを統一的に呼び出す。
-
-
盤面のコピー
- 深コピーを実装し、状態を保持。
-
テスト
- JUnit で単体テストを作成。
- ランダムケースでプレッシャーテスト。
-
デバッグ
- ブレークポイント+ロギングで状態を追跡。
初心者は「コードを書きすぎている」「すべてを一度に扱おうとした」ことがバグの主因です。
「小さく、一度に一つのことだけを正しく動かす」 という設計思想を守れば、バグを見つけやすく、修正もしやすくなります。
数独ゲームを作る過程で出てくるバグは、プログラミングのロジックとデバッグ力を鍛える絶好の機会です。
本ガイドを参考に、まずは簡単なルールチェックから実装をスタートし、徐々に機能を増やしていくことで、安定した数独アプリを完成させてください。

コメント