「益智遊戲」和「抽象弈棋」
头像
ejsoon
一枝独秀一枝独秀
帖子: 6862
注册时间: 2022年 11月 18日 17:36
联系:

Re: 又有一個新的弈棋創意:拚數棋!

帖子 ejsoon »

把遊戲名字改為「拚數迷陣」,英文為「NumClash Labyrinth」,副標題為「孤獨或生存,敵人亦朋友」,英文為「Isolation or survival, enemies are friends」。
上次由 ejsoon 在 2026年 9月 22日 15:01,总共编辑 1 次。
https://ejsoon.vip/
金梭越空:極速暢遊天地
头像
ejsoon
一枝独秀一枝独秀
帖子: 6862
注册时间: 2022年 11月 18日 17:36
联系:

Re: 又有一個新的弈棋創意:拚數棋!

帖子 ejsoon »

在這一局中:
q d6m e6m d3q e4u e3o c6t g3l f6u c1m b5p b7s b6j a6s b4r d2r a5n g1m f1o f2s e1t g4q a2l f5r b1o g7n

AI控制的後手方,在最後兩手棋下出了f5r和g7n,這兩手棋都是直接把它的棋子變成「孤獨數」,直接扣分的。為什麼AI會下出直接使它自己扣分的棋?AI是否理解到底怎麼下會得分,怎麼下會送分?

檢查代碼,查找原因並修復。如果有要修改的地方,給出修改代碼的python腳本。
https://ejsoon.vip/
金梭越空:極速暢遊天地
头像
ejsoon
一枝独秀一枝独秀
帖子: 6862
注册时间: 2022年 11月 18日 17:36
联系:

Re: 又有一個新的弈棋創意:拚數棋!

帖子 ejsoon »

當點擊控制區的棋子時,棋子會因點擊的不同位置而轉到不同的角度。現在增加功能,當電腦鼠標或手機觸控按住拖動時,所拖動的方向將是棋子要旋轉的方向。
https://ejsoon.vip/
金梭越空:極速暢遊天地
头像
ejsoon
一枝独秀一枝独秀
帖子: 6862
注册时间: 2022年 11月 18日 17:36
联系:

Re: 又有一個新的弈棋創意:拚數棋!

帖子 ejsoon »

移除 tanh,改用原始分差。(不要一會兒使用目前行動方視角、一會兒使用根節點玩家視角。回傳視角必須與 backpropagation 的正負號邏輯一致。移除 tanh 後,要重新調整 UCT exploration constant,因為 exploitation 從 [-1, 1] 變成了實際分數單位)
根節點候選提高至約 12~32 個,依時間調整。
強制納入所有會改變孤獨數狀態的候選。
rollout 隨機率從 17% 降低

代码: 全选

此前的分析是:

根節點並不是一次完整比較全部走法,而是每次抽取少量候選:

const sampleSize = Math.min(
  pool.remaining,
  mode === 'tree' ? 6 : 5
);

rollout 還有 17% 隨機選擇:

const epsilon =
  mode === 'tree'
    ? 0.02
    : 0.17;

在七乘七棋盤的大分支數下,3~12 秒的 MCTS 結果可能有相當大的抽樣噪音。

 最終分數經 tanh 壓縮

return Math.tanh(score / 12);

當 rollout 結果的絕對分數較大時,tanh 接近 -1 或 1,相差 2~3 分的影響會被壓得很小。這使確定的孤獨數扣分容易被隨機 rollout 的差異掩蓋。

分析完畢。

現在要修改為:
移除 tanh,改用原始分差。(不要一會兒使用目前行動方視角、一會兒使用根節點玩家視角。回傳視角必須與 backpropagation 的正負號邏輯一致。移除 tanh 後,要重新調整 UCT exploration constant,因為 exploitation 從 [-1, 1] 變成了實際分數單位)
根節點候選提高至約 12~32 個,依時間調整。
強制納入所有會改變孤獨數狀態的候選。
rollout 隨機率從 17% 降低

回答要求:給出修改代碼的python腳本。
附件
battlenumber245.html.7z
(47.88 KiB) 尚未被下载
https://ejsoon.vip/
金梭越空:極速暢遊天地
头像
ejsoon
一枝独秀一枝独秀
帖子: 6862
注册时间: 2022年 11月 18日 17:36
联系:

Re: 又有一個新的弈棋創意:拚數棋!

帖子 ejsoon »

battlenumber250.html

還沒加上「對方下一手連接模擬」及「關鍵接點封鎖」判定。

https://gpt.quanquan.space/share/T5vVjg ... 5rfYpFrXjE
附件
battlenumber250.html.7z
(61.84 KiB) 已下载 1 次
https://ejsoon.vip/
金梭越空:極速暢遊天地
头像
ejsoon
一枝独秀一枝独秀
帖子: 6862
注册时间: 2022年 11月 18日 17:36
联系:

Re: 又有一個新的弈棋創意:拚數棋!

帖子 ejsoon »

o g3o c4m d3i d2r c3n b4t g1o d5l g4q f3l f2j f4q g2l d6s c5v e2i f1h e1r b5i b6o a5q b7s a4h

應該少放「單一」和「叉零」,注意盡量多的放「圍三」。
https://ejsoon.vip/
金梭越空:極速暢遊天地
头像
ejsoon
一枝独秀一枝独秀
帖子: 6862
注册时间: 2022年 11月 18日 17:36
联系:

Re: 又有一個新的弈棋創意:拚數棋!

帖子 ejsoon »

檢查代碼,是否這些調整有促使AI更多的下「單一」和「叉零」

代码: 全选

經過前面的調整之後,現在AI都傾向於優先落下「單一」和「叉零」,這是不對的,因為「圍三」、「雙二」和「角二」在後面都很容易成為「孤獨棋」,而「圍三」如果隨意放的話會被對放封口,因此應優先放置不會被對方封口的「雙二」和「角二」,而當「圍三」能接上己方的棋子時,就應優先把握機會。

之前在沒有加入這些調整時(包括不會落下一個直接成為「孤獨棋」、不會落下一個棋子被對方下一回合封口等),MCTS雖然有時會落下一個孤獨棋,但是倒是能算出優先落下「雙二」或「角二」,然後用「圍三」接上。現在加入了這些調整後,反倒不會了。檢查代碼,是否這些調整有促使AI更多的下「單一」和「叉零」。

但是不是說「單一」和「叉零」以後就都不能下了,也要根據棋局的情況,比如當對方有一個棋子被封口後就會成為「孤獨棋」,則我方就有可能下「單一」封上,為了創造貼上「叉零」而不成為「孤獨棋」的條件,而會落下一個「叉零」,等等。這些都可以交給MCTS算。

回答要求:給出修改代碼的python腳本。
sol

代码: 全选

我會先定位候選硬性篩選、棋型編碼、孤獨棋/封口檢查及 MCTS 根節點選擇邏輯,確認是哪一層把「雙二、角二、圍三」過早排除,再據此製作可重複執行的補丁腳本。

有,現有 `battlenumber250.html` 中有三處會促使 AI 偏向「單一」和「叉零」:

1. `preferLowerNarrowClosureRisk()` 把封閉風險當成**硬性篩選**。  
   「單一」通常有三個開口,「叉零」分值為零,較容易取得風險 0;「雙二、角二」只剩兩個開口,較容易被整批排除。

2. `chooseMultiPlyRootEntries()` 找到少量已證明安全的棋後,最多只把約 6 手交給 MCTS。  
   其餘沒有被搜尋到的「雙二、角二、接己方棋的圍三」也會被提前刪除。

3. `applyAiAction()` 會在 MCTS 完成後,再用非 MCTS 的 `fallbackAction()` 替換結果。  
   fallback 主要看當前分數,同分時近似隨機,可能把 MCTS 算出的戰略棋換成「單一」或「叉零」。

以下補丁會:

- 保留「立即形成己方孤獨棋」的硬性防護;
- 保留「對方下一手能封口/立即得分」的硬性防護;
- 只排除已經被多層搜尋**明確證明**會強制失分的候選;
- 不再因為只搜尋到少量安全棋,就把其他未搜尋候選全部刪除;
- 把狹窄區域風險改成 MCTS 的**軟性先驗**,不再硬切候選;
- 軟性優先展開安全的「雙二、角二」;
- 「圍三」的唯一開口若能接上己方棋子,給予更高先驗;
- 不禁止「單一」和「叉零」;
- 立即封住對方孤獨棋等戰術,仍由原本的 MCTS 戰術機制優先處理;
- 最終執行前只攔截直接危險,不再因長期風險用 fallback 覆蓋 MCTS 結果。

將以下內容保存為 `patch_battlenumber251.py`:

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
修正 battlenumber250.html 中根節點安全過濾過度偏向
「單一」及「叉零」的問題。

預設:
    輸入:battlenumber250.html
    輸出:battlenumber251.html

用法:
    python patch_battlenumber251.py

或:
    python patch_battlenumber251.py \
        battlenumber250.html \
        battlenumber251.html
"""

from __future__ import annotations

import argparse
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path


PATCH_MARKER = "STRATEGIC_PIECE_BALANCE_MCTS_PATCH_V1"


def replace_once(
    text: str,
    old: str,
    new: str,
    label: str,
) -> str:
    count = text.count(old)

    if count != 1:
        raise RuntimeError(
            f"{label}:預期找到 1 個替換位置,"
            f"實際找到 {count} 個。"
        )

    return text.replace(old, new, 1)


def replace_region(
    text: str,
    start_marker: str,
    end_marker: str,
    replacement: str,
    label: str,
) -> str:
    start_count = text.count(start_marker)

    if start_count != 1:
        raise RuntimeError(
            f"{label}:起始標記預期找到 1 個,"
            f"實際找到 {start_count} 個。"
        )

    start = text.find(start_marker)

    if start < 0:
        raise RuntimeError(
            f"{label}:找不到起始標記。"
        )

    end = text.find(
        end_marker,
        start + len(start_marker),
    )

    if end < 0:
        raise RuntimeError(
            f"{label}:找不到結束標記。"
        )

    return (
        text[:start]
        + replacement
        + text[end:]
    )


def patch_html(source: str) -> str:
    # ----------------------------------------------------------
    # 1. 加入版本標記
    # ----------------------------------------------------------
    source = replace_once(
        source,
        """    <!-- NARROW_MIXED_CLOSURE_RISK_GUARD_V1 -->
    <script id="mctsWorkerSource" type="text/plain">""",
        """    <!-- NARROW_MIXED_CLOSURE_RISK_GUARD_V1 -->
    <!-- STRATEGIC_PIECE_BALANCE_MCTS_PATCH_V1 -->
    <script id="mctsWorkerSource" type="text/plain">""",
        "加入戰略棋種平衡版本標記",
    )

    # ----------------------------------------------------------
    # 2. Worker:加入棋種發展的軟性先驗
    # ----------------------------------------------------------
    worker_strategic_code = r"""
      // STRATEGIC_PIECE_BALANCE_MCTS_PATCH_V1
      //
      // 只作為 MCTS 展開次序、rollout policy 及 prior 的軟性提示,
      // 不會直接禁止任何棋種。
      //
      // 目的:
      //   1. 安全的雙二、角二應較早進入搜尋;
      //   2. 圍三的唯一開口若能接上己方棋子,應把握機會;
      //   3. 單一和叉零仍可由 MCTS 在有戰術價值時選擇;
      //   4. 狹窄混合區域風險改為先驗扣分,不再硬刪候選。
      function placementConnectionProfile(
        state,
        action
      ) {
        const empty = {
          occupiedContacts: 0,
          ownContacts: 0,
          opponentContacts: 0,
          ownOpenConnections: 0,
          opponentOpenConnections: 0
        };

        if (
          !state ||
          !action ||
          action.kind !== 'place' ||
          !Number.isInteger(action.i) ||
          action.i < 0 ||
          action.i >= SIZE
        ) {
          return empty;
        }

        const player = state.turn;

        const orient =
          action._o !== undefined
            ? action._o
            : orientationId(
                action.t,
                action.r
              );

        const candidateEdges =
          EDGE_MASK[orient];

        const row =
          Math.floor(action.i / N);

        const col =
          action.i % N;

        let occupiedContacts = 0;
        let ownContacts = 0;
        let opponentContacts = 0;
        let ownOpenConnections = 0;
        let opponentOpenConnections = 0;

        for (
          let direction = 0;
          direction < 4;
          direction++
        ) {
          const nextRow =
            row + D4[direction][0];

          const nextCol =
            col + D4[direction][1];

          if (
            nextRow < 0 ||
            nextRow >= N ||
            nextCol < 0 ||
            nextCol >= N
          ) {
            continue;
          }

          const neighbour =
            state.board[
              nextRow * N + nextCol
            ];

          if (!neighbour) {
            continue;
          }

          occupiedContacts++;

          const neighbourPlayer =
            tilePlayer(neighbour);

          if (neighbourPlayer === player) {
            ownContacts++;
          } else {
            opponentContacts++;
          }

          const candidateHasEdge =
            (
              candidateEdges &
              (1 << direction)
            ) !== 0;

          const neighbourHasEdge =
            codeHasEdge(
              neighbour,
              (direction + 2) & 3
            );

          // 兩邊都沒有實邊,才是真正的區域連接。
          if (
            !candidateHasEdge &&
            !neighbourHasEdge
          ) {
            if (neighbourPlayer === player) {
              ownOpenConnections++;
            } else {
              opponentOpenConnections++;
            }
          }
        }

        return {
          occupiedContacts,
          ownContacts,
          opponentContacts,
          ownOpenConnections,
          opponentOpenConnections
        };
      }


      function strategicPieceDevelopmentHeuristic(
        state,
        action
      ) {
        if (
          !state ||
          !action ||
          action.kind !== 'place'
        ) {
          return 0;
        }

        const orient =
          action._o !== undefined
            ? action._o
            : orientationId(
                action.t,
                action.r
              );

        const type =
          ORIENT_TYPE[orient];

        const profile =
          placementConnectionProfile(
            state,
            action
          );

        const totalPlaced =
          state.place0 + state.place1;

        const progress =
          clamp(
            totalPlaced /
              Math.max(1, SIZE - 1),
            0,
            1
          );

        let value = 0;

        if (
          type === TYPE_DOUBLE ||
          type === TYPE_CORNER
        ) {
          // 雙二、角二後期較難找到兩個安全開口,
          // 因此在已通過直接安全檢查後,較早展開。
          value +=
            1.10 +
            progress * 0.55;

          value +=
            profile.ownOpenConnections *
              0.18;

          // 與己方棋相鄰但沒有真正連通,仍只有極小提示,
          // 避免單純貼邊被誤當成有效連接。
          value +=
            Math.max(
              0,
              profile.ownContacts -
                profile.ownOpenConnections
            ) *
              0.025;
        } else if (
          type === TYPE_SURROUND
        ) {
          if (
            profile.ownOpenConnections > 0
          ) {
            // 圍三只有一個開口。
            // 該開口能接入己方棋群時,是應把握的機會。
            value +=
              1.80 +
              progress * 0.45 +
              Math.min(
                0.40,
                (
                  profile.ownOpenConnections -
                  1
                ) *
                  0.20
              );
          } else if (
            profile.opponentOpenConnections >
            0
          ) {
            // 接入對方棋群不一定是壞棋,
            // 但不能取得「接己方棋」的優先獎勵。
            value += 0.04;
          }
        } else if (
          type === TYPE_SINGLE
        ) {
          // 單一不被禁止。
          // 真正接上己方棋時只給極小提示。
          value +=
            profile.ownOpenConnections *
              0.06;
        } else if (
          type === TYPE_CROSS
        ) {
          // 叉零仍可為提子路徑、解除孤獨數等目的落下。
          // 不給負分,也不因數值為零而硬性降低優先級。
          value +=
            profile.ownContacts > 0
              ? 0.04
              : 0;
        }

        return value;
      }


      function rootPlacementSoftRiskAdjustment(
        action
      ) {
        if (!action) {
          return 0;
        }

        const maximum =
          Number(
            action
              ._rootClosureMaximumLoss
          );

        const weighted =
          Number(
            action
              ._rootClosureWeightedLoss
          );

        const safeMaximum =
          Number.isFinite(maximum)
            ? Math.max(0, maximum)
            : 0;

        const safeWeighted =
          Number.isFinite(weighted)
            ? Math.max(0, weighted)
            : 0;

        // 狹窄混合區域仍會影響展開次序,
        // 但不再在 MCTS 開始前直接刪除整個候選。
        return -(
          safeMaximum * 0.65 +
          safeWeighted * 0.30
        );
      }


"""

    source = replace_once(
        source,
        """      function localPlacementHeuristic(state, action) {""",
        worker_strategic_code
        + """      function localPlacementHeuristic(state, action) {""",
        "插入 Worker 棋種發展軟性先驗",
    )

    # 把棋種發展及狹窄區域風險加入局部啟發值。
    source = replace_once(
        source,
        """        return lonelySwing * 14 + contactCount * 0.08;""",
        """        return (
          lonelySwing * 14 +
          contactCount * 0.08 +
          strategicPieceDevelopmentHeuristic(
            state,
            action
          ) +
          rootPlacementSoftRiskAdjustment(
            action
          )
        );""",
        "更新 Worker 局部落子啟發值",
    )

    # ----------------------------------------------------------
    # 3. Worker:把封閉風險附加在根行動上,供軟性 prior 使用
    # ----------------------------------------------------------
    source = replace_once(
        source,
        """          action._rootLonelyCreated =
            effect.rootLonelyCreated;

          action
            ._rootNextReplyLonelyValue =""",
        """          action._rootLonelyCreated =
            effect.rootLonelyCreated;

          action._rootClosureMaximumLoss =
            Number(
              effect
                .latentClosureMaximumLoss
            ) || 0;

          action._rootClosureWeightedLoss =
            Number(
              effect
                .latentClosureWeightedLoss
            ) || 0;

          action
            ._rootNextReplyLonelyValue =""",
        "為 alpha-beta 根行動附加軟性封閉風險",
    )

    source = replace_once(
        source,
        """              action._rootLonelyCreated =
                effect.rootLonelyCreated;

              entries.push({""",
        """              action._rootLonelyCreated =
                effect.rootLonelyCreated;

              action._rootClosureMaximumLoss =
                Number(
                  effect
                    .latentClosureMaximumLoss
                ) || 0;

              action._rootClosureWeightedLoss =
                Number(
                  effect
                    .latentClosureWeightedLoss
                ) || 0;

              entries.push({""",
        "為 MCTS 根行動附加軟性封閉風險",
    )

    # ----------------------------------------------------------
    # 4. Worker:多層搜尋只刪除已證明 forced 的候選
    #
    # 舊版一旦找到少量 provenSafe,就只回傳那幾手,
    # 未被搜尋的候選也全部消失,令 MCTS 無法比較棋種發展。
    # ----------------------------------------------------------
    worker_choose_function = r"""      function chooseMultiPlyRootEntries(
        state,
        entries,
        rootPlayer,
        context
      ) {
        if (
          !entries.length ||
          !context
        ) {
          return entries;
        }

        const ordered =
          entries.map(
            entry => ({
              entry,
              order:
                rootEntryTacticalOrder(
                  state,
                  entry
                )
            })
          );

        ordered.sort(
          (first, second) =>
            second.order -
            first.order
        );

        const candidates =
          ordered
            .slice(
              0,
              Math.min(
                context.candidateLimit,
                ordered.length
              )
            )
            .map(
              item => item.entry
            );

        for (
          const entry of candidates
        ) {
          const result =
            forcedFutureScoringThreat(
              state,
              entry.action,
              rootPlayer,
              context
            );

          entry.effect
            .futureThreatStatus =
              result.status;

          entry.effect
            .forcedFutureScoreLoss =
              result.loss;

          entry.effect
            .forcedFutureDistance =
              result.distance;

          entry.effect
            .forcedFutureLine =
              result.line;
        }

        // 關鍵修正:
        //
        //   safe:
        //     已證明安全,保留。
        //
        //   unknown:
        //     尚未搜完,不可視為失敗,保留交給 MCTS。
        //
        //   未進入 tactical candidate limit:
        //     同樣沒有失敗證明,保留交給 MCTS。
        //
        //   forced:
        //     只有這一類候選才會在存在其他選擇時被排除。
        const nonForced =
          entries.filter(
            entry =>
              entry.effect
                .futureThreatStatus !==
              'forced'
          );

        if (nonForced.length) {
          return nonForced;
        }

        // 若所有候選都已被明確證明會失分,
        // 才使用原本的最小損失、最晚失分策略。
        let remaining =
          entries.slice();

        let minimumLoss =
          Infinity;

        for (
          const entry of remaining
        ) {
          minimumLoss = Math.min(
            minimumLoss,
            Number(
              entry.effect
                .forcedFutureScoreLoss
            ) || 0
          );
        }

        remaining =
          remaining.filter(
            entry =>
              Math.abs(
                (
                  Number(
                    entry.effect
                      .forcedFutureScoreLoss
                  ) || 0
                ) -
                minimumLoss
              ) <=
              TACTICAL_SCORE_EPSILON
          );

        let maximumDistance = 0;

        for (
          const entry of remaining
        ) {
          maximumDistance = Math.max(
            maximumDistance,
            Number(
              entry.effect
                .forcedFutureDistance
            ) || 0
          );
        }

        remaining =
          remaining.filter(
            entry =>
              (
                Number(
                  entry.effect
                    .forcedFutureDistance
                ) || 0
              ) >=
              maximumDistance
          );

        let bestImmediate =
          -Infinity;

        for (
          const entry of remaining
        ) {
          bestImmediate = Math.max(
            bestImmediate,
            entry.effect.immediateDelta
          );
        }

        return remaining.filter(
          entry =>
            entry.effect.immediateDelta >=
            bestImmediate -
              TACTICAL_SCORE_EPSILON
        );
      }


"""

    source = replace_region(
        source,
        """      function chooseMultiPlyRootEntries(
""",
        """      // ROOT_LONELY_SAFETY_POLICY_V2""",
        worker_choose_function,
        "更新 Worker 多層根候選保留策略",
    )

    # ----------------------------------------------------------
    # 5. Worker:狹窄區域風險不再硬性只保留最低值
    # ----------------------------------------------------------
    source = replace_once(
        source,
        """          const closurePreferredEntries =
            preferLowerNarrowClosureRisk(
              onePlySafeEntries
            );

          return chooseMultiPlyRootEntries(
            state,
            closurePreferredEntries,
            rootPlayer,
            tacticalContext
          );""",
        """          // 狹窄區域風險已透過
          // rootPlacementSoftRiskAdjustment 影響 MCTS prior。
          //
          // 不再硬性只保留風險絕對最低的一組,避免單一和叉零
          // 因較容易取得零風險而壟斷根候選。
          return chooseMultiPlyRootEntries(
            state,
            onePlySafeEntries,
            rootPlayer,
            tacticalContext
          );""",
        "取消 Worker 狹窄區域風險硬篩選",
    )

    # ----------------------------------------------------------
    # 6. 主執行緒:加入相同的棋種發展啟發值
    # ----------------------------------------------------------
    main_strategic_code = r"""
        // STRATEGIC_PIECE_BALANCE_MCTS_PATCH_V1_MAIN
        //
        // 主執行緒只把這個值用於 fallback 的戰術搜尋次序。
        // 它不是合法性規則,也不會禁止單一或叉零。
        function strategicPieceDevelopmentHeuristicMain(
          gameState,
          action
        ) {
          if (
            !gameState ||
            !action ||
            action.kind !== 'place' ||
            !Number.isInteger(action.i) ||
            action.i < 0 ||
            action.i >= SIZE
          ) {
            return 0;
          }

          const player =
            gameState.turn;

          const tile = {
            player,
            type: action.t,
            rot: action.r
          };

          const row =
            Math.floor(action.i / N);

          const col =
            action.i % N;

          let ownContacts = 0;
          let ownOpenConnections = 0;
          let opponentOpenConnections = 0;

          for (
            let direction = 0;
            direction <
              DIRECTIONS.length;
            direction++
          ) {
            const nextRow =
              row +
              DIRECTIONS[direction][0];

            const nextCol =
              col +
              DIRECTIONS[direction][1];

            if (
              nextRow < 0 ||
              nextRow >= N ||
              nextCol < 0 ||
              nextCol >= N
            ) {
              continue;
            }

            const neighbour =
              gameState.board[
                nextRow * N + nextCol
              ];

            if (!neighbour) {
              continue;
            }

            if (
              neighbour.player === player
            ) {
              ownContacts++;
            }

            const connected =
              !tileHasEdge(
                tile,
                direction
              ) &&
              !tileHasEdge(
                neighbour,
                (direction + 2) & 3
              );

            if (!connected) {
              continue;
            }

            if (
              neighbour.player === player
            ) {
              ownOpenConnections++;
            } else {
              opponentOpenConnections++;
            }
          }

          const totalPlaced =
            gameState.placementCount[0] +
            gameState.placementCount[1];

          const progress =
            Math.max(
              0,
              Math.min(
                1,
                totalPlaced /
                  Math.max(1, SIZE - 1)
              )
            );

          if (
            action.t === TYPE_DOUBLE ||
            action.t === TYPE_CORNER
          ) {
            return (
              1.10 +
              progress * 0.55 +
              ownOpenConnections * 0.18 +
              Math.max(
                0,
                ownContacts -
                  ownOpenConnections
              ) *
                0.025
            );
          }

          if (
            action.t === TYPE_SURROUND
          ) {
            if (ownOpenConnections > 0) {
              return (
                1.80 +
                progress * 0.45 +
                Math.min(
                  0.40,
                  (
                    ownOpenConnections -
                    1
                  ) *
                    0.20
                )
              );
            }

            return opponentOpenConnections > 0
              ? 0.04
              : 0;
          }

          if (
            action.t === TYPE_SINGLE
          ) {
            return (
              ownOpenConnections * 0.06
            );
          }

          if (
            action.t === TYPE_CROSS
          ) {
            return ownContacts > 0
              ? 0.04
              : 0;
          }

          return 0;
        }


"""

    source = replace_once(
        source,
        """        function tacticalActionOrderMain(
          gameState,
          action
        ) {""",
        main_strategic_code
        + """        function tacticalActionOrderMain(
          gameState,
          action
        ) {""",
        "插入主執行緒棋種發展軟性先驗",
    )

    source = replace_once(
        source,
        """          return (
            neighbours * 20 +
            TRIANGLES[action.t] *
              0.15 +
            Math.random() * 0.01
          );""",
        """          return (
            neighbours * 20 +
            strategicPieceDevelopmentHeuristicMain(
              gameState,
              action
            ) +
            TRIANGLES[action.t] *
              0.03 +
            Math.random() * 0.01
          );""",
        "更新主執行緒戰術行動排序",
    )

    # ----------------------------------------------------------
    # 7. 主執行緒:多層搜尋保留 safe、unknown 及未搜尋候選
    # ----------------------------------------------------------
    main_choose_function = r"""        function chooseMultiPlyRootEntriesMain(
          gameState,
          entries,
          context
        ) {
          if (
            !entries.length ||
            !context
          ) {
            return entries;
          }

          const ordered =
            entries.map(
              entry => ({
                entry,
                order:
                  entry.immediateDelta *
                    16 +
                  tacticalActionOrderMain(
                    gameState,
                    entry.action
                  ) -
                  (
                    Number(
                      entry
                        .nextReplyScoringReplies
                    ) || 0
                  ) *
                    0.15 +
                  Math.random() * 0.2
              })
            );

          ordered.sort(
            (first, second) =>
              second.order -
              first.order
          );

          const candidates =
            ordered
              .slice(
                0,
                Math.min(
                  context.candidateLimit,
                  ordered.length
                )
              )
              .map(
                item => item.entry
              );

          for (
            const entry of candidates
          ) {
            const result =
              forcedFutureScoringThreatMain(
                gameState,
                entry.action,
                {
                  context
                }
              );

            entry.futureThreatStatus =
              result.status;

            entry.forcedFutureScoreLoss =
              result.loss;

            entry.forcedFutureDistance =
              result.distance;

            entry.forcedFutureLine =
              result.line;
          }

          // 只排除已經被明確證明為 forced 的行動。
          //
          // unknown 及未進入 tactical candidate limit 的行動
          // 必須保留,不能因搜尋時間不足而被當成壞棋。
          const nonForced =
            entries.filter(
              entry =>
                entry.futureThreatStatus !==
                'forced'
            );

          if (nonForced.length) {
            return nonForced;
          }

          let remaining =
            entries.slice();

          let minimumLoss =
            Infinity;

          for (
            const entry of remaining
          ) {
            minimumLoss = Math.min(
              minimumLoss,
              Number(
                entry
                  .forcedFutureScoreLoss
              ) || 0
            );
          }

          remaining =
            remaining.filter(
              entry =>
                Math.abs(
                  (
                    Number(
                      entry
                        .forcedFutureScoreLoss
                    ) || 0
                  ) -
                  minimumLoss
                ) <=
                TACTICAL_MAIN_EPSILON
            );

          let maximumDistance = 0;

          for (
            const entry of remaining
          ) {
            maximumDistance = Math.max(
              maximumDistance,
              Number(
                entry
                  .forcedFutureDistance
              ) || 0
            );
          }

          remaining =
            remaining.filter(
              entry =>
                (
                  Number(
                    entry
                      .forcedFutureDistance
                  ) || 0
                ) >=
                maximumDistance
            );

          let bestImmediate =
            -Infinity;

          for (
            const entry of remaining
          ) {
            bestImmediate = Math.max(
              bestImmediate,
              entry.immediateDelta
            );
          }

          return remaining.filter(
            entry =>
              entry.immediateDelta >=
              bestImmediate -
                TACTICAL_MAIN_EPSILON
          );
        }


"""

    source = replace_region(
        source,
        """        function chooseMultiPlyRootEntriesMain(
""",
        """        // 主執行緒版根節點安全過濾。""",
        main_choose_function,
        "更新主執行緒多層根候選保留策略",
    )

    # ----------------------------------------------------------
    # 8. 主執行緒 fallback:取消狹窄區域風險硬篩選
    # ----------------------------------------------------------
    source = replace_once(
        source,
        """            const closurePreferred =
              preferLowerNarrowClosureRiskMain(
                onePlySafe
              );

            const selected =
              chooseMultiPlyRootEntriesMain(
                gameState,
                closurePreferred,
                tacticalContext
              );""",
        """            // fallback 同樣不再因狹窄區域風險,
            // 硬性刪除雙二、角二及接己方棋的圍三。
            const selected =
              chooseMultiPlyRootEntriesMain(
                gameState,
                onePlySafe,
                tacticalContext
              );""",
        "取消主執行緒狹窄區域風險硬篩選",
    )

    # ----------------------------------------------------------
    # 9. 最終行動檢查:
    #
    # 只處理可確定的直接危險。
    # 不再因狹窄區域或多層靜態判定,以 fallback 覆蓋 MCTS。
    # ----------------------------------------------------------
    final_safety_block = r"""          // STRATEGIC_PIECE_BALANCE_MCTS_PATCH_V1_FINAL_CHECK
          //
          // MCTS 完成後只攔截三種可確定的直接危險:
          //
          //   1. 新棋立即成為己方孤獨數;
          //   2. 對方下一手能把新棋封成孤獨數;
          //   3. 對方下一手能令目前實際區域分差下降。
          //
          // 狹窄區域及多層未來棋形已交回 MCTS 比較,
          // 不再用只看當前分數的 fallback 覆蓋搜尋結果。
          if (
            action &&
            action.kind === 'place'
          ) {
            const createsImmediateLonely =
              placementCreatesOwnLonelyNumberMain(
                state,
                action
              );

            const replyThreat =
              nextReplyLonelyThreatMain(
                state,
                action
              );

            const replyScoreThreat =
              nextReplyScoringThreatMain(
                state,
                action,
                false
              );

            const hasDirectDanger =
              createsImmediateLonely ||
              replyThreat.value > 0 ||
              replyScoreThreat.loss >
                TACTICAL_MAIN_EPSILON;

            if (hasDirectDanger) {
              const saferAction =
                fallbackAction(action);

              if (
                saferAction &&
                actionIsLegal(saferAction)
              ) {
                const saferImmediateLonely =
                  placementCreatesOwnLonelyNumberMain(
                    state,
                    saferAction
                  );

                const saferReplyThreat =
                  nextReplyLonelyThreatMain(
                    state,
                    saferAction
                  );

                const saferReplyScoreThreat =
                  nextReplyScoringThreatMain(
                    state,
                    saferAction,
                    false
                  );

                // 依序比較:
                //   1. 是否立即製造己方孤獨數;
                //   2. 對方下一手最大得分;
                //   3. 新棋被封成孤獨數的分值;
                //   4. 對方得分回覆數;
                //   5. 對方封新棋回覆數。
                //
                // 只有 fallback 確實更安全時才替換,
                // 同級時保留 MCTS 原本選出的行動。
                const originalDanger = [
                  createsImmediateLonely
                    ? 1
                    : 0,
                  Number(
                    replyScoreThreat.loss
                  ) || 0,
                  Number(
                    replyThreat.value
                  ) || 0,
                  Number(
                    replyScoreThreat.replies
                  ) || 0,
                  Number(
                    replyThreat.replies
                  ) || 0
                ];

                const candidateDanger = [
                  saferImmediateLonely
                    ? 1
                    : 0,
                  Number(
                    saferReplyScoreThreat.loss
                  ) || 0,
                  Number(
                    saferReplyThreat.value
                  ) || 0,
                  Number(
                    saferReplyScoreThreat.replies
                  ) || 0,
                  Number(
                    saferReplyThreat.replies
                  ) || 0
                ];

                let strictlySafer = false;

                for (
                  let index = 0;
                  index <
                    originalDanger.length;
                  index++
                ) {
                  if (
                    candidateDanger[index] <
                    originalDanger[index] -
                      TACTICAL_MAIN_EPSILON
                  ) {
                    strictlySafer = true;
                    break;
                  }

                  if (
                    candidateDanger[index] >
                    originalDanger[index] +
                      TACTICAL_MAIN_EPSILON
                  ) {
                    break;
                  }
                }

                if (strictlySafer) {
                  action = saferAction;
                }
              }
            }
          }

"""

    source = replace_region(
        source,
        """          // ONE_PLY_LONELY_REPLY_GUARD_V1_FINAL_CHECK""",
        """          if (action.kind === 'place') {
            commitPlacement(action, 2100);""",
        final_safety_block,
        "更新 AI 最終行動安全檢查",
    )

    # ----------------------------------------------------------
    # 10. 靜態驗證
    # ----------------------------------------------------------
    required_fragments = [
        "<!-- STRATEGIC_PIECE_BALANCE_MCTS_PATCH_V1 -->",
        "function placementConnectionProfile(",
        "function strategicPieceDevelopmentHeuristic(",
        "function rootPlacementSoftRiskAdjustment(",
        "strategicPieceDevelopmentHeuristic(\n            state,\n            action",
        "action._rootClosureMaximumLoss =",
        "const nonForced =\n          entries.filter(",
        "function strategicPieceDevelopmentHeuristicMain(",
        "const nonForced =\n            entries.filter(",
        "STRATEGIC_PIECE_BALANCE_MCTS_PATCH_V1_FINAL_CHECK",
        "const originalDanger = [",
        "const candidateDanger = [",
        "if (strictlySafer) {",
    ]

    missing = [
        fragment
        for fragment in required_fragments
        if fragment not in source
    ]

    if missing:
        raise RuntimeError(
            "補丁完成後驗證失敗,缺少以下內容:\n  - "
            + "\n  - ".join(missing)
        )

    unique_functions = [
        "function placementConnectionProfile(",
        "function strategicPieceDevelopmentHeuristic(",
        "function rootPlacementSoftRiskAdjustment(",
        "function strategicPieceDevelopmentHeuristicMain(",
        "function chooseMultiPlyRootEntries(",
        "function chooseMultiPlyRootEntriesMain(",
    ]

    duplicates = [
        fragment
        for fragment in unique_functions
        if source.count(fragment) != 1
    ]

    if duplicates:
        raise RuntimeError(
            "補丁完成後發現函式重複或缺失:\n  - "
            + "\n  - ".join(duplicates)
        )

    forbidden_fragments = [
        "const closurePreferredEntries =",
        "const closurePreferred =",
        "const hasNarrowClosureDanger =",
        "const futureScoreThreat =\n"
        "              forcedFutureScoringThreatMain(",
    ]

    remaining_old_logic = [
        fragment
        for fragment in forbidden_fragments
        if fragment in source
    ]

    if remaining_old_logic:
        raise RuntimeError(
            "補丁完成後仍殘留會覆蓋 MCTS 的舊邏輯:\n  - "
            + "\n  - ".join(remaining_old_logic)
        )

    return source


def optional_node_syntax_check(
    html: str,
) -> str:
    """
    如果系統有 Node.js,就檢查 Worker 與主程式的 JavaScript 語法。
    沒有 Node.js 時不視為錯誤。
    """
    node = shutil.which("node")

    if not node:
        return "未找到 Node.js,已略過 JavaScript 語法檢查。"

    worker_match = re.search(
        r'<script\s+id="mctsWorkerSource"'
        r'\s+type="text/plain">\s*(.*?)'
        r'</script>',
        html,
        flags=re.DOTALL,
    )

    if not worker_match:
        raise RuntimeError(
            "找不到 Worker JavaScript,無法執行語法檢查。"
        )

    script_matches = re.findall(
        r"<script(?:\s[^>]*)?>(.*?)</script>",
        html,
        flags=re.DOTALL,
    )

    if not script_matches:
        raise RuntimeError(
            "找不到主執行緒 JavaScript。"
        )

    worker_source = worker_match.group(1)
    main_source = script_matches[-1]

    with tempfile.TemporaryDirectory(
        prefix="battle_number_js_check_"
    ) as temp_directory:
        temp_path = Path(temp_directory)

        worker_path = (
            temp_path / "worker.js"
        )

        main_path = (
            temp_path / "main.js"
        )

        worker_path.write_text(
            worker_source,
            encoding="utf-8",
        )

        main_path.write_text(
            main_source,
            encoding="utf-8",
        )

        for label, path in [
            ("Worker", worker_path),
            ("主執行緒", main_path),
        ]:
            result = subprocess.run(
                [node, "--check", str(path)],
                capture_output=True,
                text=True,
                encoding="utf-8",
                errors="replace",
            )

            if result.returncode != 0:
                details = (
                    result.stderr.strip()
                    or result.stdout.strip()
                    or "未知語法錯誤"
                )

                raise RuntimeError(
                    f"{label} JavaScript 語法檢查失敗:\n"
                    f"{details}"
                )

    return "Worker 與主執行緒 JavaScript 語法檢查通過。"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "修正 Battle Number 250 的棋種偏向,"
            "恢復 MCTS 對雙二、角二及接己方圍三的比較能力。"
        )
    )

    parser.add_argument(
        "input",
        nargs="?",
        default="battlenumber250.html",
        help="輸入 HTML,預設為 battlenumber250.html",
    )

    parser.add_argument(
        "output",
        nargs="?",
        default="battlenumber251.html",
        help="輸出 HTML,預設為 battlenumber251.html",
    )

    parser.add_argument(
        "--skip-node-check",
        action="store_true",
        help="不執行可選的 Node.js 語法檢查",
    )

    return parser.parse_args()


def main() -> int:
    args = parse_args()

    input_path = Path(args.input)
    output_path = Path(args.output)

    if not input_path.is_file():
        print(
            f"錯誤:找不到輸入檔案:{input_path}",
            file=sys.stderr,
        )
        return 1

    try:
        source = input_path.read_text(
            encoding="utf-8"
        )
    except Exception as error:
        print(
            f"錯誤:無法讀取輸入檔案:{error}",
            file=sys.stderr,
        )
        return 1

    if PATCH_MARKER in source:
        try:
            output_path.parent.mkdir(
                parents=True,
                exist_ok=True,
            )

            output_path.write_text(
                source,
                encoding="utf-8",
            )
        except Exception as error:
            print(
                f"錯誤:無法寫入輸出檔案:{error}",
                file=sys.stderr,
            )
            return 1

        print(
            f"輸入檔案已包含 {PATCH_MARKER};"
            f"未重複修改,已輸出至:{output_path}"
        )
        return 0

    try:
        patched = patch_html(source)

        if args.skip_node_check:
            syntax_message = (
                "已依參數略過 JavaScript 語法檢查。"
            )
        else:
            syntax_message = (
                optional_node_syntax_check(
                    patched
                )
            )
    except Exception as error:
        print(
            f"補丁失敗:{error}",
            file=sys.stderr,
        )
        return 1

    try:
        output_path.parent.mkdir(
            parents=True,
            exist_ok=True,
        )

        output_path.write_text(
            patched,
            encoding="utf-8",
        )
    except Exception as error:
        print(
            f"錯誤:無法寫入輸出檔案:{error}",
            file=sys.stderr,
        )
        return 1

    print("補丁完成。")
    print(f"輸入:{input_path}")
    print(f"輸出:{output_path}")
    print(syntax_message)
    print()
    print("主要修正:")
    print(
        "  1. 狹窄混合區域風險由硬篩選改為軟性 MCTS prior"
    )
    print(
        "  2. 未完成多層搜尋的候選不再被當成失敗"
    )
    print(
        "  3. 只排除已明確證明為 forced 的多層失分棋"
    )
    print(
        "  4. 安全的雙二、角二會較早進入搜尋"
    )
    print(
        "  5. 圍三的唯一開口接上己方棋時獲得較高先驗"
    )
    print(
        "  6. 單一和叉零沒有被禁止或固定扣分"
    )
    print(
        "  7. 最終檢查不再因長期風險用 fallback 覆蓋 MCTS"
    )
    print(
        "  8. 立即己方孤獨棋及對方下一手封口防護仍然保留"
    )

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```

執行:

```bash
python patch_battlenumber251.py battlenumber250.html battlenumber251.html
```

如果電腦沒有安裝 Node.js,腳本仍可正常產生輸出,只會顯示略過 JavaScript 語法檢查。也可以主動略過:

```bash
python patch_battlenumber251.py \
  battlenumber250.html \
  battlenumber251.html \
  --skip-node-check
```

這個版本的決策關係是:

```text
立即形成己方孤獨棋
或對方下一手可直接封口/得分
        ↓
存在安全棋時硬性排除

已明確證明的多層強制失分
        ↓
存在其他候選時排除

雙二、角二的發展價值
圍三接上己方棋的機會
狹窄混合區域的長期風險
單一封住對方棋的戰術
叉零解除孤獨棋或建立提子路徑
        ↓
作為 MCTS 的軟性先驗及完整模擬內容
```

因此「雙二、角二、接己方棋的圍三」會較早獲得搜尋資源,但「單一、叉零」仍然可以在封閉對方孤獨棋、保護己方棋或建立提子路徑時由 MCTS 選出。
附件
battlenumber251.html.7z
(63.46 KiB) 尚未被下载
https://ejsoon.vip/
金梭越空:極速暢遊天地
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

正浏览此版面之用户: ejsoon 和 8 访客