1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Mini Sudoku を Pythonで解く

Last updated at Posted at 2025-08-21

Mini Sudoku

皆さんは Mini Sudokuというゲームをご存知でしょうか。

Mini Sudoku もまた、Queensと同じくパズルゲームです。グリッド上マス目に1つずつ数字を入れ、各行・各列にそれぞれ1から6までの数字が揃っていないと駄目、なおかつ、6x6のマス目は 3x2 ずつに分割されており、この 3x2 のマス目の中にも1から6までの数字がダブることなくそろっていなくてはならないというゲームです。
新しいパネルはLinkedIn上で毎日公開され、通知設定で配信を受け取るよう登録できますが、一度挑戦すると毎日毎日お知らせが来ます。最初のうちは楽しいですが、最近はなんだかやらされ気味になってきました。

Mini sudokuの、完成したゲーム画面。黒い数字がプリセット(出題時に置かれている数字)

気力があるうちはいいのですが、私の場合わずか10日で飽きてきました。やめてしまえばいいのですが、なにか負けたような気がしてなりません。しょうがねえのでこの際プログラムのネタにでもなってもらおうかと思い筆をとりました。

構想

じゃあ、プログラムでこれを解こうとして、実際どうやって解くのかと言うと、速いCPUを利用した全探索がいいんじゃないの?的なことは誰だって考えます。私もそう考えました。

課題は LinkedIn のゲームページに掲載されるのをマウスでポチポチ解くタイプなので、まずそのページの中から盤をスクリーンショットで切り出します。6x6で固定なので、Queensの時より楽です。
なのでQueensでもやったようにまずはクリップボードにこの画像を収めます。これに、数字の初期配置をOCRライブラリである pytesseract を使って読み込んで行列として展開し、あとはバックトラックで計算してしまえ、というわけです。

あとは回答をどう表示するかです。出てきた回答をLinkedInのページにポチポチと入力するわけですから、できればオリジナルに沿った画像がどこかに出てきて欲しい、ということでOpenCV使おうかなとも思いましたが芸がなさすぎるのでここは pygameを使いました。

準備

ライブラリをインストール:

コマンドプロンプトやターミナルで以下を実行します。

% pip install pygame Pillow opencv-python pytesseract

みたいな感じでインストールしていけば良いのでしょうか。大抵のご家庭のPCにはすでにインストール済みかと思いますが、「ないよ」とエラーが出たらそれをインストールする方向でお願いします。

コード

次に、プログラム本体です。エディタでもメモ帳でも開いて次のファイルをコピペして、保存して下さい。
名前は sudoku.py とかでいいと思います。
保存は、一応 C:\Users\(あなたの名前) ってことにしておきましょう。

import pygame
from PIL import ImageGrab, Image
import io
import sys
import os
import cv2
import numpy as np
import pytesseract

# Tesseract OCRのパスを設定(ご自身の環境に合わせて変更してください)
# Windowsの場合の例:
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'

# Pygameの初期化
pygame.init()

# 画面設定
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 400
CELL_SIZE = SCREEN_WIDTH // 6
LINE_WIDTH = 3
THICK_LINE_WIDTH = 5

# 色の定義
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)

# フォントの設定
font = pygame.font.Font(None, 40)

def get_board_from_image():
    """
    クリップボードの画像から数独の初期配置を読み込む
    """
    try:
        # クリップボードからデータを取得
        clipboard_data = ImageGrab.grabclipboard()
        
        if clipboard_data is None:
            print("クリップボードにデータがありません。")
            return None
        
        # データがリスト形式の場合(ファイルパスのリストなど)
        if isinstance(clipboard_data, list):
            if not clipboard_data: return None
            # リストの最初の要素を画像パスと仮定して読み込み
            pil_image = Image.open(clipboard_data[0])
        
        # データがPIL Image形式の場合
        elif isinstance(clipboard_data, Image.Image):
            pil_image = clipboard_data
        
        else:
            print("クリップボードのデータは画像形式ではありません。")
            return None

        # PIL ImageをOpenCV形式に変換し、グレースケールと二値化
        cv_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
        gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
        thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV)[1]

        # 輪郭を検出
        contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
        board = [[0] * 6 for _ in range(6)]
        
        if not contours:
            print("画像からグリッドが見つかりませんでした。")
            return board

        # 最も大きい輪郭を数独のマス目として特定
        main_contour = max(contours, key=cv2.contourArea)
        x, y, w, h = cv2.boundingRect(main_contour)
        grid_image = gray[y:y+h, x:x+w]

        # 各セルを切り出してOCR
        cell_w = w // 6
        cell_h = h // 6
        
        for row in range(6):
            for col in range(6):
                cell_img = grid_image[row*cell_h:(row+1)*cell_h, col*cell_w:(col+1)*cell_w]
                
                # 数字を認識 (OCR)
                config = '--psm 10 -c tessedit_char_whitelist=123456'
                text = pytesseract.image_to_string(cell_img, config=config).strip()
                
                if text.isdigit() and '1' <= text <= '6':
                    board[row][col] = int(text)

        return board
    
    except Exception as e:
        print(f"画像処理中にエラーが発生しました: {e}")
        return None

def draw_grid(screen, board, initial_board):
    """数独のグリッドを描画する関数"""
    screen.fill(WHITE)
    
    # 罫線を描画
    for i in range(7):
        if i % 2 == 0:
            pygame.draw.line(screen, BLACK, (0, i * CELL_SIZE), (SCREEN_WIDTH, i * CELL_SIZE), THICK_LINE_WIDTH)
        else:
            pygame.draw.line(screen, BLACK, (0, i * CELL_SIZE), (SCREEN_WIDTH, i * CELL_SIZE), LINE_WIDTH)
    for i in range(7):
        if i % 3 == 0:
            pygame.draw.line(screen, BLACK, (i * CELL_SIZE, 0), (i * CELL_SIZE, SCREEN_HEIGHT), THICK_LINE_WIDTH)
        else:
            pygame.draw.line(screen, BLACK, (i * CELL_SIZE, 0), (i * CELL_SIZE, SCREEN_HEIGHT), LINE_WIDTH)

    # 数字を描画
    for row in range(6):
        for col in range(6):
            if board[row][col] != 0:
                color = BLACK
                if initial_board[row][col] == 0:
                    color = BLUE # 解答の数字を青色で表示
                
                text_surface = font.render(str(board[row][col]), True, color)
                text_rect = text_surface.get_rect(center=(col * CELL_SIZE + CELL_SIZE // 2, row * CELL_SIZE + CELL_SIZE // 2))
                screen.blit(text_surface, text_rect)

def is_valid(board, num, row, col):
    """指定された位置に数字を置けるかチェックする関数"""
    for x in range(6):
        if board[row][x] == num or board[x][col] == num:
            return False
            
    start_row = (row // 2) * 2
    start_col = (col // 3) * 3
    for i in range(2):
        for j in range(3):
            if board[start_row + i][start_col + j] == num:
                return False
                
    return True

def solve_sudoku(board):
    """バックトラッキングアルゴリズムで数独を解く関数"""
    for row in range(6):
        for col in range(6):
            if board[row][col] == 0:
                for num in range(1, 7):
                    if is_valid(board, num, row, col):
                        board[row][col] = num
                        if solve_sudoku(board):
                            return True
                        board[row][col] = 0
                return False
    return True

def main():
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption("Sudoku Solver")

    print("クリップボードに数独の画像をコピーしてください。")
    print("その後、このプログラムを実行してください。")

    initial_board = get_board_from_image()
    if initial_board is None:
        pygame.quit()
        return

    solved_board = [row[:] for row in initial_board]
    solve_success = solve_sudoku(solved_board)

    if solve_success:
        print("数独が解けました!")
    else:
        print("この数独は解けませんでした。")
        
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
        
        draw_grid(screen, solved_board, initial_board)
        pygame.display.flip()

    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()

実行

クリップボードに盤があるという条件下で、sudoku.py を実行すれば回答の盤が画面に現われます。
Windowsなら Win+Shift+Sキーでまず出題された盤をコピーし、クリップボードに入れます。
そのうえで`PowerShellIかコマンドプロンプトを開いて

C:\Users\(あなたの名前)> python sudoku.py

と実行すると、あなたのPCのCPUにもよりますが、比較的早い秒のオーダで解答が表示されるはずです。
表示された解答を、自分では解かなかった後ろめたさ、自分では解けなかった悔しさを噛み締めながらLinkedInの画面に書き写してください。自分は何をしてるんだろうという虚無感に襲われます。力にならないのに継続しても仕方ないと分かれば大人です。

(2025/08/21)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?