分享和讨论Javascript相关的话题
回复
头像
ejsoon
一枝独秀
帖子: 6281
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

思考完畢要顯示總搜尋次數
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6281
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

跟「困難」等級打了個平手,非常難得!
Screenshot from 2026-07-09 14-14-56.png
Screenshot from 2026-07-09 14-14-56.png (197.53 KiB) 查看 87 次

代码: 全选

[(0,0),(1,0)] [(0,-2),(0,-1)] [(0,1),(0,2)] [(1,-2),(1,-1)] [(-1,-3),(0,-3)] [(-1,3),(0,3)] [(-1,0),(-1,1)] [(-2,0),(-2,1)] [(1,1),(1,2)] [(-2,2),(-1,2)] [(1,-3),(2,-3)] [(2,-1),(2,0)] [(3,-1),(3,0)] [(1,3),(2,3)] [(2,-2),(3,-2)] [(-3,1),(-3,2)] [(-2,3),(-2,4)] [(-3,5),(-2,5)] [(0,4),(0,5)] [(3,1),(4,1)] [(2,4),(2,5)] [(4,-1),(4,0)] [(-1,4),(-1,5)] [(-2,6),(-1,6)] [(1,4),(1,5)] [(2,-5),(2,-4)] [(2,-6),(3,-6)] [(3,-4),(3,-3)] [(4,-4),(4,-3)] [(2,1),(2,2)] [(4,2),(4,3)] [(-3,3),(-3,4)] [(3,2),(3,3)] [(1,-5),(1,-4)] [(3,-5),(4,-5)] [(-1,-5),(-1,-4)]
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6281
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

當棋手從AI切換為人類時,應改變棋子狀態(不再是disable),並且立即停止思考。

處於旋轉狀態則立即切換成人類,並且不能再切換回AI。
上次由 ejsoon 在 2026年 7月 9日 14:32,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6281
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

trirhombus287.html
附件
trirhombus287.html.7z
(26.79 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6281
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

當點擊「下載」時,程式會直接下載當前的svg棋盤。現在改為彈出一個全屏窗口,其具體要求描述如下:

代码: 全选

點擊下載功能的改進
當點擊「下載」時,程式會直接下載當前的svg棋盤。現在改為彈出一個全屏窗口,其具體要求描述如下:

一,窗口右上角有叉形關閉按鈕,其內部有svg顯示區,下方選項區,以及下載按鈕。所有的元素都要展示出來。

二,svg顯示區將等比例顯示所要下載的svg圖像文件,其內容為svg棋盤的clone。

三,選項區有「是否顯示網格,是否顯示數字,是否下載動畫」,做成現代美觀的切換按鈕,其左方文字為「網格、數字、動畫」。把三者排在一行,平分間隔。

四,網格默認是勾選的,當取消時,將把clone中的網格去掉。

五,數字默認不勾選,當勾選時,將在ejdrop內的最後加上text標籤:

<text x="棋子横坐標" y="棋子縱坐標" stroke="none" fill="black" font-size="24" font-family="ZT Nature">棋子次序</text>

按clone中每個use棋子的次序添加對應的數字,在代碼中寫明數字的横坐標和縱坐標相對於棋子中心坐標的偏移量,並分為一位數和兩位數,使我在測試中可以修改。

之後在clone的.etboard中添加:

```
<style>
@font-face {
    font-family: 'ZT Nature';
    src: url(...) format('woff2');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}
</style>
```

我會自行補充url的內容。

六,當動畫勾選時,數字將不能勾選,二者相斥。

動畫也會在clone的.etboard中添加同樣的style。

動畫的步驟為:

1.為.etdrop增加屬性opacity=0,同時在其內部最前面添加動畫「<animate id="tr0" begin="0;trend.end" attributeName="opacity" values="0;0;1" fill="freeze" dur="2s"/>」。

2.每個use都有transform="translate(x,y)",取到它們的屬性坐標(x,y),在所有的<use>內部添加動畫「<animateTransform attributeName="transform" attributeType="XML" type="translate" from="屬性坐標" to="視圖外不遠的坐標,我方在下面,對方在上面" dur="1s" fill="freeze" begin="tr0.begin"/>」。

3.按次序為每一個use內部添加動畫「<animateTransform id="tr(n)" attributeName="transform" attributeType="XML" type="translate" from="視圖外坐標" to="屬性坐標" dur="1s" fill="freeze" begin="tr(n-1).end"/>」。例如第一個use內加的是「<animateTransform id="tr1" attributeName="transform" attributeType="XML" type="translate" from="視圖外坐標" to="屬性坐標" dur="1s" fill="freeze" begin="tr0.end"/>」。

4.當最後一個use棋子添加動畫(假設id="tr36")完畢後,挑出對於我方得分無用的棋子(算法是去掉一個棋子,如果分數不變則無用),這些棋子將以朝向當前視圖中心的反方向,添加往視圖外移動再移回來的動畫「<animateTransform id="trme" attributeName="transform" attributeType="XML" type="translate" values="屬性坐標;視圖外坐標;視圖外坐標;視圖外坐標;屬性坐標" dur="4s" fill="freeze" begin="tr36.end+1s"/>」。

5.同時在.etboard內部的最後添加一個text標籤「<text id="trmetxt" x="視圖外距右邊界12" y="視圖內距下邊界12" stroke="none" fill="black" font-size="60" font-family="ZT Nature">我方得分</text>」,

動畫「<animateTransform id="trme" attributeName="transform" attributeType="XML" type="translate" values="屬性坐標;視圖外坐標;視圖外坐標;視圖外坐標;屬性坐標" dur="4s" fill="freeze" begin="tr36.end+1s"/>」
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6281
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

當點擊「下載」時,程式會直接下載當前的svg棋盤。現在改為彈出一個窗口,其具體要求描述如下:

代码: 全选

點擊下載功能的改進
當點擊「下載」時,程式會直接下載當前的svg棋盤。現在改為彈出一個窗口,其具體要求描述如下:

一,窗口右上角有叉形關閉按鈕,其內部有svg顯示區,下方選項區,以及下載按鈕。所有的元素都要展示出來。

二,svg顯示區將等比例顯示所要下載的svg圖像文件,其內容為svg棋盤的clone。

三,選項區有「是否顯示網格,是否顯示數字,是否下載動畫」,做成現代美觀的切換按鈕,其左方文字為「網格、數字、動畫」。把三者排在一行,平分間隔。

四,網格默認是勾選的,當取消時,將把clone中的網格去掉。

五,數字默認不勾選,當勾選時,將在ejdrop內的最後加上text標籤:

<text x="棋子横坐標" y="棋子縱坐標" stroke="none" fill="black" font-size="24" font-family="ZT Nature">棋子次序</text>

按clone中每個use棋子的次序添加對應的數字,在代碼中寫明數字的横坐標和縱坐標相對於棋子中心坐標的偏移量,並分為一位數和兩位數,使我在測試中可以修改。

之後在clone的.etboard中添加:

```
<style>
@font-face {
    font-family: 'ZT Nature';
    src: url(...) format('woff2');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}
</style>
```

我會自行補充url的內容。

六,當動畫勾選時,數字將不能勾選,二者相斥。

動畫也會在clone的.etboard中添加同樣的style。

動畫的步驟為:

1.為.etdrop增加屬性opacity=0,同時在其內部最前面添加動畫「<animate id="tr0" begin="0;trend.end" attributeName="opacity" values="0;0;1" fill="freeze" dur="2s"/>」。

2.每個use都有transform="translate(x,y)",取到它們的屬性坐標(x,y),在所有的<use>內部添加動畫「<animateTransform attributeName="transform" attributeType="XML" type="translate" from="屬性坐標" to="視圖外不遠的坐標,我方在下面,對方在上面" dur="1s" fill="freeze" begin="tr0.begin"/>」。

3.按次序為每一個use內部添加動畫「<animateTransform id="tr(n)" attributeName="transform" attributeType="XML" type="translate" from="視圖外坐標" to="屬性坐標" dur="1s" fill="freeze" begin="tr(n-1).end"/>」。例如第一個use內加的是「<animateTransform id="tr1" attributeName="transform" attributeType="XML" type="translate" from="視圖外坐標" to="屬性坐標" dur="1s" fill="freeze" begin="tr0.end"/>」。

4.當最後一個use棋子添加動畫(假設id="tr36")完畢後,挑出對於我方得分無用的棋子(算法是去掉一個棋子,如果分數不變則無用),這些棋子將以朝向當前視圖中心的反方向,添加往視圖外移動再移回來的動畫「<animateTransform id="trme" attributeName="transform" attributeType="XML" type="translate" values="屬性坐標;視圖外坐標;視圖外坐標;視圖外坐標;屬性坐標" dur="4s" fill="freeze" begin="tr36.end+1s"/>」。

5.同時在.etboard內部的最後添加一個text標籤「<text id="trmetxt" x="視圖外距右邊界12" y="視圖內距下邊界12+自身高" stroke="none" fill="black" font-size="60" font-family="ZT Nature">我方得分</text>」,在其下方添加動畫將其移到視圖內,使之x="視圖內距右邊界12+自身寬":「<animateTransform href="trmetxt" attributeName="transform" attributeType="XML" type="translate" values="屬性坐標;視圖內坐標;視圖內坐標;視圖內坐標;屬性坐標" dur="4s" fill="freeze" begin="trme.begin"/>」。

注意「我方得分」是指一個整數,不要含有數字之外的任何字符。下方的「對方得分」也一樣。

6.之後挑出對於對方得分無用的棋子,這些棋子將添加添加往視圖外移動再移回來的動畫,朝向為當前視圖中心的反方向:「<animateTransform id="trhe" attributeName="transform" attributeType="XML" type="translate" values="屬性坐標;視圖外坐標;視圖外坐標;視圖外坐標;屬性坐標" dur="4s" fill="freeze" begin="trme.end+1s"/>」。

7.同時在.etboard內部的最後添加一個text標籤「<text id="trhetxt" x="視圖外距右邊界12" y="視圖內距下邊界12+自身高" stroke="none" fill="black" font-size="60" font-family="ZT Nature">對方得分</text>」,在其下方添加動畫將其移到視圖內,使之x="視圖內距右邊界12+自身寬":「<animateTransform href="trhetxt" attributeName="transform" attributeType="XML" type="translate" values="屬性坐標;視圖內坐標;視圖內坐標;視圖內坐標;屬性坐標" dur="4s" fill="freeze" begin="trhe.begin"/>」。

8.最後在.etdrop內部最前面添加動畫「<animate id="trend" begin="trhe.end+1s" attributeName="opacity" values="1;1;0" fill="freeze" dur="2s"/>」。

如果取消勾選動畫,只要刪除clone內的所有animate和animateTransform標籤,以及添加的text和style。

需要明確給出所要修改的地方和所要替換的代碼。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6281
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

gemini 3.1 pro 有點聰明

代码: 全选

這是一個非常酷的升級!將原本單純的下載功能改為具有預覽、網格切換、標示棋子順序,甚至帶有完整播放與結算動畫的高級匯出器。

為了達到您的要求,我們需要修改原程式碼的三個部分:**CSS 樣式**、**HTML 結構**、以及 **JavaScript 邏輯**。

以下是具體的修改位置與替換的程式碼:

### 第一部分:新增 CSS 樣式

請在 `<head>` 區塊內的 `<style>` 標籤中(可以放在 `/* 彈出視窗 */` 的相關樣式附近),加入以下供下載視窗與切換按鈕使用的現代化樣式:

```css
      /* 下載預覽視窗樣式 */
      #download-modal {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.6);
        backdrop-filter: blur(3px);
        z-index: 300;
        display: none;
        justify-content: center;
        align-items: center;
      }
      .dl-modal-content {
        background: #fff;
        padding: 25px;
        border-radius: 16px;
        width: 450px;
        max-width: 95%;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
        position: relative;
        display: flex;
        flex-direction: column;
        gap: 15px;
      }
      .dl-svg-container {
        width: 100%;
        aspect-ratio: 1 / 1;
        background: #f8f9fa;
        border-radius: 8px;
        border: 1px solid #ddd;
        overflow: hidden;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      .dl-svg-container svg {
        width: 100%;
        height: 100%;
      }
      .dl-options {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 10px 0;
      }
      /* 現代化 Switch 切換開關 */
      .toggle-group {
        display: flex;
        align-items: center;
        gap: 8px;
        font-weight: bold;
        color: #555;
      }
      .switch {
        position: relative;
        display: inline-block;
        width: 44px;
        height: 24px;
      }
      .switch input {
        opacity: 0;
        width: 0;
        height: 0;
      }
      .slider {
        position: absolute;
        cursor: pointer;
        top: 0; left: 0; right: 0; bottom: 0;
        background-color: #ccc;
        transition: .3s;
        border-radius: 24px;
      }
      .slider:before {
        position: absolute;
        content: "";
        height: 18px;
        width: 18px;
        left: 3px;
        bottom: 3px;
        background-color: white;
        transition: .3s;
        border-radius: 50%;
      }
      input:checked + .slider { background-color: var(--p1-color); }
      input:checked + .slider:before { transform: translateX(20px); }

```

---

### 第二部分:新增 HTML 結構

請找到 `<div id="export-modal" ...>` 區塊的**結尾處**(也就是在 `</div>` 之後,`<div id="test-box">` 之前),插入以下新的下載對話框 DOM 結構:

```html
    <div id="download-modal">
      <div class="dl-modal-content">
        <svg class="close-btn" onclick="document.getElementById('download-modal').style.display = 'none'" viewBox="0 0 24 24">
          <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
        </svg>
        <h3 style="margin: 0; border-bottom: 2px solid #eee; padding-bottom: 10px; color: #333;">下載棋盤</h3>
        
        <div class="dl-svg-container" id="dl-preview-container">
          </div>

        <div class="dl-options">
          <div class="toggle-group">
            <span>網格</span>
            <label class="switch">
              <input type="checkbox" id="toggle-grid" checked onchange="updateDownloadPreview()">
              <span class="slider"></span>
            </label>
          </div>
          <div class="toggle-group">
            <span>數字</span>
            <label class="switch">
              <input type="checkbox" id="toggle-numbers" onchange="handleDownloadToggle('numbers')">
              <span class="slider"></span>
            </label>
          </div>
          <div class="toggle-group">
            <span>動畫</span>
            <label class="switch">
              <input type="checkbox" id="toggle-anim" onchange="handleDownloadToggle('anim')">
              <span class="slider"></span>
            </label>
          </div>
        </div>

        <button class="btn-primary" onclick="executeDownload()" style="width: 100%; margin: 0;">⬇ 確認下載 SVG</button>
      </div>
    </div>

```

---

### 第三部分:替換 JavaScript 邏輯

請在 `<script>` 區塊內,找到原本的 `function downloadBoardSVG()` 函數(大約在「下載與導入/導出功能」區塊中),**將原本的 `downloadBoardSVG()` 整個刪除,並替換為以下完整的全新邏輯**。

同時,我們在此處定義了數字的偏移量常數,方便您隨時修改:

```javascript
      // --- 下載預覽與匯出 SVG 核心邏輯 ---

      // 供測試與修改的數字偏移量 (相對於棋子中心坐標)
      const OFFSET_1_DIGIT_X = -7;
      const OFFSET_1_DIGIT_Y = 8;
      const OFFSET_2_DIGIT_X = -14;
      const OFFSET_2_DIGIT_Y = 8;

      let dlPreviewSvg = null;

      function downloadBoardSVG() {
        document.getElementById('download-modal').style.display = 'flex';
        updateDownloadPreview();
      }

      function handleDownloadToggle(type) {
        let toggleNum = document.getElementById('toggle-numbers');
        let toggleAnim = document.getElementById('toggle-anim');
        
        // 數字與動畫互斥
        if (type === 'numbers' && toggleNum.checked) toggleAnim.checked = false;
        if (type === 'anim' && toggleAnim.checked) toggleNum.checked = false;
        
        updateDownloadPreview();
      }

      function updateDownloadPreview() {
        const originalSvg = document.getElementById('etani');
        dlPreviewSvg = originalSvg.cloneNode(true);
        dlPreviewSvg.removeAttribute('id');

        const showGrid = document.getElementById('toggle-grid').checked;
        const showNumbers = document.getElementById('toggle-numbers').checked;
        const showAnim = document.getElementById('toggle-anim').checked;

        const etboard = dlPreviewSvg.querySelector('.etboard');
        const etdrop = dlPreviewSvg.querySelector('#etanidrop');

        // 強制將預覽 SVG 的坐標置中,忽略當前使用者的拖拽與縮放
        etdrop.setAttribute('transform', 'translate(240, 240) scale(1)');

        // 1. 處理網格
        if (!showGrid) {
          const paths = etboard.querySelectorAll('path');
          paths.forEach(p => p.remove());
        }

        // 如果啟用數字或動畫,添加自訂字型
        if (showNumbers || showAnim) {
          const styleNode = document.createElementNS("http://www.w3.org/2000/svg", "style");
          styleNode.textContent = `
            @font-face {
              font-family: 'ZT Nature';
              src: url('your_font_url_here.woff2') format('woff2');
              font-weight: normal;
              font-style: normal;
              font-display: swap;
            }
          `;
          etboard.appendChild(styleNode);
        }

        // 獲取場上依序落下的所有棋子
        const pieces = dlPreviewSvg.querySelectorAll('.tiledropped');

        // 2. 處理數字
        if (showNumbers) {
          pieces.forEach((piece, index) => {
            let order = index + 1;
            let transform = piece.getAttribute('transform');
            let match = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
            if (match) {
              let cx = parseFloat(match[1]);
              let cy = parseFloat(match[2]);
              
              let offsetX = order >= 10 ? OFFSET_2_DIGIT_X : OFFSET_1_DIGIT_X;
              let offsetY = order >= 10 ? OFFSET_2_DIGIT_Y : OFFSET_1_DIGIT_Y;

              let textNode = document.createElementNS("http://www.w3.org/2000/svg", "text");
              textNode.setAttribute("x", cx + offsetX);
              textNode.setAttribute("y", cy + offsetY);
              textNode.setAttribute("stroke", "none");
              textNode.setAttribute("fill", "black");
              textNode.setAttribute("font-size", "24");
              textNode.setAttribute("font-family", "ZT Nature");
              textNode.textContent = order;
              etdrop.appendChild(textNode);
            }
          });
        }

        // 3. 處理過場動畫
        if (showAnim) {
          // 初始化整體透明度
          etdrop.setAttribute('opacity', '0');
          const animDrop = document.createElementNS("http://www.w3.org/2000/svg", "animate");
          animDrop.setAttribute("id", "tr0");
          animDrop.setAttribute("begin", "0;trend.end");
          animDrop.setAttribute("attributeName", "opacity");
          animDrop.setAttribute("values", "0;0;1");
          animDrop.setAttribute("fill", "freeze");
          animDrop.setAttribute("dur", "2s");
          etdrop.insertBefore(animDrop, etdrop.firstChild);

          let lastAnimId = "tr0";

          // 棋子依序飛入動畫
          pieces.forEach((piece, index) => {
            let order = index + 1;
            let transform = piece.getAttribute('transform');
            let match = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
            if(!match) return;
            let cx = parseFloat(match[1]), cy = parseFloat(match[2]);

            // 判斷玩家方向:利用 moveHistory,若無對應記錄則用 index 奇偶性兜底
            let mHist = moveHistory[index];
            let player = mHist ? mHist.player : (index % 2 === 0 ? 1 : 2);
            let outY = player === 1 ? cy + 400 : cy - 400; // 視圖外不遠處:我方在下,對方在上

            const animOut = document.createElementNS("http://www.w3.org/2000/svg", "animateTransform");
            animOut.setAttribute("attributeName", "transform");
            animOut.setAttribute("attributeType", "XML");
            animOut.setAttribute("type", "translate");
            animOut.setAttribute("from", `${cx},${cy}`);
            animOut.setAttribute("to", `${cx},${outY}`);
            animOut.setAttribute("dur", "1s");
            animOut.setAttribute("fill", "freeze");
            animOut.setAttribute("begin", "tr0.begin");
            piece.appendChild(animOut);

            const animIn = document.createElementNS("http://www.w3.org/2000/svg", "animateTransform");
            animIn.setAttribute("id", `tr${order}`);
            animIn.setAttribute("attributeName", "transform");
            animIn.setAttribute("attributeType", "XML");
            animIn.setAttribute("type", "translate");
            animIn.setAttribute("from", `${cx},${outY}`);
            animIn.setAttribute("to", `${cx},${cy}`);
            animIn.setAttribute("dur", "1s");
            animIn.setAttribute("fill", "freeze");
            animIn.setAttribute("begin", `${lastAnimId}.end`);
            piece.appendChild(animIn);

            lastAnimId = `tr${order}`;
          });

          // 計算對得分無用的棋子 (虛擬拔除算法)
          let p1Useless = [], p2Useless = [];
          let baseScores = gameLogic.calculateScores();

          moveHistory.forEach((m, idx) => {
            let dummy = gameLogic.clone();
            dummy.set(m.t1.idx, m.t1.N, 0);
            dummy.set(m.t2.idx, m.t2.N, 0);
            if (m.pid === 0) { // 藍棋特殊處理
              dummy.bluePieces = dummy.bluePieces.filter(bp => !(bp.x === m.bpObj.x && bp.y === m.bpObj.y));
              dummy.bpLookup[dummy._getIndex(m.bpObj.x, m.bpObj.y)] = null;
              dummy.bpLookup[dummy._getIndex(m.bpObj.x+1, m.bpObj.y)] = null;
            }
            let newScores = dummy.calculateScores();
            if (m.player === 1 && newScores.p1Score === baseScores.p1Score) p1Useless.push(idx);
            if (m.player === 2 && newScores.p2Score === baseScores.p2Score) p2Useless.push(idx);
          });

          // 我方 (P1) 無用棋子剔除動畫
          p1Useless.forEach((pieceIdx, arrayIdx) => {
            let piece = pieces[pieceIdx];
            if(!piece) return;
            let match = piece.getAttribute('transform').match(/translate\(([^,]+),\s*([^)]+)\)/);
            if(!match) return;
            let cx = parseFloat(match[1]), cy = parseFloat(match[2]);

            let dist = Math.hypot(cx, cy) || 1;
            let outX = cx + (cx/dist)*600;
            let outY = cy + (cy/dist)*600;

            let animUseless = document.createElementNS("http://www.w3.org/2000/svg", "animateTransform");
            if (arrayIdx === 0) animUseless.setAttribute("id", "trme");
            animUseless.setAttribute("attributeName", "transform");
            animUseless.setAttribute("attributeType", "XML");
            animUseless.setAttribute("type", "translate");
            animUseless.setAttribute("values", `${cx},${cy};${outX},${outY};${outX},${outY};${outX},${outY};${cx},${cy}`);
            animUseless.setAttribute("dur", "4s");
            animUseless.setAttribute("fill", "freeze");
            animUseless.setAttribute("begin", `${lastAnimId}.end+1s`);
            piece.appendChild(animUseless);
          });

          // 我方 (P1) 得分飛入動畫
          let textP1 = document.createElementNS("http://www.w3.org/2000/svg", "text");
          textP1.setAttribute("id", "trmetxt");
          textP1.setAttribute("x", "0");
          textP1.setAttribute("y", "0");
          textP1.setAttribute("stroke", "none");
          textP1.setAttribute("fill", "black");
          textP1.setAttribute("font-size", "60");
          textP1.setAttribute("font-family", "ZT Nature");
          textP1.textContent = baseScores.p1Score;
          etboard.appendChild(textP1);

          // 由於 viewBox 中心是 (0,0),右邊界約為 +240,下邊界為 +240
          let tP1_outX = 252, tP1_inX = 128, tY_P1 = 228;
          let animTextP1 = document.createElementNS("http://www.w3.org/2000/svg", "animateTransform");
          animTextP1.setAttribute("attributeName", "transform");
          animTextP1.setAttribute("attributeType", "XML");
          animTextP1.setAttribute("type", "translate");
          animTextP1.setAttribute("values", `${tP1_outX},${tY_P1};${tP1_inX},${tY_P1};${tP1_inX},${tY_P1};${tP1_inX},${tY_P1};${tP1_outX},${tY_P1}`);
          animTextP1.setAttribute("dur", "4s");
          animTextP1.setAttribute("fill", "freeze");
          animTextP1.setAttribute("begin", p1Useless.length > 0 ? "trme.begin" : `${lastAnimId}.end+1s`);
          textP1.appendChild(animTextP1);

          let trmeEndEvent = p1Useless.length > 0 ? "trme.end" : `${lastAnimId}.end+5s`;

          // 對方 (P2) 無用棋子剔除動畫
          p2Useless.forEach((pieceIdx, arrayIdx) => {
            let piece = pieces[pieceIdx];
            if(!piece) return;
            let match = piece.getAttribute('transform').match(/translate\(([^,]+),\s*([^)]+)\)/);
            if(!match) return;
            let cx = parseFloat(match[1]), cy = parseFloat(match[2]);

            let dist = Math.hypot(cx, cy) || 1;
            let outX = cx + (cx/dist)*600;
            let outY = cy + (cy/dist)*600;

            let animUseless = document.createElementNS("http://www.w3.org/2000/svg", "animateTransform");
            if(arrayIdx === 0) animUseless.setAttribute("id", "trhe");
            animUseless.setAttribute("attributeName", "transform");
            animUseless.setAttribute("attributeType", "XML");
            animUseless.setAttribute("type", "translate");
            animUseless.setAttribute("values", `${cx},${cy};${outX},${outY};${outX},${outY};${outX},${outY};${cx},${cy}`);
            animUseless.setAttribute("dur", "4s");
            animUseless.setAttribute("fill", "freeze");
            animUseless.setAttribute("begin", `${trmeEndEvent}+1s`);
            piece.appendChild(animUseless);
          });

          // 對方 (P2) 得分飛入動畫 (放置於視圖內上方)
          let textP2 = document.createElementNS("http://www.w3.org/2000/svg", "text");
          textP2.setAttribute("id", "trhetxt");
          textP2.setAttribute("x", "0");
          textP2.setAttribute("y", "0");
          textP2.setAttribute("stroke", "none");
          textP2.setAttribute("fill", "black");
          textP2.setAttribute("font-size", "60");
          textP2.setAttribute("font-family", "ZT Nature");
          textP2.textContent = baseScores.p2Score;
          etboard.appendChild(textP2);

          let tP2_outX = 252, tP2_inX = 128, tY_P2 = -168; // 放置於頂部區域附近
          let animTextP2 = document.createElementNS("http://www.w3.org/2000/svg", "animateTransform");
          animTextP2.setAttribute("attributeName", "transform");
          animTextP2.setAttribute("attributeType", "XML");
          animTextP2.setAttribute("type", "translate");
          animTextP2.setAttribute("values", `${tP2_outX},${tY_P2};${tP2_inX},${tY_P2};${tP2_inX},${tY_P2};${tP2_inX},${tY_P2};${tP2_outX},${tY_P2}`);
          animTextP2.setAttribute("dur", "4s");
          animTextP2.setAttribute("fill", "freeze");
          let trheBeginEvent = p2Useless.length > 0 ? "trhe.begin" : `${trmeEndEvent}+1s`;
          animTextP2.setAttribute("begin", trheBeginEvent);
          textP2.appendChild(animTextP2);

          let trheEndEvent = p2Useless.length > 0 ? "trhe.end" : `${trmeEndEvent}+5s`;

          // 最後的結尾淡出動畫
          const animEnd = document.createElementNS("http://www.w3.org/2000/svg", "animate");
          animEnd.setAttribute("id", "trend");
          animEnd.setAttribute("begin", `${trheEndEvent}+1s`);
          animEnd.setAttribute("attributeName", "opacity");
          animEnd.setAttribute("values", "1;1;0");
          animEnd.setAttribute("fill", "freeze");
          animEnd.setAttribute("dur", "2s");
          etdrop.insertBefore(animEnd, etdrop.firstChild);
        }

        // 將處理完畢的 clone 放入預覽容器中
        const container = document.getElementById('dl-preview-container');
        container.innerHTML = '';
        container.appendChild(dlPreviewSvg);
      }

      function executeDownload() {
        if (!dlPreviewSvg) return;
        let source = new XMLSerializer().serializeToString(dlPreviewSvg);
        if (!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) {
          source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
        }
        const a = document.createElement('a');
        a.href = URL.createObjectURL(new Blob([source], { type: 'image/svg+xml;charset=utf-8' }));
        a.download = 'trirhombus_board.svg';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      }

```

由於這段實作採用的是**每次勾選按鈕,就重新去拿原生棋盤 `clone` 並掛載對應屬性**,所以不需要撰寫任何拔除 `animate` 標籤的程式碼,只要玩家取消勾選,再次生成的 `dlPreviewSvg` 就會是純淨狀態!不要忘記將代碼中的 `'your_font_url_here.woff2'` 修改為您正確的字型網址。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6281
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

在前面的基礎上,在最下方中間有下載按鈕,在按鈕下方標注文檔大小byte。

檔案名trirhombus_yyyymmdd_hhiiss.svg,如果動畫加上動畫英文,數字加上數字英文。



gemini 3.1 pro 趕快加 下載按鈕

代码: 全选

```python
with open("trirhombus287.html", "r", encoding="utf-8") as f:
    content = f.read()

# Let's find downloadBoardSVG or download button
import re
print("Length of file:", len(content))
download_matches = [m.start() for m in re.finditer("downloadBoardSVG", content)]
print("downloadBoardSVG positions:", download_matches)
for m in download_matches:
    print(content[m:m+300])



```

```text
Length of file: 129896
downloadBoardSVG positions: [23866, 82856]
downloadBoardSVG()" viewBox="0 0 24 24" title="下載棋盤">
            <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" fill="#333" />
          </svg>
          <svg class="tool-btn" onclick="openExportModal()" viewBox="0 0 24 24" title="導出/導入">
            <path
              d="M14 2H6c-1.1 0-1.99.
downloadBoardSVG() {
        const svg = document.getElementById('etani');
        let source = new XMLSerializer().serializeToString(svg);
        if (!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/))
          source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000


```

```python
def generate_filename(has_num, has_anim):
    import datetime
    now = datetime.datetime.now()
    base = "trirhombus_" + now.strftime("%Y%m%d_%H%M%S")
    if has_num:
        base += "_num"
    if has_anim:
        base += "_anim"
    return base + ".svg"

print(generate_filename(True, False))
print(generate_filename(False, True))



```

```text
trirhombus_20260710_044935_num.svg
trirhombus_20260710_044935_anim.svg


```

在前面的基礎上,我們需要對 HTML 結構和 JavaScript 邏輯進行小幅修改,來加入檔案大小(byte)的計算與顯示,並動態生成帶有時間戳與英文後綴的檔案名稱。

請按照以下兩個步驟替換程式碼:

### 第一部分:修改 HTML 結構

請在 `<div id="download-modal">` 內部,找到最下方原本的單行 `<button>` 按鈕:

```html
        <button class="btn-primary" onclick="executeDownload()" style="width: 100%; margin: 0;">⬇ 確認下載 SVG</button>

```

**將其替換為以下程式碼:**
(這會將按鈕置中,並在按鈕下方加入一行顯示位元組大小的文字區域)

```html
        <div style="text-align: center; margin-top: 10px;">
          <button class="btn-primary" onclick="executeDownload()" style="width: 100%; margin: 0 0 5px 0;">⬇ 確認下載 SVG</button>
          <div id="dl-file-size" style="font-size: 0.9rem; color: #666; font-family: monospace;">檔案大小: 計算中...</div>
        </div>

```

---

### 第二部分:修改 JavaScript 邏輯

**1. 修改 `updateDownloadPreview()` 的末尾部分:**
找到 `updateDownloadPreview()` 函數**最後面**的這三行:

```javascript
        // 將處理完畢的 clone 放入預覽容器中
        const container = document.getElementById('dl-preview-container');
        container.innerHTML = '';
        container.appendChild(dlPreviewSvg);

```

**將其替換為以下程式碼:**
(每次預覽更新時,即時計算 SVG 轉字串後的 Byte 大小並顯示)

```javascript
        // 將處理完畢的 clone 放入預覽容器中
        const container = document.getElementById('dl-preview-container');
        container.innerHTML = '';
        container.appendChild(dlPreviewSvg);

        // --- 新增:計算並顯示檔案大小 (bytes) ---
        let source = new XMLSerializer().serializeToString(dlPreviewSvg);
        if (!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) {
          source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
        }
        let byteSize = new Blob([source], { type: 'image/svg+xml;charset=utf-8' }).size;
        document.getElementById('dl-file-size').innerText = `檔案大小: ${byteSize.toLocaleString()} bytes`;

```

**2. 替換 `executeDownload()` 函數:**
找到原來的 `executeDownload()` 函數,**將整個函數刪除,並替換為以下程式碼:**
(此邏輯會自動獲取當前時間格式化為 `yyyymmdd_hhiiss`,並根據 checkbox 的狀態補上 `_anim` 或 `_num` 後綴)

```javascript
      function executeDownload() {
        if (!dlPreviewSvg) return;
        let source = new XMLSerializer().serializeToString(dlPreviewSvg);
        if (!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) {
          source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
        }
        
        // 取得當前時間生成檔案名 yyyymmdd_hhiiss
        const now = new Date();
        const yyyy = now.getFullYear();
        const mm = String(now.getMonth() + 1).padStart(2, '0');
        const dd = String(now.getDate()).padStart(2, '0');
        const hh = String(now.getHours()).padStart(2, '0');
        const ii = String(now.getMinutes()).padStart(2, '0'); // i 代表分鐘
        const ss = String(now.getSeconds()).padStart(2, '0');
        
        let filename = `trirhombus_${yyyy}${mm}${dd}_${hh}${ii}${ss}`;
        
        // 根據勾選狀態加上英文後綴
        if (document.getElementById('toggle-numbers').checked) {
          filename += '_num';
        } else if (document.getElementById('toggle-anim').checked) {
          filename += '_anim';
        }
        filename += '.svg';

        const a = document.createElement('a');
        a.href = URL.createObjectURL(new Blob([source], { type: 'image/svg+xml;charset=utf-8' }));
        a.download = filename; // 套用新的動態檔名
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6281
注册时间: 2022年 11月 18日 17:36
联系:

Re: 準備為三菱棋做遊戲和MCTS搜尋

帖子 ejsoon »

打開狀態窗口,顯示第一或第二局

所有坐標值去掉末尾的0

精簡三角形網格。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

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