📘

claude codeに数独の問題作るコード考えさせてみた

に公開

今回はclaude codeに数独の問題を作るコードを作成させてみました。単純に自分が数独が好きで、なんとなく問題作らせてみたいなと思ってやってみました。

数独とは?

数独とは、一般的には9x9の盤面に数字が配置されており、以下のルールに基づいて数字が配置されています。

  • 縦横どちらでみても同じ数字は1度しか出てこない
  • 盤面を縦横それぞれ均等に3分割したそれぞれの枠内(3x3)で数字の重複はない

そのような盤面から数字を隠した時に、隠された場所に元々あった数字を当てるというゲームです。みなさん一度は目にしたことがあると思いますが、今回はそれの問題と回答を作らせてみます。

https://ja.wikipedia.org/wiki/数独

早速やってみる!

instruction.mdの実装

今回は以下のinstruction.mdに指示を書きました(毎度のことながら適当な指示になっています)。

instruction.md
## インストラクション
- 数独の問題を自動生成できること
    - 問題はproblem_X.txt形式で、Xの部分には通し番号
    - answer_X.txtで回答も合わせて作って
- 難易度をlow,middle,high,extremeの4種類指定できるようにして
- 実装はPythonで

問題はproblem_X.txtという形で何問も作れるようにしており、answer_X.txtというファイル名で対応する問題の解答を生成させています。また、lowからextremeの4段階で問題の難しさを変えれるようにしてみました。

実行結果

それではclaude codeが実際に作った結果をみてみましょう。まずはソースコードです。

ソースコード
sudoku_generator.py
#!/usr/bin/env python3
"""
Sudoku puzzle generator with multiple difficulty levels
"""

import random
import copy
import os
from datetime import datetime
from typing import List, Tuple, Optional


class SudokuGenerator:
    def __init__(self):
        self.size = 9
        self.box_size = 3
        self.difficulty_settings = {
            'low': {'min_clues': 45, 'max_clues': 50},
            'middle': {'min_clues': 35, 'max_clues': 44},
            'high': {'min_clues': 28, 'max_clues': 34},
            'extreme': {'min_clues': 22, 'max_clues': 27}
        }
        
    def is_valid(self, board: List[List[int]], row: int, col: int, num: int) -> bool:
        """Check if placing num at board[row][col] is valid"""
        # Check row
        for x in range(9):
            if board[row][x] == num:
                return False
        
        # Check column
        for x in range(9):
            if board[x][col] == num:
                return False
        
        # Check 3x3 box
        start_row = row - row % 3
        start_col = col - col % 3
        for i in range(3):
            for j in range(3):
                if board[i + start_row][j + start_col] == num:
                    return False
        
        return True
    
    def solve_sudoku(self, board: List[List[int]]) -> bool:
        """Solve sudoku using backtracking"""
        for row in range(9):
            for col in range(9):
                if board[row][col] == 0:
                    for num in range(1, 10):
                        if self.is_valid(board, row, col, num):
                            board[row][col] = num
                            
                            if self.solve_sudoku(board):
                                return True
                            
                            board[row][col] = 0
                    
                    return False
        return True
    
    def generate_complete_board(self) -> List[List[int]]:
        """Generate a complete valid Sudoku board"""
        board = [[0 for _ in range(9)] for _ in range(9)]
        
        # Fill diagonal boxes first (they don't affect each other)
        for box in range(0, 9, 3):
            nums = list(range(1, 10))
            random.shuffle(nums)
            idx = 0
            for i in range(box, box + 3):
                for j in range(box, box + 3):
                    board[i][j] = nums[idx]
                    idx += 1
        
        # Solve the rest
        self.solve_sudoku(board)
        return board
    
    def count_solutions(self, board: List[List[int]], limit: int = 2) -> int:
        """Count the number of solutions (up to limit)"""
        count = [0]
        
        def solve_count(board):
            if count[0] >= limit:
                return
            
            for row in range(9):
                for col in range(9):
                    if board[row][col] == 0:
                        for num in range(1, 10):
                            if self.is_valid(board, row, col, num):
                                board[row][col] = num
                                solve_count(board)
                                board[row][col] = 0
                        return
            count[0] += 1
        
        solve_count(copy.deepcopy(board))
        return count[0]
    
    def create_puzzle(self, difficulty: str) -> Tuple[List[List[int]], List[List[int]]]:
        """Create a puzzle with given difficulty"""
        if difficulty not in self.difficulty_settings:
            raise ValueError(f"Invalid difficulty. Choose from: {list(self.difficulty_settings.keys())}")
        
        # Generate complete board
        solution = self.generate_complete_board()
        puzzle = copy.deepcopy(solution)
        
        # Determine number of clues based on difficulty
        settings = self.difficulty_settings[difficulty]
        num_clues = random.randint(settings['min_clues'], settings['max_clues'])
        cells_to_remove = 81 - num_clues
        
        # Remove cells randomly while ensuring unique solution
        cells = [(r, c) for r in range(9) for c in range(9)]
        random.shuffle(cells)
        
        removed = 0
        attempts = 0
        max_attempts = cells_to_remove * 3
        
        for row, col in cells:
            if removed >= cells_to_remove or attempts >= max_attempts:
                break
            
            backup = puzzle[row][col]
            puzzle[row][col] = 0
            
            # For extreme difficulty, we allow some ambiguity
            if difficulty == 'extreme' or self.count_solutions(puzzle, 2) == 1:
                removed += 1
            else:
                puzzle[row][col] = backup
            
            attempts += 1
        
        return puzzle, solution
    
    def board_to_string(self, board: List[List[int]]) -> str:
        """Convert board to string representation"""
        result = []
        for i, row in enumerate(board):
            if i % 3 == 0 and i != 0:
                result.append("-" * 21)
            
            row_str = ""
            for j, num in enumerate(row):
                if j % 3 == 0 and j != 0:
                    row_str += "| "
                row_str += str(num) if num != 0 else "."
                row_str += " "
            result.append(row_str)
        
        return "\n".join(result)
    
    def save_puzzle(self, puzzle: List[List[int]], solution: List[List[int]], 
                   problem_number: int, difficulty: str) -> Tuple[str, str]:
        """Save puzzle and solution to files"""
        problem_filename = f"problem_{problem_number}.txt"
        answer_filename = f"answer_{problem_number}.txt"
        
        # Save problem
        with open(problem_filename, 'w') as f:
            f.write(f"Sudoku Puzzle #{problem_number}\n")
            f.write(f"Difficulty: {difficulty}\n")
            f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write("=" * 30 + "\n\n")
            f.write(self.board_to_string(puzzle))
        
        # Save answer
        with open(answer_filename, 'w') as f:
            f.write(f"Sudoku Solution #{problem_number}\n")
            f.write(f"Difficulty: {difficulty}\n")
            f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write("=" * 30 + "\n\n")
            f.write(self.board_to_string(solution))
        
        return problem_filename, answer_filename


def main():
    """Main function to generate sudoku puzzles"""
    import argparse
    import json
    
    parser = argparse.ArgumentParser(description='Generate Sudoku puzzles')
    parser.add_argument('--difficulty', type=str, choices=['low', 'middle', 'high', 'extreme'],
                       default='middle', help='Difficulty level')
    parser.add_argument('--count', type=int, default=1, help='Number of puzzles to generate')
    parser.add_argument('--start-number', type=int, default=1, help='Starting number for puzzle files')
    
    args = parser.parse_args()
    
    # Create log directory
    log_dir = './claude/log'
    os.makedirs(log_dir, exist_ok=True)
    
    # Setup logging
    log_file = os.path.join(log_dir, f"sudoku_generation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
    
    generator = SudokuGenerator()
    log_entries = []
    
    print(f"Generating {args.count} puzzle(s) with difficulty: {args.difficulty}")
    
    for i in range(args.count):
        problem_number = args.start_number + i
        print(f"Generating puzzle #{problem_number}...", end=" ")
        
        try:
            puzzle, solution = generator.create_puzzle(args.difficulty)
            problem_file, answer_file = generator.save_puzzle(
                puzzle, solution, problem_number, args.difficulty
            )
            
            log_entry = {
                'timestamp': datetime.now().isoformat(),
                'problem_number': problem_number,
                'difficulty': args.difficulty,
                'problem_file': problem_file,
                'answer_file': answer_file,
                'status': 'success'
            }
            log_entries.append(log_entry)
            print(f"Done! Saved as {problem_file} and {answer_file}")
            
        except Exception as e:
            log_entry = {
                'timestamp': datetime.now().isoformat(),
                'problem_number': problem_number,
                'difficulty': args.difficulty,
                'status': 'failed',
                'error': str(e)
            }
            log_entries.append(log_entry)
            print(f"Failed! Error: {e}")
    
    # Save log
    with open(log_file, 'w') as f:
        json.dump({
            'generation_session': {
                'start_time': log_entries[0]['timestamp'] if log_entries else None,
                'end_time': datetime.now().isoformat(),
                'total_puzzles': args.count,
                'difficulty': args.difficulty,
                'entries': log_entries
            }
        }, f, indent=2)
    
    print(f"\nGeneration log saved to: {log_file}")


if __name__ == "__main__":
    main()

SudokuGeneratorクラスに問題を生成するコードが書かれています。まずは完全な正解の盤面を作った後に、空白を作ることで作成しているようですね。なお、コードをみるとfor文をネストしていて処理効率が悪そうなところがあるのでitertools.productとかを使ってきれいにしたいところですが、今回は生成されたものをそのまま使ってみます。

それでは試しにコードを実行してみましょう。各レベルごとに生成した結果を貼り付けますが、実行する際は以下のように実行します。

python sudoku_generator.py --difficulty <low/middle/high/extreme> --count 生成するパズルの個数 --start-number <採番>

生成された結果は以下のようになりました

  • 難易度:low
# 問題
Sudoku Puzzle #1
Difficulty: low
Generated: 2025-09-30 20:19:48
==============================

. 8 9 | 3 . 2 | 5 . . 
6 5 . | . . . | 1 2 . 
. . 1 | 6 8 5 | . 3 . 
---------------------
5 . 7 | 1 3 8 | . . 2 
3 6 . | 5 9 . | 8 4 1 
9 . 8 | . . 4 | . 5 3 
---------------------
. . . | 8 5 3 | 6 1 . 
. 7 5 | 9 2 . | . 8 . 
. 3 . | 7 4 1 | 2 9 .

# 正解
Sudoku Solution #1
Difficulty: low
Generated: 2025-09-30 20:19:48
==============================

4 8 9 | 3 1 2 | 5 7 6 
6 5 3 | 4 7 9 | 1 2 8 
7 2 1 | 6 8 5 | 4 3 9 
---------------------
5 4 7 | 1 3 8 | 9 6 2 
3 6 2 | 5 9 7 | 8 4 1 
9 1 8 | 2 6 4 | 7 5 3 
---------------------
2 9 4 | 8 5 3 | 6 1 7 
1 7 5 | 9 2 6 | 3 8 4 
8 3 6 | 7 4 1 | 2 9 5
  • 難易度:middle
# 問題
Sudoku Puzzle #2
Difficulty: middle
Generated: 2025-09-30 20:20:06
==============================

. 8 . | . 1 6 | 5 7 4 
6 . . | 4 7 . | . . 9 
4 . . | . . 9 | 2 . 6 
---------------------
2 . . | . 6 . | 9 . . 
5 . . | 8 . . | 7 . . 
. 6 7 | . . . | 8 . . 
---------------------
. . . | 6 . . | 1 3 . 
. . 1 | . . . | 6 9 . 
. 9 6 | . 5 1 | 4 . 8

# 正解
Sudoku Solution #2
Difficulty: middle
Generated: 2025-09-30 20:20:06
==============================

9 8 2 | 3 1 6 | 5 7 4 
6 1 5 | 4 7 2 | 3 8 9 
4 7 3 | 5 8 9 | 2 1 6 
---------------------
2 4 8 | 1 6 7 | 9 5 3 
5 3 9 | 8 2 4 | 7 6 1 
1 6 7 | 9 3 5 | 8 4 2 
---------------------
7 2 4 | 6 9 8 | 1 3 5 
8 5 1 | 2 4 3 | 6 9 7 
3 9 6 | 7 5 1 | 4 2 8
  • 難易度:high
# 問題
Sudoku Puzzle #3
Difficulty: high
Generated: 2025-09-30 20:21:08
==============================

8 7 1 | 4 2 . | . . . 
. . 4 | 1 . . | 2 . . 
. . 2 | . . 9 | . . . 
---------------------
. . 6 | . 9 . | 8 . . 
. . . | . . . | . 9 6 
7 . 9 | 2 . 1 | . . . 
---------------------
. . . | . . . | . . 5 
2 1 3 | 5 4 6 | . . . 
. . . | 9 . . | 4 . 3 

# 正解
Sudoku Solution #3
Difficulty: high
Generated: 2025-09-30 20:21:08
==============================

8 7 1 | 4 2 3 | 5 6 9 
9 6 4 | 1 5 7 | 2 3 8 
5 3 2 | 6 8 9 | 7 4 1 
---------------------
1 4 6 | 3 9 5 | 8 7 2 
3 2 5 | 8 7 4 | 1 9 6 
7 8 9 | 2 6 1 | 3 5 4 
---------------------
4 9 8 | 7 3 2 | 6 1 5 
2 1 3 | 5 4 6 | 9 8 7 
6 5 7 | 9 1 8 | 4 2 3
  • 難易度:extreme
# 問題
Sudoku Puzzle #4
Difficulty: extreme
Generated: 2025-09-30 20:20:59
==============================

. . . | . . . | . . . 
. . . | 7 . . | . 3 . 
. . . | 6 9 4 | . . . 
---------------------
. . 7 | 5 6 . | . . . 
. . 9 | . . 8 | . 2 6 
. . . | . . 7 | 5 . 1 
---------------------
. 7 1 | 8 5 . | . 4 . 
. . 8 | . 7 . | . . . 
. 9 . | . . . | 8 . . 

# 正解
Sudoku Solution #4
Difficulty: extreme
Generated: 2025-09-30 20:20:59
==============================

7 1 4 | 3 2 5 | 9 6 8 
9 6 5 | 7 8 1 | 2 3 4 
8 3 2 | 6 9 4 | 1 5 7 
---------------------
1 2 7 | 5 6 9 | 4 8 3 
3 5 9 | 4 1 8 | 7 2 6 
4 8 6 | 2 3 7 | 5 9 1 
---------------------
2 7 1 | 8 5 3 | 6 4 9 
5 4 8 | 9 7 6 | 3 1 2 
6 9 3 | 1 4 2 | 8 7 5

正直実際に解いたわけではないのでhighとextremeが本当に難易度的に違うかとかは多hしかめられていないですが、少なくともlowとextremeはあからさまに難易度が違うことがわかります。middleくらいからあまりさはみる限りは感じないですが、もしかするとやってみると難しいのかもしれません。

まとめ

今回はclaude codeに数独を生成させてみました。今回は問題を作らせただけであり実際に一番の醍醐味である問題を解くところができていないので、次回は問題を回答するプログラムを組んでみようと思います。

Discussion