ポンコツ夫婦のGame Trial Log Vol.2:ナンプレ編

Pythonでゲーム作成

I. 挑戦:AIと格闘!ナンプレアプリ制作

「「Game Trial Log」シリーズ Vol.2へようこそ。 本シリーズの核となるテーマは、プログラミング無知な筆者Dr.takodemousが、AI(Gemini)の力を借りて『AIコーダー』としてどこまで通用するか?という挑戦です。

Vol.1ではオセロの完成版をご紹介しましたが、今回はナンプレ(数独)アプリの制作に挑みました。

GeminiとDr.takodemousが共同制作したナンプレアプリの完成版を、動画で公開します。

ナンプレのルールしか知らない筆者が、Geminiと共同制作した超初級者バージョンの動作確認をする動画です。必勝法など知らない筆者が、デバッグ段階で苦戦格闘する様子をそのまま収録しました。この苦労こそが、プログラミング初心者の醍醐味です。

さらに、動画のアウトロには、「Redkabagonは解けるのか!?」という煽りテロップを入れています。

上記の煽りに応え、Redkabagonがナンプレに挑戦する動画を限定公開でブログ内に挿入します。ナンプレが苦手な方は、彼女の予想外な苦戦ぶりを見て、「夫婦対決」を楽しみつつ、勇気をもらえるかもしれません。(概要欄に限定公開である旨を告知しています)


ナンプレは、ロジックと集中力が試され、認知機能の維持・向上に役立つとされています。特にロジックを組み立てる工程は、海馬の活性化に繋がると言われています。

そして、筆者が今回のアプリ制作でR60世代に向けて強くメッセージしたいのは、AIを駆使すれば、「脳機能を鍛える」という明確な目的を持ったアプリさえも、知識ゼロから作成できるということです。一緒にこの未知の世界へ飛び込みましょう。

今回も前回同様、PR TIMESさんのサイトよりプロのコーダーが作成した脳トレWebゲーム『Dr.脳トレ』のナンプレ版を紹介します。このサイトの完成度と、筆者(AIと共同制作)のコードを対比させることで、AIサポートの凄さが浮き彫りになります。

したがって、プロのアプリの機能と対比しながら、筆者のコードを紹介します。プログラミング未経験以前の無知な筆者でも、AIを駆使すれば、コーダーと言えるレベルのプログラムがつくれるという事実は驚きです。

具体的には、プロンプトには、「ナンプレの盤面生成」「解答の確認機能」「難易度調整」といった複雑な指示が含まれます。その結果、AIが生成してきたコードは、初心者には理解できないほどの論理的な美しさを備えていました。

プログラミングに興味がある方は、このコードの進化の道のりをぜひ参考にしてください。


一方で、Redkabagonは、これまで通りG5 Entertainment提供のアイテム探し&3マッチゲームをクリアする動画を投稿し、ブログにリンクしています。彼女の着実なゲームクリアも、本シリーズの隠れた見どころです。

アイテム探し&3マッチ

本シリーズは、今後も継続的に新しいアプリ開発に挑んでいきます。

Redkabagonさんの公道デビューはネタ化していますが、プログラミング初心者の筆者も、AIの力を借りて『AIコーダー』としてデビューできるのか、どうか!?という挑戦は続きます。

次回は別のパズルゲームアプリの作成、またはプロの脳トレアプリの機能との比較検証を予定しています。

どうぞ、Geminiのプロンプトの世界に飛び込んでみてください。


本動画の撮影機材や新しい動画編集ソフト「PowerDirector 365」に関する情報、アフィリエイト情報については、「The Gear」カテゴリーで詳しくご紹介しています。

ホーム » ブログ

ポンコツ夫婦のGame Trial Log Vol.1

Pythonでゲーム作成

カテゴリーの変更から

カテゴリー変更の説明がフォーマット化している点、ご容赦ください。今回は脱コロナをイメージしての変更ではなく、ゲームに関わる事柄を扱う新しい独立したカテゴリーとして「Game Trial Log」を立ち上げます。

Redkabagonはこれまで通り、G5Entertainmentの提供するアイテム探し&マッチ3を行い、その様子を録画・公開します。筆者Dr.takodemousは新たにゲームアプリを作成し、アプリのコード紹介と動作状況の録画公開に挑戦します

ゲームアプリ作成はGeminiのコーディングサポートを受けますが、時間がかかるため更新頻度は遅くなります。気長にお待ちください。また、機材に関わる内容は「The Gear」とクロスオーバーしますので、ゲームに興味はないけど機材には興味があるという方々のご来場もお待ちしております。

経歴の詳細は割愛しますが、2012年7月にサーティファイWEBクリエイター能力認定試験の上級(合格点65点に対し66点というブービー賞!)を45歳で取得。現在に至るまで、WEB業界で生計を立てられているとは言い難い状況です。他にIllustrator、Photoshop、Javascript、今では使われなくなったflashをかいつまんだ程度に理解している状況です。

Pythonのコード作成は未経験以前の無知。という状況でゲームを作成します。そこでR60世代に向けて是非ともメッセージしたい事があります。AI生成技術は産業革命に匹敵すると言われていますが、(筆者は産業革命を経験していないので匹敵するかどうかは不明!)AIは経験上、未知の世界へ飛び込むことに大いに役立ちます。R60世代の皆様も、一緒に飛び込みましょう!

【動画】AIと共同制作したオセロ、Dr.takodemousが勝利! Geminiと共同制作したオセロの完成版を、筆者がAI相手に打ち勝った動画です。ほぼ強制的に設置されたヒント表示を使用せず勝利したのは、ある程度のゲームのコツを知っていたからです。(コツは本ブログの趣旨と異なるため省略します。)

伝えたいのは、無知なレベルであってもAIを駆使すると、コーダーと言えるレベルのプログラムがつくれるという事です。ゲームやプログラムに興味がある方は是非参考にしてください。

プロンプトの参考になるように、最初に作成したオセロのコードから完成版までの道のりをフォルダに添付しておきます。プログラムに興味のある方は最初から順番に、ゲームに興味のある方は完全版を試してください。全てPythonのコードとなります。

redkabagonは今まで通り、G5Entertainmentの提供するアイテム探し&マッチ3をゲームしてもらい、その状況を録画そしてYou Tubeに投稿&本ブログにリンクする流れを行ってもらっています。

CapCutでの動画編集は現在のチャンネル登録分で最終となります。今後はCyberlinkの提供するPowerDirector 365を使用して作成していきます。PowerDirector 365についても、機材説明の機会に「The Gear」カテゴリーで紹介したいと思います。

映像作成に興味のある方は、是非ご来場ください。

pyramid of mahjong

前回まで五回にわたって記載してきたこの題目を、総括として終了したいと思います。コロナ禍が与えた影響は心身ともに、無意識下にダメージを与えていたと筆者は感じます。

漠然とした日々の不安や拠り所のない希望など、記載内容は意図的であれ無意識であれ、ネガティブな内容であった気がします。その当時はその状態が仕方ないとしても、現在の状況は自己選択が出来る状態です。

総じてカテゴリーの変更を行った後は、楽しみながら老いていくを実践しているようなものです。つまり最近のブログの内容は、R60のポンコツ夫婦が楽しみながら老いていく姿を紹介しているのです。やたら、参考にしてくださいというメッセージが多発するのは、自己選択の結果なような気がします。

やりがいや生きがいは、認知機能や肉体機能を維持するのに役立つと言われています。真意は不明といえばそれまでなのですが、信じてメッセージしていきたいと思います。

PR TIMESさんのサイトより”認知症予防の脳トレWebゲーム『Dr.脳トレ』をリリース。”のページを引用しています

今回からは、無知なAIコーダー(筆者)VSプロのコーダーの決戦の意味合いも含みます。脳科学には触れたことすらない筆者が「脳機能を鍛える」とプロンプトに指示して生まれたコードが、どこまでプロに肉薄できるか、自身でもウキウキしています。

第四回目も海馬を鍛えるシリーズになります。先ずはゲストモードで試してみて、効果を感じた場合アカウントを作成する感じで手軽に始めることが出来ます。このコナーのゲームは記憶の形成、空間学習、感情の制御を鍛えるというゲームとなっています。筆者も同類のアプリの作成にチャレンジするつもりです。

動画に登場しているバージョンは**プロンプト No.6です。DEPTH=4という設定で、レベルは中級者以上となっています。実行するには、Pythonを入手する必要があります。入手は自己責任ですが、世界中で使用されているプログラムを実行するためには必需品です。入手方法や設定方法などのリンクを貼っておきます。

どうぞGeminiのプロンプトの世界に、飛び込んでみてください。

プロンプトno1(外殻の設定と初期動作:相互手動)
# othello.py

# 1. ゲームボードの初期化
def create_board():
    board = []
    for i in range(8):
        row = [' '] * 8
        board.append(row)
    
    board[3][3] = 'W'
    board[3][4] = 'B'
    board[4][3] = 'B'
    board[4][4] = 'W'
    return board

# 2. ボードの表示
def print_board(board):
    print('  a b c d e f g h')
    print(' +-+-+-+-+-+-+-+-+')
    for i in range(8):
        print(f'{i+1}|', end='')
        for j in range(8):
            print(f'{board[i][j]}|', end='')
        print(f' {i+1}')
    print(' +-+-+-+-+-+-+-+-+')
    print('  a b c d e f g h')

# 3. 駒を置ける場所の判定と駒の反転
def is_valid_move(board, row, col, player, opponent):
    if not (0 <= row < 8 and 0 <= col < 8) or board[row][col] != ' ':
        return False

    directions = [
        (-1, -1), (-1, 0), (-1, 1),
        (0, -1),          (0, 1),
        (1, -1),  (1, 0),  (1, 1)
    ]
    
    found_flips = []

    for dr, dc in directions:
        r, c = row + dr, col + dc
        path = []
        while 0 <= r < 8 and 0 <= c < 8 and board[r][c] == opponent:
            path.append((r, c))
            r, c = r + dr, c + dc
        
        if 0 <= r < 8 and 0 <= c < 8 and board[r][c] == player and path:
            found_flips.extend(path)
            
    return found_flips

def flip_pieces(board, pieces_to_flip, player):
    for r, c in pieces_to_flip:
        board[r][c] = player

# 4. メインゲームループ
def main():
    board = create_board()
    player_turn = 'B'
    opponent_turn = 'W'

    while True:
        print_board(board)
        print(f'現在のターン: {player_turn}')

        try:
            move_str = input('駒を置く場所を入力してください (例: d3): ')
            col_char = move_str[0].lower()
            row_char = move_str[1:]
            
            # 入力を座標に変換
            col = 'abcdefgh'.index(col_char)
            row = int(row_char) - 1
            
            if not (0 <= row < 8 and 0 <= col < 8):
                print('無効な入力です。')
                continue
            
            # 有効な手か判定
            pieces_to_flip = is_valid_move(board, row, col, player_turn, opponent_turn)
            
            if pieces_to_flip:
                board[row][col] = player_turn
                flip_pieces(board, pieces_to_flip, player_turn)
                
                # ターンを交代
                player_turn, opponent_turn = opponent_turn, player_turn
            else:
                print('その場所には置けません。有効な手を打ってください。')
        
        except (ValueError, IndexError):
            print('無効な入力です。例: d3 のように入力してください。')

# プログラムの実行
if __name__ == '__main__':
    main()
プロンプトno2(GUIの設定1と初期動作:相互手動)
# 1. ゲームボードの初期化
# 盤面を表すための定数を定義します
# '・' : 空, '⚫' : 黒, '⚪' : 白
EMPTY = '・'
BLACK = '⚫'
WHITE = '⚪'

def create_board():
    """
    8x8の空の盤面を作成し、初期配置の石を配置します。
    """
    board = []
    for _ in range(8):
        row = [EMPTY] * 8
        board.append(row)
    
    board[3][3] = WHITE
    board[3][4] = BLACK
    board[4][3] = BLACK
    board[4][4] = WHITE
    return board

# 2. ボードの表示
def print_board(board):
    """
    現在の盤面をきれいに表示します。
    """
    # 列の表記を修正
    print('    a    b    c    d    e    f    g    h')
    # 罫線の表示
    print('  +---+---+---+---+---+---+---+---+---+---')
    for i in range(8):
        # 行の表示と升目の調整
        row_str = f'{i+1} |'
        for j in range(8):
            row_str += f' {board[i][j]} |'
        print(row_str)
        print('  +---+---+---+---+---+---+---+---+---+---')
    
# 3. 駒を置ける場所の判定と駒の反転
def is_valid_move(board, row, col, player, opponent):
    """
    指定された位置に石を置くことができるか判定し、
    ひっくり返せる石のリストを返します。
    """
    if not (0 <= row < 8 and 0 <= col < 8) or board[row][col] != EMPTY:
        return None

    # 8つの方向を定義 (行の変化, 列の変化)
    directions = [
        (-1, -1), (-1, 0), (-1, 1),
        (0, -1),            (0, 1),
        (1, -1),  (1, 0),  (1, 1)
    ]
    
    pieces_to_flip = []

    for dr, dc in directions:
        r, c = row + dr, col + dc
        path = []
        while 0 <= r < 8 and 0 <= c < 8 and board[r][c] == opponent:
            path.append((r, c))
            r, c = r + dr, c + dc
        
        # 挟み込んでいるかチェック
        if 0 <= r < 8 and 0 <= c < 8 and board[r][c] == player and path:
            pieces_to_flip.extend(path)
            
    return pieces_to_flip if pieces_to_flip else None

def flip_pieces(board, pieces_to_flip, player):
    """
    ひっくり返す石のリストを、指定されたプレイヤーの石に変換します。
    """
    for r, c in pieces_to_flip:
        board[r][c] = player

def get_valid_moves(board, player, opponent):
    """
    現在のプレイヤーが石を置けるすべての場所(有効な手)のリストを返します。
    """
    valid_moves = []
    for r in range(8):
        for c in range(8):
            if board[r][c] == EMPTY:
                if is_valid_move(board, r, c, player, opponent):
                    valid_moves.append((r, c))
    return valid_moves

def count_pieces(board):
    """
    盤面上の石の数を数え、勝敗を表示します。
    """
    black_count = 0
    white_count = 0
    for r in range(8):
        for c in range(8):
            if board[r][c] == BLACK:
                black_count += 1
            elif board[r][c] == WHITE:
                white_count += 1
    
    print(f"最終結果:黒 = {black_count}、白 = {white_count}")
    
    if black_count > white_count:
        print("黒の勝ちです!")
    elif white_count > black_count:
        print("白の勝ちです!")
    else:
        print("引き分けです。")

# 4. メインゲームループ
def main():
    """
    ゲームのメイン処理を実行します。
    """
    board = create_board()
    player_turn = BLACK
    opponent_turn = WHITE
    pass_count = 0  # 連続でパスした回数をカウント

    while True:
        print_board(board)
        
        valid_moves = get_valid_moves(board, player_turn, opponent_turn)
        
        if not valid_moves:
            pass_count += 1
            print(f'現在のターン: {"黒" if player_turn == BLACK else "白"}')
            print("置ける場所がありません。パスします。")
            
            if pass_count == 2:
                print("両プレイヤーがパスしました。ゲームを終了します。")
                break
            
            player_turn, opponent_turn = opponent_turn, player_turn
            continue
        else:
            pass_count = 0
            
        print(f'現在のターン: {"黒" if player_turn == BLACK else "白"}')
        
        try:
            move_str = input('駒を置く場所を入力してください (例: d3): ')
            
            if move_str.lower() == 'q':
                print("ゲームを終了します。")
                break

            col_char = move_str[0].lower()
            row_char = move_str[1:]
            
            col = 'abcdefgh'.index(col_char)
            row = int(row_char) - 1
            
            if (row, col) not in valid_moves:
                print('その場所には置けません。有効な手を打ってください。')
                continue
            
            pieces_to_flip = is_valid_move(board, row, col, player_turn, opponent_turn)
            board[row][col] = player_turn
            flip_pieces(board, pieces_to_flip, player_turn)
            
            player_turn, opponent_turn = opponent_turn, player_turn
        
        except (ValueError, IndexError):
            print('無効な入力です。例: d3 のように入力してください。')
    
    print("ゲーム終了!")
    count_pieces(board)
    
    input("結果を確認後、何かキーを押して終了してください...")

# プログラムの実行
if __name__ == '__main__':
    main()
プロンプトno3(GUIの設定2と初期動作:相互手動:盤面ほぼ完成)
import tkinter as tk
from tkinter import messagebox

# 盤面を表すための定数
EMPTY = '・'
BLACK = '⚫'
WHITE = '⚪'

# ウィンドウとキャンバスのサイズ
BOARD_SIZE = 400
CELL_SIZE = BOARD_SIZE // 8

# グローバル変数としてボードの状態を保持
game_board = None
canvas = None
current_player = BLACK
pass_count = 0

def create_board():
    """
    8x8の空の盤面を作成し、初期配置の石を配置します。
    """
    board = []
    for _ in range(8):
        row = [EMPTY] * 8
        board.append(row)
    
    board[3][3] = WHITE
    board[3][4] = BLACK
    board[4][3] = BLACK
    board[4][4] = WHITE
    return board

def is_valid_move(board, row, col, player, opponent):
    """
    指定された位置に石を置くことができるか判定し、
    ひっくり返せる石のリストを返します。
    """
    if not (0 <= row < 8 and 0 <= col < 8) or board[row][col] != EMPTY:
        return None

    directions = [
        (-1, -1), (-1, 0), (-1, 1),
        (0, -1),            (0, 1),
        (1, -1),  (1, 0),  (1, 1)
    ]
    
    pieces_to_flip = []

    for dr, dc in directions:
        r, c = row + dr, col + dc
        path = []
        while 0 <= r < 8 and 0 <= c < 8 and board[r][c] == opponent:
            path.append((r, c))
            r, c = r + dr, c + dc
        
        if 0 <= r < 8 and 0 <= c < 8 and board[r][c] == player and path:
            pieces_to_flip.extend(path)
            
    return pieces_to_flip if pieces_to_flip else None

def flip_pieces(board, pieces_to_flip, player):
    """
    ひっくり返す石のリストを、指定されたプレイヤーの石に変換します。
    """
    for r, c in pieces_to_flip:
        board[r][c] = player

def get_valid_moves(board, player, opponent):
    """
    現在のプレイヤーが石を置けるすべての場所(有効な手)のリストを返します。
    """
    valid_moves = []
    for r in range(8):
        for c in range(8):
            if board[r][c] == EMPTY:
                if is_valid_move(board, r, c, player, opponent):
                    valid_moves.append((r, c))
    return valid_moves

def count_pieces(board):
    """
    盤面上の石の数を数え、勝敗を決定します。
    """
    black_count = sum(row.count(BLACK) for row in board)
    white_count = sum(row.count(WHITE) for row in board)
    return black_count, white_count

def end_game_check():
    """
    ゲームが終了したかチェックし、終了していれば勝敗を表示します。
    """
    global pass_count
    
    black_count, white_count = count_pieces(game_board)
    
    if pass_count == 2 or (black_count + white_count) == 64:
        # ゲーム終了
        if black_count > white_count:
            message = f"ゲーム終了!\n黒の勝ちです!\n(黒: {black_count}, 白: {white_count})"
        elif white_count > black_count:
            message = f"ゲーム終了!\n白の勝ちです!\n(黒: {black_count}, 白: {white_count})"
        else:
            message = f"ゲーム終了!\n引き分けです!\n(黒: {black_count}, 白: {white_count})"
            
        messagebox.showinfo("ゲーム終了", message)
        return True
    return False

def draw_pieces():
    """
    Pythonのボード情報に基づいて、canvas上に石を描画します。
    """
    canvas.delete("all")
    
    # 盤面の升目を再描画
    for i in range(8):
        for j in range(8):
            x1 = j * CELL_SIZE
            y1 = i * CELL_SIZE
            x2 = x1 + CELL_SIZE
            y2 = y1 + CELL_SIZE
            canvas.create_rectangle(x1, y1, x2, y2, outline="black", fill="green")
            
    # 石を描画
    for row_idx in range(8):
        for col_idx in range(8):
            piece = game_board[row_idx][col_idx]
            if piece == BLACK:
                color = "black"
            elif piece == WHITE:
                color = "white"
            else:
                continue
            
            x1 = col_idx * CELL_SIZE + 5
            y1 = row_idx * CELL_SIZE + 5
            x2 = x1 + CELL_SIZE - 10
            y2 = y1 + CELL_SIZE - 10
            
            canvas.create_oval(x1, y1, x2, y2, fill=color, outline="black")

def on_click(event):
    """
    マウスがクリックされたときに呼び出される関数です。
    """
    global current_player, pass_count
    
    col = event.x // CELL_SIZE
    row = event.y // CELL_SIZE
    
    opponent_player = WHITE if current_player == BLACK else BLACK
    
    pieces_to_flip = is_valid_move(game_board, row, col, current_player, opponent_player)

    if pieces_to_flip:
        # 有効な手であれば、石を置く
        game_board[row][col] = current_player
        flip_pieces(game_board, pieces_to_flip, current_player)
        
        # プレイヤーを交代
        current_player = opponent_player
        pass_count = 0  # パスカウントをリセット

    else:
        # 無効な手の場合、何もしない
        return
        
    draw_pieces()
    
    # 相手プレイヤーに有効な手があるかチェック
    if not get_valid_moves(game_board, current_player, opponent_player):
        print(f'{current_player}は置ける場所がありません。パスします。')
        pass_count += 1
        current_player = opponent_player  # プレイヤーを再度交代
        
        # 連続パスの場合、ゲーム終了チェック
        if pass_count == 2:
            end_game_check()
        else:
            # パスしたので、盤面を再描画して次のプレイヤーの番
            draw_pieces()
            
    # ゲーム終了条件を常にチェック
    if not end_game_check():
        pass
    else:
        print("ゲームが終了しました。")

def create_gui_board():
    """
    tkinterのウィンドウとオセロ盤のGUIを作成します。
    """
    global game_board, canvas
    
    root = tk.Tk()
    root.title("オセロゲーム")

    canvas = tk.Canvas(root, width=BOARD_SIZE, height=BOARD_SIZE, bg="green")
    canvas.pack()
    
    canvas.bind("<Button-1>", on_click)
            
    game_board = create_board()
    draw_pieces()

    root.mainloop()

# プログラムの実行
if __name__ == "__main__":
    create_gui_board()
プロンプトno4(AI対戦相手のプログラムと初期動作)
import tkinter as tk
from tkinter import messagebox
import random # <--- ここを追加

# 盤面を表すための定数
EMPTY = '・'
BLACK = '⚫'
WHITE = '⚪'

# ウィンドウとキャンバスのサイズ
BOARD_SIZE = 400
CELL_SIZE = BOARD_SIZE // 8

# グローバル変数としてボードの状態を保持
game_board = None
canvas = None
current_player = BLACK
pass_count = 0

# AIの設定
AI_PLAYER = WHITE # AIを白のプレイヤーに設定
IS_AI_TURN = False # AIのターンかどうかを示すフラグ

def create_board():
    """
    8x8の空の盤面を作成し、初期配置の石を配置します。
    """
    board = []
    for _ in range(8):
        row = [EMPTY] * 8
        board.append(row)
    
    board[3][3] = WHITE
    board[3][4] = BLACK
    board[4][3] = BLACK
    board[4][4] = WHITE
    return board

# ----------------- AIロジックの追加 -----------------
def get_ai_move(valid_moves):
    """
    有効な手の中からランダムに手を選択します。
    """
    return random.choice(valid_moves)

def handle_ai_turn():
    """
    AIのターンを処理します。
    """
    global current_player, pass_count, IS_AI_TURN
    
    opponent_player = BLACK # AIは白なので、相手は黒
    
    valid_moves = get_valid_moves(game_board, AI_PLAYER, opponent_player)

    if not valid_moves:
        pass_count += 1
        print("AIは置ける場所がありません。パスします。")
        current_player = BLACK # プレイヤーを交代
        IS_AI_TURN = False
        end_game_check()
        draw_pieces()
    else:
        pass_count = 0
        
        # AIがランダムに手を選択
        row, col = get_ai_move(valid_moves)
        
        # 石を置く処理
        pieces_to_flip = is_valid_move(game_board, row, col, AI_PLAYER, opponent_player)
        game_board[row][col] = AI_PLAYER
        flip_pieces(game_board, pieces_to_flip, AI_PLAYER)
        
        current_player = BLACK # プレイヤーを交代
        IS_AI_TURN = False
        
        draw_pieces()
        
        if not end_game_check():
            pass
        else:
            print("ゲームが終了しました。")
            
def on_click(event):
    """
    マウスがクリックされたときに呼び出される関数です。
    """
    global current_player, pass_count, IS_AI_TURN
    
    if IS_AI_TURN:
        return # AIのターン中はプレイヤーの入力を無視
    
    col = event.x // CELL_SIZE
    row = event.y // CELL_SIZE
    
    opponent_player = WHITE # プレイヤーは黒なので、相手は白
    
    pieces_to_flip = is_valid_move(game_board, row, col, current_player, opponent_player)

    if pieces_to_flip:
        game_board[row][col] = current_player
        flip_pieces(game_board, pieces_to_flip, current_player)
        
        pass_count = 0
        current_player = opponent_player
        IS_AI_TURN = True # 次のターンはAI
        
        draw_pieces()
        
        if not end_game_check():
            canvas.after(500, handle_ai_turn) # 0.5秒後にAIを動かす
        else:
            print("ゲームが終了しました。")
    else:
        print("その場所には置けません。有効な手を打ってください。")


# ----------------- ここから下の関数は変更なし -----------------
def is_valid_move(board, row, col, player, opponent):
    if not (0 <= row < 8 and 0 <= col < 8) or board[row][col] != EMPTY:
        return None
    directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
    pieces_to_flip = []
    for dr, dc in directions:
        r, c = row + dr, col + dc
        path = []
        while 0 <= r < 8 and 0 <= c < 8 and board[r][c] == opponent:
            path.append((r, c))
            r, c = r + dr, c + dc
        if 0 <= r < 8 and 0 <= c < 8 and board[r][c] == player and path:
            pieces_to_flip.extend(path)
    return pieces_to_flip if pieces_to_flip else None

def flip_pieces(board, pieces_to_flip, player):
    for r, c in pieces_to_flip:
        board[r][c] = player

def get_valid_moves(board, player, opponent):
    valid_moves = []
    for r in range(8):
        for c in range(8):
            if board[r][c] == EMPTY:
                if is_valid_move(board, r, c, player, opponent):
                    valid_moves.append((r, c))
    return valid_moves

def count_pieces(board):
    black_count = sum(row.count(BLACK) for row in board)
    white_count = sum(row.count(WHITE) for row in board)
    return black_count, white_count

def end_game_check():
    global pass_count
    black_count, white_count = count_pieces(game_board)
    if pass_count == 2 or (black_count + white_count) == 64:
        if black_count > white_count:
            message = f"ゲーム終了!\n黒の勝ちです!\n(黒: {black_count}, 白: {white_count})"
        elif white_count > black_count:
            message = f"ゲーム終了!\n白の勝ちです!\n(黒: {black_count}, 白: {white_count})"
        else:
            message = f"ゲーム終了!\n引き分けです!\n(黒: {black_count}, 白: {white_count})"
        messagebox.showinfo("ゲーム終了", message)
        return True
    return False

def draw_pieces():
    canvas.delete("all")
    for i in range(8):
        for j in range(8):
            x1 = j * CELL_SIZE
            y1 = i * CELL_SIZE
            x2 = x1 + CELL_SIZE
            y2 = y1 + CELL_SIZE
            canvas.create_rectangle(x1, y1, x2, y2, outline="black", fill="green")
    for row_idx in range(8):
        for col_idx in range(8):
            piece = game_board[row_idx][col_idx]
            if piece == BLACK:
                color = "black"
            elif piece == WHITE:
                color = "white"
            else:
                continue
            x1 = col_idx * CELL_SIZE + 5
            y1 = row_idx * CELL_SIZE + 5
            x2 = x1 + CELL_SIZE - 10
            y2 = y1 + CELL_SIZE - 10
            canvas.create_oval(x1, y1, x2, y2, fill=color, outline="black")

def create_gui_board():
    global game_board, canvas
    root = tk.Tk()
    root.title("オセロゲーム")
    canvas = tk.Canvas(root, width=BOARD_SIZE, height=BOARD_SIZE, bg="green")
    canvas.pack()
    canvas.bind("<Button-1>", on_click)
    game_board = create_board()
    draw_pieces()
    root.mainloop()

if __name__ == "__main__":
    create_gui_board()
プロンプトno5(AI対戦相手のGUI改善:ヒント表示と初期動作)
import tkinter as tk
from tkinter import messagebox
import copy
import time 
import sys

# --- ゲーム定数 ---
BOARD_SIZE = 8
CELL_SIZE = 60
WINDOW_SIZE = BOARD_SIZE * CELL_SIZE
SIDE_PANEL_WIDTH = 150
AI_PLAYER = 2 # 2: 黒 (後攻) / 1: 白 (先攻)
SEARCH_DEPTH = 4 # AIの探索深さ

# --- グローバル変数 ---
root = None
canvas = None
board = None
current_player = 1 # 1:白, 2:黒
game_running = True
score_label = None
turn_label = None
history = [] 

# --- 初期化 ---

def create_initial_board():
    """初期盤面を作成 (0:空, 1:白, 2:黒)"""
    new_board = [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)]
    new_board[3][3] = 1 # 白
    new_board[3][4] = 2 # 黒
    new_board[4][3] = 2 # 黒
    new_board[4][4] = 1 # 白
    return new_board

# --- ルールとAIロジック ---

def is_valid_move(r, c, player, current_board):
    """指定されたマスに駒を置けるかチェックし、裏返せる駒のリストを返す"""
    if current_board[r][c] != 0:
        return []

    opponent = 3 - player
    flips = []

    # 8方向をチェック
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)]

    for dr, dc in directions:
        line_flips = []
        rr, cc = r + dr, c + dc

        # 相手の駒が続く限り進む
        while 0 <= rr < BOARD_SIZE and 0 <= cc < BOARD_SIZE and current_board[rr][cc] == opponent:
            line_flips.append((rr, cc))
            rr, cc = rr + dr, cc + dc
        
        # 終点が自分の駒で、間に裏返せる駒がある場合
        if line_flips and 0 <= rr < BOARD_SIZE and 0 <= cc < BOARD_SIZE and current_board[rr][cc] == player:
            flips.extend(line_flips)

    return flips

def get_valid_moves(player, current_board):
    """合法手の一覧を (r, c) のリストで返す"""
    valid_moves = []
    for r in range(BOARD_SIZE):
        for c in range(BOARD_SIZE):
            if is_valid_move(r, c, player, current_board):
                valid_moves.append((r, c))
    return valid_moves

def apply_move(r, c, player, current_board):
    """実際に駒を置き、盤面を更新する (新しい盤面を返す)"""
    new_board = [row[:] for row in current_board]
    flips = is_valid_move(r, c, player, current_board)
    
    if not flips:
        return new_board # 無効な手の場合は元の盤面を返す

    new_board[r][c] = player
    for fr, fc in flips:
        new_board[fr][fc] = player
        
    return new_board

def evaluate_board(current_board, player):
    """評価関数: 駒の数の差を計算"""
    p1_count = sum(row.count(1) for row in current_board)
    p2_count = sum(row.count(2) for row in current_board)
    
    if player == 1:
        return p1_count - p2_count
    else:
        return p2_count - p1_count

def minimax(current_board, depth, is_maximizing_player, alpha, beta):
    """Minimax with Alpha-Beta Pruning"""
    current_player_minimax = 2 if is_maximizing_player else 1
    
    # 終端条件
    valid_moves = get_valid_moves(current_player_minimax, current_board)
    if depth == 0 or (not valid_moves and not get_valid_moves(3 - current_player_minimax, current_board)):
        return evaluate_board(current_board, 2), None # AI(黒=2)視点の評価を返す

    best_move = None
    
    if is_maximizing_player: # AI (黒=2) の手番
        max_eval = -float('inf')
        for r, c in valid_moves:
            new_board = apply_move(r, c, 2, current_board)
            current_eval, _ = minimax(new_board, depth - 1, False, alpha, beta)
            
            if current_eval > max_eval:
                max_eval = current_eval
                best_move = (r, c)
            
            alpha = max(alpha, max_eval)
            if beta <= alpha:
                break
        return max_eval, best_move
        
    else: # プレイヤー (白=1) の手番
        min_eval = float('inf')
        for r, c in valid_moves:
            new_board = apply_move(r, c, 1, current_board)
            current_eval, _ = minimax(new_board, depth - 1, True, alpha, beta)
            
            if current_eval < min_eval:
                min_eval = current_eval
                best_move = (r, c)
                
            beta = min(beta, min_eval)
            if beta <= alpha:
                break
        return min_eval, best_move

def find_best_move(current_board, depth):
    start_time = time.time()
    _, best_move = minimax(current_board, depth, True, -float('inf'), float('inf'))
    end_time = time.time()
    print(f"Minimax探索時間: {end_time - start_time:.2f}秒 (深さ: {depth})")
    return best_move

# --- UIと描画 ---

def draw_board():
    """盤面を描画し、駒を配置する"""
    canvas.delete("all")
    
    # 盤面のグリッド線を描画
    for i in range(BOARD_SIZE + 1):
        # 垂直線
        canvas.create_line(i * CELL_SIZE, 0, i * CELL_SIZE, WINDOW_SIZE, fill="black")
        # 水平線
        canvas.create_line(0, i * CELL_SIZE, WINDOW_SIZE, i * CELL_SIZE, fill="black")

    # 駒の配置
    for r in range(BOARD_SIZE):
        for c in range(BOARD_SIZE):
            player = board[r][c]
            if player != 0:
                x1 = c * CELL_SIZE + 5
                y1 = r * CELL_SIZE + 5
                x2 = (c + 1) * CELL_SIZE - 5
                y2 = (r + 1) * CELL_SIZE - 5
                color = "white" if player == 1 else "black"
                canvas.create_oval(x1, y1, x2, y2, fill=color, outline="black")

    update_score_display()

def update_score_display():
    """スコアとターン表示を更新する"""
    p1_count = sum(row.count(1) for row in board)
    p2_count = sum(row.count(2) for row in board)
    
    score_text = f"白: {p1_count}\n黒: {p2_count}"
    turn_text = f"現在のターン: {'白' if current_player == 1 else '黒'}"
    
    if score_label and turn_label:
        score_label.config(text=score_text)
        turn_label.config(text=turn_text)

def show_hint():
    """合法手があるマスをハイライト表示する"""
    canvas.delete("hint")
    valid_moves = get_valid_moves(current_player, board)
    
    for r, c in valid_moves:
        x1 = c * CELL_SIZE + 2
        y1 = r * CELL_SIZE + 2
        x2 = (c + 1) * CELL_SIZE - 2
        y2 = (r + 1) * CELL_SIZE - 2
        canvas.create_rectangle(x1, y1, x2, y2, outline="yellow", width=3, tags="hint")

# --- イベントハンドラ ---

def handle_click(event):
    """マウスがクリックされたときの処理"""
    global board, current_player, game_running
    
    if not game_running or current_player == AI_PLAYER:
        return

    c = event.x // CELL_SIZE
    r = event.y // CELL_SIZE
    
    if not (0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE):
        return

    flips = is_valid_move(r, c, current_player, board)
    
    if flips:
        save_state() # 履歴保存
        
        board = apply_move(r, c, current_player, board)
        
        next_player = 3 - current_player
        end_turn(next_player)
    else:
        messagebox.showinfo("無効な手", "そこには置けません。")

def end_turn(next_player):
    """ターン終了時の処理"""
    global current_player, game_running
    current_player = next_player
    
    draw_board()
    
    p1_moves = get_valid_moves(1, board)
    p2_moves = get_valid_moves(2, board)
    
    if not p1_moves and not p2_moves:
        # 両者とも置けない = ゲーム終了
        game_running = False
        p1_count = sum(row.count(1) for row in board)
        p2_count = sum(row.count(2) for row in board)
        
        winner = "引き分け"
        if p1_count > p2_count: winner = "白の勝利"
        elif p2_count > p1_count: winner = "黒の勝利"
            
        messagebox.showinfo("ゲーム終了", f"最終スコア 白:{p1_count}, 黒:{p2_count}\n{winner}")
        return
        
    elif not get_valid_moves(current_player, board):
        # パス (合法手が無い場合は相手のターンに戻る)
        messagebox.showinfo("パス", f"{'白' if current_player == 1 else '黒'}は置く場所がないためパスします。")
        current_player = 3 - current_player
        draw_board() # プレイヤーが変わったことを表示

    show_hint()
    
    if current_player == AI_PLAYER and game_running:
        root.after(500, make_ai_move) # 0.5秒後にAIを起動

def make_ai_move():
    """AIが指し手を決定し、実行する"""
    global board, current_player, game_running
    
    if not game_running or current_player != AI_PLAYER:
        return

    save_state() # 履歴保存
    
    best_move = find_best_move(board, SEARCH_DEPTH)
    
    if best_move:
        r, c = best_move
        board = apply_move(r, c, AI_PLAYER, board)
        
        next_player = 3 - AI_PLAYER
        end_turn(next_player)
    else:
        # AIが置けない場合もパス処理で end_turn が実行される
        end_turn(3 - AI_PLAYER)

def save_state():
    """現在のゲーム状態を履歴に保存する (Undo用)"""
    global history
    state = (
        [row[:] for row in board],
        current_player
    )
    history.append(state)

def undo_move():
    """一手前の状態に戻す"""
    global board, current_player, game_running, history
    
    if current_player == AI_PLAYER:
        messagebox.showinfo("警告", "AIの手番中はUndoできません。")
        return
        
    if len(history) <= 1:
        messagebox.showinfo("警告", "初期状態のため、これ以上戻せません。")
        return
    
    # プレイヤーとAIの2手分を戻す
    if len(history) >= 2:
        history.pop() # AIの手
        history.pop() # プレイヤーの手
    else:
        # 初期盤面直後の履歴の場合
        history.pop() 

    prev_board, prev_player = history[-1]
    
    board = [row[:] for row in prev_board]
    current_player = prev_player
    game_running = True
    
    draw_board()
    show_hint()
    
    if current_player == AI_PLAYER and game_running:
        root.after(500, make_ai_move)
        
# --- GUIメイン関数 ---

def create_board_ui():
    global root, canvas, board, score_label, turn_label
    
    root = tk.Tk()
    root.title(f"オセロゲーム (AI深さ: {SEARCH_DEPTH})")
    
    # --- UI中央配置のための改善 ---
    
    # 1. すべての要素を保持するメインフレームを作成
    main_frame = tk.Frame(root)
    
    # 2. main_frameを親ウィンドウの中央に配置
    #    expand=True で利用可能なスペースを広げ、anchor='center' で中央に寄せます。
    main_frame.pack(expand=True, anchor='center') 
    
    # --------------------------------

    # 3. ボード(キャンバス)を main_frame の中に配置 (親を root から main_frame に変更)
    canvas = tk.Canvas(main_frame, width=WINDOW_SIZE, height=WINDOW_SIZE, bg="green")
    canvas.pack(side=tk.LEFT)
    
    # 4. スコアパネル(サイドパネル)を main_frame の中に配置 (親を root から main_frame に変更)
    score_frame = tk.Frame(main_frame, width=SIDE_PANEL_WIDTH)
    score_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=10, pady=10)
    
    # --- スコアとターン表示 ---
    
    score_label = tk.Label(score_frame, text="スコア", font=("Arial", 16))
    score_label.pack(pady=20)
    
    turn_label = tk.Label(score_frame, text="ターン", font=("Arial", 14))
    turn_label.pack(pady=10)

    # --- ボタン ---
    
    undo_button = tk.Button(score_frame, text="一手戻る (Undo)", command=undo_move, 
                            height=2, bg="#B0C4DE", fg="black")
    undo_button.pack(pady=20)
    
    hint_button = tk.Button(score_frame, text="ヒント表示", command=show_hint,
                            height=2, bg="#FFFACD", fg="black")
    hint_button.pack(pady=10)
    
    # --- ゲームの起動 ---
    
    board = create_initial_board()
    canvas.bind("<Button-1>", handle_click)
    
    # 初期状態を履歴に保存
    save_state()
    
    draw_board()
    show_hint()
    
    def on_closing():
        root.destroy()
        sys.exit()
        
    root.protocol("WM_DELETE_WINDOW", on_closing)
    root.mainloop()

if __name__ == "__main__":
    create_board_ui()
プロンプトno6(AI対戦相手の完全版ヒント表示の選択機能の追加と初期動作)
import tkinter as tk
from tkinter import messagebox
import copy
import time 
import sys

# --- ゲーム定数 ---
BOARD_SIZE = 8
CELL_SIZE = 60
WINDOW_SIZE = BOARD_SIZE * CELL_SIZE
SIDE_PANEL_WIDTH = 150
AI_PLAYER = 2 # 2: 黒 (後攻) / 1: 白 (先攻)
SEARCH_DEPTH = 4 # AIの探索深さ

# --- グローバル変数 ---
root = None
canvas = None
board = None
current_player = 1 # 1:白, 2:黒
game_running = True
score_label = None
turn_label = None
history = [] 

# --- 初期化 ---

def create_initial_board():
    """初期盤面を作成 (0:空, 1:白, 2:黒)"""
    new_board = [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)]
    new_board[3][3] = 1 # 白
    new_board[3][4] = 2 # 黒
    new_board[4][3] = 2 # 黒
    new_board[4][4] = 1 # 白
    return new_board

# --- ルールとAIロジック ---

def is_valid_move(r, c, player, current_board):
    """指定されたマスに駒を置けるかチェックし、裏返せる駒のリストを返す"""
    if current_board[r][c] != 0:
        return []

    opponent = 3 - player
    flips = []

    # 8方向をチェック
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)]

    for dr, dc in directions:
        line_flips = []
        rr, cc = r + dr, c + dc

        # 相手の駒が続く限り進む
        while 0 <= rr < BOARD_SIZE and 0 <= cc < BOARD_SIZE and current_board[rr][cc] == opponent:
            line_flips.append((rr, cc))
            rr, cc = rr + dr, cc + dc
        
        # 終点が自分の駒で、間に裏返せる駒がある場合
        if line_flips and 0 <= rr < BOARD_SIZE and 0 <= cc < BOARD_SIZE and current_board[rr][cc] == player:
            flips.extend(line_flips)

    return flips

def get_valid_moves(player, current_board):
    """合法手の一覧を (r, c) のリストで返す"""
    valid_moves = []
    for r in range(BOARD_SIZE):
        for c in range(BOARD_SIZE):
            if is_valid_move(r, c, player, current_board):
                valid_moves.append((r, c))
    return valid_moves

def apply_move(r, c, player, current_board):
    """実際に駒を置き、盤面を更新する (新しい盤面を返す)"""
    new_board = [row[:] for row in current_board]
    flips = is_valid_move(r, c, player, current_board)
    
    if not flips:
        return new_board # 無効な手の場合は元の盤面を返す

    new_board[r][c] = player
    for fr, fc in flips:
        new_board[fr][fc] = player
        
    return new_board

def evaluate_board(current_board, player):
    """評価関数: 駒の数の差を計算"""
    p1_count = sum(row.count(1) for row in current_board)
    p2_count = sum(row.count(2) for row in current_board)
    
    if player == 1:
        return p1_count - p2_count
    else:
        return p2_count - p1_count

def minimax(current_board, depth, is_maximizing_player, alpha, beta):
    """Minimax with Alpha-Beta Pruning"""
    current_player_minimax = 2 if is_maximizing_player else 1
    
    # 終端条件
    valid_moves = get_valid_moves(current_player_minimax, current_board)
    if depth == 0 or (not valid_moves and not get_valid_moves(3 - current_player_minimax, current_board)):
        return evaluate_board(current_board, 2), None # AI(黒=2)視点の評価を返す

    best_move = None
    
    if is_maximizing_player: # AI (黒=2) の手番
        max_eval = -float('inf')
        for r, c in valid_moves:
            new_board = apply_move(r, c, 2, current_board)
            current_eval, _ = minimax(new_board, depth - 1, False, alpha, beta)
            
            if current_eval > max_eval:
                max_eval = current_eval
                best_move = (r, c)
            
            alpha = max(alpha, max_eval)
            if beta <= alpha:
                break
        return max_eval, best_move
        
    else: # プレイヤー (白=1) の手番
        min_eval = float('inf')
        for r, c in valid_moves:
            new_board = apply_move(r, c, 1, current_board)
            current_eval, _ = minimax(new_board, depth - 1, True, alpha, beta)
            
            if current_eval < min_eval:
                min_eval = current_eval
                best_move = (r, c)
                
            beta = min(beta, min_eval)
            if beta <= alpha:
                break
        return min_eval, best_move

def find_best_move(current_board, depth):
    start_time = time.time()
    _, best_move = minimax(current_board, depth, True, -float('inf'), float('inf'))
    end_time = time.time()
    print(f"Minimax探索時間: {end_time - start_time:.2f}秒 (深さ: {depth})")
    return best_move

# --- UIと描画 ---

def draw_board():
    """盤面を描画し、駒を配置する"""
    canvas.delete("all")
    
    # 盤面のグリッド線を描画
    for i in range(BOARD_SIZE + 1):
        # 垂直線
        canvas.create_line(i * CELL_SIZE, 0, i * CELL_SIZE, WINDOW_SIZE, fill="black")
        # 水平線
        canvas.create_line(0, i * CELL_SIZE, WINDOW_SIZE, i * CELL_SIZE, fill="black")

    # 駒の配置
    for r in range(BOARD_SIZE):
        for c in range(BOARD_SIZE):
            player = board[r][c]
            if player != 0:
                x1 = c * CELL_SIZE + 5
                y1 = r * CELL_SIZE + 5
                x2 = (c + 1) * CELL_SIZE - 5
                y2 = (r + 1) * CELL_SIZE - 5
                color = "white" if player == 1 else "black"
                canvas.create_oval(x1, y1, x2, y2, fill=color, outline="black")

    update_score_display()

def update_score_display():
    """スコアとターン表示を更新する"""
    p1_count = sum(row.count(1) for row in board)
    p2_count = sum(row.count(2) for row in board)
    
    score_text = f"白: {p1_count}\n黒: {p2_count}"
    turn_text = f"現在のターン: {'白' if current_player == 1 else '黒'}"
    
    if score_label and turn_label:
        score_label.config(text=score_text)
        turn_label.config(text=turn_text)

def show_hint():
    """合法手があるマスをハイライト表示する"""
    canvas.delete("hint")
    valid_moves = get_valid_moves(current_player, board)
    
    for r, c in valid_moves:
        x1 = c * CELL_SIZE + 2
        y1 = r * CELL_SIZE + 2
        x2 = (c + 1) * CELL_SIZE - 2
        y2 = (r + 1) * CELL_SIZE - 2
        canvas.create_rectangle(x1, y1, x2, y2, outline="yellow", width=3, tags="hint")

# --- イベントハンドラ ---

def handle_click(event):
    """マウスがクリックされたときの処理"""
    global board, current_player, game_running
    
    if not game_running or current_player == AI_PLAYER:
        return

    c = event.x // CELL_SIZE
    r = event.y // CELL_SIZE
    
    if not (0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE):
        return

    flips = is_valid_move(r, c, current_player, board)
    
    if flips:
        save_state() # 履歴保存
        
        board = apply_move(r, c, current_player, board)
        
        next_player = 3 - current_player
        end_turn(next_player)
    else:
        messagebox.showinfo("無効な手", "そこには置けません。")

def end_turn(next_player):
    """ターン終了時の処理"""
    global current_player, game_running
    current_player = next_player
    
    draw_board()
    
    p1_moves = get_valid_moves(1, board)
    p2_moves = get_valid_moves(2, board)
    
    if not p1_moves and not p2_moves:
        # 両者とも置けない = ゲーム終了
        game_running = False
        p1_count = sum(row.count(1) for row in board)
        p2_count = sum(row.count(2) for row in board)
        
        winner = "引き分け"
        if p1_count > p2_count: winner = "白の勝利"
        elif p2_count > p1_count: winner = "黒の勝利"
            
        messagebox.showinfo("ゲーム終了", f"最終スコア 白:{p1_count}, 黒:{p2_count}\n{winner}")
        return
        
    elif not get_valid_moves(current_player, board):
        # パス (合法手が無い場合は相手のターンに戻る)
        messagebox.showinfo("パス", f"{'白' if current_player == 1 else '黒'}は置く場所がないためパスします。")
        current_player = 3 - current_player
        draw_board() # プレイヤーが変わったことを表示

    show_hint()
    
    if current_player == AI_PLAYER and game_running:
        root.after(500, make_ai_move) # 0.5秒後にAIを起動

def make_ai_move():
    """AIが指し手を決定し、実行する"""
    global board, current_player, game_running
    
    if not game_running or current_player != AI_PLAYER:
        return

    save_state() # 履歴保存
    
    best_move = find_best_move(board, SEARCH_DEPTH)
    
    if best_move:
        r, c = best_move
        board = apply_move(r, c, AI_PLAYER, board)
        
        next_player = 3 - AI_PLAYER
        end_turn(next_player)
    else:
        # AIが置けない場合もパス処理で end_turn が実行される
        end_turn(3 - AI_PLAYER)

def save_state():
    """現在のゲーム状態を履歴に保存する (Undo用)"""
    global history
    state = (
        [row[:] for row in board],
        current_player
    )
    history.append(state)

def undo_move():
    """一手前の状態に戻す"""
    global board, current_player, game_running, history
    
    if current_player == AI_PLAYER:
        messagebox.showinfo("警告", "AIの手番中はUndoできません。")
        return
        
    if len(history) <= 1:
        messagebox.showinfo("警告", "初期状態のため、これ以上戻せません。")
        return
    
    # プレイヤーとAIの2手分を戻す
    if len(history) >= 2:
        history.pop() # AIの手
        history.pop() # プレイヤーの手
    else:
        # 初期盤面直後の履歴の場合
        history.pop() 

    prev_board, prev_player = history[-1]
    
    board = [row[:] for row in prev_board]
    current_player = prev_player
    game_running = True
    
    draw_board()
    show_hint()
    
    if current_player == AI_PLAYER and game_running:
        root.after(500, make_ai_move)
        
# --- GUIメイン関数 ---

def create_board_ui():
    global root, canvas, board, score_label, turn_label
    
    root = tk.Tk()
    root.title(f"オセロゲーム (AI深さ: {SEARCH_DEPTH})")
    
    # --- UI中央配置のための改善 ---
    
    # 1. すべての要素を保持するメインフレームを作成
    main_frame = tk.Frame(root)
    
    # 2. main_frameを親ウィンドウの中央に配置
    #    expand=True で利用可能なスペースを広げ、anchor='center' で中央に寄せます。
    main_frame.pack(expand=True, anchor='center') 
    
    # --------------------------------

    # 3. ボード(キャンバス)を main_frame の中に配置 (親を root から main_frame に変更)
    canvas = tk.Canvas(main_frame, width=WINDOW_SIZE, height=WINDOW_SIZE, bg="green")
    canvas.pack(side=tk.LEFT)
    
    # 4. スコアパネル(サイドパネル)を main_frame の中に配置 (親を root から main_frame に変更)
    score_frame = tk.Frame(main_frame, width=SIDE_PANEL_WIDTH)
    score_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=10, pady=10)
    
    # --- スコアとターン表示 ---
    
    score_label = tk.Label(score_frame, text="スコア", font=("Arial", 16))
    score_label.pack(pady=20)
    
    turn_label = tk.Label(score_frame, text="ターン", font=("Arial", 14))
    turn_label.pack(pady=10)

    # --- ボタン ---
    
    undo_button = tk.Button(score_frame, text="一手戻る (Undo)", command=undo_move, 
                            height=2, bg="#B0C4DE", fg="black")
    undo_button.pack(pady=20)
    
    hint_button = tk.Button(score_frame, text="ヒント表示", command=show_hint,
                            height=2, bg="#FFFACD", fg="black")
    hint_button.pack(pady=10)
    
    # --- ゲームの起動 ---
    
    board = create_initial_board()
    canvas.bind("<Button-1>", handle_click)
    
    # 初期状態を履歴に保存
    save_state()
    
    draw_board()
    show_hint()
    
    def on_closing():
        root.destroy()
        sys.exit()
        
    root.protocol("WM_DELETE_WINDOW", on_closing)
    root.mainloop()

if __name__ == "__main__":
    create_board_ui()

次回はナンプレ(数独)のアプリを作成と動作確認を予定しています。楽しみにお待ちください。

ホーム » ブログ

ポンコツ夫婦のBIKE日記 with Insta360 Vol.4(後編)

「いつかは二人でツーリングコースを走る」という目標を掲げつつ、漢のロマンを語るDr.takodemousのBIKE日記です。今回Insta360 Ace Proが捉えたのは、筆者の青春の思い出が詰まった国営木曽三川公園138タワーパーク周辺。動画と合わせて、この地域の謎めいた歴史をお届けします。

一宮ヘリクラブ飛行場付近からスタート

vol.3では国営木曽三川公園 138タワーパーク周辺をツーリングし、青春時代の思い出などを紹介しました。実はVol.4(前編・後編)はVol.3と連動しています。正直に言うと動画を分割したに過ぎないのですが、R60世代が無理なく一日かけて過ごせる、ツーリング+αを提案できればと思い、敢えて別のナンバリングをしています。

動画の見どころは、蘇南公園 多目的グラウンド付近の堤防です。実はこのコースは、ロケ班のような下見をしていた時に筆者が偶然見つけた珍しい区間なのです。

【見逃し厳禁】全国でも稀な「二重堤防道路」

前回までのブログにも記載していますが、木曽川の堤防の歴史と相まって、堤防道路が木曽川左岸に対して二つ並行して存在する区間があるのです。筆者自身も調べ始めて気がつきました。このような構造は全国でも稀な存在です。皆さんも是非ツーリングして、この稀なコースを堪能してはいかがでしょうか。

木曽川左岸から川島堤防道路へ

本ブログVol.4(後編)と前回Vol.4(前編)を合わせると、木曽川左岸と川島堤防道路のツーリングが完成します。近郊に住む地元民でもあまり知られていない事実があります。それは、川島町が木曽川の中州に位置する地域であり、かつては全国で唯一、全域が川に囲まれた単独の町であった歴史があるということです。

中州でありながら広大な地形をしており、相当数の住居があることから、町(普通地方公共団体)として成立しました。現在は各務原市へと編入しています。

各務原市川島町の地理上の歴史

川島町は、以下のような非常に特異な地理的・歴史的背景を持っています。

  • 中州・河川島の集落: 木曽川の広大な流れの中にできた「川の島(河川島)」に集落が形成されました。
  • 治水との闘い(輪中): 度重なる木曽川の洪水に見舞われ、水害から集落を守るために、輪中(集落の周囲を堤防で囲む治水形態)の文化や技術が発達しました。
  • 渡船の歴史: かつては橋がなく、対岸との往来は渡し船に頼っていました(例:川島の渡し)。

各務原市川島町の魅力

  • 河川環境楽園(木曽川水園): 国営木曽三川公園の一部。広大な敷地内で水辺の生き物や植物を観察しながら、四季折々の美しい景色を楽しめます。
  • 水と共にある独特な文化の歴史: 輪中の文化や渡し船の歴史が深く根付いており、人々が水とどのように共生し、生活を守ってきたのかという、濃尾平野の治水の歴史を肌で感じることができます。
  • 複合レジャー施設としての利便性: 敷地が隣接する「オアシスパーク」は、高速道路(東海北陸自動車道)からも一般道からもアクセスできる複合レジャー施設です。
  • 国宝 犬山城と城下町:木曽川を遡った対岸、愛知県犬山市に位置します。現存する天守が国宝に指定されている犬山城は、木曽川のほとりの小高い山に建ち、その景観は圧巻です。城下町には、昔ながらの街並みが保存・整備されており、グルメや散策を楽しめます。川島町からは、木曽川沿いの道路をツーリングするルートとしても人気です。
  • 国営木曽三川公園 138タワーパーク:木曽川を下流(一宮市)方面に進んだところに位置します。公園のシンボルである「ツインアーチ138」は、遠くからでも目立つランドマークです。タワーの展望階からは、木曽三川の流れや濃尾平野の雄大な景色を一望でき、ツーリングの休憩や目的地として最適です。
  • 伊木山と伊木山城址:木曽川沿いにある山で、自然景観と歴史が融合したスポットです。頂上付近にはかつてのお城の跡があり、比較的低山ながらも、山頂からの眺めは木曽川の流れと周辺の平野を見下ろすことができ、清々しい気分になれます。ツーリング途中に少し汗を流すための軽いハイキングを楽しむのにも良いでしょう。
  • 岐阜かかみがはら航空宇宙博物館(そらはく):各務原市内にあり、日本の航空宇宙産業の歴史を学べる大規模な博物館です。実物の航空機やロケット関連の展示が豊富で、乗り物好きのツーリング客にとっては、知的好奇心を刺激される魅力的な立ち寄り先となります。

筆者のツーリングは、相変わらず一人のショートコースが中心です。Redkabagonさんの公道デビューは、どうやら「来年こそは!」というセリフが恒例行事になりそうな予感がしています。当ブログの名物企画として、公道デビューまでの道のり(の長さ)を、これからも気長に見届けてください。

次回は馬飼ビーチ周辺のバイク日記を投稿予定としています。お楽しみに。

ホーム » ブログ

ポンコツ夫婦のBIKE日記 with Insta360 Vol.4(前編)

「いつかは二人でツーリングコースを走る」という目標を掲げつつ、漢のロマンを語るDr.takodemousのBIKE日記です。今回Insta360 Ace Proが捉えたのは、筆者の青春の思い出が詰まった国営木曽三川公園138タワーパーク周辺。動画と合わせて、この地域の謎めいた歴史をお届けします。(なお、機材の詳細は今後「The Gear」カテゴリーでご紹介します。)

光明寺公園 球技場からスタート

vol.3では国営木曽三川公園 138タワーパーク周辺をツーリングし,青春時代の思い出などを紹介しました。実はVol.4(前編・後編)はVol.3と連動しています。正直に言うと動画を分割したに過ぎないのですが、R60世代が無理なく一日かけて過ごせる、ツーリング+αを提案できればと思い、敢えて別のナンバリングをしています。

動画の見どころは、ズバリ光明寺公園の駐車場をでた直後の堤防です。このコースは、筆者の青春時代にはまだ相互通行ができていました。ご覧になれば一目瞭然に、このコースのどこで自動車がすれ違えるのだろうと感じるくらい、道幅が狭くなっていて、現在では一方通行となっています。そして、旧極楽寺側からの一方通行と相対する事となります。動画では右折迂回路を経由していますが、左折をすると国営木曽三川公園 138タワーパークの駐車場へと向かいます。桜の季節には本コースも咲き乱れますので、読者の皆様もツーリングの一考にしてみてはいかがですか。

木曽川左岸から川島堤防道路へ

本ブログVol.4(前編)と次回Vol.4(後編)を合わせると、木曽川左岸と川島堤防道路のツーリングが完成します。近郊に住んでいる地元民でもあまり知られていない事実があります。それは、川島町は木曽川の中洲に位置する地域であり、木曽川の作る大小二つの島の中洲が一体となっており、その地形的特徴から、かつては単独の市町村で全域が川に囲まれた、全国で唯一の町であった歴史があります。

少し砕いて言うと、中州でありながら広大な地形をしており、相当数の住居がある事から、町(普通地方公共団体)の人口や都市的要件を満たしており、川島町として成立したという事になります。現在は各務原市へと編入しています。詳細はvol.4(後半)で紹介しますのでお待ちください。

自然を満喫しながらのサイクリング

木曽三川公園のサイクリングコースは、広大な敷地と河川沿いの景観を活かして整備されており、特に近年になってコースが延伸され、より広範囲を楽しめるようになりました。

歴史的経緯

  • 2016年(平成28年)2月13日に、138タワーパーク内のサイクリングロードが新たに開通しました。
  • この開通により、犬山市の木曽川犬山緑地から一宮市里小牧にある木曽川緑地公園まで、約18kmのコースがつながりました。
  • このコースは自転車用と歩行者用が色分けされており、安全にサイクリングやウォーキングを楽しめるように整備されています。

138タワーパーク 周辺のおすすめスポット

138タワーパークからのサイクリングで、気軽に立ち寄れるスポットをご紹介します。

1. 木曽川資料館(木曽川水系流域の歴史と治水)

木曽川をテーマにした資料館で、木曽三川分流工事(デ・レーケの治水事業)や、輪中の歴史、水との闘いに関する貴重な資料が展示されています。サイクリング休憩がてら、この地域の地理や歴史の深さを知るのに最適です。

2. 一宮市立尾西歴史民俗資料館(地域の文化と歴史)

旧尾西市(現一宮市の一部)の歴史や、かつて繊維産業で栄えた地域の文化を学べる施設です。機織り(はたおり)に関する展示などが充実しており、木曽川沿いの産業史に触れることができます。

3. 浅井山公園(休憩と自然)

一宮市北部にある自然豊かな公園です。大きな池があり、緑が多く、サイクリングの途中でベンチに座って休憩したり、静かな水辺の景色を楽しんだりするのに適しています。

4. 川島大橋・付近の河川敷(景観と開放感)

木曽川に架かる川島大橋の周辺は、特に河川敷の開放感があり、雄大な木曽川の流れを間近に感じることができます。休憩するのに適したスペースもあり、ゆったりとした景色を楽しむのに最適です。

木曽三川公園のサイクリングコースは、自然や歴史、そしてグルメを楽しみながら、自分のペースでゆっくりと散策するのにぴったりの場所です。

橋の歴史

vol.3では紹介していなかった橋の歴史シリーズを、vol4(前半)と併せて紹介いたします。また、次回vol.4(後半)についても本ブログと被っている動画を掲載することになりますので、併記としてご覧ください。

尾濃大橋の歴史(びのうおおはし)

  • 開通年: 1957年(昭和32年)
  • 建設の経緯: 尾濃大橋が架かる以前は、木曽川の渡船が利用されていました。しかし、交通量の増加と度重なる洪水のたびに渡し船が流されたり、運航が停止したりする事態が続いていました。地域の安全と交通の利便性を確保するため、橋の建設が強く求められるようになりました。
  • 特徴: 尾濃大橋の建設は、地域の発展に大きく貢献しました。特に、当時の自動車交通の増加に対応するため、重要な幹線道路として機能しました。

木曽川橋 (きそがわばし)

  • 開通: 1937年(昭和12年)に開通した道路橋です。
  • 特徴: 名古屋と岐阜を結ぶ主要な街道の要所に位置しており、日本の土木技術の発展を示す貴重な橋として知られています。
  • 現在の役割: 現在も交通量の多い幹線道路の一部として利用されています。

新木曽川橋(しんきそがわばし)

  • 1937年(昭和12年)10月: 最初に木曽川に架けられた橋は、現在の新木曽川橋よりも上流にある「木曽川橋」として開通しました。これは、現在の国道22号の旧道にあたる部分に位置していました。
  • 1969年(昭和44年): 高度経済成長期の交通量増大に対応するため、当時の木曽川橋の下流に、新たな橋として「新木曽川橋」が開通しました。この橋は、当初は名古屋方面から岐阜方面への下り線として使用されました。
  • 1975年(昭和50年): 上り線(岐阜方面から名古屋方面)の橋も完成し、上下線が分離した4車線道路として供用を開始しました。これにより、交通渋滞が大幅に緩和されました。

一宮川島線渡橋 (いちのみやかわしませんわたりばし)

  • 開通: 1964年(昭和39年)11月に開通しました。
  • 特徴: 以前は「渡船(わたしぶね)」がありましたが、度重なる洪水による欠航や、1959年(昭和34年)の伊勢湾台風で流失したことをきっかけに、橋の建設が進められました。
  • 歴史的背景: 橋の建設前には、木造の仮橋が何度か架けられましたが、その都度洪水で流されるなど、この地域が水害に悩まされてきた歴史を物語っています。

これらの橋は、それぞれの時代において、人々の生活や経済、そして地域の安全を守るために建設され、現在に至っています。

筆者のツーリングは、相変わらず一人のショートコースが中心です。Redkabagonさんの公道デビューは、どうやら「来年こそは!」というセリフが恒例行事になりそうな予感がしています。当ブログの名物企画として、公道デビューまでの道のり(の長さ)を、これからも気長に見届けてください。

次回は川島堤防道路から思いやり橋までのバイク日記を投稿予定としています。お楽しみに。

ホーム » ブログ

ポンコツ夫婦のBIKE日記with Insta 360 vol.3

昨年から「ポンコツ夫婦の旅日記」など、いくつかのカテゴリー変更を実施しています。今回は、MonkeyとZoomerのカテゴリーを統合し、「ポンコツ夫婦のBIKE日記」へと変更しました。

このカテゴリーは「The Gear」ともクロスオーバーさせ、今後は自動車(JimnyとEvery Wagon)関連の投稿にも広げていく予定です。まだ計画段階ですが、楽しみにしていてください。

「BIKE日記」のメッセージは、Dr.takodemousが①漢のロマンを語る、②熱血指導をする、③いつかは二人でツーリングコースを走る、の3つです。特に②は期待できると思います。

今回のテーマは「③いつかは二人でツーリングコースを走る」です。Insta360 Ace Proを搭載し、近所の堤防をツーリングした様子を掲載していきます。

岐阜県羽島市某所に住む筆者にとって、木曽川や長良川、揖斐川の堤防は、身近でツーリングに最適な環境です。

動画には、実際に乗車している感覚を伝えるため、あえてBGMを挿入していません。ただ、自動車学校のシミュレーターのように、自分が正面を向いているのに映像が傾くことがあります。苦手な方は、フル画面を避けたり、画質を下げたりしてご覧ください。

筆者は「R60仲良し夫婦コーディネーター」になることを目指しており、このブログはそのモチベーションで続けています。

これまではiPhoneで動画や画像を撮影していましたが、WordPressへのアップロード時に容量制限の問題がありました。

そこで、よりレベルの高い撮影と編集が可能な「Insta360 Ace Pro」を導入しました。このアイテムについては、別途「The Gear」カテゴリーで詳しくご紹介しますので、お楽しみに。

尾濃大橋からスタート

vol.3では筆者の青春時代を過ごした、現在の国営木曽三川公園 138タワーパーク周辺をツーリングします。高校時代は部活動でのランニングコース、大学時代はジムニーを駆ってのオフロードや、軟式野球のグランドとして過ごした思い出の場所です。

動画の見どころは、名鉄本線を超えた辺りから見えてくる、堤防越しの木曽川の風景です。現在、サイクリングロードを施工している最中です。総延長は、60kmに達するサイクリングコースになると広報されており、他の堤防では観る事とのできないコラボレーションを期待しながら、ツーリングができます。

木曽川左岸を上流へ

前回のポンコツ夫婦のBIKE日記でもご紹介したしまたが、新濃尾大橋と同様に尾濃大橋の知名度はまだまだ低いといえます。やはり、大垣側への道路が長良川で止まっているためだと推測します。

新濃尾大橋と周辺道路との違いは、羽島側と安八側での陸地部分の道路はほぼルートが整備されており、あとは新長良橋(仮名)の着工と羽島側の若干の土地買収が残されているだけという状況です。

日頃から一宮と大垣の間で渋滞に悩まされている住民としては、早期解消を願うばかりですが、若干尾濃大橋から大垣に抜けるルートの着工が早く、完成するのではと期待を寄せています。

国営木曽三川公園 138タワーパーク

現在の国営木曽三川公園 138タワーパークは、冒頭にも記載しておりますが、筆者の青春時代である大学生時代まで、ブログでは書けないような内容も含んだ思い出の地です。旧の名称は極楽寺公園という呼ばれ方をしていました。

そんな思い出の場所も、時代と共に姿を変え、現在は新しい魅力にあふれています。

国営公園としての誕生

138タワーパークは、木曽川・長良川・揖斐川の木曽三川が有する広大な河川敷を活用し、レクリエーション需要の増大に応えるために設置された「国営木曽三川公園」の一部です。特に、上流地区の拠点として位置づけられています。

開園と名称の由来

  • 開園: 1995年(平成7年)4月29日に開園しました。
  • 名称: 公園のシンボルである「ツインアーチ138」にちなんで名付けられました。
  • タワーの高さ: タワーの高さが138mであるのは、所在地である一宮市(いち=1、のみ=3、=8)の語呂合わせに由来しています。

特徴と役割

138タワーパークは、開園当時、広大な芝生広場や、四季折々の花を楽しめる庭園、そして展望タワーを備えた施設として、地域住民や観光客に親しまれてきました。タワーからは、木曽川の雄大な流れや濃尾平野の景色を一望できます。

このように、138タワーパークは、国営木曽三川公園のプロジェクトの一環として、地域のランドマークであり、レクリエーションと景観の拠点として整備されました。

地理上の歴史

vol.3では地理上の歴史にスポットを当てて紹介します。動画をご覧になると、堤防の内側(堤防より河川側)に住居が多数見受けられると思います。この現象は、日本の一級河川の最上位を誇る、長良川や揖斐川ですら見受けることができません。何故このような堤防の内側に住居が存在するのかと言うと、地理的条件と歴史背景が関係します。vol.4ではいつも通り橋の歴史をお伝えしますので、お待ちください。

愛知県一宮市木曽川町や川島町など、木曽川沿いの堤防よりも川側に住宅がある地域の歴史的背景は、この地域の特有な地理的条件と、度重なる洪水との闘いに深く関わっています。

「堤外地」という場所

堤防よりも川側の地域は、一般的に「堤外地(ていがいち)」と呼ばれます。ここは河川が氾濫した際に水が流れ込むことが想定されている場所です。にもかかわらず、なぜそこに人々が住み、家が建てられてきたのか、その理由にはいくつかの歴史的な背景があります。

1. 「御囲い堤」と「輪中」の歴史

  • 御囲い堤(おかいづつみ): 江戸時代、徳川家康が尾張(現在の愛知県)を治水するために築かせた強固な堤防です。この堤防は、尾張側を洪水から守る一方で、対岸の美濃(現在の岐阜県)側よりも堤防の高さを低くするよう定められていました。
  • 輪中(わじゅう): このため、美濃側の人々は自分たちの集落を守るため、集落の周りを堤防で囲む「輪中」という独自の治水システムを発達させました。

一宮市木曽川町や川島町は、この治水システムの境界に位置していました。尾張側にあるこの地域は、当初は御囲い堤によって守られていましたが、時代が下るにつれて、堤防の外側、つまり川側にも住宅や田畑が広がるようになりました。

2. 「二重堤防」の建設

堤防よりも川側に住宅ができた後も、この地域はたびたび水害に見舞われました。特に、昭和初期に発生した大洪水は大きな被害をもたらしました。

この水害を教訓として、一宮市木曽川町では、在来の堤防(御囲い堤)の内側に加えて、「二重堤防」が建設されました。これにより、堤防よりも川側の住宅地を囲む形で、もう一つの堤防が築かれ、水害から地域を守ろうとする取り組みが進められました。

3. 歴史的経緯と生活圏の拡大

このような地域に住宅ができたのは、以下のような経緯が考えられます。

  • 生活の場としての利用: 昔から、河川敷は生活に必要な資材(砂利など)の採取や、畑として利用されてきました。洪水が起きても、水が引けばまた生活に戻れるという考え方がありました。
  • 土地利用の拡大: 人口が増加するにつれて、限られた土地を有効活用するために、比較的土地の値段が安価な堤外地にも住宅が建てられるようになりました。
  • 近代的な治水工事: 明治以降に行われた大規模な治水工事(明治改修など)により、洪水の頻度が減り、堤外地でも生活が可能になるという認識が広まったことも一因です。

これらの歴史的背景から、一宮市木曽川町や川島町などの堤防よりも川側に住宅がある地域は、先人たちが度重なる洪水と闘い、独自の治水システムを築きながら生活の場を確保してきた、その努力の証と言えるでしょう。

筆者のツーリングは、相変わらず一人のショートコースが中心です。Redkabagonさんが公道デビューできるその日まで、この身近なコースを走り込み、技術と経験を磨いていきます。この日記を通して、二人の成長をぜひ見届けてください。

次回は国営木曽三川公園 138タワーパークから川島までのバイク日記を投稿予定としています。お楽しみに。

ホーム » ブログ