共享会话
🎮 哥尼斯堡遊戲程式修改
分享于 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思考消息框不必改動。
回答要求:
明確給出所要修改的地方和所要替換的代碼。
QuanAI我會先分段讀取完整 HTML,定位結構錯誤、設定視窗、勝負判定、玩家與 AI 行動流程,再按原程式的實際函式給出可直接替換的代碼。
首次讀取未成功,我改用較小分段,先取得文件開頭與樣式區,避免長檔案輸出超限。
已確認目前把 AI 設定直接作為單一彈窗,且 SVG 的 viewbox 大小寫不正確。接著並行讀取其餘腳本,定位初始化、點擊事件、勝負檢查及 AI 落子函式。
以下修改以你上傳的 konigsberg20260606cannotmove7954.html 為基礎。現有程式沒有明顯缺少結束標籤,但 HTML 結構、設定視窗及回合狀態需要一起調整。
一、HTML 結構檢查結果
1. <style> 不應放在 <body> 中
目前有兩個 <style> 位於 <body>:
<body>
<style>
...
</style>以及:
<div id="detail_div"></div>
<style>
...
</style>請把兩段 CSS 全部移入 <head>,結構改為:
<!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 大小寫錯誤
把:
viewbox="0 0 720 480"替換為:
viewBox="0 0 720 480"SVG 屬性區分大小寫,使用 viewbox 可能導致部分瀏覽器縮放異常。
3. 預設語言不應為英文
把:
<html lang="en">替換為:
<html lang="zh-Hant">語言切換時,JavaScript 會動態改成 en 或 zh-Hant。
4. 其他結構問題
以下不是致命錯誤,但建議修正:
#titing很可能是#tiling的拼字錯誤。不過 JavaScript 中也全部使用titing,暫時不要單獨改名,否則會造成引用失效。#konigsberg已經是position:absolute,所以float:left沒有效果,可以刪除。.man是空的 CSS 規則,可以保留,也可以刪除。- 原本的
#setting_div是棋譜 JSON 編輯視窗,不是 AI 設定視窗。建議繼續保留它,遊戲設定視窗仍使用原本的#mcts_modal。
二、替換遊戲設定視窗 HTML
找到從:
<div id="thinking_ui">🤔 思考中...</div>
<div id="mcts_modal">開始,到 #mcts_modal 對應的結束 </div> 為止,整段替換成:
<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 前面:
<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 的最後面:
/* ---------- 遊戲設定視窗 ---------- */
#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;
}
}五、新增多國語言程式
放在:
const PRESETS = {之前:
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 中,加入語言按鈕及教學按鈕事件。
找到:
displayGame();
// 綁定模式按鈕事件替換為:
displayGame();
/* 語言切換 */
document.querySelectorAll(".lang_btn").forEach(button => {
button.addEventListener("click", () => {
setLanguage(button.dataset.lang);
});
});
/*
* 教學功能暫時預留。
* 以後只需要替換這個事件內容。
*/
document.getElementById("tutorial_start_btn").addEventListener("click", () => {
// startTutorialMode();
});
// 綁定模式按鈕事件然後把原來的展開/收回事件:
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 = '▸ 展開';
}
});替換為:
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 最後面的:
openMctsModal();之前加入:
setLanguage("zh");最後部分應為:
document.getElementById("mcts_auto_blue").checked = false;
document.getElementById("mcts_auto_green").checked = true;
setLanguage("zh");
openMctsModal();七、新增勝負消息框控制程式
放在 judgeResult() 原本所在位置之前,或者直接與下一節的新勝負邏輯放在一起:
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() 前面:
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()
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()
人物不再用來啟動回合,只在人物位於目標節點上時,將點擊交給節點行動:
function manOnClick() {
if (isMctsRunning) return;
if (game_status === 2 || game_status === 4) {
dotOnClick.call(this);
}
}這樣就完全取消了「先點擊自己人物」的操作。
九、加入自動進入行動狀態、困斃與射殺檢查
把下面程式放在 displayGame() 前面:
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(),替換為:
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(),替換為:
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
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 最後加入
把原來手動處理完成按鈕的這段:
if (route_tmp.length == 1) {
this.classList.add("hide");
btn_cancel.classList.add("hide");
}
// finish button
let otherman_i = parseInt(...);
if (...) {
...
}刪除,改成:
updateActionButtons();也就是 btn_back.onclick 在重建虛線路徑之後,統一執行:
updateActionButtons();3. 修改 btn_cancel.onclick 最後部分
把:
btn_finish.classList.remove("hide");
btn_back.classList.add("hide");
this.classList.add("hide");替換為:
updateActionButtons();取消後 route_tmp 只有起點,因此打勾、上一步和取消都會隱藏。
十三、修改移動動畫結束處
1. createAnimateMotion() 的防呆分支
把:
if (pathPoints.length < 2) {
men_top[current_man].classList.add('hide');
men_bottom[current_man].classList.remove('hide');
judgeResult();
return;
}替換為:
if (pathPoints.length < 2) {
men_top[current_man].classList.add("hide");
men_bottom[current_man].classList.remove("hide");
declareCurrentPlayerTrapped();
return;
}2. turnBridge() 最後
把:
setTimeout(function() {
men_top[current_man].classList.add("hide");
men_bottom[current_man].classList.remove("hide");
judgeResult();
}, 1200);替換為:
setTimeout(function() {
men_top[current_man].classList.add("hide");
men_bottom[current_man].classList.remove("hide");
finalizeTurn();
}, 1200);十四、修改重新開始邏輯
在:
btn_play.onclick = function() {開頭加入:
hideGameResult();
clearMoveTargets();變成:
btn_play.onclick = function() {
hideGameResult();
clearMoveTargets();
men_bottom[0].setAttribute("stroke", "none");
men_bottom[1].setAttribute("stroke", "none");
detail_div.innerHTML = "";
...
};所有終局分支中原本的:
detail_div.innerHTML = "...";
btn_play.classList.remove("hide");都不再需要,勝負只由 finishGame() 和勝負消息框顯示。
十五、修改歷史記錄中的射擊線
在 historyCommen() 中找到:
addShotLine(history_route_tmp.slice(1), false);替換為:
addShotLine(history_route_tmp.slice(1));新的 addShotLine() 會畫一條紅色折線,而且不會在約 1 秒後消失。
十六、修改設定視窗開關條件
1. 替換 openMctsModal()
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()
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(),替換為:
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 的事件中,把:
if (![1, 3].includes(game_status)) {
document.getElementById('mcts_results').innerHTML =
"請在輪到玩家且未點擊時搜尋。";
return;
}替換為:
if (![2, 4].includes(game_status)) {
document.getElementById("mcts_results").textContent =
t("searchOnlyDuringTurn");
return;
}把:
this.innerText = "停止搜尋";替換為:
this.textContent = t("stopSearch");把:
document.getElementById('mcts_results').innerHTML =
"搜尋中,請稍候...";替換為:
document.getElementById("mcts_results").textContent =
t("searchingWait");程式中所有:
document.getElementById('mcts_start_btn').innerText = "開始搜尋";都改成:
document.getElementById("mcts_start_btn").textContent =
t("startSearch");十九、修改 AI 可搜尋路線規則
舊的 getLegalMoves() 只把「已經放新橋」的走法交給 AI,這會使 AI 永遠不會正確模擬「沒有放橋而判負」的情形。
刪除完整的舊 getLegalMoves(),替換為:
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 前面加入:
const mustBuildBridge =
state.stack_arrow.length / 2 < 12;即:
function applyMove(state, route) {
const mustBuildBridge =
state.stack_arrow.length / 2 < 12;
let nextState = {
...
};然後在:
nextState.stack_arrow = sim_stack_arrow;以及更新人物位置後:
if (state.turn === 0) {
nextState.p0 = route[route.length - 1];
} else {
nextState.p1 = route[route.length - 1];
}緊接著加入:
/*
* 仍可放木橋但整條路線沒有放置任何新橋,
* 此行動方立即判負。
*/
if (mustBuildBridge && new_arrow.length === 0) {
return {
terminal: true,
winner: 1 - state.turn,
reason: "noBridge"
};
}這項檢查必須放在「重複局面」與「射殺檢查」之前,因為沒有放橋的判負優先發生。
二十一、修改 AI 無路可走分支
在 runMCTS() 中找到:
if (rootMoves.length === 0) {
...
}刪除整個舊分支,替換為:
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() 中目前有一大段:
// --- 新增:在展開路徑前,先檢查當前局面是否已經可以直接射殺對手 ---
let shooter = ...
...
if (canShootNow) {
...
}請整段刪除。
原因是射擊現在已由 prepareCurrentTurn() 在玩家及 AI 的回合開始時統一檢查。保留這段會造成:
- AI 射擊延遲 1.5 秒;
- 再次模擬點擊人物;
- 可能重複寫入終局歷史;
- 不符合「直接顯示紅色折線並判負」。
二十三、修改 AI 搜尋結果的多國語言
刪除舊的完整 renderMctsResults(),替換為:
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() 中,不需要改變它的右側位置,因為恢復按鈕固定使用左側。不過建議在非全螢幕分支加入:
document.getElementById("result_restore").style.left = "7px";即:
} 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 搜尋也會把「未放木橋」視為立即敗北。