import shogi import time import random import gradio as gr import re # --- AI 思考ロジック (守備・持久戦特化) --- BASE_VALUES = { shogi.PAWN: 100, shogi.LANCE: 300, shogi.KNIGHT: 400, shogi.SILVER: 500, shogi.GOLD: 600, shogi.BISHOP: 800, shogi.ROOK: 1000, shogi.KING: 20000, shogi.PROM_PAWN: 300, shogi.PROM_LANCE: 500, shogi.PROM_KNIGHT: 500, shogi.PROM_SILVER: 600, shogi.PROM_BISHOP: 1100, shogi.PROM_ROOK: 1300, } def get_dynamic_multipliers(move_count): if move_count < 40: return {"NAME": "序盤: 陣形構築", "LEFT": 1.1, "CENTER": 1.0, "RIGHT": 1.1, "DEFENSE": 1.4, "ATTACK": 0.7} elif move_count < 100: return {"NAME": "中盤: 鉄壁ガード", "LEFT": 1.0, "CENTER": 1.8, "RIGHT": 1.0, "DEFENSE": 2.5, "ATTACK": 0.2} else: return {"NAME": "終盤: 極限持久戦", "LEFT": 1.3, "CENTER": 3.0, "RIGHT": 1.3, "DEFENSE": 5.0, "ATTACK": 0.0} def evaluate_board(board, my_color): opp_color = not my_color move_count = len(board.move_stack) dm = get_dynamic_multipliers(move_count) score = 0 for square in shogi.SQUARES: piece = board.piece_at(square) if piece is None: continue file = 9 - (square % 9) rank = (square // 9) + 1 area = "LEFT" if file >= 7 else "CENTER" if file >= 4 else "RIGHT" val = BASE_VALUES.get(piece.piece_type, 0) * dm[area] if piece.color == my_color: rel_rank = rank if my_color == shogi.BLACK else (10 - rank) val *= dm["DEFENSE"] if rel_rank <= 3 else dm["ATTACK"] if rel_rank >= 7 else 1.0 score += val else: score -= val hand_weight = 1.8 + (move_count / 120.0) for pt in [shogi.PAWN, shogi.LANCE, shogi.KNIGHT, shogi.SILVER, shogi.GOLD, shogi.BISHOP, shogi.ROOK]: score += board.pieces_in_hand[my_color][pt] * BASE_VALUES.get(pt, 0) * hand_weight score -= board.pieces_in_hand[opp_color][pt] * BASE_VALUES.get(pt, 0) * (hand_weight * 0.8) return score def alpha_beta(board, depth, alpha, beta, is_max, color, start_time, limit): if time.time() - start_time > limit or board.is_game_over() or depth == 0: return evaluate_board(board, color) moves = list(board.legal_moves) if is_max: v = -float('inf') for m in moves: board.push(m) v = max(v, alpha_beta(board, depth-1, alpha, beta, False, color, start_time, limit)) board.pop() alpha = max(alpha, v) if beta <= alpha: break return v else: v = float('inf') for m in moves: board.push(m) v = min(v, alpha_beta(board, depth-1, alpha, beta, True, color, start_time, limit)) board.pop() beta = min(beta, v) if beta <= alpha: break return v def select_best_move(board): start = time.time() best = None best_v = -float('inf') moves = list(board.legal_moves) if not moves: return None for m in moves: board.push(m) v = alpha_beta(board, 2, -float('inf'), float('inf'), False, board.turn, start, 2.5) board.pop() if v > best_v: best_v, best = v, m return best # --- 日本語指し手解析 --- KANJI_NUM = {"一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9} PIECE_NAME_MAP = {"歩": shogi.PAWN, "香": shogi.LANCE, "桂": shogi.KNIGHT, "銀": shogi.SILVER, "金": shogi.GOLD, "角": shogi.BISHOP, "飛": shogi.ROOK, "玉": shogi.KING, "王": shogi.KING, "と": shogi.PROM_PAWN, "杏": shogi.PROM_LANCE, "圭": shogi.PROM_KNIGHT, "全": shogi.PROM_SILVER, "馬": shogi.PROM_BISHOP, "龍": shogi.PROM_ROOK, "竜": shogi.PROM_ROOK} def get_candidates(board, jp_text): jp_text = jp_text.strip().replace(" ", "") if not jp_text: return [] target_square = None if jp_text.startswith("同"): if not board.move_stack: return [] target_square = board.move_stack[-1].to_square rest = jp_text[1:] else: m = re.match(r"([1-91-9一二三四五六七八九])([1-91-9一二三四五六七八九])", jp_text) if not m: return [] f_num = KANJI_NUM.get(m.group(1), int(m.group(1)) if m.group(1).isdigit() else 0) r_num = KANJI_NUM.get(m.group(2), int(m.group(2)) if m.group(2).isdigit() else 0) target_square = (r_num - 1) * 9 + (9 - f_num) rest = jp_text[2:] piece_type = next((v for k, v in PIECE_NAME_MAP.items() if k in rest), None) if not piece_type: return [] is_promote = "成" in rest and "不" not in rest is_unpromote = "不成" in rest is_drop = "打" in rest candidates = [] for move in board.legal_moves: if move.to_square == target_square: if move.drop_piece_type: if move.drop_piece_type == piece_type and not ("不成" in rest or "成" in rest): candidates.append(move) else: p = board.piece_at(move.from_square) if p and p.piece_type == piece_type: if is_promote and move.promotion: candidates.append(move) elif is_unpromote and not move.promotion: candidates.append(move) elif not is_promote and not is_unpromote: candidates.append(move) return candidates # --- UI 描画 --- def render_board_html(board): html = "" html += "" + "".join([f"" for i in range(9)]) + "" ranks = ["一", "二", "三", "四", "五", "六", "七", "八", "九"] for r in range(9): html += f"" for f in range(9): p = board.piece_at(r * 9 + f) bg = "#f3d291" if (r+f)%2==0 else "#ebc881" if p: sym = {1:"歩",2:"香",3:"桂",4:"銀",5:"金",6:"角",7:"飛",8:"玉",11:"と",12:"杏",13:"圭",14:"全",16:"馬",17:"龍"}.get(p.piece_type, "?") color = "black" if p.color == shogi.BLACK else "#d00" mark = "▲" if p.color == shogi.BLACK else "△" html += f"" else: html += f"" html += f"" html += "
{9-i}
{ranks[r]}{mark}{sym}{r+1}
" return html # --- 状態管理・コールバック --- game_board = shogi.Board() current_candidates = [] def get_move_label(board, move): if move.drop_piece_type: return "持ち駒から打つ" f_sq = move.from_square f_name = f"{9-(f_sq%9)}{ (f_sq//9)+1 }" res = f"{f_name}から移動" if move.promotion: res += "(成)" return res def process_input(user_input): global game_board, current_candidates if not user_input: return render_board_html(game_board), "入力してください。", gr.update(visible=False), [] # 1. USIか日本語か判定して候補取得 try: single_move = shogi.Move.from_usi(user_input) if single_move in game_board.legal_moves: current_candidates = [single_move] else: current_candidates = get_candidates(game_board, user_input) except: current_candidates = get_candidates(game_board, user_input) if not current_candidates: return render_board_html(game_board), "合法手が見つかりません。", gr.update(visible=False), [] if len(current_candidates) == 1: return execute_move(current_candidates[0]) # あいまいな場合:ボタンを表示 labels = [get_move_label(game_board, m) for m in current_candidates] # ボタンの更新(最大5候補まで対応) updates = [gr.update(value=labels[i], visible=True) for i in range(len(labels))] updates += [gr.update(visible=False) for _ in range(5 - len(labels))] return render_board_html(game_board), "どの駒を動かしますか? ボタンを選択してください。", gr.update(visible=True), updates def execute_move(move): global game_board game_board.push(move) log = f"あなた: {move.usi()}\n" if game_board.is_game_over(): return render_board_html(game_board), log + "終局しました。", gr.update(visible=False), [gr.update(visible=False)]*5 # AIの手 dm = get_dynamic_multipliers(len(game_board.move_stack)) ai_move = select_best_move(game_board) if ai_move: game_board.push(ai_move) log += f"AI: {ai_move.usi()}\nフェーズ: {dm['NAME']}\n(守備重視モード)" else: log += "AIが投了しました。" return render_board_html(game_board), log, gr.update(visible=False), [gr.update(visible=False)]*5 def on_candidate_click(idx): if idx < len(current_candidates): return execute_move(current_candidates[idx]) return render_board_html(game_board), "エラー", gr.update(visible=False), [gr.update(visible=False)]*5 def reset_game(): global game_board game_board = shogi.Board() return render_board_html(game_board), "リセットしました。", gr.update(visible=False), [gr.update(visible=False)]*5 # --- Gradio UI --- with gr.Blocks(css=".cand_btn { margin-bottom: 5px; }") as demo: gr.Markdown("# 🏯 Zengoma Extreme Guard - 厳密解析モード") with gr.Row(): with gr.Column(scale=3): board_view = gr.HTML(render_board_html(game_board)) with gr.Column(scale=1): move_input = gr.Textbox(label="指し手入力", placeholder="7六歩、同角、5五銀など") btn_submit = gr.Button("指す", variant="primary") # あいまい回避用ボタンエリア with gr.Column(visible=False) as cand_area: gr.Markdown("### 候補を選択してください") c_btns = [gr.Button("", visible=False, elem_classes="cand_btn") for _ in range(5)] btn_reset = gr.Button("盤面リセット") status_box = gr.TextArea(label="ログ", interactive=False) # 候補ボタンのクリックイベント for i in range(5): c_btns[i].click(fn=lambda i=i: on_candidate_click(i), outputs=[board_view, status_output := status_box, cand_area, *c_btns]) btn_submit.click(process_input, [move_input], [board_view, status_box, cand_area, *c_btns]) move_input.submit(process_input, [move_input], [board_view, status_box, cand_area, *c_btns]) btn_reset.click(reset_game, outputs=[board_view, status_box, cand_area, *c_btns]) if __name__ == "__main__": demo.launch()