共享会话
規則更改
分享于 2026年9月20日 15:37
QuanAI以下腳本會針對附件原版 battlenumber227.html 完成修改:
- 被相鄰「叉零」封邊的單格區域不再判定為孤獨數,並同步修改主程式及 MCTS Worker。
- 提子階段不再顯示可提起點或下一步目標,只保留已選路徑;點擊無法到達己方叉零的棋子時仍顯示消息。
- 當前行動方的控制區背景高亮。
- 將棋盤 SVG 的
fill、stroke、stroke-width、pointer-events等呈現設定移入 SVG 元素,不再依賴 CSS。 - 同步更新中英文規則文字。
- 棋譜版本提升為 6。
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
修改 battlenumber227.html。
用法:
python patch_battlenumber.py battlenumber227.html -o battlenumber228.html
原地覆蓋:
python patch_battlenumber.py battlenumber227.html --in-place
如果來源檔案曾被修改、但仍想嘗試套用:
python patch_battlenumber.py changed.html -o patched.html --force
"""
from __future__ import annotations
import argparse
import hashlib
import re
import sys
from pathlib import Path
EXPECTED_SHA256 = (
"251b1c96ff3949d31bab726eb52d2dfb59dbe0e0b295ec0e8100e395ce503ee2"
)
class PatchError(RuntimeError):
pass
def replace_exact(
text: str,
old: str,
new: str,
label: str,
expected_count: int = 1,
) -> str:
count = text.count(old)
if count != expected_count:
raise PatchError(
f"{label}:預期找到 {expected_count} 處,實際找到 {count} 處。"
)
return text.replace(old, new)
def replace_regex_once(
text: str,
pattern: str,
replacement: str,
label: str,
flags: int = 0,
) -> str:
result, count = re.subn(pattern, replacement, text, count=1, flags=flags)
if count != 1:
raise PatchError(f"{label}:預期找到 1 處,實際找到 {count} 處。")
return result
def patch_html(text: str) -> str:
# ------------------------------------------------------------------
# 1. 當前行動方的控制區背景高亮
# ------------------------------------------------------------------
old = """ .player-panel.player-two {
background: linear-gradient(180deg, rgba(255, 198, 189, 0.72), rgba(255, 250, 240, 0.96));
}
"""
new = """ .player-panel.player-two {
background: linear-gradient(180deg, rgba(255, 198, 189, 0.72), rgba(255, 250, 240, 0.96));
}
/* 當前行動方不只高亮邊框,也高亮整個控制區背景。 */
.player-panel.player-one.current-player {
background: linear-gradient(
180deg,
rgba(255, 228, 154, 0.98) 0%,
rgba(200, 210, 255, 0.88) 46%,
rgba(255, 250, 240, 0.98) 100%
);
}
.player-panel.player-two.current-player {
background: linear-gradient(
180deg,
rgba(255, 228, 154, 0.98) 0%,
rgba(255, 198, 189, 0.88) 46%,
rgba(255, 250, 240, 0.98) 100%
);
}
"""
text = replace_exact(text, old, new, "加入控制區背景高亮")
# ------------------------------------------------------------------
# 2. 移除棋盤 SVG 的 CSS 呈現規則
#
# 保留 .board-shell;從 #boardSvg 開始到 Toolbar 標題之前,
# 原本皆為棋盤 SVG 的 CSS 呈現規則。
# ------------------------------------------------------------------
text = replace_regex_once(
text,
r"""
\#boardSvg\s*\{
.*?
/\*\s*----------\s+Toolbar\s+----------\s*\*/
""",
"""
/* 棋盤 SVG 的呈現屬性均直接寫在 SVG 元素上。 */
/* ---------- Toolbar ---------- */""",
"移除棋盤 SVG 的 CSS 呈現規則",
flags=re.DOTALL | re.VERBOSE,
)
# ------------------------------------------------------------------
# 3. 將 boardSvg 本身的尺寸和介面樣式改成內聯設定
# ------------------------------------------------------------------
old = """ preserveAspectRatio="xMidYMid meet"
aria-label="Battle Number board"
>
"""
new = """ preserveAspectRatio="xMidYMid meet"
width="100%"
height="100%"
aria-label="Battle Number board"
style="display:block;min-width:0;min-height:0;filter:drop-shadow(0 20px 26px rgba(0,0,0,0.27));touch-action:manipulation"
>
"""
text = replace_exact(text, old, new, "加入 boardSvg 內聯設定")
# ------------------------------------------------------------------
# 4. 棋盤靜態 SVG 元素改用內聯屬性
# ------------------------------------------------------------------
old = """ </defs>
<rect class="board-bg" x="23" y="23" width="434" height="434" rx="14" />
<rect class="board-frame" x="30" y="30" width="420" height="420" />
"""
new = """ <filter
id="lastPlacementShadow"
x="-50%"
y="-50%"
width="200%"
height="200%"
color-interpolation-filters="sRGB"
>
<feDropShadow
dx="0"
dy="1"
stdDeviation="1"
flood-color="#000000"
flood-opacity="0.45"
/>
</filter>
</defs>
<rect
class="board-bg"
x="23"
y="23"
width="434"
height="434"
rx="14"
fill="#fffaf0"
stroke="#d8cdb2"
stroke-width="3"
/>
<rect
class="board-frame"
x="30"
y="30"
width="420"
height="420"
fill="none"
stroke="#314e4a"
stroke-width="3"
/>
"""
text = replace_exact(text, old, new, "內聯棋盤背景和外框屬性")
old = """ <path
class="board-grid"
d="
"""
new = """ <path
class="board-grid"
fill="none"
stroke="#9e9b90"
stroke-width="1.25"
d="
"""
text = replace_exact(text, old, new, "內聯棋盤格線屬性")
old = """ <circle id="centerDot" class="center-dot" cx="240" cy="240" r="6" />
"""
new = """ <circle
id="centerDot"
class="center-dot"
cx="240"
cy="240"
r="6"
fill="#78499b"
/>
"""
text = replace_exact(text, old, new, "內聯中心圓點屬性")
# ------------------------------------------------------------------
# 5. 動態棋格矩形的 SVG 屬性改為直接輸出
# ------------------------------------------------------------------
old = """ function cellRect(index, className) {
const row = Math.floor(index / N);
const col = index % N;
return `
<rect
class="${className}"
x="${30 + col * 60 + 3}"
y="${30 + row * 60 + 3}"
width="54"
height="54"
rx="7"
></rect>
`;
}
"""
new = """ function cellRect(index, className) {
const row = Math.floor(index / N);
const col = index % N;
// 棋盤 SVG 的呈現屬性全部直接寫入元素,
// 不再依賴 class 對應的 CSS 規則。
const attributes = {
'legal-place':
'fill="#4bb686" fill-opacity="0.22" stroke="#2fa571" stroke-width="3" stroke-dasharray="7 5" pointer-events="none"',
'forced-cell':
'fill="#d7a947" fill-opacity="0.23" stroke="#c28c20" stroke-width="3" pointer-events="none"',
'extract-start':
'fill="#ffe49a" fill-opacity="0.14" stroke="#d7a947" stroke-width="4" pointer-events="none"',
'extract-target':
'fill="#4bb686" fill-opacity="0.25" stroke="#1d9f6b" stroke-width="4" pointer-events="none"',
'path-cell':
'fill="#6f50b0" fill-opacity="0.16" stroke="#7650b4" stroke-width="4" pointer-events="none"'
};
const inlineAttributes = attributes[className] || '';
const animation =
className === 'forced-cell'
? '<animate attributeName="opacity" values="1;0.45;1" dur="1.2s" repeatCount="indefinite"></animate>'
: '';
return `
<rect
class="${className}"
x="${30 + col * 60 + 3}"
y="${30 + row * 60 + 3}"
width="54"
height="54"
rx="7"
${inlineAttributes}
>${animation}</rect>
`;
}
"""
text = replace_exact(text, old, new, "改寫 cellRect 的 SVG 內聯屬性")
# ------------------------------------------------------------------
# 6. 棋盤棋子、最近落子標記改用內聯 SVG 屬性
# ------------------------------------------------------------------
old = """ <use
class="board-piece"
href="#tile${id}"
xlink:href="#tile${id}"
transform="translate(${60 + col * 60} ${60 + row * 60}) rotate(${tile.rot * 90})"
></use>
"""
new = """ <use
class="board-piece"
href="#tile${id}"
xlink:href="#tile${id}"
transform="translate(${60 + col * 60} ${60 + row * 60}) rotate(${tile.rot * 90})"
pointer-events="none"
stroke-linecap="round"
stroke-linejoin="round"
></use>
"""
text = replace_exact(text, old, new, "內聯棋盤棋子屬性")
old = """ <circle
class="last-placement-dot"
cx="${60 + col * 60}"
cy="${60 + row * 60}"
r="3.6"
></circle>
"""
new = """ <circle
class="last-placement-dot"
cx="${60 + col * 60}"
cy="${60 + row * 60}"
r="3.6"
fill="#ffffff"
stroke="none"
paint-order="stroke fill"
pointer-events="none"
filter="url(#lastPlacementShadow)"
></circle>
"""
text = replace_exact(text, old, new, "內聯最近落子標記屬性")
# ------------------------------------------------------------------
# 7. 提子階段移除所有可提起點和可到達目標提示
#
# 仍保留已經實際選取的路徑及路徑序號。
# ------------------------------------------------------------------
extraction_hint_pattern = r""" // 落子階段不再顯示合法落子格提示。
// 提子階段仍顯示可選起點、可到達目標及目前路徑。
if \(
started &&
state\.status === 'playing' &&
controllers\[state\.turn\] === 'human' &&
state\.phase === 'extract'
\) \{
.*?
\}
(?= if \(aiExtractionAnimating && manualPath\.length\) \{)"""
extraction_hint_replacement = """ // 落子及提子階段均不顯示可走位置提示。
// 提子階段只保留玩家已經實際選取的路徑。
if (
started &&
state.status === 'playing' &&
controllers[state.turn] === 'human' &&
state.phase === 'extract' &&
manualPath.length
) {
manualPath.forEach((index, order) => {
const row = Math.floor(index / N);
const col = index % N;
paths.push(cellRect(index, 'path-cell'));
paths.push(`
<text
class="path-number"
x="${60 + col * 60}"
y="${60 + row * 60}"
>${order + 1}</text>
`);
});
}
"""
text = replace_regex_once(
text,
extraction_hint_pattern,
extraction_hint_replacement,
"移除提子階段可走位置提示",
flags=re.DOTALL,
)
# ------------------------------------------------------------------
# 8. 提子路徑文字改成 SVG 內聯屬性
#
# 一處是人工選路,另一處是 AI 動畫。
# ------------------------------------------------------------------
old = """ <text
class="path-number"
x="${60 + col * 60}"
y="${60 + row * 60}"
>${order + 1}</text>
"""
new = """ <text
class="path-number"
x="${60 + col * 60}"
y="${60 + row * 60}"
fill="#ffffff"
stroke="#172f2c"
stroke-width="4"
paint-order="stroke"
text-anchor="middle"
dominant-baseline="middle"
font-size="17"
font-weight="900"
pointer-events="none"
>${order + 1}</text>
"""
text = replace_exact(
text,
old,
new,
"內聯提子路徑文字屬性",
expected_count=2,
)
# ------------------------------------------------------------------
# 9. 棋盤點擊層改用 SVG 內聯屬性
# ------------------------------------------------------------------
old = """ <rect
class="cell-hit"
data-index="${index}"
x="${30 + col * 60}"
y="${30 + row * 60}"
width="60"
height="60"
></rect>
"""
new = """ <rect
class="cell-hit"
data-index="${index}"
x="${30 + col * 60}"
y="${30 + row * 60}"
width="60"
height="60"
fill="transparent"
cursor="pointer"
></rect>
"""
text = replace_exact(text, old, new, "內聯棋盤點擊層屬性")
# ------------------------------------------------------------------
# 10. 區域資料記錄單格區域的位置
#
# 同一段結構在 Worker 和主程式各出現一次。
# ------------------------------------------------------------------
old = """ cellCount: 0,
emptyCount: 0,
owners: [false, false],
triangles: [0, 0]
"""
new = """ cellCount: 0,
emptyCount: 0,
singleIndex: -1,
owners: [false, false],
triangles: [0, 0]
"""
text = replace_exact(
text,
old,
new,
"加入單格區域索引",
expected_count=2,
)
old = """ group.cellCount++;
if (!tile) {
"""
new = """ group.cellCount++;
group.singleIndex = index;
if (!tile) {
"""
text = replace_exact(
text,
old,
new,
"記錄區域棋格索引",
expected_count=2,
)
# ------------------------------------------------------------------
# 11. 修改 MCTS Worker 的孤獨數判定
#
# 單格區域若有任一相鄰叉零,代表叉零為該數增加了四分之一
# 的空間,因此不視為孤獨數。
# ------------------------------------------------------------------
old = """ // 只有一格的封閉區域為「孤獨數」。
// 玩家一的孤獨數令玩家一失分;玩家二亦然。
if (group.cellCount === 1) {
if (group.owners[0]) {
score -= group.triangles[0];
} else if (group.owners[1]) {
score += group.triangles[1];
}
continue;
}
"""
new = """ // 只有一格的封閉區域才可能是「孤獨數」。
// 如果該格有相鄰叉零,叉零會為數字增加四分之一格空間,
// 因此該數不視為孤獨數。
if (group.cellCount === 1) {
const index = group.singleIndex;
const row = Math.floor(index / N);
const col = index % N;
let sealedByCrossZero = false;
for (const [dr, dc] of D4) {
const nextRow = row + dr;
const nextCol = col + dc;
if (nextRow < 0 || nextRow >= N || nextCol < 0 || nextCol >= N) {
continue;
}
const neighbour = board[nextRow * N + nextCol];
if (neighbour && neighbour[1] === TYPE_CROSS) {
sealedByCrossZero = true;
break;
}
}
if (sealedByCrossZero) {
continue;
}
if (group.owners[0]) {
score -= group.triangles[0];
} else if (group.owners[1]) {
score += group.triangles[1];
}
continue;
}
"""
text = replace_exact(text, old, new, "修改 Worker 孤獨數判定")
# ------------------------------------------------------------------
# 12. 修改主程式的孤獨數判定
# ------------------------------------------------------------------
old = """ // 被困在單一棋格中的數為孤獨數。
if (group.cellCount === 1) {
if (group.owners[0]) {
score -= group.triangles[0];
} else if (group.owners[1]) {
score += group.triangles[1];
}
continue;
}
"""
new = """ // 被困在單一棋格中的數才可能是孤獨數。
// 如果封邊中包含相鄰叉零,叉零會為該數增加四分之一格
// 空間,因此該數不算孤獨數。
if (group.cellCount === 1) {
const index = group.singleIndex;
const row = Math.floor(index / N);
const col = index % N;
let sealedByCrossZero = false;
for (const [dr, dc] of DIRECTIONS) {
const nextRow = row + dr;
const nextCol = col + dc;
if (nextRow < 0 || nextRow >= N || nextCol < 0 || nextCol >= N) {
continue;
}
const neighbour = board[nextRow * N + nextCol];
if (neighbour && neighbour.type === TYPE_CROSS) {
sealedByCrossZero = true;
break;
}
}
if (sealedByCrossZero) {
continue;
}
if (group.owners[0]) {
score -= group.triangles[0];
} else if (group.owners[1]) {
score += group.triangles[1];
}
continue;
}
"""
text = replace_exact(text, old, new, "修改主程式孤獨數判定")
# ------------------------------------------------------------------
# 13. 更新中文規則文字
# ------------------------------------------------------------------
old = (
"一般只含一方棋子的區域不計分;但若一個數被困在只有一個棋格的區域內,"
"該數成為「孤獨數」,其所屬方按三角形數量扣分,對方取得相應分數。"
"後手未落下的棋子亦視為孤獨數並直接扣分。"
)
new = (
"一般只含一方棋子的區域不計分;但若一個數被困在只有一個棋格的區域內,"
"該數成為「孤獨數」,其所屬方按三角形數量扣分,對方取得相應分數。"
"如果封住該數的邊界中包含「叉零」,由於「叉零」為該數增加了四分之一格空間,"
"該數不視為孤獨數。後手未落下的棋子亦視為孤獨數並直接扣分。"
)
text = replace_exact(text, old, new, "更新中文孤獨數規則")
# ------------------------------------------------------------------
# 14. 更新英文規則文字
# ------------------------------------------------------------------
old = (
"A normal region containing pieces from only one player scores zero. "
"However, a number trapped inside a one-square region is a Lonely Number: "
"its owner loses its triangle value and the opponent gains the same amount. "
"Every piece left unplaced by the second player is also treated as a Lonely Number."
)
new = (
"A normal region containing pieces from only one player scores zero. "
"However, a number trapped inside a one-square region is a Lonely Number: "
"its owner loses its triangle value and the opponent gains the same amount. "
"If any part of that number's enclosure is formed by an adjacent Cross Zero, "
"the Cross Zero adds one quarter of a square of space, so the number is not treated "
"as a Lonely Number. Every piece left unplaced by the second player is also treated "
"as a Lonely Number."
)
text = replace_exact(text, old, new, "更新英文孤獨數規則")
# ------------------------------------------------------------------
# 15. 提升導出棋譜版本
# ------------------------------------------------------------------
text = replace_exact(
text,
" version: 5,\n",
" version: 6,\n",
"提升棋譜版本",
)
# ------------------------------------------------------------------
# 16. 完成後檢查
# ------------------------------------------------------------------
style_match = re.search(r"<style>(.*?)</style>", text, flags=re.DOTALL)
if not style_match:
raise PatchError("找不到 style 區塊。")
style_text = style_match.group(1)
forbidden_svg_css = [
"#boardSvg",
".board-bg",
".board-grid",
".board-frame",
".center-dot",
".cell-hit",
".legal-place",
".forced-cell",
".extract-start",
".extract-target",
".path-cell",
".path-number",
".board-piece",
".last-placement-dot",
]
remaining = [
selector for selector in forbidden_svg_css if selector in style_text
]
if remaining:
raise PatchError(
"仍有棋盤 SVG 呈現規則留在 CSS 中:"
+ ", ".join(remaining)
)
if "highlights.push(cellRect(start, 'extract-start'))" in text:
raise PatchError("提子起點提示仍然存在。")
if "highlights.push(cellRect(destination, 'extract-target'))" in text:
raise PatchError("提子目標提示仍然存在。")
if text.count("sealedByCrossZero") < 4:
raise PatchError("孤獨數的新判定未完整加入主程式和 Worker。")
if text.count("player-panel.player-one.current-player") != 1:
raise PatchError("玩家一控制區背景高亮未正確加入。")
if text.count("player-panel.player-two.current-player") != 1:
raise PatchError("玩家二控制區背景高亮未正確加入。")
return text
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="修改 Battle Number 227 HTML。"
)
parser.add_argument(
"input",
nargs="?",
default="battlenumber227.html",
help="來源 HTML,預設為 battlenumber227.html",
)
parser.add_argument(
"-o",
"--output",
help="輸出 HTML;未指定時輸出為 <來源檔名>.patched.html",
)
parser.add_argument(
"--in-place",
action="store_true",
help="直接覆蓋來源檔案",
)
parser.add_argument(
"--force",
action="store_true",
help="來源 SHA-256 不符時仍嘗試套用",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
input_path = Path(args.input)
if not input_path.is_file():
print(f"錯誤:找不到來源檔案:{input_path}", file=sys.stderr)
return 1
if args.in_place and args.output:
print(
"錯誤:--in-place 和 --output 不能同時使用。",
file=sys.stderr,
)
return 1
raw = input_path.read_bytes()
actual_sha256 = hashlib.sha256(raw).hexdigest()
if actual_sha256 != EXPECTED_SHA256:
warning = (
"來源檔案 SHA-256 與 battlenumber227.html 原版不符。\n"
f"預期:{EXPECTED_SHA256}\n"
f"實際:{actual_sha256}"
)
if not args.force:
print(f"錯誤:{warning}", file=sys.stderr)
print(
"如確定仍要嘗試修改,請加入 --force。",
file=sys.stderr,
)
return 1
print(f"警告:{warning}", file=sys.stderr)
try:
source_text = raw.decode("utf-8")
except UnicodeDecodeError as exc:
print(f"錯誤:來源檔案不是有效 UTF-8:{exc}", file=sys.stderr)
return 1
try:
result = patch_html(source_text)
except PatchError as exc:
print(f"修改失敗:{exc}", file=sys.stderr)
return 1
if args.in_place:
output_path = input_path
elif args.output:
output_path = Path(args.output)
else:
output_path = input_path.with_name(
f"{input_path.stem}.patched{input_path.suffix}"
)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(result, encoding="utf-8", newline="")
output_sha256 = hashlib.sha256(
result.encode("utf-8")
).hexdigest()
print("修改完成。")
print(f"來源:{input_path}")
print(f"輸出:{output_path}")
print(f"輸出 SHA-256:{output_sha256}")
return 0
if __name__ == "__main__":
raise SystemExit(main())執行範例:
bash
python patch_battlenumber.py battlenumber227.html -o battlenumber228.html