QuanAI只读快照

共享会话

🎮 哥尼斯堡遊戲程式修改

分享于 2026年8月21日 01:46
提问者

改進程式 檢查html結構:

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

增加勝負消息框:

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

更改設置窗口:

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

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

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

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

更改控制行動邏輯:

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

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

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

回答要求:

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

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

我會先分段讀取完整 HTML,定位結構錯誤、設定視窗、勝負判定、玩家與 AI 行動流程,再按原程式的實際函式給出可直接替換的代碼。

首次讀取未成功,我改用較小分段,先取得文件開頭與樣式區,避免長檔案輸出超限。

已確認目前把 AI 設定直接作為單一彈窗,且 SVG 的 viewbox 大小寫不正確。接著並行讀取其餘腳本,定位初始化、點擊事件、勝負檢查及 AI 落子函式。

以下修改以你上傳的 konigsberg20260606cannotmove7954.html 為基礎。現有程式沒有明顯缺少結束標籤,但 HTML 結構、設定視窗及回合狀態需要一起調整。


一、HTML 結構檢查結果

1. <style> 不應放在 <body>

目前有兩個 <style> 位於 <body>

html
<body>
    <style>
        ...
    </style>

以及:

html
<div id="detail_div"></div>
<style>
    ...
</style>

請把兩段 CSS 全部移入 <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>
        /* 原本第一段 CSS */

        /* 原本第二段 CSS */

        /* 下文提供的新增 CSS */
    </style>
</head>

<body>
    ...
</body>
</html>

2. SVG 的 viewbox 大小寫錯誤

把:

html
viewbox="0 0 720 480"

替換為:

html
viewBox="0 0 720 480"

SVG 屬性區分大小寫,使用 viewbox 可能導致部分瀏覽器縮放異常。

3. 預設語言不應為英文

把:

html
<html lang="en">

替換為:

html
<html lang="zh-Hant">

語言切換時,JavaScript 會動態改成 enzh-Hant

4. 其他結構問題

以下不是致命錯誤,但建議修正:

  • #titing 很可能是 #tiling 的拼字錯誤。不過 JavaScript 中也全部使用 titing,暫時不要單獨改名,否則會造成引用失效。
  • #konigsberg 已經是 position:absolute,所以 float:left 沒有效果,可以刪除。
  • .man 是空的 CSS 規則,可以保留,也可以刪除。
  • 原本的 #setting_div 是棋譜 JSON 編輯視窗,不是 AI 設定視窗。建議繼續保留它,遊戲設定視窗仍使用原本的 #mcts_modal

二、替換遊戲設定視窗 HTML

找到從:

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"
        class="mcts_close"
        id="game_settings_close"
        onclick="closeMctsModal()"
        data-i18n-title="close"
        aria-label="關閉">×</button>

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

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

        <div id="language_buttons">
            <button type="button"
                    class="lang_btn"
                    data-lang="en">
                English
            </button>

            <span class="language_separator">|</span>

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

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

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

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

        <div class="mode_header">
            <button type="button" id="toggle_params">▸ 展開</button>

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

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

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

                <button class="mode_btn"
                        type="button"
                        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="maxPath">
                    最大步數:
                </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="firstPath">
                    首回限步:
                </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 data-i18n="forcedSearch">
                    必勝搜尋(橋數 ➔ 深度):
                </div>

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

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

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

                <div class="mcts_row forced_depth_row">
                    <label for="fcd_depth4">19</label>
                    <input type="number"
                           id="fcd_depth4"
                           class="mcts_param_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 id="mcts_play_btn"
                type="button"
                class="settings_main_button"
                data-i18n="startGame"
                style="display:none;">
            開始遊戲
        </button>

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

        <div id="mcts_results"></div>
    </section>
</div>

三、新增勝負消息框 HTML

放在 #mcts_modal 後面、#setting_div 前面:

html
<div id="result_modal"
     class="hide"
     role="dialog"
     aria-modal="true"
     aria-labelledby="result_title">

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

    <h3 id="result_title"></h3>

    <div id="result_message"></div>

    <button type="button"
            id="result_restart"
            data-i18n="restart">
        重新開始
    </button>
</div>

<div id="result_restore"
     class="trbtn hide"
     data-i18n-title="restoreWindow"
     title="恢復窗口">

    <svg viewBox="0 0 24 24"
         width="24"
         height="24"
         fill="white"
         aria-hidden="true">
        <path d="M7 3h14v14h-4v4H3V7h4V3zm2 4h8v8h2V5H9v2zm6 2H5v10h10V9z"></path>
    </svg>
</div>

四、新增及覆蓋 CSS

把以下 CSS 放在 <head> 中原有 CSS 的最後面:

css
/* ---------- 遊戲設定視窗 ---------- */

#mcts_modal {
    position: absolute;
    top: 5%;
    left: 50%;
    transform: translateX(-50%);
    width: 620px;
    max-width: calc(100% - 32px);
    max-height: 90%;
    padding: 20px;
    overflow-y: auto;
    background: #fff;
    border: 2px solid #333;
    border-radius: 10px;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
    font-size: 16px;
    z-index: 20;
    display: none;
}

#mcts_modal h2 {
    margin: 0 40px 18px;
    text-align: center;
    font-size: 28px;
    line-height: 1.25;
}

#mcts_modal .mcts_close {
    position: absolute;
    top: 8px;
    right: 10px;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    background: transparent;
    color: #333;
    font-size: 28px;
    line-height: 32px;
    font-weight: bold;
    cursor: pointer;
}

.settings_section {
    margin-top: 14px;
    padding: 14px;
    border: 1px solid #d8d8d8;
    border-radius: 8px;
    background: #fafafa;
}

.settings_section > h3 {
    margin: 0 0 12px;
    text-align: center;
    font-size: 20px;
}

#language_buttons {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
}

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

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

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

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

#tutorial_start_btn {
    padding: 9px 22px;
    border: 0;
    border-radius: 6px;
    background: #4caf50;
    color: #fff;
    font-size: 16px;
    cursor: pointer;
}

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

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

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

.forced_search_group {
    margin: 8px 0;
}

.forced_depth_row {
    padding-left: 12px;
}

.forced_depth_row input {
    width: 52px;
}

.ai_player_row {
    justify-content: space-around;
}

.settings_main_button,
.settings_secondary_button {
    width: 100%;
    margin-top: 8px;
    padding: 9px;
    border-radius: 5px;
    font-size: 16px;
    cursor: pointer;
}

.settings_main_button {
    border: 0;
    background: #8278f5;
    color: #fff;
}

.settings_secondary_button {
    border: 1px solid #aaa;
    background: #fff;
    color: #333;
}

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

/* ---------- 可移動節點 ---------- */

circle.move-target {
    stroke: #f1a0a0 !important;
    stroke-width: 4 !important;
}

/* ---------- 勝負消息框 ---------- */

#result_modal {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 30;
    width: 420px;
    max-width: 86%;
    padding: 26px 24px 22px;
    transform: translate(-50%, -50%);
    border: 2px solid #333;
    border-radius: 12px;
    background: #fff;
    color: #333;
    text-align: center;
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.42);
}

#result_modal h3 {
    margin: 0 40px 14px;
    font-size: 25px;
}

#result_message {
    min-height: 54px;
    margin-bottom: 18px;
    line-height: 1.6;
    font-size: 18px;
}

#result_minimize {
    position: absolute;
    top: 8px;
    right: 8px;
    display: flex;
    width: 34px;
    height: 34px;
    padding: 5px;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 5px;
    background: #777;
    color: #fff;
    cursor: pointer;
}

#result_restart {
    min-width: 150px;
    padding: 10px 20px;
    border: 0;
    border-radius: 7px;
    background: #8278f5;
    color: #fff;
    font-size: 17px;
    cursor: pointer;
}

#result_restart:hover {
    background: #6f65dc;
}

/*
 * 位置參照右上角的 #fullscreen,
 * 但改用 left,顯示在左上角。
 */
#result_restore {
    top: 7px;
    left: 7px;
    right: auto;
    background: #8278f5;
}

/* 必須放在上面各項規則之後 */
.hide {
    display: none !important;
}

@media (max-width: 640px) {
    #mcts_modal {
        top: 4%;
        width: 90%;
        max-width: 90%;
        max-height: 92%;
        padding: 16px 12px;
    }

    #mcts_modal h2 {
        margin-left: 32px;
        margin-right: 32px;
        font-size: 23px;
    }

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

    #mode_buttons {
        justify-content: center;
    }

    .settings_section {
        padding: 11px 8px;
    }

    #result_modal {
        width: 86%;
        padding-left: 16px;
        padding-right: 16px;
    }
}

五、新增多國語言程式

放在:

js
const PRESETS = {

之前:

js
let currentLanguage = "zh";
let lastGameResult = null;

const I18N = {
    zh: {
        pageTitle: "哥尼斯堡——路與橋",
        gameTitle: "哥尼斯堡——路與橋",

        languageSelection: "語言選擇",
        tutorialMode: "教學模式",
        startTutorialMode: "開始教學模式",
        aiSearchSettings: "AI 搜尋設定",

        expand: "▸ 展開",
        collapse: "▾ 收回",

        easy: "簡單",
        hard: "困難",
        expert: "專家",
        custom: "自定",

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

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

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

        resultTitle: "本局結果",
        drawTitle: "和棋",
        resultShot: "{winner}勝利;{loser}被射殺。",
        resultTrapped: "{winner}勝利;{loser}無路可走,困斃落敗。",
        resultNoBridge: "{winner}勝利;{loser}仍可放置木橋,但本回合沒有放置任何木橋。",
        resultDraw: "局面重複,判定為和棋。",

        restart: "重新開始",
        minimize: "最小化",
        restoreWindow: "恢復窗口",
        close: "關閉",
        fullscreen: "全螢幕",
        exportSvg: "導出 SVG 動畫",

        searchComplete: "搜尋完成!共 {total} 條路徑:",
        totalTime: "總耗時:{time} 秒",
        previousPage: "上一頁",
        nextPage: "下一頁",
        pageNumber: "頁數:{current} / {total}",
        fatalMove: "💀 致命步",
        forcedLoss: "⚠️ 必敗步",
        forcedWin: "🎯 必殺走法",
        winRate: "勝率:{rate}%",
        searchOnlyDuringTurn: "請在玩家行動階段搜尋。",
        searchingWait: "搜尋中,請稍候……",
        searchAborted: "搜尋已中止。"
    },

    en: {
        pageTitle: "Königsberg - Path & Bridge",
        gameTitle: "Königsberg - Path & Bridge",

        languageSelection: "Language",
        tutorialMode: "Tutorial Mode",
        startTutorialMode: "Start Tutorial Mode",
        aiSearchSettings: "AI Search Settings",

        expand: "▸ Expand",
        collapse: "▾ Collapse",

        easy: "Easy",
        hard: "Hard",
        expert: "Expert",
        custom: "Custom",

        simulations: "Simulations:",
        searchDepth: "Search depth:",
        maxPath: "Maximum path length:",
        firstPath: "First-turn limit:",
        forcedSearch: "Forced-win search (bridges ➔ depth):",

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

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

        resultTitle: "Game Result",
        drawTitle: "Draw",
        resultShot: "{winner} wins; {loser} was shot.",
        resultTrapped: "{winner} wins; {loser} has no legal path and is trapped.",
        resultNoBridge: "{winner} wins; {loser} could still build a wooden bridge but built none this turn.",
        resultDraw: "The position has repeated. The game is a draw.",

        restart: "Restart",
        minimize: "Minimize",
        restoreWindow: "Restore Window",
        close: "Close",
        fullscreen: "Fullscreen",
        exportSvg: "Export SVG Animation",

        searchComplete: "Search complete! {total} paths found:",
        totalTime: "Total time: {time} seconds",
        previousPage: "Previous",
        nextPage: "Next",
        pageNumber: "Page: {current} / {total}",
        fatalMove: "💀 Fatal move",
        forcedLoss: "⚠️ Forced loss",
        forcedWin: "🎯 Forced win",
        winRate: "Win rate: {rate}%",
        searchOnlyDuringTurn: "Search is available only during a player's action phase.",
        searchingWait: "Searching, please wait...",
        searchAborted: "Search aborted."
    }
};

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

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

    return text;
}

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

function updateToggleText() {
    const button = document.getElementById("toggle_params");
    const container = document.getElementById("params_container");

    if (!button || !container) return;

    button.textContent =
        container.style.display === "none" ? t("expand") : t("collapse");
}

function applyLanguage() {
    document.documentElement.lang =
        currentLanguage === "en" ? "en" : "zh-Hant";

    document.title = t("pageTitle");

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

    document.querySelectorAll("[data-i18n-title]").forEach(element => {
        const translatedTitle = t(element.dataset.i18nTitle);
        element.title = translatedTitle;
        element.setAttribute("aria-label", translatedTitle);
    });

    document.querySelectorAll(".lang_btn").forEach(button => {
        button.classList.toggle(
            "active",
            button.dataset.lang === currentLanguage
        );
    });

    fullscreenbtn.title = t("fullscreen");
    exportsvgbtn.title = t("exportSvg");

    updateToggleText();
    renderGameResult();

    if (currentMctsResults.length > 0) {
        renderMctsResults();
    }
}

function setLanguage(language) {
    if (!I18N[language]) return;

    currentLanguage = language;
    applyLanguage();
}

六、修改 window.onload

window.onload 中,加入語言按鈕及教學按鈕事件。

找到:

js
displayGame();

// 綁定模式按鈕事件

替換為:

js
displayGame();

/* 語言切換 */
document.querySelectorAll(".lang_btn").forEach(button => {
    button.addEventListener("click", () => {
        setLanguage(button.dataset.lang);
    });
});

/*
 * 教學功能暫時預留。
 * 以後只需要替換這個事件內容。
 */
document.getElementById("tutorial_start_btn").addEventListener("click", () => {
    // startTutorialMode();
});

// 綁定模式按鈕事件

然後把原來的展開/收回事件:

js
document.getElementById('toggle_params').addEventListener('click', function() {
    const container = document.getElementById('params_container');
    if (container.style.display === 'none') {
        container.style.display = 'block';
        this.innerText = '▾ 收回';
    } else {
        container.style.display = 'none';
        this.innerText = '▸ 展開';
    }
});

替換為:

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

    container.style.display =
        container.style.display === "none" ? "block" : "none";

    updateToggleText();
});

window.onload 最後面的:

js
openMctsModal();

之前加入:

js
setLanguage("zh");

最後部分應為:

js
document.getElementById("mcts_auto_blue").checked = false;
document.getElementById("mcts_auto_green").checked = true;

setLanguage("zh");
openMctsModal();

七、新增勝負消息框控制程式

放在 judgeResult() 原本所在位置之前,或者直接與下一節的新勝負邏輯放在一起:

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

    const titleElement = document.getElementById("result_title");
    const messageElement = document.getElementById("result_message");

    if (lastGameResult.type === "draw") {
        titleElement.textContent = t("drawTitle");
        messageElement.textContent = t("resultDraw");
        return;
    }

    const winner = getPlayerName(lastGameResult.winner);
    const loser = getPlayerName(lastGameResult.loser);

    titleElement.textContent = t("resultTitle");

    if (lastGameResult.type === "shot") {
        messageElement.textContent = t("resultShot", {
            winner,
            loser
        });
    } else if (lastGameResult.type === "trapped") {
        messageElement.textContent = t("resultTrapped", {
            winner,
            loser
        });
    } else if (lastGameResult.type === "noBridge") {
        messageElement.textContent = t("resultNoBridge", {
            winner,
            loser
        });
    }
}

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

    renderGameResult();

    document.getElementById("result_restore").classList.add("hide");
    document.getElementById("result_modal").classList.remove("hide");
}

function hideGameResult() {
    lastGameResult = null;

    document.getElementById("result_modal").classList.add("hide");
    document.getElementById("result_restore").classList.add("hide");
}

document.getElementById("result_minimize").addEventListener("click", () => {
    document.getElementById("result_modal").classList.add("hide");
    document.getElementById("result_restore").classList.remove("hide");
});

document.getElementById("result_restore").addEventListener("click", () => {
    document.getElementById("result_restore").classList.add("hide");
    document.getElementById("result_modal").classList.remove("hide");
});

document.getElementById("result_restart").addEventListener("click", () => {
    hideGameResult();
    btn_play.onclick.call(btn_play);
});

八、替換玩家行動邏輯

1. 新增可走節點高亮函式

放在 dotOnClick() 前面:

js
function clearMoveTargets() {
    dots.forEach(dot => {
        dot.classList.remove("move-target");
    });
}

function getAvailableNextDots() {
    if (route_tmp.length === 0) return [];

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

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

    return [...new Set(nextDots)].filter(dotIndex => {
        return !route_tmp.includes(dotIndex);
    });
}

function highlightAvailableNextDots() {
    clearMoveTargets();

    if (game_status !== 2 && game_status !== 4) return;

    getAvailableNextDots().forEach(dotIndex => {
        dots[dotIndex].classList.add("move-target");
    });
}

function updateActionButtons() {
    const hasWalked = route_tmp.length > 1;
    const lastDot = route_tmp[route_tmp.length - 1];

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

    /*
     * 未走至少一步時不顯示打勾。
     * 停在對方人物所在位置時也不能完成回合。
     */
    btn_finish.classList.toggle(
        "hide",
        !hasWalked || lastDot === opponentDot
    );

    btn_back.classList.toggle("hide", !hasWalked);
    btn_cancel.classList.toggle("hide", !hasWalked);

    highlightAvailableNextDots();
}

2. 替換整個 dotOnClick()

js
function dotOnClick() {
    if (isMctsRunning) return;
    if (game_status !== 2 && game_status !== 4) return;

    const targetDot = parseInt(this.getAttribute("i"));
    const availableDots = getAvailableNextDots();

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

    drawPlanRouteGo(targetDot);
    route_tmp.push(targetDot);

    addBridge();
    updateActionButtons();
}

3. 替換整個 manOnClick()

人物不再用來啟動回合,只在人物位於目標節點上時,將點擊交給節點行動:

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

    if (game_status === 2 || game_status === 4) {
        dotOnClick.call(this);
    }
}

這樣就完全取消了「先點擊自己人物」的操作。


九、加入自動進入行動狀態、困斃與射殺檢查

把下面程式放在 displayGame() 前面:

js
function findShotPath(startDot, targetDot, map = arrow_map) {
    const queue = [[startDot]];
    const visited = new Set([startDot]);

    while (queue.length > 0) {
        const path = queue.shift();
        const currentDot = path[path.length - 1];

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

            const nextPath = path.concat(nextDot);

            if (nextDot === targetDot) {
                return nextPath;
            }

            visited.add(nextDot);
            queue.push(nextPath);
        }
    }

    return null;
}

function shotPathToLinePairs(path) {
    const pairs = [];

    for (let i = 0; i < path.length - 1; i++) {
        pairs.push(path[i], path[i + 1]);
    }

    return pairs;
}

function linePairsToShotPath(pairs) {
    if (!pairs || pairs.length < 2) return [];

    const path = [pairs[0], pairs[1]];

    for (let i = 3; i < pairs.length; i += 2) {
        path.push(pairs[i]);
    }

    return path;
}

function drawShotPolyline(path) {
    if (!path || path.length < 2) return;

    const oldLines = document.querySelectorAll(".redline");
    oldLines.forEach(line => line.remove());

    const polyline = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "polyline"
    );

    const points = path.map(dotIndex => {
        return [
            dots[dotIndex].getAttribute("cx"),
            dots[dotIndex].getAttribute("cy")
        ].join(",");
    }).join(" ");

    polyline.classList.add("redline");
    polyline.setAttribute("points", points);
    polyline.setAttribute("fill", "none");
    polyline.setAttribute("stroke", "#e33");
    polyline.setAttribute("stroke-width", "7");
    polyline.setAttribute("stroke-linecap", "round");
    polyline.setAttribute("stroke-linejoin", "round");

    middlearea.appendChild(polyline);
}

/*
 * 保留舊的函式名稱,讓歷史記錄功能仍可使用。
 */
function addShotLine(linesArray) {
    drawShotPolyline(linePairsToShotPath(linesArray));
}

/*
 * 判斷是否存在至少一條可以結束的行走路線。
 * 對方人物所在位置可以經過,但不能在該位置結束回合。
 */
function hasAtLeastOneWalk(startDot, opponentDot) {
    const visited = new Set([startDot]);

    function dfs(currentDot) {
        const nextDots = [
            ...(empty_map[currentDot] || []),
            ...(arrow_map[currentDot] || [])
        ];

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

            /*
             * 只要能走到非對手所在位置,就有至少一條
             * 可以正常結束的行走路線。
             */
            if (nextDot !== opponentDot) {
                return true;
            }

            visited.add(nextDot);

            if (dfs(nextDot)) {
                return true;
            }

            visited.delete(nextDot);
        }

        return false;
    }

    return dfs(startDot);
}

function updateTutorialVisibility() {
    const tutorialSection = document.getElementById("tutorial_section");

    /*
     * 只有尚未開始第一局,也就是 game_status === 0 時顯示。
     * 遊戲結束後仍視為已經開始過,因此不再顯示。
     */
    tutorialSection.style.display =
        game_status === 0 ? "block" : "none";
}

function finishGame(type, winner = null, loser = null, shotPath = null) {
    clearTimeout(autoPlayTimer);
    abortMcts = true;
    isMctsRunning = false;

    game_status = 7;

    clearMoveTargets();

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

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

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

    if (type === "shot" && shotPath) {
        drawShotPolyline(shotPath);
    }

    updateTutorialVisibility();
    showGameResult(type, winner, loser);
}

function declareCurrentPlayerTrapped() {
    const startDot = parseInt(
        men_bottom[current_man].getAttribute("i")
    );

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

    finishGame(
        "trapped",
        1 - current_man,
        current_man
    );
}

function prepareCurrentTurn() {
    if (game_status !== 1 && game_status !== 3) return;

    /*
     * 設定視窗打開時先暫停,不在視窗後面啟動行動。
     */
    if (document.getElementById("mcts_modal").style.display === "block") {
        return;
    }

    const currentDot = parseInt(
        men_bottom[current_man].getAttribute("i")
    );

    const opponent = 1 - current_man;

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

    /*
     * 目前行動方若能沿橋方向射到對方,
     * 立即顯示紅色折線並判對方輸。
     */
    const shotPath = findShotPath(currentDot, opponentDot);

    if (shotPath) {
        game_history.push(
            JSON.stringify(
                [-1].concat(shotPathToLinePairs(shotPath))
            )
        );

        finishGame(
            "shot",
            current_man,
            opponent,
            shotPath
        );

        return;
    }

    /*
     * 沒有任何可以完成的行走路線,立即困斃。
     */
    if (!hasAtLeastOneWalk(currentDot, opponentDot)) {
        declareCurrentPlayerTrapped();
        return;
    }

    /*
     * 自動進入行動狀態,不再等待點擊人物。
     */
    game_status = current_man === 0 ? 2 : 4;

    route_tmp = [currentDot];
    new_arrow = [];

    drawPlanRouteStart(currentDot);

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

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

    highlightAvailableNextDots();
    scheduleAutoPlayer();
}

function scheduleAutoPlayer() {
    clearTimeout(autoPlayTimer);

    if (game_status !== 2 && game_status !== 4) return;

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

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

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

    const shouldRun =
        (current_man === 0 && autoBlue) ||
        (current_man === 1 && autoGreen);

    if (!shouldRun) return;

    autoPlayTimer = setTimeout(() => {
        updateMctsParams();
        runMCTS(true);
    }, 500);
}

十、替換 displayGame()

刪除原本完整的 displayGame(),替換為:

js
function displayGame() {
    if (game_status === 0) {
        if (dots.length === 0) {
            addDots();
        }

        men_bottom[0].classList.add("hide");
        men_bottom[1].classList.add("hide");
        men_top[0].classList.add("hide");
        men_top[1].classList.add("hide");

        men_blue.classList.add("hide");
        men_green.classList.add("hide");

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

        btn_history_prev.classList.add("hide");
        btn_history_next.classList.add("hide");

        btn_play.classList.remove("hide");
        btn_history_set.classList.remove("hide");
    } else if (game_status === 1 || game_status === 3) {
        men_bottom[0].classList.remove("hide");
        men_bottom[1].classList.remove("hide");
        men_top[0].classList.add("hide");
        men_top[1].classList.add("hide");

        if (current_man === 0) {
            men_blue.classList.remove("hide");
            men_green.classList.add("hide");
        } else {
            men_blue.classList.add("hide");
            men_green.classList.remove("hide");
        }

        btn_play.classList.add("hide");

        /*
         * 畫面更新完成後,自動檢查射殺、困斃,
         * 然後進入行動狀態。
         */
        setTimeout(prepareCurrentTurn, 0);
    }

    if (game_history.length > 1) {
        btn_history_prev.classList.remove("hide");
    } else {
        btn_history_prev.classList.add("hide");
    }

    updateTutorialVisibility();
}

原本 displayGame() 最後的「自動下棋」區塊必須刪除,因為現在 AI 是在 prepareCurrentTurn() 進入行動狀態後才啟動。


十一、替換原本的 judgeResult()

刪除完整的舊 judgeResult(),替換為:

js
function finalizeTurn() {
    const routeCopy = route_tmp.slice();

    /*
     * 正常情況不會發生,因為未走一步時打勾按鈕是隱藏的。
     * 此處仍保留防呆。
     */
    if (routeCopy.length <= 1) {
        declareCurrentPlayerTrapped();
        route_tmp = [];
        new_arrow = [];
        return;
    }

    /*
     * turnBridge() 執行前已經把新橋加入 stack_arrow。
     * 少於 12 座橋時,仍要求本回合至少放置一座新橋。
     */
    const mustBuildBridge = stack_arrow.length < 24;

    if (mustBuildBridge && new_arrow.length === 0) {
        game_history.push(
            JSON.stringify([-3].concat(routeCopy))
        );

        finishGame(
            "noBridge",
            1 - current_man,
            current_man
        );

        route_tmp = [];
        new_arrow = [];
        return;
    }

    /*
     * 本回合新橋正式完成,移除新橋外框。
     */
    const firstNewBridgeIndex =
        (stack_arrow.length - new_arrow.length) / 2;

    for (let i = 0; i < new_arrow.length / 2; i++) {
        const arrowElement = arrows[firstNewBridgeIndex + i];

        if (arrowElement) {
            arrowElement.setAttribute("stroke", "none");
        }
    }

    const blueDot = parseInt(
        men_bottom[0].getAttribute("i")
    );

    const greenDot = parseInt(
        men_bottom[1].getAttribute("i")
    );

    const boardState = [
        blueDot,
        greenDot
    ].concat(stack_arrow);

    const boardStateString = JSON.stringify(boardState);
    const isDraw = game_history.includes(boardStateString);
    const historyCode = isDraw ? -4 : -5;

    game_history.push(
        JSON.stringify([historyCode].concat(routeCopy))
    );

    game_history.push(boardStateString);
    game_hp++;

    route_tmp = [];
    new_arrow = [];

    if (isDraw) {
        finishGame("draw");
        return;
    }

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

    displayGame();
}

十二、修改完成、上一步、取消按鈕

1. 替換 btn_finish.onclick

js
btn_finish.onclick = function() {
    if (route_tmp.length <= 1) return;

    const lastDot = route_tmp[route_tmp.length - 1];
    const opponentDot = parseInt(
        men_bottom[1 - current_man].getAttribute("i")
    );

    if (lastDot === opponentDot) return;

    clearMoveTargets();

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

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

    createAnimateMotion();
};

2. 在 btn_back.onclick 最後加入

把原來手動處理完成按鈕的這段:

js
if (route_tmp.length == 1) {
    this.classList.add("hide");
    btn_cancel.classList.add("hide");
}

// finish button
let otherman_i = parseInt(...);
if (...) {
    ...
}

刪除,改成:

js
updateActionButtons();

也就是 btn_back.onclick 在重建虛線路徑之後,統一執行:

js
updateActionButtons();

3. 修改 btn_cancel.onclick 最後部分

把:

js
btn_finish.classList.remove("hide");
btn_back.classList.add("hide");
this.classList.add("hide");

替換為:

js
updateActionButtons();

取消後 route_tmp 只有起點,因此打勾、上一步和取消都會隱藏。


十三、修改移動動畫結束處

1. createAnimateMotion() 的防呆分支

把:

js
if (pathPoints.length < 2) {
    men_top[current_man].classList.add('hide');
    men_bottom[current_man].classList.remove('hide');
    judgeResult();
    return;
}

替換為:

js
if (pathPoints.length < 2) {
    men_top[current_man].classList.add("hide");
    men_bottom[current_man].classList.remove("hide");
    declareCurrentPlayerTrapped();
    return;
}

2. turnBridge() 最後

把:

js
setTimeout(function() {
    men_top[current_man].classList.add("hide");
    men_bottom[current_man].classList.remove("hide");
    judgeResult();
}, 1200);

替換為:

js
setTimeout(function() {
    men_top[current_man].classList.add("hide");
    men_bottom[current_man].classList.remove("hide");
    finalizeTurn();
}, 1200);

十四、修改重新開始邏輯

在:

js
btn_play.onclick = function() {

開頭加入:

js
hideGameResult();
clearMoveTargets();

變成:

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

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

所有終局分支中原本的:

js
detail_div.innerHTML = "...";
btn_play.classList.remove("hide");

都不再需要,勝負只由 finishGame() 和勝負消息框顯示。


十五、修改歷史記錄中的射擊線

historyCommen() 中找到:

js
addShotLine(history_route_tmp.slice(1), false);

替換為:

js
addShotLine(history_route_tmp.slice(1));

新的 addShotLine() 會畫一條紅色折線,而且不會在約 1 秒後消失。


十六、修改設定視窗開關條件

1. 替換 openMctsModal()

js
function openMctsModal() {
    abortMcts = true;

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

    /*
     * 現在玩家的實際行動階段是 2 或 4,
     * 因為 1、3 會自動轉入 2、4。
     */
    if (game_status === 2 || game_status === 4) {
        document.getElementById("mcts_start_btn").style.display = "block";
        document.getElementById("mcts_results").style.display = "block";
        document.getElementById("mcts_play_btn").style.display = "none";
    } else {
        document.getElementById("mcts_start_btn").style.display = "none";
        document.getElementById("mcts_results").style.display = "none";
        document.getElementById("mcts_play_btn").style.display = "block";
    }

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

2. 替換 closeMctsModal()

js
function closeMctsModal() {
    abortMcts = true;

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

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

    /*
     * 若還停在回合開始狀態,先自動進入行動。
     */
    if (game_status === 1 || game_status === 3) {
        setTimeout(prepareCurrentTurn, 0);
        return;
    }

    /*
     * 已在行動狀態則檢查是否由 AI 控制。
     */
    if (game_status === 2 || game_status === 4) {
        isMctsRunning = false;
        scheduleAutoPlayer();
    }
}

十七、修改 AI 模擬落子

刪除舊的完整 simulateMove(),替換為:

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

    /*
     * 不再模擬點擊人物。
     * 玩家和 AI 在回合開始時都已自動進入 2/4 狀態。
     */
    if (game_status !== 2 && game_status !== 4) {
        document.getElementById("thinking_ui").style.display = "none";
        return;
    }

    let step = 1;

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

            setTimeout(clickNext, 240);
            return;
        }

        if (
            game_status === 2 ||
            game_status === 4
        ) {
            /*
             * AI 路線可能沒有放橋。
             * 仍然完成該回合,然後由 finalizeTurn()
             * 依規則直接判 AI 輸。
             */
            if (route_tmp.length > 1) {
                btn_finish.onclick();
            }

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

    setTimeout(clickNext, 240);
}

十八、修改 AI 搜尋按鈕的狀態判斷

mcts_start_btn 的事件中,把:

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

替換為:

js
if (![2, 4].includes(game_status)) {
    document.getElementById("mcts_results").textContent =
        t("searchOnlyDuringTurn");
    return;
}

把:

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

替換為:

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

把:

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

替換為:

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

程式中所有:

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

都改成:

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

十九、修改 AI 可搜尋路線規則

舊的 getLegalMoves() 只把「已經放新橋」的走法交給 AI,這會使 AI 永遠不會正確模擬「沒有放橋而判負」的情形。

刪除完整的舊 getLegalMoves(),替換為:

js
function getLegalMoves(state, isFirstTurn = false) {
    const start =
        state.turn === 0 ? state.p0 : state.p1;

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

    const moves = [];

    const currentMaxPath =
        isFirstTurn
            ? PARAMS.FIRST_MAX_PATH
            : PARAMS.MAX_PATH_LENGTH;

    function dfs(currentDot, path) {
        /*
         * 只要走過至少一步,且沒有停在對方位置,
         * 就是一個可以按下完成的行動。
         *
         * 是否有放新橋改由 applyMove() 判斷;
         * 若沒有放橋,該行動會被標記為立即敗北。
         */
        if (
            path.length > 1 &&
            currentDot !== opponentPosition
        ) {
            moves.push(path.slice());
        }

        if (path.length - 1 >= currentMaxPath) return;

        for (const nextDot of state.empty_map[currentDot] || []) {
            if (path.includes(nextDot)) continue;

            path.push(nextDot);
            dfs(nextDot, path);
            path.pop();
        }

        for (const nextDot of state.arrow_map[currentDot] || []) {
            if (path.includes(nextDot)) continue;

            path.push(nextDot);
            dfs(nextDot, path);
            path.pop();
        }
    }

    dfs(start, [start]);

    return moves;
}

二十、修改 AI 的 applyMove()

applyMove(state, route) 開頭,nextState 前面加入:

js
const mustBuildBridge =
    state.stack_arrow.length / 2 < 12;

即:

js
function applyMove(state, route) {
    const mustBuildBridge =
        state.stack_arrow.length / 2 < 12;

    let nextState = {
        ...
    };

然後在:

js
nextState.stack_arrow = sim_stack_arrow;

以及更新人物位置後:

js
if (state.turn === 0) {
    nextState.p0 = route[route.length - 1];
} else {
    nextState.p1 = route[route.length - 1];
}

緊接著加入:

js
/*
 * 仍可放木橋但整條路線沒有放置任何新橋,
 * 此行動方立即判負。
 */
if (mustBuildBridge && new_arrow.length === 0) {
    return {
        terminal: true,
        winner: 1 - state.turn,
        reason: "noBridge"
    };
}

這項檢查必須放在「重複局面」與「射殺檢查」之前,因為沒有放橋的判負優先發生。


二十一、修改 AI 無路可走分支

runMCTS() 中找到:

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

刪除整個舊分支,替換為:

js
if (rootMoves.length === 0) {
    isMctsRunning = false;

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

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

    /*
     * 正常情況 prepareCurrentTurn() 已先檢查困斃;
     * 此處作為 AI 搜尋的第二層防呆。
     */
    declareCurrentPlayerTrapped();
    return;
}

二十二、移除 runMCTS() 中重複的射擊處理

runMCTS() 中目前有一大段:

js
// --- 新增:在展開路徑前,先檢查當前局面是否已經可以直接射殺對手 ---
let shooter = ...
...
if (canShootNow) {
    ...
}

請整段刪除。

原因是射擊現在已由 prepareCurrentTurn() 在玩家及 AI 的回合開始時統一檢查。保留這段會造成:

  • AI 射擊延遲 1.5 秒;
  • 再次模擬點擊人物;
  • 可能重複寫入終局歷史;
  • 不符合「直接顯示紅色折線並判負」。

二十三、修改 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", { 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;
        ">
            <button
                onclick="changeMctsPage(-1)"
                ${currentMctsPage === 0 ? "disabled" : ""}>
                ${t("previousPage")}
            </button>

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

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

    for (let i = start; i < end; i++) {
        const stat = currentMctsResults[i];
        const pathText = 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}])">
                ${i + 1}. [${pathText}] | ${scoreText}
            </div>
        `;
    }

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

二十四、全螢幕時同步恢復按鈕位置

winresize() 中,不需要改變它的右側位置,因為恢復按鈕固定使用左側。不過建議在非全螢幕分支加入:

js
document.getElementById("result_restore").style.left = "7px";

即:

js
} else {
    konigsberg.style.height = "100%";
    konigsberg.style.width = "100%";
    tiling_inner.classList.remove("rotate90");

    fullscreenbtn.style.right = "7px";
    exportsvgbtn.style.right = "7px";

    document.getElementById("result_restore").style.left = "7px";

    ...
}

修改後的主要流程

流程图
正在绘制流程图…

這些修改會同時完成:

  • 修正 HTML/CSS 結構;
  • 將 AI 設定改為完整遊戲設定;
  • 桌面設定視窗約 620px,手機為 90%;
  • 新增中英文切換,預設中文;
  • 新增教學模式欄目;
  • 遊戲開始後隱藏教學模式;
  • 新增可最小化、可恢復的勝負消息框;
  • 不再把勝負文字直接寫到棋盤;
  • 玩家及 AI 都不再需要點擊人物來開始行動;
  • 移除非終局的約 1 秒紅線;
  • 每一步高亮當前可到達節點;
  • 未走一步時不顯示打勾;
  • 困斃、射殺、未放橋及和棋均立即彈出勝負消息框;
  • AI 搜尋也會把「未放木橋」視為立即敗北。