QuanAI只读快照

共享会话

🛠️ 程式修改與勝負邏輯

分享于 2026年8月20日 23:35
提问者

改進程式 檢查html結構:

檢查程式代碼中的html結構是否正確,如不正確,請給出修復方案。

增加勝負消息框:

之前在判勝負時會把文字直接寫到棋盤上,現在要新增「勝負消息框」,其內容下方將出現「重新開始」,點擊則進入新的一局。其右上角有一個svg最小化按鈕,點擊之後此消息框將消失,而左上角將出現「恢復窗口svg按鈕」,其css位置參考#fullscreen(注意#fullscreen是在右邊)。

更改設置窗口:

把設置窗口(將不再只是AI設置窗口,而是遊戲設置窗口)的寬度改為更寬一些(電腦端比300px更寬一倍左右,手機端要改為90%),增加大標題「哥尼斯堡——路與橋」,英文是「Königsberg - Path & Bridge」。

增加「語言選擇」欄目,下方是「English|中文」語言切換按鈕(水平居中),「English」和「中文」是分開的,所選中的將高亮。一開始默認是中文。同時在script中增加多國語言,包括消息框,以及設置窗口中的文字。

新增一個「教學模式」欄目在「語言選擇」下方,其內容是一個「開始教學模式」按鈕(水平居中)。如果遊戲開始,則這個欄目將消失。「開始教學模式」按鈕的功能將在以後說明。

把原來的「AI搜尋設定」欄目放在「教學模式」欄目的下方。

更改控制行動邏輯:

當輪到一方行動時,不需要再點擊一次本方人物,也不會再出現紅色折線,將自動使其進入行動狀態。他所能走到的circle的stroke將變為淺紅色,stroke-width將變為4。每走一步更新。當他沒有走至少一步時,「打勾」按鈕將不會出現。

當一方不能走至少一步時,程式將直接判其「困斃」輸棋,並彈出勝負消息框。當一方會被對方射殺時,將直接顯示紅色折線並判負,同時彈出勝負消息框。當一方在仍可放木橋時沒放至少一個木橋,則直接判負並彈出勝負消息框。如果判為和棋也要彈出勝負消息框。

注意將AI控制一並修改。AI思考消息框不必改動。

回答要求:

明確給出所要修改的地方和所要替換的代碼。

konigsberg20260606cannotmove7954.htmltext/plain · 109.7 KB预览下载
QuanAI

以下修改以你上傳的 konigsberg20260606cannotmove7954.html 為基礎。為避免貼出整個 10 萬多字元檔案,下面按「搜尋原代碼 → 替換代碼」給出可直接套用的修改。


一、HTML 結構檢查結果

目前 HTML 的開始、結束標籤基本配對正確,沒有明顯缺失的 </div></svg></script></body>

但有以下問題需要修正:

  1. 兩個 <style> 都放在 <body> 內。瀏覽器通常能執行,但不符合較嚴格的 HTML 結構規範,應移入 <head>
  2. 預設語言是中文,但目前是:
html
<html lang="en">

應改成:

html
<html lang="zh-Hant">
  1. SVG 的:
html
viewbox="0 0 720 480"

大小寫錯誤,應改成:

html
viewBox="0 0 720 480"
  1. 文件標題應改為遊戲正式名稱。
  2. 原始 judgeResult() 中的 current_historycurrent_history_strlose_num 沒有用 let/const 宣告,會建立隱式全域變數。下方替換後的 judgeResult() 已修正。
  3. 設定窗口原本使用大量沒有語義的 <div>,建議改為 h1sectionbutton 等結構;下方已提供完整替換碼。

修改 <head>

將開頭改為:

html
<!DOCTYPE html>
<html lang="zh-Hant">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>哥尼斯堡——路與橋</title>

    <style>
        /*
         * 把目前位於 body 內的兩個 <style>...</style>
         * 中的 CSS 全部移到這裡。
         *
         * 然後再加入本文下一節提供的新增/替換 CSS。
         */
    </style>
</head>

<body>

移動完成後,刪除 <body> 內原本的兩組 <style>...</style> 標籤,避免重複。


二、替換設定窗口並新增勝負消息框

搜尋從:

html
<div id="thinking_ui">🤔 思考中...</div>
<div id="mcts_modal">

開始,到原本 #mcts_modal 的結束 </div> 為止,整段替換成:

html
<div id="thinking_ui">🤔 思考中...</div>

<div id="mcts_modal"
     role="dialog"
     aria-modal="true"
     aria-labelledby="game_settings_title">

    <button type="button"
            id="mcts_close_btn"
            class="mcts_close"
            aria-label="關閉"
            title="關閉">×</button>

    <h1 id="game_settings_title" data-i18n="gameTitle">
        哥尼斯堡——路與橋
    </h1>

    <!-- 語言選擇 -->
    <section class="settings_section">
        <h2 data-i18n="languageSelection">語言選擇</h2>

        <div class="language_switch" role="group" aria-label="語言選擇">
            <button type="button"
                    id="lang_en"
                    class="language_btn"
                    data-lang="en">English</button>

            <span class="language_separator" aria-hidden="true">|</span>

            <button type="button"
                    id="lang_zh"
                    class="language_btn active"
                    data-lang="zh">中文</button>
        </div>
    </section>

    <!-- 教學模式 -->
    <section id="tutorial_section" class="settings_section">
        <h2 data-i18n="tutorialMode">教學模式</h2>

        <div class="center_button_row">
            <button type="button"
                    id="tutorial_start_btn"
                    class="primary_setting_button"
                    data-i18n="startTutorial">
                開始教學模式
            </button>
        </div>
    </section>

    <!-- AI 搜尋設定 -->
    <section class="settings_section">
        <h2 data-i18n="aiSearchSettings">AI 搜尋設定</h2>

        <div class="settings_toolbar">
            <button type="button"
                    id="toggle_params"
                    class="toggle_params_button"
                    data-expanded="false">
                ▸ 展開
            </button>

            <div id="mode_buttons" class="mode_buttons">
                <button type="button"
                        class="mode_btn"
                        data-mode="easy"
                        data-i18n="easy">簡單</button>

                <button type="button"
                        class="mode_btn"
                        data-mode="hard"
                        data-i18n="hard">困難</button>

                <button type="button"
                        class="mode_btn"
                        data-mode="expert"
                        data-i18n="expert">專家</button>

                <button type="button"
                        class="mode_btn"
                        data-mode="custom"
                        data-i18n="custom">自定</button>
            </div>
        </div>

        <div id="params_container" style="display:none;">
            <div class="mcts_row">
                <label for="mcts_sims" data-i18n="simulations">模擬次數:</label>
                <input type="number"
                       id="mcts_sims"
                       class="mcts_param_input"
                       step="1"
                       min="1">
            </div>

            <div class="mcts_row">
                <label for="mcts_depth" data-i18n="searchDepth">搜尋深度:</label>
                <input type="number"
                       id="mcts_depth"
                       class="mcts_param_input"
                       step="1"
                       min="1">
            </div>

            <div class="mcts_row">
                <label for="mcts_max_path" data-i18n="maxSteps">最大步數:</label>
                <input type="number"
                       id="mcts_max_path"
                       class="mcts_param_input"
                       max="9"
                       step="1"
                       min="1">
            </div>

            <div class="mcts_row">
                <label for="mcts_first_path" data-i18n="firstTurnLimit">首回限步:</label>
                <input type="number"
                       id="mcts_first_path"
                       class="mcts_param_input"
                       max="9"
                       step="1"
                       min="1">
            </div>

            <div class="forced_search_group">
                <div class="forced_search_title"
                     data-i18n="forcedSearch">
                    必勝搜尋(橋數 ➔ 深度):
                </div>

                <div class="mcts_row depth_row">
                    <label for="fcd_depth1">7</label>
                    <input type="number"
                           id="fcd_depth1"
                           class="mcts_param_input depth_input"
                           step="1"
                           min="1">
                </div>

                <div class="mcts_row depth_row">
                    <label for="fcd_depth2">12</label>
                    <input type="number"
                           id="fcd_depth2"
                           class="mcts_param_input depth_input"
                           step="1"
                           min="1">
                </div>

                <div class="mcts_row depth_row">
                    <label for="fcd_depth3">16</label>
                    <input type="number"
                           id="fcd_depth3"
                           class="mcts_param_input depth_input"
                           step="1"
                           min="1">
                </div>

                <div class="mcts_row depth_row">
                    <label for="fcd_depth4">19</label>
                    <input type="number"
                           id="fcd_depth4"
                           class="mcts_param_input depth_input"
                           step="1"
                           min="1">
                </div>
            </div>
        </div>

        <hr>

        <div class="mcts_row ai_player_row">
            <label>
                <input type="checkbox" id="mcts_auto_blue">
                <span data-i18n="aiBlue">AI 藍方</span>
            </label>

            <label>
                <input type="checkbox" checked id="mcts_auto_green">
                <span data-i18n="aiGreen">AI 綠方</span>
            </label>
        </div>

        <button type="button"
                id="mcts_play_btn"
                class="full_width_primary"
                style="display:none;"
                data-i18n="startGame">
            開始遊戲
        </button>

        <button type="button"
                id="mcts_start_btn"
                class="full_width_button"
                data-i18n="startSearch">
            開始搜尋
        </button>

        <div id="mcts_results"
             class="mcts_results"
             aria-live="polite"></div>
    </section>
</div>

然後在 #setting_div 結束標籤之後、#fullscreen 之前,加入勝負消息框和恢復按鈕:

html
<!-- 勝負消息框 -->
<div id="result_modal"
     class="hide"
     role="dialog"
     aria-modal="true"
     aria-labelledby="result_title">

    <button type="button"
            id="result_minimize"
            class="result_minimize"
            aria-label="最小化"
            title="最小化">
        <svg viewBox="0 0 24 24"
             width="24"
             height="24"
             fill="currentColor"
             aria-hidden="true">
            <path d="M5 11h14v2H5z"></path>
        </svg>
    </button>

    <h2 id="result_title">遊戲結果</h2>

    <div id="result_message" aria-live="assertive"></div>

    <button type="button"
            id="result_restart"
            class="result_restart">
        重新開始
    </button>
</div>

<!-- 最小化後顯示在左上角 -->
<div id="result_restore"
     class="trbtn hide"
     role="button"
     tabindex="0"
     aria-label="恢復勝負窗口"
     title="恢復勝負窗口">

    <svg viewBox="0 0 24 24"
         fill="white"
         width="24"
         height="24"
         aria-hidden="true">
        <path d="M5 5h14v14H5V5zm2 2v10h10V7H7zm2 2h6v2H9V9zm0 4h6v2H9v-2z"></path>
    </svg>
</div>

同時把 SVG 棋盤的:

html
viewbox="0 0 720 480"

替換成:

html
viewBox="0 0 720 480"

三、CSS 修改

3.1 替換原本的 #mcts_modal

刪除原本的:

css
#mcts_modal {
    position: absolute;
    top: 10%;
    left: 50%;
    transform: translateX(-50%);
    background: white;
    border: 2px solid #333;
    padding: 15px;
    z-index: 20;
    display: none;
    width: 300px;
    max-height: 80%;
    overflow-y: auto;
    font-size: 16px;
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

#mcts_modal h3 {
    margin-top: 0;
    margin-bottom: 10px;
}

替換為:

css
#mcts_modal {
    position: absolute;
    top: 5%;
    left: 50%;
    transform: translateX(-50%);
    display: none;

    width: 620px;
    max-width: calc(100% - 32px);
    max-height: 90%;

    padding: 22px 24px;
    overflow-y: auto;

    color: #333;
    background: #fff;
    border: 2px solid #333;
    border-radius: 12px;
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);

    z-index: 20;
    font-size: 16px;
}

#mcts_modal h1 {
    margin: 0 36px 22px;
    text-align: center;
    font-size: 30px;
    line-height: 1.25;
    color: #24214f;
}

#mcts_modal h2 {
    margin: 0 0 12px;
    text-align: center;
    font-size: 20px;
    line-height: 1.3;
}

.settings_section {
    margin: 0 0 20px;
    padding: 16px;
    border: 1px solid #d7d7df;
    border-radius: 10px;
    background: #fafaff;
}

.settings_section:last-child {
    margin-bottom: 0;
}

.language_switch {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
}

.language_btn {
    min-width: 92px;
    padding: 7px 14px;
    color: #333;
    background: #fff;
    border: 1px solid #aaa;
    border-radius: 6px;
    cursor: pointer;
    font-size: 16px;
}

.language_btn:hover {
    background: #f0efff;
}

.language_btn.active {
    color: #fff;
    background: #8278f5;
    border-color: #8278f5;
    font-weight: bold;
    box-shadow: 0 0 0 2px rgba(130, 120, 245, 0.2);
}

.language_separator {
    color: #777;
    font-size: 20px;
}

.center_button_row {
    display: flex;
    justify-content: center;
}

.primary_setting_button,
.full_width_primary,
.result_restart {
    color: #fff;
    background: #8278f5;
    border: 0;
    border-radius: 6px;
    cursor: pointer;
}

.primary_setting_button {
    padding: 9px 22px;
    font-size: 16px;
}

.full_width_primary,
.full_width_button {
    width: 100%;
    margin-top: 7px;
    padding: 9px;
    border-radius: 5px;
    cursor: pointer;
    font-size: 16px;
}

.full_width_button {
    color: #333;
    background: #f4f4f4;
    border: 1px solid #aaa;
}

.settings_toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 10px;
}

.toggle_params_button {
    flex: 0 0 auto;
    padding: 5px 8px;
    color: #333;
    background: transparent;
    border: 0;
    cursor: pointer;
    font-weight: bold;
    font-size: 15px;
}

.mode_buttons {
    display: flex;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: 5px;
}

.forced_search_group {
    margin: 12px 0 5px;
}

.forced_search_title {
    margin-bottom: 7px;
}

.depth_row {
    padding-left: 12px;
}

.depth_input {
    width: 48px !important;
}

.ai_player_row {
    align-items: center;
}

.ai_player_row label {
    display: flex;
    align-items: center;
    gap: 5px;
}

.mcts_results {
    margin-top: 10px;
    font-size: 14px;
}

.mcts_close {
    position: absolute;
    top: 7px;
    right: 10px;

    width: 34px;
    height: 34px;

    padding: 0;
    color: #333;
    background: transparent;
    border: 0;
    border-radius: 50%;

    cursor: pointer;
    font-weight: bold;
    font-size: 26px;
    line-height: 34px;
}

.mcts_close:hover {
    color: #fff;
    background: #8f2929;
}

3.2 加入勝負框及可到達點樣式

在 CSS 最後加入:

css
/* 當前位置能走到的節點 */
.reachable-dot {
    stroke: #f0a0a0 !important;
    stroke-width: 4 !important;
}

/* 勝負消息框 */
#result_modal {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);

    width: 420px;
    max-width: 88%;
    min-height: 190px;

    padding: 30px 28px 24px;

    color: #2d2d2d;
    background: rgba(255, 255, 255, 0.97);
    border: 3px solid #8278f5;
    border-radius: 14px;
    box-shadow: 0 10px 36px rgba(0, 0, 0, 0.42);

    z-index: 30;
    text-align: center;
}

#result_modal h2 {
    margin: 0 34px 18px;
    color: #3e377f;
    font-size: 27px;
}

#result_message {
    min-height: 54px;
    margin-bottom: 20px;
    font-size: 21px;
    line-height: 1.55;
}

.result_minimize {
    position: absolute;
    top: 8px;
    right: 8px;

    display: flex;
    align-items: center;
    justify-content: center;

    width: 34px;
    height: 34px;
    padding: 0;

    color: #333;
    background: #eee;
    border: 1px solid #aaa;
    border-radius: 6px;

    cursor: pointer;
}

.result_minimize:hover {
    color: #fff;
    background: #8278f5;
    border-color: #8278f5;
}

.result_restart {
    min-width: 150px;
    padding: 10px 20px;
    font-size: 18px;
}

.result_restart:hover,
.primary_setting_button:hover,
.full_width_primary:hover {
    background: #655bdc;
}

/*
 * 位置與 #fullscreen 同樣是 top:7px,
 * 但 #fullscreen 在右邊,恢復按鈕在左邊。
 */
#result_restore {
    top: 7px;
    left: 7px;
    right: auto;
    background: #8278f5;
    z-index: 29;
}

/* 手機端設定窗口為 90% */
@media (max-width: 600px) {
    #mcts_modal {
        top: 3%;
        width: 90%;
        max-width: 90%;
        max-height: 94%;
        padding: 16px 14px;
        font-size: 15px;
    }

    #mcts_modal h1 {
        margin: 4px 32px 18px;
        font-size: 23px;
    }

    #mcts_modal h2 {
        font-size: 18px;
    }

    .settings_section {
        padding: 13px 10px;
    }

    .settings_toolbar {
        flex-direction: column;
        align-items: stretch;
    }

    .mode_buttons {
        justify-content: center;
    }

    .mcts_row {
        gap: 10px;
    }

    #result_modal {
        width: 88%;
        padding: 28px 18px 22px;
    }

    #result_modal h2 {
        font-size: 23px;
    }

    #result_message {
        font-size: 18px;
    }
}

注意不要在 #result_modal 中寫:

css
display: block;

否則可能蓋過 .hide { display:none; }


四、加入多國語言程式

在:

js
let lastSearchTime = 0;

之後加入:

js
let currentLanguage = "zh";

const I18N = {
    zh: {
        gameTitle: "哥尼斯堡——路與橋",
        languageSelection: "語言選擇",
        tutorialMode: "教學模式",
        startTutorial: "開始教學模式",
        aiSearchSettings: "AI 搜尋設定",

        expand: "展開",
        collapse: "收回",
        easy: "簡單",
        hard: "困難",
        expert: "專家",
        custom: "自定",

        simulations: "模擬次數:",
        searchDepth: "搜尋深度:",
        maxSteps: "最大步數:",
        firstTurnLimit: "首回限步:",
        forcedSearch: "必勝搜尋(橋數 ➔ 深度):",

        aiBlue: "AI 藍方",
        aiGreen: "AI 綠方",
        startGame: "開始遊戲",
        startSearch: "開始搜尋",
        stopSearch: "停止搜尋",
        close: "關閉",

        resultTitle: "遊戲結果",
        restart: "重新開始",
        minimize: "最小化",
        restoreResult: "恢復勝負窗口",

        blue: "藍方",
        green: "綠方",

        shotResult: "{winner}勝利!{loser}被射殺。",
        trappedResult: "{loser}無路可走而困斃,{winner}勝利!",
        noBridgeResult: "{loser}沒有放置至少一座木橋,{winner}勝利!",
        drawResult: "本局和棋!",

        searchComplete: "搜尋完成!共 {count} 條路徑:",
        totalTime: "總耗時:{time} 秒",
        previousPage: "上一頁",
        nextPage: "下一頁",
        page: "頁數:{current} / {total}",
        fatalMove: "💀 致命步",
        forcedLoss: "⚠️ 必敗步",
        forcedWin: "🎯 必殺走法",
        winRate: "勝率:{rate}%",
        searchOnlyAtTurnStart: "請在輪到玩家且尚未移動時搜尋。",
        searchingPleaseWait: "搜尋中,請稍候……",
        searchAborted: "搜尋已中止。",
        noLegalRoute: "沒有能完成合法回合的路徑。"
    },

    en: {
        gameTitle: "Königsberg - Path & Bridge",
        languageSelection: "Language",
        tutorialMode: "Tutorial Mode",
        startTutorial: "Start Tutorial Mode",
        aiSearchSettings: "AI Search Settings",

        expand: "Expand",
        collapse: "Collapse",
        easy: "Easy",
        hard: "Hard",
        expert: "Expert",
        custom: "Custom",

        simulations: "Simulations:",
        searchDepth: "Search depth:",
        maxSteps: "Maximum steps:",
        firstTurnLimit: "First-turn limit:",
        forcedSearch: "Forced-win search (bridges ➔ depth):",

        aiBlue: "AI Blue",
        aiGreen: "AI Green",
        startGame: "Start Game",
        startSearch: "Start Search",
        stopSearch: "Stop Search",
        close: "Close",

        resultTitle: "Game Result",
        restart: "Restart",
        minimize: "Minimize",
        restoreResult: "Restore result window",

        blue: "Blue",
        green: "Green",

        shotResult: "{winner} wins! {loser} was shot.",
        trappedResult: "{loser} is trapped with no available move. {winner} wins!",
        noBridgeResult: "{loser} did not place at least one wooden bridge. {winner} wins!",
        drawResult: "The game is a draw!",

        searchComplete: "Search complete! {count} paths found:",
        totalTime: "Total time: {time} seconds",
        previousPage: "Previous",
        nextPage: "Next",
        page: "Page: {current} / {total}",
        fatalMove: "💀 Fatal move",
        forcedLoss: "⚠️ Forced loss",
        forcedWin: "🎯 Forced win",
        winRate: "Win rate: {rate}%",
        searchOnlyAtTurnStart: "Search only at the beginning of the player's turn.",
        searchingPleaseWait: "Searching, please wait...",
        searchAborted: "Search aborted.",
        noLegalRoute: "No route can complete a legal turn."
    }
};

function t(key, variables = {}) {
    const languageTable = I18N[currentLanguage] || I18N.zh;
    let text = languageTable[key] ?? I18N.zh[key] ?? key;

    for (const [name, value] of Object.entries(variables)) {
        text = text.replaceAll(`{${name}}`, String(value));
    }

    return text;
}

function playerName(playerIndex) {
    return t(playerIndex === 0 ? "blue" : "green");
}

function applyLanguage(language) {
    currentLanguage = language === "en" ? "en" : "zh";

    document.documentElement.lang =
        currentLanguage === "en" ? "en" : "zh-Hant";

    document.title = t("gameTitle");

    document.querySelectorAll("[data-i18n]").forEach(element => {
        const key = element.getAttribute("data-i18n");
        element.textContent = t(key);
    });

    document.querySelectorAll(".language_btn").forEach(button => {
        const selected = button.dataset.lang === currentLanguage;
        button.classList.toggle("active", selected);
        button.setAttribute("aria-pressed", selected ? "true" : "false");
    });

    const toggleButton = document.getElementById("toggle_params");

    if (toggleButton) {
        const expanded = toggleButton.dataset.expanded === "true";
        toggleButton.textContent =
            expanded ? `▾ ${t("collapse")}` : `▸ ${t("expand")}`;
    }

    const closeButton = document.getElementById("mcts_close_btn");

    if (closeButton) {
        closeButton.title = t("close");
        closeButton.setAttribute("aria-label", t("close"));
    }

    const minimizeButton = document.getElementById("result_minimize");

    if (minimizeButton) {
        minimizeButton.title = t("minimize");
        minimizeButton.setAttribute("aria-label", t("minimize"));
    }

    const restoreButton = document.getElementById("result_restore");

    if (restoreButton) {
        restoreButton.title = t("restoreResult");
        restoreButton.setAttribute("aria-label", t("restoreResult"));
    }

    const resultTitle = document.getElementById("result_title");
    const restartButton = document.getElementById("result_restart");

    if (resultTitle) resultTitle.textContent = t("resultTitle");
    if (restartButton) restartButton.textContent = t("restart");

    const startSearchButton = document.getElementById("mcts_start_btn");

    if (startSearchButton) {
        startSearchButton.textContent =
            isMctsRunning ? t("stopSearch") : t("startSearch");
    }

    if (typeof lastGameResult !== "undefined" && lastGameResult) {
        renderGameResult();
    }

    if (
        typeof currentMctsResults !== "undefined" &&
        currentMctsResults.length > 0
    ) {
        renderMctsResults();
    }
}

五、修改 window.onload

window.onload 中,將模式按鈕事件:

js
document.querySelectorAll('.mode_btn').forEach(btn => {
    btn.addEventListener('click', (e) => setMode(e.target.getAttribute('data-mode')));
});

替換成:

js
document.querySelectorAll(".mode_btn").forEach(button => {
    button.addEventListener("click", event => {
        setMode(event.currentTarget.dataset.mode);
    });
});

將原本「展開/收回」事件整段替換成:

js
document.getElementById("toggle_params").addEventListener("click", function() {
    const container = document.getElementById("params_container");
    const expanding = container.style.display === "none";

    container.style.display = expanding ? "block" : "none";
    this.dataset.expanded = expanding ? "true" : "false";
    this.textContent =
        expanding ? `▾ ${t("collapse")}` : `▸ ${t("expand")}`;
});

window.onload 的結尾、openMctsModal(); 之前加入:

js
document.querySelectorAll(".language_btn").forEach(button => {
    button.addEventListener("click", () => {
        applyLanguage(button.dataset.lang);
    });
});

document.getElementById("mcts_close_btn").addEventListener("click", () => {
    closeMctsModal();
});

/*
 * 教學功能預留。
 * 目前按鈕可點擊,但不改變遊戲狀態。
 */
document.getElementById("tutorial_start_btn").addEventListener("click", () => {
    // 教學模式功能將在之後加入。
});

applyLanguage("zh");

保留:

js
openMctsModal();

因此初次開啟時預設顯示中文。


六、新增勝負窗口控制

在元素變數區,搜尋:

js
const exportsvgbtn = document.getElementById("exportsvg");

在其後加入:

js
const resultModal = document.getElementById("result_modal");
const resultTitle = document.getElementById("result_title");
const resultMessage = document.getElementById("result_message");
const resultRestart = document.getElementById("result_restart");
const resultMinimize = document.getElementById("result_minimize");
const resultRestore = document.getElementById("result_restore");
const tutorialSection = document.getElementById("tutorial_section");

let lastGameResult = null;

在變數宣告區後加入:

js
function updateTutorialVisibility() {
    if (!tutorialSection) return;

    /*
     * 只有尚未開始任何一局,即 game_status === 0 時顯示。
     * 遊戲開始、結束或進入歷史模式後都隱藏。
     */
    tutorialSection.classList.toggle("hide", game_status !== 0);
}

function renderGameResult() {
    if (!lastGameResult) return;

    resultTitle.textContent = t("resultTitle");

    if (lastGameResult.kind === "draw") {
        resultMessage.textContent = t("drawResult");
        return;
    }

    const variables = {
        winner: playerName(lastGameResult.winner),
        loser: playerName(lastGameResult.loser)
    };

    if (lastGameResult.kind === "shot") {
        resultMessage.textContent = t("shotResult", variables);
    } else if (lastGameResult.kind === "trapped") {
        resultMessage.textContent = t("trappedResult", variables);
    } else if (lastGameResult.kind === "noBridge") {
        resultMessage.textContent = t("noBridgeResult", variables);
    }
}

function showGameResult(kind, winner = null, loser = null) {
    lastGameResult = {
        kind,
        winner,
        loser
    };

    renderGameResult();

    resultModal.classList.remove("hide");
    resultRestore.classList.add("hide");
}

function hideGameResult() {
    lastGameResult = null;
    resultModal.classList.add("hide");
    resultRestore.classList.add("hide");
}

function clearReachableDots() {
    dots.forEach(dot => {
        dot.classList.remove("reachable-dot");
    });
}

function endGame(kind, winner = null, loser = null) {
    game_status = 7;

    abortMcts = true;
    isMctsRunning = false;

    clearTimeout(autoPlayTimer);
    clearReachableDots();

    route.classList.add("hide");
    btn_finish.classList.add("hide");
    btn_back.classList.add("hide");
    btn_cancel.classList.add("hide");
    btn_play.classList.remove("hide");

    document.getElementById("thinking_ui").style.display = "none";
    document.getElementById("mcts_start_btn").textContent = t("startSearch");

    men_top.forEach(man => man.classList.add("hide"));
    men_bottom.forEach(man => man.classList.remove("hide"));

    men_bottom[0].setAttribute("stroke", "none");
    men_bottom[1].setAttribute("stroke", "none");

    if (kind === "draw") {
        men_bottom[0].setAttribute("stroke", "#e77");
        men_bottom[1].setAttribute("stroke", "#e77");
    } else if (loser !== null) {
        men_bottom[loser].setAttribute("stroke", "#e77");
    }

    detail_div.textContent = "";

    showGameResult(kind, winner, loser);
    updateTutorialVisibility();
    displayGame();
}

resultMinimize.addEventListener("click", () => {
    resultModal.classList.add("hide");
    resultRestore.classList.remove("hide");
});

function restoreResultWindow() {
    if (!lastGameResult) return;

    resultRestore.classList.add("hide");
    resultModal.classList.remove("hide");
}

resultRestore.addEventListener("click", restoreResultWindow);

resultRestore.addEventListener("keydown", event => {
    if (event.key === "Enter" || event.key === " ") {
        event.preventDefault();
        restoreResultWindow();
    }
});

resultRestart.addEventListener("click", () => {
    hideGameResult();
    btn_play.onclick();
});

七、自動進入行動狀態及標示可走節點

addShotLine() 後面加入以下函式:

js
function getAvailableNextDots(lastDot) {
    if (lastDot === undefined || lastDot === null) return [];

    const visited = new Set(route_tmp);
    const result = new Set();

    for (const nextDot of empty_map[lastDot] || []) {
        if (!visited.has(nextDot)) result.add(nextDot);
    }

    for (const nextDot of arrow_map[lastDot] || []) {
        if (!visited.has(nextDot)) result.add(nextDot);
    }

    return Array.from(result);
}

function updateReachableDots() {
    clearReachableDots();

    if (![2, 4].includes(game_status) || route_tmp.length === 0) {
        return;
    }

    const lastDot = route_tmp[route_tmp.length - 1];

    for (const dotIndex of getAvailableNextDots(lastDot)) {
        dots[dotIndex].classList.add("reachable-dot");
    }
}

/*
 * 找出射手沿有向橋到達目標的實際路線。
 * 回傳格式為 [起點, 下一點, 下一點, ...]。
 */
function findShotRoute(shooterDot, targetDot) {
    const queue = [shooterDot];
    const parent = new Map();

    parent.set(shooterDot, null);

    while (queue.length > 0) {
        const currentDot = queue.shift();

        for (const nextDot of arrow_map[currentDot] || []) {
            if (parent.has(nextDot)) continue;

            parent.set(nextDot, currentDot);

            if (nextDot === targetDot) {
                const nodeRoute = [];
                let traceDot = targetDot;

                while (traceDot !== null) {
                    nodeRoute.push(traceDot);
                    traceDot = parent.get(traceDot);
                }

                nodeRoute.reverse();

                const lineArray = [];

                for (let index = 0; index < nodeRoute.length - 1; index++) {
                    lineArray.push(nodeRoute[index], nodeRoute[index + 1]);
                }

                return lineArray;
            }

            queue.push(nextDot);
        }
    }

    return null;
}

function scheduleAutoTurn() {
    clearTimeout(autoPlayTimer);

    if (
        ![2, 4].includes(game_status) ||
        route_tmp.length !== 1 ||
        document.getElementById("mcts_modal").style.display === "block"
    ) {
        return;
    }

    const autoBlue =
        document.getElementById("mcts_auto_blue")?.checked === true;

    const autoGreen =
        document.getElementById("mcts_auto_green")?.checked === true;

    if (
        (current_man === 0 && autoBlue) ||
        (current_man === 1 && autoGreen)
    ) {
        autoPlayTimer = setTimeout(() => {
            updateMctsParams();
            runMCTS(true);
        }, 500);
    }
}

/*
 * 每一回合開始時自動執行,不再要求玩家點擊本方人物。
 */
function beginTurn() {
    if (![1, 3].includes(game_status)) return;

    current_man = game_status === 1 ? 0 : 1;

    const startDot = parseInt(
        men_bottom[current_man].getAttribute("i"),
        10
    );

    const opponent = 1 - current_man;

    const opponentDot = parseInt(
        men_bottom[opponent].getAttribute("i"),
        10
    );

    route_tmp = [startDot];
    new_arrow = [];

    clearReachableDots();
    route.classList.add("hide");

    btn_finish.classList.add("hide");
    btn_back.classList.add("hide");
    btn_cancel.classList.add("hide");

    /*
     * 先判斷當前玩家是否可以沿有向橋射殺對方。
     * 如果可以,直接顯示紅線並結束。
     */
    const shotLines = findShotRoute(startDot, opponentDot);

    if (shotLines !== null) {
        addShotLine(shotLines, false);

        game_history.push(
            JSON.stringify([-1].concat(shotLines))
        );

        endGame("shot", current_man, opponent);
        return;
    }

    /*
     * 完全沒有第一步可走,直接判困斃。
     */
    if (getAvailableNextDots(startDot).length === 0) {
        game_history.push(
            JSON.stringify([-2, startDot])
        );

        endGame("trapped", opponent, current_man);
        return;
    }

    game_status = game_status === 1 ? 2 : 4;

    men_top.forEach(man => man.classList.add("hide"));
    men_bottom.forEach(man => man.classList.remove("hide"));

    men_bottom[current_man].classList.add("hide");
    men_top[current_man].classList.remove("hide");

    updateReachableDots();
    scheduleAutoTurn();
}

八、修改 displayGame()

在原本 displayGame() 底部,刪除這一整段:

js
// 自動下棋
clearTimeout(autoPlayTimer);
if ((game_status == 1 || game_status == 3) && document.getElementById('mcts_modal').style.display !== 'block') {
    let autoBlue = document.getElementById('mcts_auto_blue') && document.getElementById('mcts_auto_blue').checked;
    let autoGreen = document.getElementById('mcts_auto_green') && document.getElementById('mcts_auto_green').checked;
    if ((current_man === 0 && autoBlue) || (current_man === 1 && autoGreen)) {
        autoPlayTimer = setTimeout(() => {
            updateMctsParams();
            runMCTS(true);
        }, 500);
    }
}

替換為:

js
clearTimeout(autoPlayTimer);
updateTutorialVisibility();

/*
 * displayGame 完成棋盤顯示後,自動令當前玩家進入行動狀態。
 */
if (game_status === 1 || game_status === 3) {
    setTimeout(beginTurn, 0);
}

這樣原本所有:

js
displayGame();

在切換到 game_status === 1game_status === 3 後,都會自動開始行動,不需要逐一追加 beginTurn()


九、替換人物及節點點擊邏輯

9.1 替換 dotOnClick()

將完整的原 dotOnClick() 替換成:

js
function dotOnClick() {
    if (isMctsRunning) return;
    if (![2, 4].includes(game_status)) return;

    const dotIndex = parseInt(this.getAttribute("i"), 10);
    const opponentDot = parseInt(
        men_bottom[1 - current_man].getAttribute("i"),
        10
    );

    if (route_tmp.length === 0) return;
    if (route_tmp.includes(dotIndex)) return;

    const lastDot = route_tmp[route_tmp.length - 1];
    const availableDots = getAvailableNextDots(lastDot);

    if (!availableDots.includes(dotIndex)) return;

    /*
     * 不再繪製規劃折線,只記錄路徑。
     */
    route_tmp.push(dotIndex);
    addBridge();

    /*
     * 至少走一步後才顯示打勾。
     * 不能停在對方人物所在節點。
     */
    if (dotIndex === opponentDot) {
        btn_finish.classList.add("hide");
    } else {
        btn_finish.classList.remove("hide");
    }

    btn_cancel.classList.remove("hide");
    btn_back.classList.remove("hide");

    updateReachableDots();
}

9.2 替換 manOnClick()

完整替換成:

js
function manOnClick() {
    if (isMctsRunning) return;

    /*
     * 不再用點擊本方人物開始行動。
     * 行動期間點擊另一方人物,仍視為點擊其所在節點。
     */
    if (game_status === 2 || game_status === 4) {
        dotOnClick.call(this);
    }
}

這樣在 game_status === 1/3 時點人物不會再開始行動;行動由 beginTurn() 自動開始。


十、替換打勾、返回、取消功能

10.1 替換 btn_finish.onclick

完整替換成:

js
btn_finish.onclick = function() {
    if (![2, 4].includes(game_status)) return;
    if (route_tmp.length <= 1) return;

    const routeEnd = route_tmp[route_tmp.length - 1];

    const opponentDot = parseInt(
        men_bottom[1 - current_man].getAttribute("i"),
        10
    );

    /*
     * 不能在對方人物所在節點完成。
     */
    if (routeEnd === opponentDot) return;

    btn_finish.classList.add("hide");
    btn_back.classList.add("hide");
    btn_cancel.classList.add("hide");

    clearReachableDots();
    route.classList.add("hide");

    men_bottom[current_man].setAttribute("i", routeEnd);
    men_top[current_man].setAttribute("i", routeEnd);

    createAnimateMotion();
};

10.2 替換 btn_back.onclick

完整替換成:

js
btn_back.onclick = function() {
    if (![2, 4].includes(game_status)) return;
    if (route_tmp.length <= 1) return;

    const removedDot = route_tmp.pop();
    const previousDot = route_tmp[route_tmp.length - 1];

    /*
     * 如果退回的那一步建立了最新的一座橋,
     * 同時撤銷該橋。
     */
    if (
        new_arrow.length >= 2 &&
        new_arrow[new_arrow.length - 2] === previousDot &&
        new_arrow[new_arrow.length - 1] === removedDot
    ) {
        new_arrow.splice(new_arrow.length - 2, 2);

        empty_map[previousDot].push(removedDot);
        empty_map[removedDot].push(previousDot);

        const arrowIndex = arrow_map[previousDot].indexOf(removedDot);

        if (arrowIndex !== -1) {
            arrow_map[previousDot].splice(arrowIndex, 1);
        }

        const removedArrow = arrows.pop();

        if (removedArrow) removedArrow.remove();
    }

    const opponentDot = parseInt(
        men_bottom[1 - current_man].getAttribute("i"),
        10
    );

    if (route_tmp.length <= 1) {
        btn_finish.classList.add("hide");
        btn_back.classList.add("hide");
        btn_cancel.classList.add("hide");
    } else {
        btn_back.classList.remove("hide");
        btn_cancel.classList.remove("hide");

        if (previousDot === opponentDot) {
            btn_finish.classList.add("hide");
        } else {
            btn_finish.classList.remove("hide");
        }
    }

    route.classList.add("hide");
    updateReachableDots();
};

10.3 替換 btn_cancel.onclick

完整替換成:

js
btn_cancel.onclick = function() {
    if (![2, 4].includes(game_status)) return;
    if (route_tmp.length <= 1) return;

    const startDot = route_tmp[0];

    for (let index = 0; index < new_arrow.length; index += 2) {
        const bridgeStart = new_arrow[index];
        const bridgeEnd = new_arrow[index + 1];

        empty_map[bridgeStart].push(bridgeEnd);
        empty_map[bridgeEnd].push(bridgeStart);

        const arrowIndex = arrow_map[bridgeStart].indexOf(bridgeEnd);

        if (arrowIndex !== -1) {
            arrow_map[bridgeStart].splice(arrowIndex, 1);
        }

        const removedArrow = arrows.pop();

        if (removedArrow) removedArrow.remove();
    }

    route_tmp = [startDot];
    new_arrow = [];

    route.classList.add("hide");

    btn_finish.classList.add("hide");
    btn_back.classList.add("hide");
    btn_cancel.classList.add("hide");

    updateReachableDots();
};

十一、替換判勝負邏輯

將原本完整的:

js
function judgeResult() {
    ...
}

替換成:

js
function judgeResult() {
    if (![2, 4].includes(game_status)) {
        return true;
    }

    const mover = current_man;
    const opponent = 1 - mover;

    /*
     * 防禦性檢查。
     * 正常情況下打勾按鈕在未移動時不會顯示。
     */
    if (route_tmp.length <= 1) {
        const startDot = parseInt(
            men_bottom[mover].getAttribute("i"),
            10
        );

        game_history.push(
            JSON.stringify([-2, startDot])
        );

        route_tmp = [];
        new_arrow = [];

        endGame("trapped", opponent, mover);
        return false;
    }

    /*
     * 尚未放滿十二座可翻轉木橋時,
     * 本回合必須至少新增一座木橋。
     */
    if (new_arrow.length === 0 && arrows.length < 12) {
        game_history.push(
            JSON.stringify([-3].concat(route_tmp))
        );

        route_tmp = [];
        new_arrow = [];

        endGame("noBridge", opponent, mover);
        return false;
    }

    /*
     * 合法完成本回合。
     */
    for (let index = 0; index < new_arrow.length; index += 2) {
        const arrowNumber =
            (stack_arrow.length - new_arrow.length) / 2 +
            index / 2;

        if (arrows[arrowNumber]) {
            arrows[arrowNumber].setAttribute("stroke", "none");
        }
    }

    const man0Dot = parseInt(
        men_bottom[0].getAttribute("i"),
        10
    );

    const man1Dot = parseInt(
        men_bottom[1].getAttribute("i"),
        10
    );

    const currentHistory =
        [man0Dot, man1Dot].concat(stack_arrow);

    const currentHistoryString =
        JSON.stringify(currentHistory);

    const repeated =
        game_history.includes(currentHistoryString);

    const historyStatus = repeated ? -4 : -5;

    game_history.push(
        JSON.stringify([historyStatus].concat(route_tmp))
    );

    game_history.push(currentHistoryString);
    game_hp++;

    route_tmp = [];
    new_arrow = [];
    clearReachableDots();

    current_man = opponent;

    if (repeated) {
        endGame("draw");
        return false;
    }

    game_status = current_man === 0 ? 1 : 3;
    displayGame();

    return true;
}

這段新邏輯的結果是:

  • 回合開始時不能走:立即困斃。
  • 可以射殺:立即紅線並勝出。
  • 沒走一步:打勾根本不顯示;防禦性判定仍會判困斃。
  • 還需要木橋但沒放木橋:完成動畫後立即判負並彈框。
  • 重複局面:立即和棋並彈框。
  • 不再把結果文字寫到 detail_div

十二、修改重新開始

在原本:

js
btn_play.onclick = function() {

開頭加入:

js
hideGameResult();
clearReachableDots();

abortMcts = true;
isMctsRunning = false;
clearTimeout(autoPlayTimer);

document.getElementById("thinking_ui").style.display = "none";
document.getElementById("mcts_start_btn").textContent = t("startSearch");

所以開頭應變成:

js
btn_play.onclick = function() {
    hideGameResult();
    clearReachableDots();

    abortMcts = true;
    isMctsRunning = false;
    clearTimeout(autoPlayTimer);

    document.getElementById("thinking_ui").style.display = "none";
    document.getElementById("mcts_start_btn").textContent = t("startSearch");

    men_bottom[0].setAttribute("stroke", "none");
    men_bottom[1].setAttribute("stroke", "none");
    detail_div.innerHTML = "";

    // 後面保留原有程式……

歷史模式中的和棋處理

btn_play.onclickgame_status == 8 分支中,搜尋:

js
if (game_history.length > 2 && -4 == JSON.parse(game_history[game_history.length - 2])[0]) {
    game_status = 7;
    detail_div.innerHTML = "Draw already";
    setTimeout(() => detail_div.innerHTML = "Draw", 3000);
    btn_play.classList.remove("hide");
    men_bottom[0].setAttribute("stroke", "#e77");
    men_bottom[1].setAttribute("stroke", "#e77");
} else {
    game_status = game_hp & 1 ? 3 : 1;
}
displayGame();

替換成:

js
if (
    game_history.length > 2 &&
    JSON.parse(game_history[game_history.length - 2])[0] === -4
) {
    endGame("draw");
    return;
} else {
    game_status = game_hp & 1 ? 3 : 1;
}

displayGame();

十三、修改設定窗口開關及 AI 自動控制

13.1 替換 openMctsModal()

js
function openMctsModal() {
    abortMcts = true;

    document.getElementById("mcts_results").innerHTML = "";
    currentMctsResults = [];
    currentMctsPage = 0;

    updateTutorialVisibility();

    if ([0, 7, 8].includes(game_status)) {
        document.getElementById("mcts_start_btn").style.display = "none";
        document.getElementById("mcts_results").style.display = "none";
        document.getElementById("mcts_play_btn").style.display = "block";
    } else {
        document.getElementById("mcts_start_btn").style.display = "block";
        document.getElementById("mcts_results").style.display = "block";
        document.getElementById("mcts_play_btn").style.display = "none";
    }

    document.getElementById("mcts_modal").style.display = "block";
}

13.2 替換 closeMctsModal()

js
function closeMctsModal() {
    abortMcts = true;

    document.getElementById("mcts_start_btn").textContent =
        t("startSearch");

    document.getElementById("mcts_modal").style.display = "none";

    /*
     * 新邏輯中 2、4 才是尚未移動或正在移動的行動狀態。
     * 只有 route_tmp.length === 1 才代表回合剛開始。
     */
    if (
        [2, 4].includes(game_status) &&
        route_tmp.length === 1
    ) {
        setTimeout(scheduleAutoTurn, 500);
    }
}

13.3 修改手動搜尋按鈕

搜尋:

js
if (![1, 3].includes(game_status)) {
    document.getElementById('mcts_results').innerHTML = "請在輪到玩家且未點擊時搜尋。";
    return;
}

替換成:

js
if (
    ![2, 4].includes(game_status) ||
    route_tmp.length !== 1
) {
    document.getElementById("mcts_results").textContent =
        t("searchOnlyAtTurnStart");

    return;
}

同一事件中的:

js
this.innerText = "停止搜尋";

替換成:

js
this.textContent = t("stopSearch");

以及:

js
document.getElementById('mcts_results').innerHTML = "搜尋中,請稍候...";

替換成:

js
document.getElementById("mcts_results").textContent =
    t("searchingPleaseWait");

所有這種:

js
document.getElementById('mcts_start_btn').innerText = "開始搜尋";

都改成:

js
document.getElementById("mcts_start_btn").textContent =
    t("startSearch");

搜尋中止訊息:

js
document.getElementById('mcts_results').innerHTML = "搜尋已中止。";

改為:

js
document.getElementById("mcts_results").textContent =
    t("searchAborted");

十四、修改 AI 執行走棋

完整替換 simulateMove()

js
function simulateMove(path) {
    document.getElementById("mcts_modal").style.display = "none";

    /*
     * 不再模擬點擊本方棋子。
     * beginTurn() 已經自動把遊戲切換到 2 或 4。
     */
    if (![2, 4].includes(game_status)) {
        document.getElementById("thinking_ui").style.display = "none";
        return;
    }

    let step = 1;

    function clickNext() {
        if (
            step < path.length &&
            [2, 4].includes(game_status)
        ) {
            dotOnClick.call(dots[path[step]]);
            step++;

            setTimeout(clickNext, 240);
            return;
        }

        if ([2, 4].includes(game_status)) {
            if (!btn_finish.classList.contains("hide")) {
                btn_finish.onclick();
            }

            setTimeout(() => {
                document.getElementById("thinking_ui").style.display =
                    "none";
            }, 1200);
        }
    }

    setTimeout(clickNext, 240);
}

十五、刪除 AI 中舊的「點人物判射殺」邏輯

runMCTS() 中,刪除從以下註解:

js
// --- 新增:在展開路徑前,先檢查當前局面是否已經可以直接射殺對手 ---

開始,到:

js
return; // 終止後續的 MCTS 展開
}

結束的整段代碼。

原因是現在 beginTurn() 已經在 AI 搜尋前自動完成:

  1. 射殺判定;
  2. 無路可走判定;
  3. 進入行動狀態;
  4. 啟動 AI。

若保留舊代碼,AI 仍會試圖呼叫 manOnClick(),與新邏輯衝突。


十六、AI 沒有合法完成路線時的處理

新增以下函式,放在 getLegalMoves() 前:

js
function getFallbackMovement(state) {
    const start =
        state.turn === 0 ? state.p0 : state.p1;

    const opponent =
        state.turn === 0 ? state.p1 : state.p0;

    const visited = new Set([start]);

    function dfs(currentDot, path) {
        /*
         * 找到至少一步,而且終點不是對手所在位置。
         * 這條路徑可能不包含新木橋,完成後會按照規則判負。
         */
        if (path.length > 1 && currentDot !== opponent) {
            return path.slice();
        }

        if (path.length >= PARAMS.MAX_PATH_LENGTH + 1) {
            return null;
        }

        const nextDots = new Set([
            ...(state.empty_map[currentDot] || []),
            ...(state.arrow_map[currentDot] || [])
        ]);

        for (const nextDot of nextDots) {
            if (visited.has(nextDot)) continue;

            visited.add(nextDot);
            path.push(nextDot);

            const result = dfs(nextDot, path);

            if (result) return result;

            path.pop();
            visited.delete(nextDot);
        }

        return null;
    }

    return dfs(start, [start]);
}

runMCTS() 中,把原本:

js
if (rootMoves.length === 0) {
    ...
}

整段替換成:

js
if (rootMoves.length === 0) {
    const elapsedSec =
        ((performance.now() - searchStartTime) / 1000).toFixed(1);

    const fallbackMove = getFallbackMovement(rootState);

    if (!isAuto) {
        document.getElementById("mcts_results").textContent =
            `${t("noLegalRoute")} ${t("totalTime", {
                time: elapsedSec
            })}`;
    } else {
        const thinkingUI = document.getElementById("thinking_ui");

        thinkingUI.innerHTML =
            `❌ <b>${t("noLegalRoute")}</b><br>` +
            `⏱️ ${elapsedSec} 秒`;

        thinkingUI.style.display = "block";

        if (fallbackMove) {
            /*
             * 有路可走但找不到能放新橋的合法完成路線:
             * AI 仍走一步,完成後由 judgeResult 判「沒有放橋」而輸。
             */
            setTimeout(() => {
                simulateMove(fallbackMove);
            }, 1200);
        } else {
            /*
             * 防禦性處理;正常情況已由 beginTurn 判困斃。
             */
            setTimeout(() => {
                thinkingUI.style.display = "none";

                game_history.push(
                    JSON.stringify([-2, rootState.turn === 0
                        ? rootState.p0
                        : rootState.p1])
                );

                endGame(
                    "trapped",
                    1 - rootState.turn,
                    rootState.turn
                );
            }, 1200);
        }
    }

    isMctsRunning = false;

    document.getElementById("mcts_start_btn").textContent =
        t("startSearch");

    return;
}

十七、讓 AI 搜尋結果跟隨語言

完整替換 renderMctsResults()

js
function renderMctsResults() {
    const maxPerPage = 5;
    const total = currentMctsResults.length;
    const totalPages = Math.ceil(total / maxPerPage) || 1;
    const start = currentMctsPage * maxPerPage;
    const end = Math.min(start + maxPerPage, total);

    let resultsHtml =
        `<b>${t("searchComplete", { count: total })}</b><br>` +
        `<span style="font-size:13px;color:#555;">` +
        `⏱️ ${t("totalTime", { time: lastSearchTime })}` +
        `</span><br>`;

    resultsHtml += `
        <div style="
            margin-top:10px;
            display:flex;
            justify-content:space-between;
            align-items:center;
            gap:8px;
        ">
            <button type="button"
                    onclick="changeMctsPage(-1)"
                    ${currentMctsPage === 0 ? "disabled" : ""}>
                ${t("previousPage")}
            </button>

            <span>
                ${t("page", {
                    current: currentMctsPage + 1,
                    total: totalPages
                })}
            </span>

            <button type="button"
                    onclick="changeMctsPage(1)"
                    ${currentMctsPage >= totalPages - 1
                        ? "disabled"
                        : ""}>
                ${t("nextPage")}
            </button>
        </div>
    `;

    for (let index = start; index < end; index++) {
        const stat = currentMctsResults[index];
        const pathString = stat.move.join(" ➔ ");

        let scoreText;

        if (stat.immediateLoss) {
            scoreText =
                `<span style="color:red;">${t("fatalMove")}</span>`;
        } else if (stat.guaranteedLoss) {
            scoreText =
                `<span style="color:orange;">${t("forcedLoss")}</span>`;
        } else if (stat.guaranteedWin) {
            scoreText =
                `<span style="color:green;">${t("forcedWin")}</span>`;
        } else {
            const rate =
                ((stat.wins / stat.plays) * 100).toFixed(1);

            scoreText = t("winRate", { rate });
        }

        resultsHtml += `
            <div class="mcts_result_item"
                 onclick="simulateMove([${stat.move.join(",")}])">
                ${index + 1}. [${pathString}] | ${scoreText}
            </div>
        `;
    }

    document.getElementById("mcts_results").innerHTML =
        resultsHtml;
}

AI 左上角的 #thinking_ui 外觀和主要顯示邏輯沒有改動,符合「AI 思考消息框不必改動」。


十八、鍵盤控制修正

目前數字鍵會優先點擊人物。在新邏輯下,開始回合不需要點人物,因此在鍵盤事件的數字鍵部分,將:

js
if (manToClick) {
    triggerClick(manToClick);
} else if (dots[dotIdx]) {
    triggerClick(dots[dotIdx]);
}

替換成:

js
/*
 * 行動階段一律以節點點擊處理。
 * 若節點上有人,dotOnClick 仍會按節點編號執行。
 */
if ([2, 4].includes(game_status) && dots[dotIdx]) {
    dotOnClick.call(dots[dotIdx]);
} else if (manToClick) {
    triggerClick(manToClick);
} else if (dots[dotIdx]) {
    triggerClick(dots[dotIdx]);
}

十九、修改後的遊戲流程

流程图
正在绘制流程图…

二十、重要行為差異

修改後:

  • 不再需要點擊本方人物。
  • 路線規劃期間不再顯示原來的折線。
  • 當前一步能到達的節點顯示淺紅色描邊:
    • stroke: #f0a0a0
    • stroke-width: 4
  • 每走一步重新計算可走節點。
  • 尚未走一步時不顯示打勾。
  • 無第一步可走時立即困斃,不需要再按打勾。
  • 射擊成立時立即顯示紅色路線並結束。
  • 應放木橋但沒放時判負。
  • 勝、負、和棋全部使用新的勝負消息框。
  • 勝負框可最小化;最小化後左上角顯示恢復 SVG 按鈕。
  • 「重新開始」直接開始新局。
  • 設定窗口桌面寬度為 620px,手機寬度為 90%
  • 教學模式只在遊戲尚未開始、即 game_status === 0 時顯示。
  • 中文為預設語言,切換 English 後設定窗口、搜尋結果及勝負消息會更新。