QuanAI只读快照

共享会话

規則更改

分享于 2026年9月20日 15:37
提问者

規則更改 被「叉零」封住的,都不算是孤獨數。因為孤獨數的定義是被封在一個小正方形的數,而「叉零」為這個數字增加了四分之一的空間。 介面更改 提子階段也去掉可提子的顯示提示,只需要在點擊到一個不可走到己方「叉零」的棋子時在消息欄給出提示。 當輪到一方時,他的控制區的背景應高亮。 程式更改 把所有棋盤svg的屬性,包括stroke,stroke-width,fill等,都改為寫到svg的內聯屬性中,而不再寫到css中。 回答要求:給出一個python修改代碼的腳本。

battlenumber227.htmltext/plain · 159.7 KB预览下载
QuanAI

以下腳本會針對附件原版 battlenumber227.html 完成修改:

  • 被相鄰「叉零」封邊的單格區域不再判定為孤獨數,並同步修改主程式及 MCTS Worker。
  • 提子階段不再顯示可提起點或下一步目標,只保留已選路徑;點擊無法到達己方叉零的棋子時仍顯示消息。
  • 當前行動方的控制區背景高亮。
  • 將棋盤 SVG 的 fillstrokestroke-widthpointer-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