from transformers import AutoModelForCausalLM, AutoTokenizer import torch import re import gradio as gr import time import spaces import copy # モデルとトークナイザーをモジュールレベルで読み込む model_name = "Qwen/Qwen2.5-7B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.bfloat16, device_map="auto") class TicTacToeBoard: def __init__(self): # 0: 空, 1: X (先手), 2: O (後手) self.board = [0] * 9 # 3x3ボード(インデックスで0-8) self.turn = 1 # 1=Xプレイヤー, 2=Oプレイヤー self.moves = [] # 指し手の履歴 self.last_move = None # 最後に指された手 def is_game_over(self): """ゲームが終了しているかをチェック""" # 勝者がいる場合 if self.get_winner() != 0: return True # 引き分け(すべてのマスが埋まっている場合) if 0 not in self.board: return True return False def get_winner(self): """勝者を返す(0=なし/引分、1=X勝ち、2=O勝ち)""" # 勝ちパターン(横・縦・斜め) win_patterns = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], # 横 [0, 3, 6], [1, 4, 7], [2, 5, 8], # 縦 [0, 4, 8], [2, 4, 6] # 斜め ] for pattern in win_patterns: if self.board[pattern[0]] != 0 and self.board[pattern[0]] == self.board[pattern[1]] == self.board[pattern[2]]: return self.board[pattern[0]] # 勝者を返す return 0 # 勝者なし def get_legal_moves(self): """合法手のリスト(0-8のインデックス)を返す""" if self.is_game_over(): return [] return [i for i in range(9) if self.board[i] == 0] def make_move(self, position): """手を指す(positionは0-8のインデックス)""" if position not in self.get_legal_moves(): raise ValueError(f"無効な手: {position}") self.board[position] = self.turn self.moves.append(position) self.last_move = position # 最後の手を記録 self.turn = 3 - self.turn # 手番を交代(1→2, 2→1) def to_string(self): """人間が読める形式で盤面を文字列化(マークダウン表形式)""" symbols = {0: "・", 1: "X", 2: "O"} result = [] # マークダウン表のヘッダー result.append("| | A | B | C |") result.append("|---|---|---|---|") # マークダウン表の本体 for i in range(3): row = [f"| {i+1} "] for j in range(3): index = i * 3 + j row.append(f"| {symbols[self.board[index]]} ") row.append("|") result.append("".join(row)) return "\n".join(result) def index_to_coord(self, index): """インデックス(0-8)を座標(A1, B2など)に変換""" if index is None: return None row = index // 3 + 1 col = chr(ord('A') + (index % 3)) return f"{col}{row}" def create_user_prompt(board, player_sym): """ユーザープロンプトを生成""" # 有効な手の一覧を生成 legal_moves = board.get_legal_moves() valid_moves = [] for move in legal_moves: row = move // 3 + 1 col = move % 3 coord = f"{chr(ord('A') + col)}{row}" valid_moves.append(coord) # 盤面の自然言語的な記述を生成 x_positions = [] o_positions = [] for i, piece in enumerate(board.board): if piece != 0: row = i // 3 + 1 col = chr(ord('A') + (i % 3)) pos = f"{col}{row}" if piece == 1: x_positions.append(pos) else: o_positions.append(pos) # 盤面の記述を構築 board_description = "盤面の説明:" if not (x_positions or o_positions): board_description += "\n空の盤面" else: if x_positions: board_description += f"\nX: {', '.join(x_positions)}" if o_positions: board_description += f"\nO: {', '.join(o_positions)}" # ユーザーの最後の手の情報を追加 user_move_info = "" if board.last_move is not None: last_move_piece = board.board[board.last_move] # プレイヤーが最後に指した手かどうかを確認 if last_move_piece == player_sym: last_move_coord = board.index_to_coord(board.last_move) user_move_info = f"ユーザーが{last_move_coord}を選びました。\n" user_prompt = ( f"{user_move_info}" f"### 現在の盤面\n{board.to_string()}\n" f"{board_description}\n\n" f"### 有効な手\n{', '.join(valid_moves)}\n\n" ) return user_prompt def extract_move(response): """応答から手を抽出""" matches = re.findall(r"(.*?)", response, re.DOTALL) return matches[-1].strip() if matches else None def extract_thinking(response): """応答から思考過程を抽出""" think_match = re.search(r"(.*?)", response, re.DOTALL) return think_match.group(1).strip() if think_match else "" def coord_to_index(coord): """座標(A1, B2など)をインデックス(0-8)に変換""" if not coord or len(coord) != 2: return None try: col = ord(coord[0].upper()) - ord('A') row = int(coord[1]) - 1 if col < 0 or col > 2 or row < 0 or row > 2: return None return row * 3 + col except: return None @spaces.GPU(duration=120) def get_ai_move(board, player, conversation_history, player_sym): """AIの手を取得""" # 新しいユーザープロンプトを作成 user_prompt = create_user_prompt(board, player_sym) # 会話履歴のディープコピーを作成して変更 messages = copy.deepcopy(conversation_history) # ユーザープロンプトを会話履歴に追加 messages.append({"role": "user", "content": user_prompt}) # モデルで推論を実行 text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) model_inputs = tokenizer([text], return_tensors="pt").to(model.device) generated_ids = model.generate(**model_inputs, max_new_tokens=512) generated_ids = [ output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) ] response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] # AIの応答を会話履歴に追加 messages.append({"role": "assistant", "content": response}) # 応答から手と思考過程を抽出 thinking = extract_thinking(response) move_coord = extract_move(response) move_index = coord_to_index(move_coord) if move_coord else None # デバッグ情報(会話履歴の長さを確認) print(f"会話履歴の現在の長さ: {len(messages)}") return move_index, thinking, messages def board_to_display(board, game_state="", ai_thinking=""): """ボードの状態を表示用に変換""" symbols = {0: " ", 1: "X", 2: "O"} board_display = [] for i in range(3): row = [] for j in range(3): index = i * 3 + j row.append(symbols[board.board[index]]) board_display.append(row) return board_display, game_state, ai_thinking def format_thinking_multi_turn(move_history): """思考プロセスをマルチターン形式でフォーマット""" if not move_history: return "### AIの思考プロセス\nまだ思考プロセスがありません。" output = [] for entry in move_history: if entry['type'] == 'ai': output.append(f"### LLMのターン") output.append(f"{entry['thinking']}") output.append(f"~~~\nLLMは {entry['move']}を えらんだ!\n~~~") elif entry['type'] == 'user': output.append(f"### ユーザーのターン") output.append(f"~~~\nユーザーは {entry['move']}を えらんだ!\n~~~") return "\n".join(output) def create_tictactoe_ui(): """Gradio UIを作成""" with gr.Blocks(title="三目並べ vs LLM") as demo: gr.Markdown("# 三目並べ vs LLM") gr.Markdown("LLMとの三目並べゲームです。盤面をクリックして手を指してください。") # ゲーム状態保持用 game_state = gr.State(None) player_symbol = gr.State(1) # デフォルト: プレイヤーがX (先手) ai_thinking_state = gr.State("") conversation_history = gr.State([]) # 会話履歴を別の状態として管理 move_history = gr.State([]) # 新たに移動履歴の状態を追加 with gr.Row(): with gr.Column(scale=1): player_choice = gr.Radio( ["X (先手)", "O (後手)"], label="あなたの選択", value="X (先手)", interactive=True ) start_button = gr.Button("ゲーム開始") reset_button = gr.Button("リセット") status = gr.Textbox(label="ゲーム状態", value="ゲームを開始してください") with gr.Column(scale=2): # 盤面表示 board_output = gr.Dataframe( headers=["A", "B", "C"], row_count=3, col_count=3, value=[ [" ", " ", " "], [" ", " ", " "], [" ", " ", " "] ], interactive=False ) # AIの思考プロセスを常にMarkdown形式で表示 ai_thinking_md = gr.Markdown("### AIの思考プロセス\nまだ思考プロセスがありません。") # クリック位置のマッピング def handle_click(evt: gr.SelectData, game, player_sym, thinking, messages, history): if game is None or game.is_game_over(): return ( [ [" ", " ", " "], [" ", " ", " "], [" ", " ", " "] ], "ゲームを開始してください", format_thinking_multi_turn(history), messages, history ) # クリック位置をマス目に変換 row, col = evt.index move_index = row * 3 + col # プレイヤーの手が有効か確認 if game.turn != player_sym or move_index not in game.get_legal_moves(): return ( board_to_display(game, "無効な手です", thinking)[0], "無効な手です", format_thinking_multi_turn(history), messages, history ) # プレイヤーの手を反映 try: game.make_move(move_index) # プレイヤーの手を履歴に追加 user_move = game.index_to_coord(move_index) updated_history = history + [{'type': 'user', 'move': user_move}] # ゲーム終了チェック if game.is_game_over(): winner = game.get_winner() if winner == 0: status_text = "引き分け!" else: symbol = "X" if winner == 1 else "O" is_player = winner == player_sym status_text = f"{symbol}の勝ち! ({'あなた' if is_player else 'AI'})" return ( board_to_display(game, status_text, thinking)[0], status_text, format_thinking_multi_turn(updated_history), messages, updated_history ) # AIの手番 ai_move, new_thinking, new_messages = get_ai_move(game, game.turn, messages, player_sym) if ai_move is not None and ai_move in game.get_legal_moves(): game.make_move(ai_move) # AIの手と思考を履歴に追加 ai_move_coord = game.index_to_coord(ai_move) updated_history = updated_history + [{'type': 'ai', 'move': ai_move_coord, 'thinking': new_thinking}] # ゲーム終了チェック if game.is_game_over(): winner = game.get_winner() if winner == 0: status_text = "引き分け!" else: symbol = "X" if winner == 1 else "O" is_player = winner == player_sym status_text = f"{symbol}の勝ち! ({'あなた' if is_player else 'AI'})" else: status_text = "あなたの番です" else: status_text = "AIが有効な手を選択できませんでした。ゲームをリセットしてください。" updated_history = history return ( board_to_display(game, status_text, new_thinking)[0], status_text, format_thinking_multi_turn(updated_history), new_messages, updated_history ) except ValueError: return ( board_to_display(game, "無効な手です", thinking)[0], "無効な手です", format_thinking_multi_turn(history), messages, history ) def start_game(choice): game = TicTacToeBoard() player_sym = 1 if choice == "X (先手)" else 2 ai_sym = 3 - player_sym # AIのシンボル # システムプロンプトを作成 symbol = "X" if ai_sym == 1 else "O" system_prompt = ( f"あなたは{symbol}としてユーザーと対戦する三目並べのプロです。\n" "まず、タグ内であなたの思考過程を考えてください。" "次に、有効な手の中から1つを選び、タグ内に座標形式(例:A1、B2、C3)で示してください。\n" "以下の形式で回答してください:\n" "\n...\n\n\n...\n" ) # 会話履歴とゲーム履歴を初期化 initial_messages = [{"role": "system", "content": system_prompt}] thinking = "" game_history = [] # プレイヤーがOの場合、AIが先手 if player_sym == 2: user_prompt = create_user_prompt(game, player_sym) initial_messages.append({"role": "user", "content": user_prompt}) # モデルで推論を実行 text = tokenizer.apply_chat_template(initial_messages, tokenize=False, add_generation_prompt=True) model_inputs = tokenizer([text], return_tensors="pt").to(model.device) generated_ids = model.generate(**model_inputs, max_new_tokens=512) generated_ids = [ output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) ] response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] # AIの応答を会話履歴に追加 initial_messages.append({"role": "assistant", "content": response}) # 応答から手と思考過程を抽出 thinking = extract_thinking(response) move_coord = extract_move(response) ai_move = coord_to_index(move_coord) if move_coord else None if ai_move is not None: game.make_move(ai_move) # ゲーム履歴にAIの手を追加 game_history.append({ 'type': 'ai', 'move': move_coord, 'thinking': thinking }) status_text = "あなたの番です" else: status_text = "AIが手を選択できませんでした。リセットしてください。" else: status_text = "あなたの番です" board_display = board_to_display(game, status_text, thinking)[0] return ( game, player_sym, board_display, status_text, format_thinking_multi_turn(game_history), thinking, initial_messages, game_history ) def reset_game(): return ( None, 1, [ [" ", " ", " "], [" ", " ", " "], [" ", " ", " "] ], "ゲームをリセットしました。開始するには「ゲーム開始」を押してください。", "### AIの思考プロセス\nまだ思考プロセスがありません。", "", [], [] ) # イベントハンドラーの設定 board_output.select( handle_click, inputs=[game_state, player_symbol, ai_thinking_state, conversation_history, move_history], outputs=[board_output, status, ai_thinking_md, conversation_history, move_history] ) start_button.click( start_game, inputs=[player_choice], outputs=[ game_state, player_symbol, board_output, status, ai_thinking_md, ai_thinking_state, conversation_history, move_history ] ) reset_button.click( reset_game, outputs=[ game_state, player_symbol, board_output, status, ai_thinking_md, ai_thinking_state, conversation_history, move_history ] ) return demo if __name__ == "__main__": demo = create_tictactoe_ui() demo.launch()