| import shogi |
| import time |
| import random |
| import gradio as gr |
| import re |
|
|
| |
| 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 |
|
|
| |
|
|
| def render_board_html(board): |
| html = "<table style='border-collapse: collapse; border: 3px solid #555; margin: 0 auto; background-color: #f3d291;'>" |
| html += "<tr><td></td>" + "".join([f"<td style='width:40px;font-weight:bold;text-align:center;'>{9-i}</td>" for i in range(9)]) + "<td></td></tr>" |
| ranks = ["一", "二", "三", "四", "五", "六", "七", "八", "九"] |
| for r in range(9): |
| html += f"<tr><td style='font-weight:bold;padding:5px;'>{ranks[r]}</td>" |
| 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"<td style='width:45px;height:45px;background-color:{bg};border:1px solid #777;color:{color};font-size:22px;text-align:center;'>{mark}{sym}</td>" |
| else: html += f"<td style='width:45px;height:45px;background-color:{bg};border:1px solid #777;'></td>" |
| html += f"<td style='padding:5px;'>{r+1}</td></tr>" |
| html += "</table>" |
| 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), [] |
|
|
| |
| 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] |
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
|
|
| 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() |