s2 / app.py
56m's picture
Update app.py
7296e89 verified
Raw
History Blame Contribute Delete
11 kB
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 = "<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), []
# 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()