I. 挑戦:AIと格闘!ナンプレアプリ制作
1. 新しい挑戦とシリーズのテーマ
「「Game Trial Log」シリーズ Vol.2へようこそ。 本シリーズの核となるテーマは、プログラミング無知な筆者Dr.takodemousが、AI(Gemini)の力を借りて『AIコーダー』としてどこまで通用するか?という挑戦です。
Vol.1ではオセロの完成版をご紹介しましたが、今回はナンプレ(数独)アプリの制作に挑みました。
2. AIとのデバッグ格闘!ナンプレアプリ動画公開
GeminiとDr.takodemousが共同制作したナンプレアプリの完成版を、動画で公開します。
ナンプレのルールしか知らない筆者が、Geminiと共同制作した超初級者バージョンの動作確認をする動画です。必勝法など知らない筆者が、デバッグ段階で苦戦格闘する様子をそのまま収録しました。この苦労こそが、プログラミング初心者の醍醐味です。
さらに、動画のアウトロには、「Redkabagonは解けるのか!?」という煽りテロップを入れています。
3. 【限定公開】Redkabagonは解けるのか!?
上記の煽りに応え、Redkabagonがナンプレに挑戦する動画を限定公開でブログ内に挿入します。ナンプレが苦手な方は、彼女の予想外な苦戦ぶりを見て、「夫婦対決」を楽しみつつ、勇気をもらえるかもしれません。(概要欄に限定公開である旨を告知しています)
II. AIコードの比較とR60世代へのメッセージ
1. ナンプレ攻略のカギと脳トレの効能
ナンプレは、ロジックと集中力が試され、認知機能の維持・向上に役立つとされています。特にロジックを組み立てる工程は、海馬の活性化に繋がると言われています。
そして、筆者が今回のアプリ制作でR60世代に向けて強くメッセージしたいのは、AIを駆使すれば、「脳機能を鍛える」という明確な目的を持ったアプリさえも、知識ゼロから作成できるということです。一緒にこの未知の世界へ飛び込みましょう。
2. 【サイト紹介】プロコーダーの脳トレアプリ(ナンプレ)
今回も前回同様、PR TIMESさんのサイトよりプロのコーダーが作成した脳トレWebゲーム『Dr.脳トレ』のナンプレ版を紹介します。このサイトの完成度と、筆者(AIと共同制作)のコードを対比させることで、AIサポートの凄さが浮き彫りになります。
3. 無知なAIコーダー VS ナンプレのコード美
したがって、プロのアプリの機能と対比しながら、筆者のコードを紹介します。プログラミング未経験以前の無知な筆者でも、AIを駆使すれば、コーダーと言えるレベルのプログラムがつくれるという事実は驚きです。
具体的には、プロンプトには、「ナンプレの盤面生成」「解答の確認機能」「難易度調整」といった複雑な指示が含まれます。その結果、AIが生成してきたコードは、初心者には理解できないほどの論理的な美しさを備えていました。
プログラミングに興味がある方は、このコードの進化の道のりをぜひ参考にしてください。
プロンプトno1(外殻の設定と初期動作)
import copy
import time
# 0 は空きマスを示す
TEST_BOARD = [
[5, 3, 0, 0, 7, 0, 0, 0, 0],
[6, 0, 0, 1, 9, 5, 0, 0, 0],
[0, 9, 8, 0, 0, 0, 0, 6, 0],
[8, 0, 0, 0, 6, 0, 0, 0, 3],
[4, 0, 0, 8, 0, 3, 0, 0, 1],
[7, 0, 0, 0, 2, 0, 0, 0, 6],
[0, 6, 0, 0, 0, 0, 2, 8, 0],
[0, 0, 0, 4, 1, 9, 0, 0, 5],
[0, 0, 0, 0, 8, 0, 0, 7, 9]
]
def find_empty(board):
"""
ボード内の最初の空きマス (0) の座標 (row, col) を見つける。
空きマスがなければ None を返す。
"""
for r in range(9):
for c in range(9):
if board[r][c] == 0:
return (r, c)
return None
def is_valid(board, num, pos):
"""
指定された位置 pos に num を置くことが数独のルールに合致するか判定する。
(行、列、3x3ブロック内での重複チェック)
"""
row, col = pos
# 1. 行のチェック
for c in range(9):
# 現在のマス自体は無視
if board[row][c] == num:
return False
# 2. 列のチェック
for r in range(9):
# 現在のマス自体は無視
if board[r][col] == num:
return False
# 3. 3x3 ブロックのチェック
box_row_start = (row // 3) * 3
box_col_start = (col // 3) * 3
for r in range(box_row_start, box_row_start + 3):
for c in range(box_col_start, box_col_start + 3):
if board[r][c] == num:
return False
return True
def solve(board):
"""
数独問題を解くための主要なバックトラック関数。
再帰的に空きマスに数字を試行し、解を見つける。
"""
# 1. 終了条件: 空きマスがなければ、すべて埋まっており、解が見つかったことになる
find = find_empty(board)
if not find:
return True # 成功
row, col = find
# 2. 探索: 1から9までの数字を試す
for num in range(1, 10):
# 3. 合法性チェック (剪定/枝刈り)
if is_valid(board, num, (row, col)):
# 4. 手の適用: 暫定的にボードに数字を置く
board[row][col] = num
# 5. 再帰コール: 次の空きマスに進む
if solve(board):
return True # 次のレベルで解が見つかったら、そのままTrueを返す
# 6. バックトラック: 失敗した場合、手を戻し、次の数字を試す
board[row][col] = 0
# 1から9まで試したが、どこにも合法な数字がなかった
return False # 失敗
def print_board(board):
"""
数独ボードを見やすい形式で出力する
"""
print("-------------------------")
for i in range(9):
if i % 3 == 0 and i != 0:
print("-------------------------")
for j in range(9):
if j % 3 == 0 and j != 0:
print(" | ", end="")
if j == 8:
print(board[i][j])
else:
print(str(board[i][j]) + " ", end="")
print("-------------------------")
# ==============================================================================
# メイン実行部
# ==============================================================================
if __name__ == "__main__":
# ボードをコピーして、元の問題を保持する
board_to_solve = copy.deepcopy(TEST_BOARD)
print("--- 解決前のボード ---")
print_board(TEST_BOARD)
start_time = time.time()
# ソルバーを実行
if solve(board_to_solve):
end_time = time.time()
print("\n--- 解決後のボード (成功) ---")
print_board(board_to_solve)
print(f"解決にかかった時間: {end_time - start_time:.4f}秒")
else:
print("\n解は見つかりませんでした。")
# 解決にかかった時間は非常に短い(数ミリ秒)はずです。
# ==============================================================================
# メイン実行部 (修正案)
# ==============================================================================
if __name__ == "__main__":
# ... (これまでの処理) ...
# 解決にかかった時間: 0.0544秒 の出力後、一時停止する
input("\nEnterキーを押して終了します...") # この行を追加
プロンプトno2(GUIの設定と解答機能の追加)
import tkinter as tk
from tkinter import messagebox
import copy
import time
# --- (A) ソルバー関数群 (前回のコードから再掲) ---
def find_empty(board):
""" ボード内の最初の空きマス (0) の座標を見つける。 """
for r in range(9):
for c in range(9):
if board[r][c] == 0:
return (r, c)
return None
def is_valid(board, num, pos):
""" 指定された位置 pos に num を置くことが合法か判定する。 """
row, col = pos
# 1. 行のチェック
for c in range(9):
if board[row][c] == num:
return False
# 2. 列のチェック
for r in range(9):
if board[r][col] == num:
return False
# 3. 3x3 ブロックのチェック
box_row_start = (row // 3) * 3
box_col_start = (col // 3) * 3
for r in range(box_row_start, box_row_start + 3):
for c in range(box_col_start, box_col_start + 3):
if board[r][c] == num:
return False
return True
def solve(board):
""" 数独問題を解くための主要なバックトラック関数。 """
find = find_empty(board)
if not find:
return True # 成功
row, col = find
for num in range(1, 10):
if is_valid(board, num, (row, col)):
board[row][col] = num
if solve(board):
return True
board[row][col] = 0 # バックトラック
return False # 失敗
# --- (B) GUIクラスの定義 ---
class SudokuSolverGUI:
def __init__(self, master):
self.master = master
master.title("数独ソルバー")
# 9x9 の Entry ウィジェットを保持する配列
self.cells = []
# セルが初期値(ユーザーが入力した問題)かどうかのフラグ
self.initial_cells = [[False] * 9 for _ in range(9)]
# メインフレーム
main_frame = tk.Frame(master, padx=10, pady=10)
main_frame.pack()
self.create_grid(main_frame)
self.create_buttons(main_frame)
# 初期ボード(テスト用問題をロード)
self.load_board([
[5, 3, 0, 0, 7, 0, 0, 0, 0],
[6, 0, 0, 1, 9, 5, 0, 0, 0],
[0, 9, 8, 0, 0, 0, 0, 6, 0],
[8, 0, 0, 0, 6, 0, 0, 0, 3],
[4, 0, 0, 8, 0, 3, 0, 0, 1],
[7, 0, 0, 0, 2, 0, 0, 0, 6],
[0, 6, 0, 0, 0, 0, 2, 8, 0],
[0, 0, 0, 4, 1, 9, 0, 0, 5],
[0, 0, 0, 0, 8, 0, 0, 7, 9]
])
def create_grid(self, parent_frame):
""" 9x9の入力マスと境界線を作成する """
grid_frame = tk.Frame(parent_frame, borderwidth=3, relief="solid")
grid_frame.grid(row=0, column=0, pady=10)
for r in range(9):
row_cells = []
for c in range(9):
# 3x3ブロックの境界線(太枠)の設定
if r % 3 == 0 and r != 0:
pady_top = 3
else:
pady_top = 1
if c % 3 == 0 and c != 0:
padx_left = 3
else:
padx_left = 1
cell = tk.Entry(grid_frame, width=2, font=('Arial', 18),
justify='center', borderwidth=1, relief="ridge")
# grid の配置: rowとcolumnに加えて、padx/padyで太枠の隙間を作る
cell.grid(row=r, column=c, padx=(padx_left, 1), pady=(pady_top, 1))
row_cells.append(cell)
self.cells.append(row_cells)
def create_buttons(self, parent_frame):
""" 「解く」「クリア」ボタンを作成する """
button_frame = tk.Frame(parent_frame)
button_frame.grid(row=1, column=0, pady=10)
solve_button = tk.Button(button_frame, text="数独を解く", command=self.solve_sudoku, font=('Arial', 12), bg='lightgreen')
solve_button.pack(side=tk.LEFT, padx=10)
clear_button = tk.Button(button_frame, text="クリア", command=self.clear_grid, font=('Arial', 12), bg='lightblue')
clear_button.pack(side=tk.LEFT, padx=10)
# 解決時間表示用のラベル
self.time_label = tk.Label(button_frame, text="", font=('Arial', 10))
self.time_label.pack(side=tk.LEFT, padx=10)
def load_board(self, board_data):
""" 外部のボードデータ (2Dリスト) をGUIにロードする """
self.clear_grid() # まず全てクリア
for r in range(9):
for c in range(9):
num = board_data[r][c]
if num != 0:
self.cells[r][c].delete(0, tk.END)
self.cells[r][c].insert(0, str(num))
self.cells[r][c].config(fg='black', bg='lightgray', state='disabled') # 初期値は変更不可
self.initial_cells[r][c] = True
else:
self.cells[r][c].config(fg='blue', bg='white', state='normal')
self.initial_cells[r][c] = False
def clear_grid(self):
""" ボードをクリアする(すべての入力を消す) """
for r in range(9):
for c in range(9):
self.cells[r][c].config(state='normal', fg='blue', bg='white')
self.cells[r][c].delete(0, tk.END)
self.initial_cells[r][c] = False
self.time_label.config(text="")
messagebox.showinfo("情報", "ボードをクリアしました。新しい問題を直接入力してください。")
def get_board_from_gui(self):
""" GUI上の現在のボード状態を2Dリストとして読み込む """
board = [[0] * 9 for _ in range(9)]
is_valid_input = True
for r in range(9):
for c in range(9):
value = self.cells[r][c].get().strip()
if value.isdigit() and 1 <= int(value) <= 9:
board[r][c] = int(value)
elif value == "":
board[r][c] = 0 # 空きマス
else:
# 不正な入力
is_valid_input = False
self.cells[r][c].config(bg='red')
if not is_valid_input:
messagebox.showerror("入力エラー", "マスには1から9の数字か、空欄のみを入力してください。不正な入力があったマスを赤色で表示しました。")
return None, None
return board, True
def solve_sudoku(self):
""" GUI上のボードを解き、結果をGUIに表示する """
# 1. GUIからボードの状態を読み取る
current_board_data, is_valid_input = self.get_board_from_gui()
if not is_valid_input:
return # 入力エラーがあった場合は終了
# Solved Board (ソルバーが直接書き込むボード)
board_to_solve = copy.deepcopy(current_board_data)
# 初期状態の合法性チェック (問題として成立しているか)
# 既に配置されている数字がルール違反していないかを確認
for r in range(9):
for c in range(9):
num = current_board_data[r][c]
if num != 0:
# 一時的にそのマスを0にしてから、is_validで合法性をチェック
board_to_solve[r][c] = 0
if not is_valid(board_to_solve, num, (r, c)):
messagebox.showerror("問題エラー", f"入力された問題の (行:{r+1}, 列:{c+1}) に既にルール違反があります。")
return
board_to_solve[r][c] = num # 元に戻す
start_time = time.time()
# 2. ソルバーの実行
if solve(board_to_solve):
end_time = time.time()
# 3. GUIに結果を書き込む
for r in range(9):
for c in range(9):
num_str = str(board_to_solve[r][c])
# ソルバーが埋めたマスのみに書き込む
if not self.initial_cells[r][c]:
self.cells[r][c].delete(0, tk.END)
self.cells[r][c].insert(0, num_str)
self.cells[r][c].config(fg='green', bg='lightyellow') # ソルバーが埋めた場所をハイライト
self.time_label.config(text=f"解決時間: {end_time - start_time:.4f}秒")
messagebox.showinfo("解決完了", "数独が解決されました!")
else:
end_time = time.time()
self.time_label.config(text=f"解決時間: {end_time - start_time:.4f}秒")
messagebox.showerror("解決失敗", "この数独には解が見つかりませんでした。問題が間違っている可能性があります。")
# ==============================================================================
# メイン実行
# ==============================================================================
if __name__ == '__main__':
root = tk.Tk()
app = SudokuSolverGUI(root)
root.mainloop()
プロンプトno3(難易度調整機能の追加と完成版)
import tkinter as tk
from tkinter import messagebox
import copy
import time
import random
# ==============================================================================
# A. ソルバー関数群 (バックトラック)
# ==============================================================================
def find_empty(board):
""" ボード内の最初の空きマス (0) の座標を見つける。 """
for r in range(9):
for c in range(9):
if board[r][c] == 0:
return (r, c)
return None
def is_valid(board, num, pos):
""" 指定された位置 pos に num を置くことが数独のルールに合致するか判定する。 """
row, col = pos
# 1. 行のチェック
for c in range(9):
if board[row][c] == num:
return False
# 2. 列のチェック
for r in range(9):
if board[r][col] == num:
return False
# 3. 3x3 ブロックのチェック
box_row_start = (row // 3) * 3
box_col_start = (col // 3) * 3
for r in range(box_row_start, box_row_start + 3):
for c in range(box_col_start, box_col_start + 3):
if board[r][c] == num:
return False
return True
def solve(board):
""" 数独問題を解くための主要なバックトラック関数。 """
find = find_empty(board)
if not find:
return True # 成功
row, col = find
for num in range(1, 10):
if is_valid(board, num, (row, col)):
board[row][col] = num
if solve(board):
return True
board[row][col] = 0 # バックトラック
return False # 失敗
# ==============================================================================
# B. 問題ジェネレーター関数群
# ==============================================================================
def generate_full_board(board):
""" 空のボードを埋め、完全な解をランダムに生成する。 """
find = find_empty(board)
if not find:
return True
row, col = find
nums = list(range(1, 10))
random.shuffle(nums)
for num in nums:
if is_valid(board, num, (row, col)):
board[row][col] = num
if generate_full_board(board):
return True
board[row][col] = 0
return False
def count_solutions(board, limit=2):
""" 与えられたボードの解の数を数える (limitを超えたら停止)。 """
find = find_empty(board)
if not find:
return 1
row, col = find
count = 0
for num in range(1, 10):
if is_valid(board, num, (row, col)):
board[row][col] = num
count += count_solutions(board, limit)
board[row][col] = 0
if count >= limit:
return count
return count
def generate_sudoku_problem(difficulty_level=50):
"""
完全なボードを生成し、一意な解を保ちながら数字を削除する。
difficulty_level は削除するマス数の目標値となる。
"""
full_board = [[0] * 9 for _ in range(9)]
# 完全に埋まるまで再試行
while not generate_full_board(full_board):
full_board = [[0] * 9 for _ in range(9)]
cells_to_remove = [(r, c) for r in range(9) for c in range(9)]
random.shuffle(cells_to_remove)
problem_board = copy.deepcopy(full_board)
removed_count = 0
for r, c in cells_to_remove:
# 難易度レベルに達したら停止
if removed_count >= difficulty_level:
break
original_num = problem_board[r][c]
problem_board[r][c] = 0
temp_board = copy.deepcopy(problem_board)
# 解が一意であることを確認
if count_solutions(temp_board, limit=2) == 1:
removed_count += 1
else:
# 一意でない場合は元に戻す
problem_board[r][c] = original_num
print(f"\n--- 問題生成完了 --- 削除数: {removed_count} / 残りヒント数: {81 - removed_count}")
return problem_board
# ==============================================================================
# C. GUIクラスの定義
# ==============================================================================
class SudokuSolverGUI:
def __init__(self, master):
self.master = master
master.title("数独ソルバー")
self.cells = []
self.initial_cells = [[False] * 9 for _ in range(9)]
# --- 【新規】ハイライト用変数 ---
self.default_initial_bg = 'lightgray' # ヒントのデフォルト背景色
self.block_highlight_bg = 'mistyrose' # ユーザー入力セルのブロックハイライト色
self.highlighted_initial_bg = 'lightcoral' # ヒントセルのブロックハイライト色
self.focused_block_coords = None # 現在フォーカスされている3x3ブロックの開始座標 (r_start, c_start)
# -----------------------------
# 難易度制御のための変数
self.difficulty_map = {
"超初心者 (ヒント56)": 25,
"初心者 (ヒント46)": 35,
"中級 (ヒント36)": 45,
"上級 (ヒント26)": 55,
}
self.difficulty_options = list(self.difficulty_map.keys())
self.selected_difficulty = tk.StringVar(master)
self.selected_difficulty.set(self.difficulty_options[1])
main_frame = tk.Frame(master, padx=10, pady=10)
main_frame.pack()
self.create_grid(main_frame)
self.create_buttons(main_frame)
default_removals = self.difficulty_map[self.selected_difficulty.get()]
self.load_board(generate_sudoku_problem(difficulty_level=default_removals))
# --- 【新規】ヘルパー関数 ---
def get_cell_coords(self, entry_widget):
""" Entryウィジェットから(r, c)座標を逆引きする """
for r in range(9):
if entry_widget in self.cells[r]:
c = self.cells[r].index(entry_widget)
return r, c
return None, None
def handle_focus_change(self, event, is_focus_in):
""" フォーカスイン/アウト時にブロックハイライトを制御する """
r, c = self.get_cell_coords(event.widget)
if r is None: return
if is_focus_in:
# FocusIn: 3x3ブロックの開始座標を計算して設定
box_r_start = (r // 3) * 3
box_c_start = (c // 3) * 3
self.focused_block_coords = (box_r_start, box_c_start)
else:
# FocusOut: ブロック座標を解除
self.focused_block_coords = None
# 全体チェック関数を呼び出し、ハイライトの適用・解除を行う
self.check_and_highlight_all()
# FocusInのときだけ、そのセル自身をさらに強調する
if is_focus_in and not self.initial_cells[r][c]:
# 選択中のマスを黄色と凹みで強調
event.widget.config(relief='sunken', bg='lightyellow')
# --------------------------
def create_grid(self, parent_frame):
""" 9x9の入力マスと境界線を作成する """
grid_frame = tk.Frame(parent_frame, borderwidth=3, relief="solid")
grid_frame.grid(row=0, column=0, pady=10)
for r in range(9):
row_cells = []
for c in range(9):
if r % 3 == 0 and r != 0:
pady_top = 3
else:
pady_top = 1
if c % 3 == 0 and c != 0:
padx_left = 3
else:
padx_left = 1
cell = tk.Entry(grid_frame, width=2, font=('Arial', 18),
justify='center', borderwidth=1, relief="ridge")
# イベントのバインド
cell.bind('<KeyRelease>', lambda e: self.check_and_highlight_all())
cell.bind('<FocusIn>', lambda e: self.handle_focus_change(e, True)) # <-- FocusInを追加
cell.bind('<FocusOut>', lambda e: self.handle_focus_change(e, False)) # <-- FocusOutを追加
cell.grid(row=r, column=c, padx=(padx_left, 1), pady=(pady_top, 1))
row_cells.append(cell)
self.cells.append(row_cells)
def get_board_state(self):
""" GUI上の現在のボード状態を2Dリストとして読み込む(フォーマットチェック付き)。 """
board = [[0] * 9 for _ in range(9)]
is_valid_format = True
# ... (変更なし)
for r in range(9):
for c in range(9):
value = self.cells[r][c].get().strip()
if value == "":
board[r][c] = 0
else:
try:
num = int(value)
if 1 <= num <= 9 and len(value) == 1:
board[r][c] = num
else:
is_valid_format = False
except ValueError:
is_valid_format = False
return board, is_valid_format
def check_and_highlight_all(self):
""" リアルタイムでボード全体の合法性をチェックし、エラーとブロックをハイライトする。 """
self.reset_solved_colors() # ソルバーによるハイライトをリセット
board, is_valid_format = self.get_board_state()
if not is_valid_format:
return
for r in range(9):
for c in range(9):
# --- 【ブロックハイライトの判定とベースBGの決定】 ---
is_in_focused_block = False
if self.focused_block_coords:
box_r_start, box_c_start = self.focused_block_coords
if box_r_start <= r < box_r_start + 3 and box_c_start <= c < box_c_start + 3:
is_in_focused_block = True
if self.initial_cells[r][c]:
# ヒントセル: ハイライトあり/なしで色を切り替え
base_bg = self.highlighted_initial_bg if is_in_focused_block else self.default_initial_bg
else:
# ユーザー入力セル: ハイライトあり/なしで色を切り替え
base_bg = self.block_highlight_bg if is_in_focused_block else 'white'
# -----------------------------------------------
num = board[r][c]
is_initial = self.initial_cells[r][c]
# 1. 入力がないマス
if num == 0:
if not is_initial:
self.cells[r][c].config(bg=base_bg, fg='blue', relief="ridge")
else:
self.cells[r][c].config(bg=base_bg, fg='black', relief="ridge")
continue
# 2. 合法性チェック
board[r][c] = 0
if is_valid(board, num, (r, c)):
# 合法な場合
if not is_initial:
self.cells[r][c].config(bg=base_bg, fg='blue', relief="ridge") # ユーザー入力
else:
self.cells[r][c].config(bg=base_bg, fg='black', relief="ridge") # 初期ヒント
else:
# 非合法な場合(ルール違反)
if not is_initial:
self.cells[r][c].config(bg='red', fg='white', relief="ridge") # エラー時は赤を強制
# チェック後、元の数字に戻す
board[r][c] = num
# 3. 選択中のマスがあれば、それを強調
if self.focused_block_coords:
r, c = self.get_cell_coords(self.master.focus_get())
if r is not None and not self.initial_cells[r][c]:
self.cells[r][c].config(bg='lightyellow', relief='sunken') # 選択中のマスをさらに強調
def reset_solved_colors(self):
""" solve() で塗られた色 (lightyellow) をリセットする """
for r in range(9):
for c in range(9):
# ソルバーが埋めた場所だけをリセット
if not self.initial_cells[r][c] and self.cells[r][c].cget('bg') == 'lightyellow':
self.cells[r][c].config(bg='white', fg='blue')
# reliefもデフォルトに戻す
if self.cells[r][c].cget('relief') == 'sunken':
self.cells[r][c].config(relief='ridge')
# ... (create_buttons, load_board, clear_grid, generate_new_problem, solve_sudoku は変更なし) ...
def create_buttons(self, parent_frame):
""" 「解く」「クリア」「生成」ボタンとラベルを作成する """
button_frame = tk.Frame(parent_frame)
button_frame.grid(row=1, column=0, pady=10)
solve_button = tk.Button(button_frame, text="数独を解く", command=self.solve_sudoku, font=('Arial', 12), bg='lightgreen')
solve_button.pack(side=tk.LEFT, padx=5)
clear_button = tk.Button(button_frame, text="クリア", command=self.clear_grid, font=('Arial', 12), bg='lightblue')
clear_button.pack(side=tk.LEFT, padx=5)
# 難易度選択メニュー
difficulty_label = tk.Label(button_frame, text="難易度:", font=('Arial', 12))
difficulty_label.pack(side=tk.LEFT, padx=(20, 0))
difficulty_menu = tk.OptionMenu(button_frame, self.selected_difficulty, *self.difficulty_options)
difficulty_menu.config(font=('Arial', 12), width=15)
difficulty_menu.pack(side=tk.LEFT, padx=5)
generate_button = tk.Button(button_frame, text="新しく生成", command=self.generate_new_problem, font=('Arial', 12), bg='orange')
generate_button.pack(side=tk.LEFT, padx=5)
self.time_label = tk.Label(button_frame, text="", font=('Arial', 10))
self.time_label.pack(side=tk.LEFT, padx=10)
self.hint_label = tk.Label(button_frame, text="", font=('Arial', 10), fg='gray')
self.hint_label.pack(side=tk.LEFT, padx=10)
def load_board(self, board_data):
""" 外部のボードデータ (2Dリスト) をGUIにロードし、初期値として設定する """
self.clear_grid(show_message=False)
hint_count = 0
for r in range(9):
for c in range(9):
num = board_data[r][c]
if num != 0:
self.cells[r][c].delete(0, tk.END)
self.cells[r][c].insert(0, str(num))
self.cells[r][c].config(fg='black', bg=self.default_initial_bg, state='disabled')
self.initial_cells[r][c] = True
hint_count += 1
else:
self.cells[r][c].config(fg='blue', bg='white', state='normal')
self.cells[r][c].delete(0, tk.END)
self.initial_cells[r][c] = False
self.hint_label.config(text=f"ヒント数: {hint_count}")
self.check_and_highlight_all()
def clear_grid(self, show_message=True):
""" ボードをクリアする(すべての入力を消す) """
for r in range(9):
for c in range(9):
self.cells[r][c].config(state='normal', fg='blue', bg='white')
self.cells[r][c].delete(0, tk.END)
self.cells[r][c].config(relief='ridge')
self.initial_cells[r][c] = False
self.time_label.config(text="")
self.hint_label.config(text="")
self.focused_block_coords = None # ハイライト解除
if show_message:
messagebox.showinfo("情報", "ボードをクリアしました。新しい問題を直接入力してください。")
def generate_new_problem(self):
""" 新しい数独問題を生成し、GUIにロードする """
difficulty_key = self.selected_difficulty.get()
difficulty_removals = self.difficulty_map[difficulty_key]
self.time_label.config(text=f"[{difficulty_key}] の問題を生成中...")
self.master.update()
new_problem = generate_sudoku_problem(difficulty_level=difficulty_removals)
self.load_board(new_problem)
self.time_label.config(text="生成完了!")
messagebox.showinfo("情報", f"新しい一意の解を持つ数独問題({difficulty_key})を生成しました。")
def solve_sudoku(self):
""" GUI上のボードを解き、結果をGUIに表示する """
current_board_data, is_valid_format = self.get_board_state()
if not is_valid_format:
messagebox.showerror("入力エラー", "マスには1から9の数字か、空欄のみを入力してください。")
return
board_to_solve = copy.deepcopy(current_board_data)
self.check_and_highlight_all()
for r in range(9):
for c in range(9):
if self.cells[r][c].cget('bg') == 'red':
messagebox.showerror("問題エラー", "入力された問題にルール違反があるため、解決できません。")
return
start_time = time.time()
if solve(board_to_solve):
end_time = time.time()
for r in range(9):
for c in range(9):
num_str = str(board_to_solve[r][c])
if not self.initial_cells[r][c]:
self.cells[r][c].delete(0, tk.END)
self.cells[r][c].insert(0, num_str)
# ソルバーが埋めた場所をハイライト(lightyellowを強制)
self.cells[r][c].config(fg='green', bg='lightyellow', relief='ridge')
self.time_label.config(text=f"解決時間: {end_time - start_time:.4f}秒")
messagebox.showinfo("解決完了", "数独が解決されました!")
else:
end_time = time.time()
self.time_label.config(text=f"解決時間: {end_time - start_time:.4f}秒")
messagebox.showerror("解決失敗", "この数独には解が見つかりませんでした。")
# ==============================================================================
# メイン実行
# ==============================================================================
if __name__ == '__main__':
root = tk.Tk()
app = SudokuSolverGUI(root)
root.mainloop()
III. Redkabagonのゲーム進展と次回予告
1. RedkabagonのG5ゲーム進展
一方で、Redkabagonは、これまで通りG5 Entertainment提供のアイテム探し&3マッチゲームをクリアする動画を投稿し、ブログにリンクしています。彼女の着実なゲームクリアも、本シリーズの隠れた見どころです。
2. シリーズの継続と次回の挑戦
本シリーズは、今後も継続的に新しいアプリ開発に挑んでいきます。
Redkabagonさんの公道デビューはネタ化していますが、プログラミング初心者の筆者も、AIの力を借りて『AIコーダー』としてデビューできるのか、どうか!?という挑戦は続きます。
次回は別のパズルゲームアプリの作成、またはプロの脳トレアプリの機能との比較検証を予定しています。
どうぞ、Geminiのプロンプトの世界に飛び込んでみてください。
【The Gear】撮影機材のご紹介
本動画の撮影機材や新しい動画編集ソフト「PowerDirector 365」に関する情報、アフィリエイト情報については、「The Gear」カテゴリーで詳しくご紹介しています。



