「益智遊戲」和「抽象弈棋」
头像
ejsoon
一枝独秀一枝独秀
帖子: 6864
注册时间: 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
一枝独秀一枝独秀
帖子: 6864
注册时间: 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
一枝独秀一枝独秀
帖子: 6864
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

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

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

帖子 ejsoon »

battlenumber250.html

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

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

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

帖子 ejsoon »

改進:
當一方已經有多個棋子構成三角形數量很多的棋群,將能連上AI控制方的弱子時,AI能封閉或擋住對方棋子的蔓延和連接。
當輪到AI落子時,AI不會落下一個棋子是會被對方下一手用他更多三角形數量的棋群接入的。
https://ejsoon.vip/
金梭越空:極速暢遊天地
头像
ejsoon
一枝独秀一枝独秀
帖子: 6864
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

o b2n c2m d6l d5l d2i b3s f2q d7s c4h d1r d3l b5q a1k c5j b4q g2o e5h f5u e6p g3s f1h e1m g1o a2k e7v a3k a5v a4q b1v a6n g6l f7i a7s b6j e3m f4l c7m g5m f6v c6v c3v c1v g7u e2v x

AI控制的玩家二直接判負!
https://ejsoon.vip/
金梭越空:極速暢遊天地
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

正浏览此版面之用户: 没有注册用户 和 23 访客