File size: 11,012 Bytes
ecb5620
 
 
 
7296e89
ecb5620
 
 
 
 
 
 
 
 
 
 
7296e89
ecb5620
7296e89
ecb5620
7296e89
ecb5620
 
 
 
 
 
 
 
 
 
 
7296e89
 
ecb5620
 
7296e89
ecb5620
7296e89
 
 
 
 
ecb5620
 
7296e89
 
 
 
 
 
 
 
 
 
 
ecb5620
7296e89
ecb5620
7296e89
 
 
 
 
 
ecb5620
7296e89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ecb5620
7296e89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ecb5620
7296e89
 
ecb5620
7296e89
 
 
 
 
 
 
ecb5620
7296e89
 
 
ecb5620
7296e89
ecb5620
7296e89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ecb5620
7296e89
 
 
 
 
ecb5620
7296e89
ecb5620
7296e89
ecb5620
 
 
 
7296e89
 
 
 
ecb5620
7296e89
 
 
 
ecb5620
 
 
 
7296e89
ecb5620
7296e89
ecb5620
7296e89
 
ecb5620
 
7296e89
 
ecb5620
7296e89
 
ecb5620
7296e89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ecb5620
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
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()