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

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

標準pgn格式:

代码: 全选

[Game "Chinese Chess"]
[Title "尹昇 先胜  赵攀伟"]
[Event "2026年第二届胡林翼杯象棋公开赛"]
[Red "尹昇"]
[RedTeam ""]
[Black "赵攀伟"]
[BlackName ""]
[Opening ""]
[Date ""]
[Site ""]
[Round ""]
[Format "ICCS"]
1. C3-C4 B7-C7
2. H2-E2 C9-E7
3. C0-A2 I9-I8
4. H0-G2 I8-B8
5. B2-D2 B8-B1
6. I0-I1 H7-H1
7. E2-E1 B1-B2
8. I1-H1 B2-D2
9. H1-H9 D2-G2
10. E1-E6 D9-E8
11. H9-G9 G2-G3
12. B0-D1 B9-D8
13. E6-E4 A9-B9
14. G9-H9 B9-B5
15. D1-F2 G3-E3
16. D0-E1 B5-E5
17. A0-B0 D8-E6
18. B0-B9 E8-D9
19. B9-B7 E5-E4
20. F2-E4 E3-E4
21. H9-H2 E7-G9
22. H2-E2 C7-G7
23. E1-D0 G7-G0
24. E0-E1 E4-E2
25. E1-E2 F9-E8
26. B7-B3 G0-D0
27. B3-E3 E6-G5
28. E3-D3 D0-A0
29. D3-D6 G5-F3
30. D6-G6 G9-E7
31. G6-G3 F3-G5
32. G3-E3 E7-G9
33. E2-E1 A6-A5
34. E1-E0 A5-A4
35. A3-A4 A0-A4
36. E3-A3 A4-B4
37. A3-A6 B4-B7
38. A6-C6 B7-E7
39. C6-G6 G9-I7
40. E0-D0 I6-I5
41. C4-C5 E8-F7
42. F0-E1 I5-I4
43. G6-H6 I4-I3
44. H6-H9 E9-E8
45. H9-D9 I3-H3
46. C5-D5 H3-G3
47. D9-D6 E8-E9
48. D6-D7 I7-G9
49. D7-C7 F7-E8
50. D5-D6 E8-D9
51. C7-C4 E7-G7
52. C4-C7 G7-E7
53. C7-C5 G5-F3
54. D6-D7 E7-E4
55. C5-F5 D9-E8
56. D7-D8 E4-D4
57. D0-E0 E8-D7
58. F5-D5 G3-G2
59. E0-D0 D7-E8
60. D0-D1 G2-G1
61. D5-F5 E8-D7
62. F5-F3 D4-D8
63. E1-D2 G9-E7
64. F3-E3 G1-F1
65. E3-E7  *
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

講解中國象棋的標準pgn格式

代码: 全选

在中國象棋(Xiangqi)的數位化記錄中,雖然我們常用 **PGN (Portable Game Notation)** 這個詞,但嚴格來說,中國象棋使用的是基於國際象棋 PGN 格式修改而來的 **XQFE** 或 **WXF** 標記標準。

以下是中國象棋標準棋譜格式的詳細拆解:

---

## 1. 檔案結構
一個標準的中國象棋 PGN 檔案通常由兩個部分組成:**標籤部份 (Tag Section)** 和 **著法部份 (Movetext Section)**。

### 標籤部份 (Tag Section)
這部分用中括號包裹,提供對局的背景資訊。常見的標籤包括:
* `[Game "Chinese Chess"]`:遊戲類型。
* `[Event "賽事名稱"]`:比賽名稱。
* `[Round "輪次"]`:第幾輪比賽。
* `[Red "棋手名"]`:紅方姓名。
* `[Black "棋手名"]`:黑方姓名。
* `[Result "1-0"]`:結果(1-0 紅勝,0-1 黑勝,1/2-1/2 和棋)。
* `[FEN "起始陣式"]`:如果不是從開局開始,需註明 FEN 字串。

### 著法部份 (Movetext Section)
這部分記錄具體的走子過程,序號後接紅方走子,再接黑方走子。

---

## 2. 座標標記法 (Algebraic Notation)
與國際象棋類似,中國象棋的標準電子格式通常採用**縱線座標法**。

### 棋盤座標系統
* **水平軸(橫向):** 從左到右用字母 **a 到 i** 表示(共 9 條線)。
* **垂直軸(縱向):** 從下到上用數字 **0 到 9** 表示(共 10 條線)。

> **注意:** 紅方底線為 `0`,黑方底線為 `9`。
> 例如:紅方左側底角車的位置是 `a0`,黑方右側底角炮的位置是 `h7`。

### 著法格式:`[起點座標][終點座標]`
不同於中文傳統棋譜(如「炮二平五」),電子格式直接記錄格點的移動:
* **h2e2**:代表位於 h2 的子移動到了 e2(相當於炮二平五)。
* **h0g2**:代表位於 h0 的子移動到了 g2(相當於馬二進三)。

---

## 3. 中文傳統格式 vs. 電子標準格式
雖然 PGN 內部存儲的是座標,但許多軟體會將其轉換為中文術語以供閱讀。

| 類型 | 電子座標 (ICCS) | 中文傳統格式 |
| :--- | :--- | :--- |
| 進步 | `h0g2` | 馬二進三 |
| 平移 | `h2e2` | 炮二平五 |
| 退步 | `e2e1` | 炮五退一 |

---

## 4. FEN (Forsyth-Edwards Notation) 記法
在 PGN 中,如果對局不是從初始狀態開始,會使用 `[FEN "..."]` 標籤。中國象棋的 FEN 與國際象棋略有不同:

* **棋子縮寫:**
    * **紅方 (大寫):** `R` (車), `N` (馬), `B` (相), `A` (仕), `K` (帥), `C` (炮), `P` (兵)。
    * **黑方 (小寫):** `r` (車), `n` (馬), `b` (象), `a` (士), `k` (將), `c` (砲), `p` (卒)。
* **結構:** 從第 9 行(黑方底線)描述到第 0 行,斜線 `/` 分隔每一行,數字代表連續的空格。

**初始局面的 FEN 範例:**
`rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1`

---

## 5. 範例展示
一個完整的中國象棋 PGN 檔案看起來像這樣:

```text
[Game "Chinese Chess"]
[Event "2024 友誼賽"]
[Red "玩家A"]
[Black "玩家B"]
[Result "1-0"]

1. h2e2 h7e7 2. h0g2 h9g7 3. i0h0 i9h9 4. g2f4 b9a7
```

> **重點提示:**
> 如果你在編寫程式或分析棋譜,請確保使用 **UTF-8** 編碼,以防止標籤中的中文姓名或賽事名稱出現亂碼。

您目前是在處理現有的棋譜檔案,還是正打算為自己的對局寫一個記錄器?
會否可以有[Date ""]
[Site ""]
[Round ""]
這樣的屬性?是否可以認為可以擅自添加屬性?

代码: 全选

是的,你的理解完全正確。在 PGN 格式中,這些屬性(Tags)不僅是**標準組成部分**,而且 PGN 協議本身就設計為**可擴展的**。

以下是針對你疑問的詳細解答:

---

### 1. 標準的「七大標籤」 (STR)
在 PGN 規範中,有七個標籤被稱為 **STR (Seven Tag Roster)**,它們被認為是必不可少的。如果你在使用專業的象棋數據庫軟體(如象棋橋、XQWizard),它們通常會自動生成這七項:

1.  `[Event ""]` (賽事)
2.  `[Site ""]` (地點)
3.  `[Date ""]` (日期,格式通常為 YYYY.MM.DD)
4.  `[Round ""]` (輪次)
5.  `[Red ""]` (紅方棋手)
6.  `[Black ""]` (黑方棋手)
7.  `[Result ""]` (結果)

所以,你提到的 `[Date ""]`、`[Site ""]`、`[Round ""]` 絕對不是擅自添加,而是**標準要求**的內容。

---

### 2. 是否可以「擅自」添加屬性?
**可以。** PGN 格式非常靈活,它允許定義「用戶自定義標籤」。只要符合格式規範(中括號包裹、標籤名在先、值在雙引號內),任何解析器都會將其視為合法的元數據。

常見的自定義或擴展標籤包括:
* `[Annotator ""]`:記錄棋譜註解者。
* `[RedTitle ""]` / `[BlackTitle ""]`:棋手頭銜(如:特級大師)。
* `[RedTeam ""]` / `[BlackTeam ""]`:所屬隊伍。
* `[ECO ""]`:開局分類代碼。
* `[Opening ""]`:開局名稱(例如:當頭炮對屏風馬)。
* `[Time ""]`:具體比賽時間。
* `[Termination ""]`:比賽結束的具體原因(如:超時、認輸)。

---

### 3. 添加屬性時的注意事項
雖然你可以隨意添加屬性,但為了確保檔案在不同軟體間的**兼容性**,建議遵循以下原則:

* **唯一性:** 標籤名稱不要與標準標籤衝突。
* **格式:** 標籤名只能包含字母、數字和下劃線(不建議使用中文做標籤名,但標籤的值可以使用中文)。
* **順序:** 雖然規範建議將 STR(七大標籤)放在最前面,但大多數軟體對自定義標籤的位置並不敏感。

---

### 4. 示例
這是一個包含了標準標籤與自定義標籤的完整頭部:

```text
[Event "2026年五一盃"]
[Site "廣州"]
[Date "2026.05.01"]
[Round "1"]
[Red "王天一"]
[Black "鄭惟桐"]
[Result "1/2-1/2"]
[RedTeam "杭州"]
[BlackTeam "四川"]
[Annotator "人工智慧助手"]
[Opening "中炮過河車對屏風馬左馬盤河"]
```

> **小貼士:**
> 如果你的 `Date` 標籤中某個部分未知,規範要求用 `??` 代替。
> 例如:`[Date "2026.??.??" ]`(只知道年份)。

您是打算用 PGN 格式來整理私人的棋譜庫,還是為了開發某種程序需要校對格式呢?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

pgn格式能否添加評論?

代码: 全选

絕對可以。PGN 格式設計的初衷之一就是為了分享與教學,因此**評論(Comments)**是其核心功能。

在中國象棋的 PGN 檔案中,添加評論主要有兩種標準方式:

---

### 1. 大括號評論 `{ ... }`
這是最常見的方式,用於輸入**文字描述**。大括號內的內容會被解析器視為註解,不會影響棋局的連貫性。

* **位置:** 可以放在著法之前或之後。
* **範例:**
    ```text
    1. h2e2 { 紅方起手當頭炮,展現進攻姿態 } h7e7 { 黑方同樣以炮封鎖 }
    2. h0g2 { 馬二進三 } 
    ```

### 2. 分號評論 `;`
如果你想在某一行的末尾添加備註,可以使用分號。分號之後到該行結束的所有內容都會被視為評論。

* **範例:**
    ```text
    1. h2e2 e7e7 ; 這是典型的順手炮開局
    2. h0g2
    ```

---

### 3. 進階評論符號 (NAG)
除了文字,PGN 還支援 **NAG (Numeric Annotation Glyphs)**。這是一些以 `$` 開頭的數字編碼,用來表達對某一步棋的評價,許多棋譜軟體會將它們轉化為特定圖示(如 `!` 或 `?`)。

常見的符號對應如下:
* `$1` 或 `!`:好棋 (Good move)
* `$2` 或 `?`:劣著 (Poor move)
* `$3` 或 `!!`:妙手 (Very good move)
* `$4` 或 `??`:大漏著 (Very poor move)
* `$11`:黑方稍優
* `$13`:紅方明顯優勢

**範例:**
`1. h2e2 $1` (代表這一步是好棋)

---

### 4. 變著 (Recursive Annotation Variations, RAV)
如果你想記錄「如果當時不這樣走,而是那樣走會如何」,可以使用**小括號 `( ... )`** 來記錄**變著**。

* **範例:**
    ```text
    1. h2e2 h7e7 
    2. h0g2 ( 2. i0h0 { 如果改走車一平二... } ) 2... h9g7
    ```

---

### 💡 實踐小建議
* **編碼格式:** 由於評論通常包含中文,請務必將檔案儲存為 **UTF-8** 編碼,否則在不同的棋譜軟體(如《象棋橋》或手機 App)中開啟時,評論極易變成亂碼。
* **軟體相容性:** 大多數現代象棋軟體都能完美支援 `{}` 和 `()`。

您是打算在整理棋譜時加入自己的心得分析,還是正在編寫一個自動生成戰報的工具呢?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

1-0結果標志是否可加可不加?前面標籤不是定義了嗎?還有哪些除了著法之外還能加的東西?

代码: 全选

這是一個非常深入的問題。這牽涉到 PGN 的**冗餘設計(Redundancy)**原則,旨在確保數據在傳輸或局部損壞時仍能保持完整。

---

### 1. 著法末尾的 `1-0` 是否可加可不加?

**答案是:強烈建議加上,且在標準規範中是「必須」的。**

雖然你在標籤區(Tag Section)已經定義了 `[Result "1-0"]`,但在著法區(Movetext Section)末尾再次出現結果標誌有以下幾個原因:

* **檔案完整性校驗:** 解析器會比對標籤區的結果與著法區末尾的標誌。如果不一致,軟體會提示檔案可能損毀或被竄改。
* **結束訊號:** 著法區的結果標誌(`1-0`, `0-1`, `1/2-1/2` 或 `*`)是解析器的「終止符」。它告訴程式:「這局棋到此結束,後面的內容(如有)可能是下一局棋或無關文字。」
* **如果不加會怎樣?**
    * 大多數現代軟體(如象棋橋、XQWizard)仍能讀取,它們會以標籤區的結果為準。
    * 但在處理「連續包含多局棋」的長 PGN 檔案時,缺少結束標誌可能導致解析器無法正確切換到下一局。

> **提示:** 如果棋局尚未結束(如打譜到一半),請使用星號 `*` 作為結束標誌。

---

### 2. 除了著法和評論,還能加什麼?

除了你已經知道的標籤、著法和 `{}` 評論,PGN 著法區還支援以下內容:

#### A. 變著 (Recursive Annotation Variations, RAV)
這在教學棋譜中最常見。你可以用**小括號 `()`** 嵌套另一個平行宇宙的走法。
* **範例:** `1. h2e2 h7e7 2. h0g2 (2. i0h0 {若改走邊車...} i9h9 3. h0h4) h9g7`
* 變著裡面甚至可以再嵌套變著(括號套括號)。

#### B. 數值註解符號 (NAG, $n$)
如果你不想寫長篇大論,只想給某步棋打分,可以使用 `$n`。
* `$1` = 好棋 (`!`)
* `$2` = 壞棋 (`?`)
* `$18` = 局面混亂
* 這些符號在軟體中通常會直接顯示為圖示。

#### C. 回合序號的修正 (Ellipsis)
當你在黑方著法後加入長篇評論,想接著寫下一回合時,通常會用 `...`。
* **範例:** `1. h2e2 h7e7 {黑方應以順手炮} 2. h0g2 ...`(這主要是為了增強人類可讀性,機器解析時通常會忽略多餘的點)。

#### D. 行動符號 (Suffix Glyphs)
雖然在電子格式中較少見(因為有 NAG),但你仍可以直接在著法後緊跟 `!`、`?`、`!!`、`??`、`!?` 或 `?!`。
* **範例:** `2. h0g2! h9g7?`

---

### 3. PGN 的「隱藏」特性:多局併存
PGN 檔案本質上是一個**文本串聯**。這意味著你可以在一個 `.pgn` 檔案中,直接把多段完整的棋譜內容(包含各自的標籤區和著法區)接在一起。

```text
[Event "第一局"]
...
1. h2e2 h7e7 1-0

[Event "第二局"]
...
1. i0h0 i9h9 1/2-1/2
```

這種結構讓它成為了極佳的「數據庫」載體。

---

### 總結一下你的疑惑:
標籤區的 `Result` 是給數據庫**索引和過濾**用的,而著法區末尾的 `1-0` 是給解析引擎**判定終止點**用的。

你現在是在嘗試手寫一個 PGN 檔案來測試某個軟體的讀取能力嗎?還是有遇到某些軟體讀不出你的評論呢?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

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

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

把導出文本中的Chinese Notation改成DP Format。

它一開始時是沒有評論的,當點擊一次時,它將顯示評論,這個維持不變。

現在要改進的是,當點擊第二次時,將顯示帶有分支的版本,按鈕顏色顯示為淡紫色。

帶分支的導入邏輯已經寫有了,參照它來做。需要說明的是,當超出20個,則使用括號加數字:(21)。

例如:

各種評論
1. 炮二平五 馬2進3
2. 馬二進三 卒7進1
3. 車一平二 ① 炮8平7
4. 兵七進一 車1進1
5. 馬八進九 ② 象3進5
6. 炮八平七 炮2進4 ③
7. 兵五進一 馬8進9
這裡是評論
8. 車九平八 車1平2
9. 兵五進一 卒5進1
10. 馬三進五 車9平8
11. 車二進九 馬9退8
12. 炮五進三 士4進5


3. 相三進一


5. 仕四進五


6. ………… 馬8進9

則「車一平二」的分支是「相三進一」,「馬八進九」的分支是「仕四進五」,「炮2進4」的分支是「馬8進9」。

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

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

gemini pro只吃難吃的:

代码: 全选

這裡為您提供需要修改的 4 個地方,以實作「DP Format」的三段切換(無評論、有評論、帶分支的淡紫色狀態)與文本生成。

### 1. 新增按鈕淡紫色狀態的 CSS
在 `<style>` 區塊中,找到 `.exp-fmt-btn.active[data-odd="1"] { background: orange; }`,在它下方新增 `[data-odd="2"]` 的樣式:

```css
    .exp-fmt-btn.active[data-odd="1"] {
        background: orange;
    }
    .exp-fmt-btn.active[data-odd="2"] {
        background: #ce93d8; /* 淡紫色 */
    }
```

### 2. 新增 `generateDPFormat` 函數
在 `generateExportText` 函數定義的後面,插入處理分支邏輯的新函數 `generateDPFormat`:

```javascript
        function generateExportText(isEnglish) {
            // ... (原本的代碼保持不變) ...
            return text.trim();
        }

        // 新增:帶有分支與標記的 DP Format 生成函數
        function generateDPFormat(isEnglish = false) {
            let text = historyFEN.fen + '\n';
            if (historyFEN.c && historyFEN.c.trim() !== '') {
                text += historyFEN.c.trim() + '\n';
            }

            let branchQueue = [];
            let branchCounter = 1;

            // 超過20個分支使用括號數字
            function getMarker(num) {
                if (num <= 20) return String.fromCharCode(0x245F + num);
                return `(${num})`;
            }

            function processSequence(startNode, absDepth, moveNum, isRed, pathArr) {
                let seqText = "";
                let curr = startNode;
                let lineBuffer = "";
                
                while (curr.v && curr.v.length > 0) {
                    let choiceIdx = 0;
                    if (pathArr && pathArr[absDepth] !== undefined) {
                        choiceIdx = pathArr[absDepth];
                    }
                    if (choiceIdx >= curr.v.length) choiceIdx = 0;
                    
                    let nextNode = curr.v[choiceIdx];
                    let moveStr = isEnglish ? nextNode.move : NotationConverter.toChinese(nextNode.move);
                    
                    let markers = "";
                    for (let i = 0; i < curr.v.length; i++) {
                        if (i !== choiceIdx) {
                            let marker = getMarker(branchCounter++);
                            markers += " " + marker;
                            branchQueue.push({
                                marker: marker,
                                parentNode: curr,
                                choiceIdx: i,
                                moveNum: moveNum,
                                isRedTurn: isRed,
                                absDepth: absDepth
                            });
                        }
                    }

                    if (isRed) {
                        if (lineBuffer === "") {
                            lineBuffer = `${moveNum}. `;
                        }
                        lineBuffer += `${moveStr}${markers}`;
                        
                        if (nextNode.c && nextNode.c.trim() !== '') {
                            seqText += lineBuffer + '\n' + nextNode.c.trim() + '\n';
                            lineBuffer = "";
                            isRed = false;
                        } else {
                            isRed = false;
                        }
                    } else {
                        if (lineBuffer === "") {
                            lineBuffer = `${moveNum}. ………… `;
                        } else {
                            lineBuffer += " ";
                        }
                        lineBuffer += `${moveStr}${markers}`;
                        
                        seqText += lineBuffer + '\n';
                        if (nextNode.c && nextNode.c.trim() !== '') {
                            seqText += nextNode.c.trim() + '\n';
                        }
                        lineBuffer = "";
                        isRed = true;
                        moveNum++;
                    }
                    
                    curr = nextNode;
                    absDepth++;
                }
                
                if (lineBuffer !== "") {
                    seqText += lineBuffer + '\n';
                }
                
                return seqText.trim();
            }

            let mainIsRed = historyFEN.fen.split(' ')[1] === 'w';
            let mainText = processSequence(historyFEN, 0, 1, mainIsRed, currentBranch);
            text += mainText;

            while (branchQueue.length > 0) {
                let b = branchQueue.shift();
                text += `\n\n${b.marker}\n`;
                
                // 為分支生成虛擬的選擇路徑(只在當前層級選擇特定分支,後續默認走主線0)
                let branchPath = [];
                for (let i = 0; i < b.absDepth; i++) {
                    branchPath.push(0); 
                }
                branchPath.push(b.choiceIdx);

                let branchText = processSequence(b.parentNode, b.absDepth, b.moveNum, b.isRedTurn, branchPath);
                text += branchText;
            }

            return text;
        }
```

### 3. 修改複製行為 (`performCopy`)
找到 `performCopy` 函數,將判定 `cn` 的複製邏輯增加對 `odd === 2` 的支持:

```javascript
        function performCopy() {
            let textToCopy = "";
            const fmt = lastExportFormat || 'cn';
            const odd = formatOddStates[fmt] || 0;

            if (fmt === 'cn') {
                if (odd === 2) {
                    textToCopy = generateDPFormat(false);
                } else if (odd === 1) {
                    textToCopy = generateTextWithComment(false);
                } else {
                    textToCopy = generateExportText(false);
                }
            } else if (fmt === 'en') {
                textToCopy = odd === 1 ? generateTextWithComment(true) : generateExportText(true);
            // ... 後續代碼不變 ...
```

### 4. 修改 UI 界面生成與點擊切換邏輯 (`renderExportTextUI`)
找到 `renderExportTextUI` 函數,並做以下幾處局部修改:

首先,在初始化文本常量的地方加上 `cnTextWithBranches`:
```javascript
            const cnText = generateExportText(false);
            const cnTextWidthComment = generateTextWithComment(false);
            const cnTextWithBranches = generateDPFormat(false); // 新增這行
            const enText = generateExportText(true);
```

接著,把按鈕的文字改成 DP Format:
```javascript
            commentDiv.innerHTML = `<div class="exp-fmt-outer">
                <div class="exp-fmt-btn ${lastExportFormat === 'cn' ? 'active' : ''}" data-fmt="cn" data-odd="${formatOddStates['cn']}">DP Format</div>
                <div class="exp-fmt-btn ${lastExportFormat === 'en' ? 'active' : ''}" data-fmt="en" data-odd="${formatOddStates['en']}">English Notation</div>
                <div class="exp-fmt-btn ${lastExportFormat === 'engine' ? 'active' : ''}" data-fmt="engine" data-odd="${formatOddStates['engine']}">Engine Format</div>
                <div class="exp-fmt-btn ${lastExportFormat === 'url' ? 'active' : ''}" data-fmt="url" data-odd="${formatOddStates['url']}">URL Parameter</div>
               </div>`;
```

然後,在按鈕 `click` 事件中的 `wasActive` 狀態切換判定裡,讓 `cn` 支持 3 段切換 (0、1、2):
```javascript
                    if (wasActive) {
                        if (fmt === 'cn') {
                            formatOddStates[fmt] = (formatOddStates[fmt] + 1) % 3; // 新增這行以支援第三階段
                        } else {
                            formatOddStates[fmt] = formatOddStates[fmt] === 1 ? 0 : 1;
                        }
                    }
                    btn.setAttribute('data-odd', formatOddStates[fmt]);
```

最後,在 `updateTextarea` 中對應綁定數值 2 的文本內容:
```javascript
            const updateTextarea = (fmt) => {
                if (fmt === 'cn') {
                    if (formatOddStates[fmt] === 2) {
                        textarea.value = cnTextWithBranches;
                    } else if (formatOddStates[fmt] === 1) {
                        textarea.value = cnTextWidthComment;
                    } else {
                        textarea.value = cnText;
                    }
                } else if (fmt === 'en') {
```

解決死循環:

代码: 全选

// Added: DP Format generation function with branches and markers (Fixed infinite loop)
        function generateDPFormat(isEnglish = false) {
            let text = historyFEN.fen + '\n';
            if (historyFEN.c && historyFEN.c.trim() !== '') {
                text += historyFEN.c.trim() + '\n';
            }

            let branchQueue = [];
            let branchCounter = 1;

            // Use circled numbers for first 20 branches, then switch to (n) format
            function getMarker(num) {
                if (num <= 20) return String.fromCharCode(0x245F + num);
                return `(${num})`;
            }

            /**
             * @param {Object} startNode - The node to begin traversal from
             * @param {number} absDepth - Current depth in the total move tree
             * @param {number} moveNum - Current move number (e.g., 1, 2, 3...)
             * @param {boolean} isRed - Whether it's Red's turn
             * @param {Array} pathArr - The selection path to follow
             * @param {boolean} skipFirstQueue - If true, ignores siblings of the first node to prevent recursion loops
             */
            function processSequence(startNode, absDepth, moveNum, isRed, pathArr, skipFirstQueue) {
                let seqText = "";
                let curr = startNode;
                let lineBuffer = "";
                let isFirstIteration = true; // Tracks the first move of the current sequence
                
                while (curr.v && curr.v.length > 0) {
                    let choiceIdx = 0;
                    if (pathArr && pathArr[absDepth] !== undefined) {
                        choiceIdx = pathArr[absDepth];
                    }
                    if (choiceIdx >= curr.v.length) choiceIdx = 0;
                    
                    let nextNode = curr.v[choiceIdx];
                    let moveStr = isEnglish ? nextNode.move : NotationConverter.toChinese(nextNode.move);
                    
                    let markers = "";
                    // Core fix: Do not re-queue siblings if we are starting a sub-branch traversal
                    if (!(isFirstIteration && skipFirstQueue)) {
                        for (let i = 0; i < curr.v.length; i++) {
                            if (i !== choiceIdx) {
                                let marker = getMarker(branchCounter++);
                                markers += " " + marker;
                                branchQueue.push({
                                    marker: marker,
                                    parentNode: curr,
                                    choiceIdx: i,
                                    moveNum: moveNum,
                                    isRedTurn: isRed,
                                    absDepth: absDepth
                                });
                            }
                        }
                    }
                    isFirstIteration = false;

                    if (isRed) {
                        if (lineBuffer === "") {
                            lineBuffer = `${moveNum}. `;
                        }
                        lineBuffer += `${moveStr}${markers}`;
                        
                        if (nextNode.c && nextNode.c.trim() !== '') {
                            seqText += lineBuffer + '\n' + nextNode.c.trim() + '\n';
                            lineBuffer = "";
                            isRed = false;
                        } else {
                            isRed = false;
                        }
                    } else {
                        if (lineBuffer === "") {
                            lineBuffer = `${moveNum}. ………… `;
                        } else {
                            lineBuffer += " ";
                        }
                        lineBuffer += `${moveStr}${markers}`;
                        
                        seqText += lineBuffer + '\n';
                        if (nextNode.c && nextNode.c.trim() !== '') {
                            seqText += nextNode.c.trim() + '\n';
                        }
                        lineBuffer = "";
                        isRed = true;
                        moveNum++;
                    }
                    
                    curr = nextNode;
                    absDepth++;
                }
                
                if (lineBuffer !== "") {
                    seqText += lineBuffer + '\n';
                }
                
                return seqText.trim();
            }

            let mainIsRed = historyFEN.fen.split(' ')[1] === 'w';
            // Main line traversal: skipFirstQueue is false to capture all initial branches
            let mainText = processSequence(historyFEN, 0, 1, mainIsRed, currentBranch, false);
            text += mainText;

            // Process the branch queue until empty
            while (branchQueue.length > 0) {
                let b = branchQueue.shift();
                text += `\n\n${b.marker}\n`;
                
                // Create a virtual path for the branch (follows main line except at the divergence point)
                let branchPath = [];
                for (let i = 0; i < b.absDepth; i++) {
                    branchPath.push(0); 
                }
                branchPath.push(b.choiceIdx);

                // Sub-branch traversal: skipFirstQueue is true to prevent infinite recursion
                let branchText = processSequence(b.parentNode, b.absDepth, b.moveNum, b.isRedTurn, branchPath, true);
                text += branchText;
            }

            return text;
        }
上次由 ejsoon 在 2026年 4月 24日 12:33,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

將所有注釋都翻譯成英文:

代码: 全选

        // 新增:帶有分支與標記的 DP Format 生成函數
        function generateDPFormat(isEnglish = false) {
            let text = historyFEN.fen + '\n';
            if (historyFEN.c && historyFEN.c.trim() !== '') {
                text += historyFEN.c.trim() + '\n';
            }

            let branchQueue = [];
            let branchCounter = 1;

            // 超過20個分支使用括號數字
            function getMarker(num) {
                if (num <= 20) return String.fromCharCode(0x245F + num);
                return `(${num})`;
            }

            function processSequence(startNode, absDepth, moveNum, isRed, pathArr) {
                let seqText = "";
                let curr = startNode;
                let lineBuffer = "";
                
                while (curr.v && curr.v.length > 0) {
                    let choiceIdx = 0;
                    if (pathArr && pathArr[absDepth] !== undefined) {
                        choiceIdx = pathArr[absDepth];
                    }
                    if (choiceIdx >= curr.v.length) choiceIdx = 0;
                    
                    let nextNode = curr.v[choiceIdx];
                    let moveStr = isEnglish ? nextNode.move : NotationConverter.toChinese(nextNode.move);
                    
                    let markers = "";
                    for (let i = 0; i < curr.v.length; i++) {
                        if (i !== choiceIdx) {
                            let marker = getMarker(branchCounter++);
                            markers += " " + marker;
                            branchQueue.push({
                                marker: marker,
                                parentNode: curr,
                                choiceIdx: i,
                                moveNum: moveNum,
                                isRedTurn: isRed,
                                absDepth: absDepth
                            });
                        }
                    }

                    if (isRed) {
                        if (lineBuffer === "") {
                            lineBuffer = `${moveNum}. `;
                        }
                        lineBuffer += `${moveStr}${markers}`;
                        
                        if (nextNode.c && nextNode.c.trim() !== '') {
                            seqText += lineBuffer + '\n' + nextNode.c.trim() + '\n';
                            lineBuffer = "";
                            isRed = false;
                        } else {
                            isRed = false;
                        }
                    } else {
                        if (lineBuffer === "") {
                            lineBuffer = `${moveNum}. ………… `;
                        } else {
                            lineBuffer += " ";
                        }
                        lineBuffer += `${moveStr}${markers}`;
                        
                        seqText += lineBuffer + '\n';
                        if (nextNode.c && nextNode.c.trim() !== '') {
                            seqText += nextNode.c.trim() + '\n';
                        }
                        lineBuffer = "";
                        isRed = true;
                        moveNum++;
                    }
                    
                    curr = nextNode;
                    absDepth++;
                }
                
                if (lineBuffer !== "") {
                    seqText += lineBuffer + '\n';
                }
                
                return seqText.trim();
            }

            let mainIsRed = historyFEN.fen.split(' ')[1] === 'w';
            let mainText = processSequence(historyFEN, 0, 1, mainIsRed, currentBranch);
            text += mainText;

            while (branchQueue.length > 0) {
                let b = branchQueue.shift();
                text += `\n\n${b.marker}\n`;
                
                // 為分支生成虛擬的選擇路徑(只在當前層級選擇特定分支,後續默認走主線0)
                let branchPath = [];
                for (let i = 0; i < b.absDepth; i++) {
                    branchPath.push(0); 
                }
                branchPath.push(b.choiceIdx);

                let branchText = processSequence(b.parentNode, b.absDepth, b.moveNum, b.isRedTurn, branchPath);
                text += branchText;
            }

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

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

代码: 全选

好的,我已經將 `generateDPFormat` 函數中的所有註釋轉換為英文。這樣可以確保代碼風格的一致性,同時避免在某些環境下出現編碼問題。

請使用以下代碼替換之前的版本:

```javascript
        // Added: DP Format generation function with branches and markers (Fixed infinite loop)
        function generateDPFormat(isEnglish = false) {
            let text = historyFEN.fen + '\n';
            if (historyFEN.c && historyFEN.c.trim() !== '') {
                text += historyFEN.c.trim() + '\n';
            }

            let branchQueue = [];
            let branchCounter = 1;

            // Use circled numbers for first 20 branches, then switch to (n) format
            function getMarker(num) {
                if (num <= 20) return String.fromCharCode(0x245F + num);
                return `(${num})`;
            }

            /**
             * @param {Object} startNode - The node to begin traversal from
             * @param {number} absDepth - Current depth in the total move tree
             * @param {number} moveNum - Current move number (e.g., 1, 2, 3...)
             * @param {boolean} isRed - Whether it's Red's turn
             * @param {Array} pathArr - The selection path to follow
             * @param {boolean} skipFirstQueue - If true, ignores siblings of the first node to prevent recursion loops
             */
            function processSequence(startNode, absDepth, moveNum, isRed, pathArr, skipFirstQueue) {
                let seqText = "";
                let curr = startNode;
                let lineBuffer = "";
                let isFirstIteration = true; // Tracks the first move of the current sequence
                
                while (curr.v && curr.v.length > 0) {
                    let choiceIdx = 0;
                    if (pathArr && pathArr[absDepth] !== undefined) {
                        choiceIdx = pathArr[absDepth];
                    }
                    if (choiceIdx >= curr.v.length) choiceIdx = 0;
                    
                    let nextNode = curr.v[choiceIdx];
                    let moveStr = isEnglish ? nextNode.move : NotationConverter.toChinese(nextNode.move);
                    
                    let markers = "";
                    // Core fix: Do not re-queue siblings if we are starting a sub-branch traversal
                    if (!(isFirstIteration && skipFirstQueue)) {
                        for (let i = 0; i < curr.v.length; i++) {
                            if (i !== choiceIdx) {
                                let marker = getMarker(branchCounter++);
                                markers += " " + marker;
                                branchQueue.push({
                                    marker: marker,
                                    parentNode: curr,
                                    choiceIdx: i,
                                    moveNum: moveNum,
                                    isRedTurn: isRed,
                                    absDepth: absDepth
                                });
                            }
                        }
                    }
                    isFirstIteration = false;

                    if (isRed) {
                        if (lineBuffer === "") {
                            lineBuffer = `${moveNum}. `;
                        }
                        lineBuffer += `${moveStr}${markers}`;
                        
                        if (nextNode.c && nextNode.c.trim() !== '') {
                            seqText += lineBuffer + '\n' + nextNode.c.trim() + '\n';
                            lineBuffer = "";
                            isRed = false;
                        } else {
                            isRed = false;
                        }
                    } else {
                        if (lineBuffer === "") {
                            lineBuffer = `${moveNum}. ………… `;
                        } else {
                            lineBuffer += " ";
                        }
                        lineBuffer += `${moveStr}${markers}`;
                        
                        seqText += lineBuffer + '\n';
                        if (nextNode.c && nextNode.c.trim() !== '') {
                            seqText += nextNode.c.trim() + '\n';
                        }
                        lineBuffer = "";
                        isRed = true;
                        moveNum++;
                    }
                    
                    curr = nextNode;
                    absDepth++;
                }
                
                if (lineBuffer !== "") {
                    seqText += lineBuffer + '\n';
                }
                
                return seqText.trim();
            }

            let mainIsRed = historyFEN.fen.split(' ')[1] === 'w';
            // Main line traversal: skipFirstQueue is false to capture all initial branches
            let mainText = processSequence(historyFEN, 0, 1, mainIsRed, currentBranch, false);
            text += mainText;

            // Process the branch queue until empty
            while (branchQueue.length > 0) {
                let b = branchQueue.shift();
                text += `\n\n${b.marker}\n`;
                
                // Create a virtual path for the branch (follows main line except at the divergence point)
                let branchPath = [];
                for (let i = 0; i < b.absDepth; i++) {
                    branchPath.push(0); 
                }
                branchPath.push(b.choiceIdx);

                // Sub-branch traversal: skipFirstQueue is true to prevent infinite recursion
                let branchText = processSequence(b.parentNode, b.absDepth, b.moveNum, b.isRedTurn, branchPath, true);
                text += branchText;
            }

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

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

ejcees20260424DPformat

為東萍格式加入分支:
附件
ejcees20260424DPformat.7z
(47.38 KiB) 已下载 27 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

將導出文本時的第二個按鈕改為PGN Format,也將可以點擊兩次,顏色變化跟DP Format一樣。

格式示例如下:

[Game "Chinese Chess"]
[FEN "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/9/1C5C1/9/RN2K2NR r - - 0 1"]
[Format "Chinese"]
{總評可以很短也可以很長還可以換行,但都要放在大括號內。}
1. 炮二平五
{這是第一步棋的評論}
將5進1
2. 馬八進七 車9進1
{這是最後一步棋的評論}
*

說明:

一,開頭第一行一定是[Game "Chinese Chess"]

二,如果開局fen不等同於INITIAL_FEN,就需要加[FEN "…"],否則不用加。

三,最後一行表示結局,如果是紅方在最後一步把對方將死或困斃就是「1-0」,如果黑方在最後一步把對方將死或困斃就是「0-1」,如果雙方都無進攻子力則是「1/2-1/2」(進攻子力指車馬炮兵),其餘情況則是「*」。

四,如果有分支,則用()將分支括起來,比如:

1. 炮二平五
{這是這步棋的評論}
將5進1 (1. ... 士4進5
2.馬八進九 車9進1
3.兵五進一{這樣輸得更慘})
2. 馬八進七 車9進1
*

則「將5進1」的分支是「士4進5」。

分支之中還可以嵌套分支,即括號內還可以再套進多層括號。

五,在ejceesrecord右上角增加一個按鈕,當點擊時則彈出一個窗口,窗口的背景色為半透明白色,其下方的所有元素都因為被遮蓋而不可點擊。

窗口為一個table,左邊為name,右邊為value,最右邊一列還有垃圾桶圖標以清空,點擊處於標題欄的垃圾桶則是全部清空。最左邊一列是序號。窗口右上角有關閉按鈕。

點擊序號a,它將選中,再點擊另一個序號b,如果a在b前面,則把a置於b的下方,如果a在b後面,則把a置於b的上方,同時取消選中。如果選中a再點一次a,則取消選中。

name的列表有:「Event、Site、Date(格式“yyyy.mm.dd”)、Round、RedTeam、Red、BlackTeam、Black、Result(只有四個選項,默認為*)、Time、TimeControl、Opening、ECCO、Annotator、Termination。最下方還有個「+」號,當點擊時則會增加一行可填的name和value。

name的以下這幾項顏色更為著重:

1. `[Event ""]` (賽事)
2. `[Site ""]` (地點)
3. `[Date ""]` (日期,格式通常為 YYYY.MM.DD)
4. `[Round ""]` (輪次)
5. `[Red ""]` (紅方棋手)
6. `[Black ""]` (黑方棋手)
7. `[Result ""]` (結果)

當窗口關閉時,如果某個name有值,則會按順序加至[Game "Chinese Chess"]的下方,每項一行,如:

[Game "Chinese Chess"]
[Event "第24届“五羊杯”全国冠军邀请赛"]
[Site "广州"]
[Date "2004.01.05"]
[Round "决赛(加赛)"]
[RedTeam "广州"]
[Red "吕钦"]
[BlackTeam "广州"]
[Black "许银川"]
[Result "1-0"]
[Opening "中炮过河炮对左三步虎"]
[ECCO "B24"]
[Format "Chinese"]
注意:如果Result改變的話,則最後一行的「*」也會換成Result的值。比如上面的Result是「1-0」,棋譜的末尾也將會是「1-0」。

六,當從別的導出格式(如DP Format)切換至PGN Format時,默認格式為[Format "Chinese"],著法為中文,即「炮二平五」。當再次點擊PGN Format時,它將切換為[Format "ICCS"],這種格式跟engine格式一樣,比如「H2E2」。點擊第三次,則[Format "WXF"],把著法換成之前的英文著法,比如「c2=5」。

只需給出所要修改的地方。
附件
ejcees20260424DPformatPrettier3.7z
(46.88 KiB) 已下载 25 次
ejcees20260424DPformatPrettier2.7z
(52.34 KiB) 已下载 24 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

gemini pro 愛啃硬骨頭:

代码: 全选

以下是針對您要求的功能所需要修改的部分代碼。請將它們對應替換到原有的 HTML 文件中。

### 1. 新增 CSS 樣式
在 `<style>` 區塊的最末尾(`</style>` 之前)加入 PGN 設置彈出窗口及按鈕的樣式:

```css
      /* --- PGN Meta Modal Styles --- */
      .pgn-settings-btn {
        position: absolute;
        top: 8px;
        right: 8px;
        width: 24px;
        height: 24px;
        background-color: #555;
        border-radius: 4px;
        cursor: pointer;
        display: flex;
        justify-content: center;
        align-items: center;
        z-index: 10;
        opacity: 0.8;
      }
      .pgn-settings-btn:hover { opacity: 1; background-color: #666; }
      .pgn-settings-btn svg { width: 16px; height: 16px; fill: #fff; }
      
      .pgn-meta-modal {
        position: fixed;
        top: 0; left: 0; width: 100vw; height: 100vh;
        background-color: rgba(255, 255, 255, 0.85);
        z-index: 888;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      .pgn-meta-box {
        background: #333; color: #fff;
        padding: 20px; border-radius: 8px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.3);
        width: 90vw; max-width: 500px;
        max-height: 90vh; overflow-y: auto;
        position: relative;
      }
      .pgn-meta-table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 14px; }
      .pgn-meta-table th, .pgn-meta-table td { border: 1px solid #555; padding: 6px; text-align: left; }
      .pgn-meta-input { width: 100%; background: #222; color: #fff; border: 1px solid #444; outline: none; padding: 4px; box-sizing: border-box; }
      .pgn-meta-highlight { color: #ffeb3b; font-weight: bold; }
      .pgn-meta-row.selected { background-color: #555; }
      .pgn-add-btn {
        margin-top: 10px; cursor: pointer; padding: 6px;
        background: #4caf50; color: #fff; text-align: center; font-weight: bold; border-radius: 4px;
      }
      .pgn-meta-trash { cursor: pointer; text-align: center; }
      .pgn-meta-trash:hover { background-color: #f44336; }
```

### 2. 全局變量與 PGN 邏輯初始化
在 `<script>` 的頂部(大約在 `let formatOddStates = { cn: 0, en: 0, engine: 0, url: 0 };` 附近),修改為 `pgn` 並加入 metadata 狀態陣列:

```javascript
      let formatOddStates = {
        cn: 0,
        pgn: 0,
        engine: 0,
        url: 0
      };

      let pgnMetadata = [
        { name: "Event", value: "" },
        { name: "Site", value: "" },
        { name: "Date", value: "" },
        { name: "Round", value: "" },
        { name: "RedTeam", value: "" },
        { name: "Red", value: "" },
        { name: "BlackTeam", value: "" },
        { name: "Black", value: "" },
        { name: "Result", value: "*" },
        { name: "Time", value: "" },
        { name: "TimeControl", value: "" },
        { name: "Opening", value: "" },
        { name: "ECCO", value: "" },
        { name: "Annotator", value: "" },
        { name: "Termination", value: "" }
      ];
      let pgnSelectedRowIndex = -1;
```

### 3. 加入 PGN 設置彈窗及 PGN 文本生成邏輯
將以下方法添加到 `<script>` 中(可以放在 `generateExportText` 或 `generateDPFormat` 的下方):

```javascript
        function showPGNMetaModal() {
          const modal = document.createElement('div');
          modal.className = 'pgn-meta-modal';
          
          const box = document.createElement('div');
          box.className = 'pgn-meta-box';
          
          const closeBtn = document.createElement('button');
          closeBtn.className = 'close-btn';
          closeBtn.style.top = '10px'; closeBtn.style.right = '10px';
          closeBtn.onclick = () => document.body.removeChild(modal);
          
          const title = document.createElement('h3');
          title.textContent = 'PGN Metadata';
          
          const table = document.createElement('table');
          table.className = 'pgn-meta-table';
          const thead = document.createElement('thead');
          thead.innerHTML = `<tr><th>No.</th><th>Name</th><th>Value</th><th class="pgn-meta-trash" id="pgn-clear-all" title="Clear All">🗑️</th></tr>`;
          const tbody = document.createElement('tbody');
          table.appendChild(thead);
          table.appendChild(tbody);

          function renderTable() {
            tbody.innerHTML = '';
            const highlights = ["Event", "Site", "Date", "Round", "Red", "Black", "Result"];
            pgnMetadata.forEach((item, index) => {
              const tr = document.createElement('tr');
              tr.className = 'pgn-meta-row';
              if (pgnSelectedRowIndex === index) tr.classList.add('selected');

              const tdNum = document.createElement('td');
              tdNum.textContent = index + 1;
              tdNum.style.cursor = 'pointer';
              tdNum.onclick = () => {
                if (pgnSelectedRowIndex === -1) {
                  pgnSelectedRowIndex = index;
                } else if (pgnSelectedRowIndex === index) {
                  pgnSelectedRowIndex = -1;
                } else {
                  const movedItem = pgnMetadata.splice(pgnSelectedRowIndex, 1)[0];
                  pgnMetadata.splice(index, 0, movedItem);
                  pgnSelectedRowIndex = -1;
                }
                renderTable();
              };
              tr.appendChild(tdNum);

              const tdName = document.createElement('td');
              const nameInput = document.createElement('input');
              nameInput.className = 'pgn-meta-input';
              if (highlights.includes(item.name)) nameInput.classList.add('pgn-meta-highlight');
              nameInput.value = item.name;
              nameInput.onchange = (e) => item.name = e.target.value;
              tdName.appendChild(nameInput);
              tr.appendChild(tdName);

              const tdValue = document.createElement('td');
              if (item.name === "Result") {
                const sel = document.createElement('select');
                sel.className = 'pgn-meta-input';
                ["*", "1-0", "0-1", "1/2-1/2"].forEach(opt => {
                  const option = document.createElement('option');
                  option.value = opt; option.textContent = opt;
                  if (opt === item.value) option.selected = true;
                  sel.appendChild(option);
                });
                sel.onchange = (e) => item.value = e.target.value;
                tdValue.appendChild(sel);
              } else {
                const valInput = document.createElement('input');
                valInput.className = 'pgn-meta-input';
                valInput.value = item.value;
                valInput.onchange = (e) => item.value = e.target.value;
                tdValue.appendChild(valInput);
              }
              tr.appendChild(tdValue);

              const tdDel = document.createElement('td');
              tdDel.className = 'pgn-meta-trash';
              tdDel.textContent = '🗑️';
              tdDel.onclick = () => {
                pgnMetadata.splice(index, 1);
                if (pgnSelectedRowIndex === index) pgnSelectedRowIndex = -1;
                else if (pgnSelectedRowIndex > index) pgnSelectedRowIndex--;
                renderTable();
              };
              tr.appendChild(tdDel);
              tbody.appendChild(tr);
            });
          }

          renderTable();

          thead.querySelector('#pgn-clear-all').onclick = () => {
            pgnMetadata.forEach(item => {
              if (item.name === "Result") item.value = "*";
              else item.value = "";
            });
            renderTable();
          };

          const addBtn = document.createElement('div');
          addBtn.className = 'pgn-add-btn';
          addBtn.textContent = '+';
          addBtn.onclick = () => {
            pgnMetadata.push({ name: "", value: "" });
            renderTable();
          };

          box.appendChild(closeBtn);
          box.appendChild(title);
          box.appendChild(table);
          box.appendChild(addBtn);
          modal.appendChild(box);
          document.body.appendChild(modal);
        }

        function generatePGNFormat(oddState) {
          let text = '[Game "Chinese Chess"]\n';
          
          pgnMetadata.forEach(meta => {
            if (meta.value && meta.value.trim() !== '') {
              text += `[${meta.name} "${meta.value}"]\n`;
            }
          });

          const initFenPart = INITIAL_FEN.split(' ')[0];
          if (historyFEN.fen.split(' ')[0] !== initFenPart) {
             let fParts = historyFEN.fen.split(' ').slice(0, 6);
             text += `[FEN "${fParts.join(' ')}"]\n`;
          }

          const formatMap = { 0: "Chinese", 1: "ICCS", 2: "WXF" };
          text += `[Format "${formatMap[oddState]}"]\n\n`;

          if (historyFEN.c && historyFEN.c.trim() !== '') {
            text += `{${historyFEN.c.trim()}}\n`;
          }

          let autoResult = "*";
          let resultMeta = pgnMetadata.find(m => m.name === "Result");
          if (resultMeta && resultMeta.value !== "*") {
             autoResult = resultMeta.value;
          } else {
             let lastNode = historyFEN;
             while(lastNode.v && lastNode.v.length > 0) lastNode = lastNode.v[0];
             let vMap = buildVirtualMap(lastNode.fen);
             
             let hasAttack = false;
             for (let id of vMap.values()) {
                 if (['R','N','C','P','r','n','c','p'].includes(id[0])) { hasAttack = true; break; }
             }
             if (!hasAttack) {
                 autoResult = "1/2-1/2";
             } else {
                 let turnNext = lastNode.fen.split(' ')[1] === 'w';
                 let moves = getAllLegalMoves(turnNext, vMap);
                 if (moves.length === 0) autoResult = turnNext ? "0-1" : "1-0";
             }
          }

          function getMoveStr(node, isRed) {
              if (oddState === 0) return NotationConverter.toChinese(node.move);
              if (oddState === 2) return node.move;
              if (oddState === 1 && node.lastMove) {
                  let dc = node.lastMove;
                  return String.fromCharCode(65 + dc.startX) + (9 - dc.startY) + String.fromCharCode(65 + dc.endX) + (9 - dc.endY);
              }
              return node.move;
          }

          function processPGNSequence(startNode, moveNum, isRed) {
            let seqText = '';
            let curr = startNode;

            while (curr.v && curr.v.length > 0) {
              let nextNode = curr.v[0];
              let moveStr = getMoveStr(nextNode, isRed);
              let currentMoveText = '';

              if (isRed) {
                  currentMoveText = `${moveNum}. ${moveStr}`;
              } else {
                  if (curr === startNode && !isRed) currentMoveText = `${moveNum}. ... ${moveStr}`;
                  else currentMoveText = `${moveStr}`;
              }

              seqText += (seqText.length > 0 && !seqText.endsWith('\n') && !isRed ? ' ' : '') + currentMoveText;

              if (nextNode.c && nextNode.c.trim() !== '') {
                  seqText += `\n{${nextNode.c.trim()}}\n`;
              }

              if (curr.v.length > 1) {
                  for (let i = 1; i < curr.v.length; i++) {
                      let bNode = curr.v[i];
                      let bMoveStr = getMoveStr(bNode, isRed);
                      let bStartText = isRed ? `${moveNum}. ${bMoveStr}` : `${moveNum}. ... ${bMoveStr}`;
                      let innerText = bStartText;

                      if (bNode.c && bNode.c.trim() !== '') innerText += `\n{${bNode.c.trim()}}`;

                      let subBranch = processPGNSequence(bNode, isRed ? moveNum : moveNum + 1, !isRed);
                      if (subBranch) innerText += (innerText.endsWith('}') ? '\n' : ' ') + subBranch;

                      seqText += ` (${innerText.trim()})`;
                  }
                  if (!seqText.endsWith('\n')) seqText += '\n';
              }

              curr = nextNode;
              if (!isRed) moveNum++;
              isRed = !isRed;
            }
            return seqText.trim();
          }

          let startIsRed = historyFEN.fen.split(' ')[1] === 'w';
          let initMoveNum = parseInt(historyFEN.fen.split(' ')[5], 10) || 1;
          let bodyText = processPGNSequence(historyFEN, initMoveNum, startIsRed);

          text += bodyText;
          if (!text.endsWith('\n')) text += '\n';
          text += autoResult;

          return text;
        }
```

### 4. 將設置按鈕加入 `renderRecordUI` 內
尋找 `function renderRecordUI()` 並在其中清空內容並加入起始 div 的地方加上生成齒輪按鈕的代碼:

```javascript
        function renderRecordUI() {
          const existingMenu = document.querySelector('.branch-menu');
          if (existingMenu) existingMenu.remove();

          const recordContainer = document.querySelector('.ejceesrecord');
          recordContainer.innerHTML = '';
          
          // 添加 PGN Metadata 設置按鈕
          const settingsBtn = document.createElement('div');
          settingsBtn.className = 'pgn-settings-btn';
          settingsBtn.title = 'PGN Metadata';
          settingsBtn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.73,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.49-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>`;
          settingsBtn.onclick = showPGNMetaModal;
          recordContainer.appendChild(settingsBtn);

          const startDiv = document.createElement('div');
          startDiv.id = 'record-start';
          //... 接後面的程式碼 startDiv.classList.add('ejceesrcdstart');
```

### 5. 替換匯出文本邏輯 `renderExportTextUI` 與全局複製
請在 `renderExportTextUI` 函數中將原有的 `"en"` 及其按鈕完全替換為 `"pgn"`:

```javascript
        // --- Refactored UI for Export Text ---
        function renderExportTextUI() {
          const recordContainer = document.querySelector('.ejceesrecord');
          const commentDiv = document.querySelector('.ejceescomment');
          const btnDiv = document.querySelector('.ejceestextbtn');

          const cnText = generateExportText(false);
          const cnTextWidthComment = generateTextWithComment(false);
          const cnTextWithBranches = generateDPFormat(false);
          
          const pgnTextCh = generatePGNFormat(0);
          const pgnTextICCS = generatePGNFormat(1);
          const pgnTextWXF = generatePGNFormat(2);

          const urlText = generateUrlHash(false);
          const urlTextToggled = generateUrlHash(true);

          // ... (Engine Text Generation 保留原樣) ...

          recordContainer.innerHTML = `<textarea class="ejceescomment-edit" id="export-textarea" style="width:100%; height:100%; resize:none; border:none; outline:none; background:#2a2a2a; color:#fff; padding:8px; font-family:monospace; font-size:14px; white-space:pre-wrap; overflow:auto;"></textarea>`;
          const textarea = document.getElementById('export-textarea');

          // 更新介面選單
          commentDiv.innerHTML = `<div class="exp-fmt-outer">
                <div class="exp-fmt-btn ${lastExportFormat === 'cn' ? 'active' : ''}" data-fmt="cn" data-odd="${
                  formatOddStates['cn']
                }">DP Format</div>
                <div class="exp-fmt-btn ${lastExportFormat === 'pgn' ? 'active' : ''}" data-fmt="pgn" data-odd="${
                  formatOddStates['pgn']
                }">PGN Format</div>
                <div class="exp-fmt-btn ${lastExportFormat === 'engine' ? 'active' : ''}" data-fmt="engine" data-odd="${
                  formatOddStates['engine']
                }">Engine Format</div>
                <div class="exp-fmt-btn ${lastExportFormat === 'url' ? 'active' : ''}" data-fmt="url" data-odd="${
                  formatOddStates['url']
                }">URL Parameter</div>
               </div>`;

          const formatBtns = commentDiv.querySelectorAll('.exp-fmt-btn');

          const updateTextarea = (fmt) => {
            if (fmt === 'cn') {
              if (formatOddStates[fmt] === 2) {
                textarea.value = cnTextWithBranches;
              } else if (formatOddStates[fmt] === 1) {
                textarea.value = cnTextWidthComment;
              } else {
                textarea.value = cnText;
              }
            } else if (fmt === 'pgn') {
              if (formatOddStates[fmt] === 2) {
                textarea.value = pgnTextWXF;
              } else if (formatOddStates[fmt] === 1) {
                textarea.value = pgnTextICCS;
              } else {
                textarea.value = pgnTextCh;
              }
            } else if (fmt === 'engine') {
              // ... 原來的 Engine 邏輯
            } else if (fmt === 'url') {
              textarea.value = formatOddStates[fmt] === 1 ? urlTextToggled : urlText;
            }
            setTimeout(() => textarea.select(), 12);
          };

          formatBtns.forEach((btn) => {
            btn.addEventListener('click', () => {
              const wasActive = btn.classList.contains('active');
              const fmt = btn.getAttribute('data-fmt');
              lastExportFormat = fmt;

              formatBtns.forEach((b) => b.classList.remove('active'));
              btn.classList.add('active');

              if (wasActive) {
                if (fmt === 'cn' || fmt === 'pgn') {
                  formatOddStates[fmt] = (formatOddStates[fmt] + 1) % 3;
                } else {
                  formatOddStates[fmt] = formatOddStates[fmt] === 1 ? 0 : 1;
                }
              }
              btn.setAttribute('data-odd', formatOddStates[fmt]);
              updateTextarea(fmt);
              
              textarea.blur();
              setTimeout(() => { textarea.focus(); textarea.select(); }, 12);
            });
          });
          
          updateTextarea(lastExportFormat);

          // ... btnDiv 設置 (原有 btn-confirm, btn-copy 等保留)
```

並對應修改全域 `performCopy` 的邏輯以適應 `pgn`:

```javascript
        // Global functions for copy and paste
        function performCopy() {
          let textToCopy = '';
          const fmt = lastExportFormat || 'cn';
          const odd = formatOddStates[fmt] || 0;

          if (fmt === 'cn') {
            if (odd === 2) textToCopy = generateDPFormat(false);
            else if (odd === 1) textToCopy = generateTextWithComment(false);
            else textToCopy = generateExportText(false);
          } else if (fmt === 'pgn') {
            textToCopy = generatePGNFormat(odd);
          } else if (fmt === 'engine') {
             // ... 原有 engine copy 邏輯 ...
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

一,沒有見到pgn-setting-btn,請查明原因。

二,[result]一開始並不會出現,只有在pgn-setting中填入之後才會出現。

代码: 全选

這兩個問題主要涉及按鈕的渲染時機以及 PGN 標籤的過濾邏輯。以下是修正後的代碼片段:

### 一、 解決 `pgn-settings-btn` 不見的問題

原因通常是 `renderRecordUI` 或 `renderExportTextUI` 在切換介面時,使用了 `innerHTML = ''` 清空了容器,導致按鈕被刪除。

**1. 修改 CSS:** 確保容器有定位基點。
```css
.ejceesrecord {
  position: relative; /* 確保按鈕能相對於此容器定位 */
}
```

**2. 建立一個獨立的按鈕渲染函數:**
在 `renderRecordUI` 和 `renderExportTextUI` 的開頭都調用它。

```javascript
function ensurePGNSettingsBtn() {
  let recordContainer = document.querySelector('.ejceesrecord');
  if (!recordContainer.querySelector('.pgn-settings-btn')) {
    const settingsBtn = document.createElement('div');
    settingsBtn.className = 'pgn-settings-btn';
    settingsBtn.title = 'PGN Metadata';
    settingsBtn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.73,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.49-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>`;
    settingsBtn.onclick = (e) => {
      e.stopPropagation();
      showPGNMetaModal();
    };
    recordContainer.appendChild(settingsBtn);
  }
}
```

然後在 `renderRecordUI` 的結尾處加入 `ensurePGNSettingsBtn();`。

---

### 二、 解決 [Result] 標籤顯示邏輯

請修改 `generatePGNFormat` 函數中的 Metadata 處理部分,並初始化 `Result` 的值為空字串或特定預設。

**1. 初始化調整:**
將 `pgnMetadata` 中的 Result 預設值改為空(或者檢查它是否被修改過)。

```javascript
// 修改後的 pgnMetadata
let pgnMetadata = [
  // ... 其他項
  { name: "Result", value: "" }, // 初始為空
  // ... 其他項
];
```

**2. 修改 `generatePGNFormat` 邏輯:**
只有在 `value` 非空時才生成 `[Tag "Value"]`。

```javascript
function generatePGNFormat(oddState) {
  let text = '[Game "Chinese Chess"]\n';
  
  // 記錄用戶是否有填寫 Result
  let userResult = "";

  pgnMetadata.forEach(meta => {
    // 只有當 value 不為空時才加入標籤
    if (meta.value && meta.value.trim() !== '') {
      text += `[${meta.name} "${meta.value}"]\n`;
      if (meta.name === "Result") {
        userResult = meta.value.trim();
      }
    }
  });

  // FEN 邏輯保持不變...
  const initFenPart = INITIAL_FEN.split(' ')[0];
  if (historyFEN.fen.split(' ')[0] !== initFenPart) {
     let fParts = historyFEN.fen.split(' ').slice(0, 6);
     text += `[FEN "${fParts.join(' ')}"]\n`;
  }

  const formatMap = { 0: "Chinese", 1: "ICCS", 2: "WXF" };
  text += `[Format "${formatMap[oddState]}"]\n\n`;

  // 總評...
  if (historyFEN.c && historyFEN.c.trim() !== '') {
    text += `{${historyFEN.c.trim()}}\n`;
  }

  // 結局符號邏輯:優先使用用戶填寫的 userResult,若無則自動計算
  let finalTerminator = "*";
  if (userResult !== "") {
    finalTerminator = userResult;
  } else {
    // 自動計算邏輯 (簡化示例)
    let lastNode = historyFEN;
    while(lastNode.v && lastNode.v.length > 0) lastNode = lastNode.v[0];
    let vMap = buildVirtualMap(lastNode.fen);
    let hasAttack = false;
    for (let id of vMap.values()) {
        if (['R','N','C','P','r','n','c','p'].includes(id[0])) { hasAttack = true; break; }
    }
    if (!hasAttack) {
        finalTerminator = "1/2-1/2";
    } else {
        let turnNext = lastNode.fen.split(' ')[1] === 'w';
        let moves = getAllLegalMoves(turnNext, vMap);
        if (moves.length === 0) finalTerminator = turnNext ? "0-1" : "1-0";
    }
  }

  // 棋譜正文生成...
  let startIsRed = historyFEN.fen.split(' ')[1] === 'w';
  let initMoveNum = parseInt(historyFEN.fen.split(' ')[5], 10) || 1;
  let bodyText = processPGNSequence(historyFEN, initMoveNum, startIsRed);

  text += bodyText;
  if (!text.endsWith('\n')) text += '\n';
  text += finalTerminator; // 最後一行顯示結局符號

  return text;
}
```

### 三、 彈窗 (Modal) 的小修正
為了確保用戶在彈窗中選擇時能正確觸發「有值」,請確保 `Result` 的 Select 選項中第一個是空值或 `*` 且視為「未填寫」。

```javascript
// 在 showPGNMetaModal 的 renderTable 內
if (item.name === "Result") {
  const sel = document.createElement('select');
  sel.className = 'pgn-meta-input';
  // 增加一個空選項表示不顯示標籤
  ["", "*", "1-0", "0-1", "1/2-1/2"].forEach(opt => {
    const option = document.createElement('option');
    option.value = opt; 
    option.textContent = opt === "" ? "(Auto/Hide Tag)" : opt;
    if (opt === item.value) option.selected = true;
    sel.appendChild(option);
  });
  sel.onchange = (e) => {
    item.value = e.target.value;
    // 關閉窗口時會自動重新生成文本,這裡可視需求觸發刷新
  };
  tdValue.appendChild(sel);
}
```

這樣修改後:
1. **按鈕**:會始終附著在 `ejceesrecord` 的右上角,無論內容如何清空重繪。
2. **Result 標籤**:只有當你在設定中選擇了特定的值(非空)後,`[Result "..."]` 才會出現在 PGN 的開頭標籤區。
3. **結尾**:PGN 最後一行會同步顯示該 Result 的值。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5847
注册时间: 2022年 11月 18日 17:36
联系:

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

當第二次點擊「PGN Format」時,它將切換為[Format "ICCS"],這種格式跟engine格式差不多,但改為大寫,且中間加一個「-」,比如「H2-E2」。

在按confirm導入文本時,新增對pgn格式的支援。它的特點是:

一,以[Game "Chinese Chess"]開頭。

二,在「總評」前有各種標籤,如「[Event "…"]
[Site "…"]
[Date "2004.01.05"]
[Round "…"]
[RedTeam "…"]
[Red "…"]」等。

把這些標籤的值都填到pgnMetadata中。

三,評論都會寫在大括號{}中。

四,分支都會放在小括號()裡。

當導入後,如果再導出成其它格式,則所有有值的標籤都會放在總論的評論前,每行改為「name:value」。其末尾的標識將作為最後一步棋的評論,其中「1-0」將轉為「紅勝」,「0-1」將轉為「黑勝」,「1/2-1/2」將轉為「平局」,「*」則等於沒有。

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

Re: 將製作ejcees(中國象棋打譜程式)

帖子 ejsoon »

gemini pro:

代码: 全选

根據您的需求,我們需要修改 PGN 的導出格式(ICCS 帶連字符)、新增 PGN 的導入解析邏輯,並在導出非 PGN 格式時,將 Metadata 標籤轉換為註釋文字。

以下是針對您要求的修改方案:

### 1. 修改 `generatePGNFormat` (支援連字符 ICCS)

請在 `generatePGNFormat` 函數中修改 `getMoveStr` 的邏輯:

```javascript
          function getMoveStr(node, isRed) {
              if (oddState === 0) return NotationConverter.toChinese(node.move);
              if (oddState === 2) return node.move;
              if (oddState === 1 && node.lastMove) {
                  let dc = node.lastMove;
                  // 修改點:改為大寫且中間加「-」,如 H2-E2
                  let start = String.fromCharCode(65 + dc.startX) + (9 - dc.startY);
                  let end = String.fromCharCode(65 + dc.endX) + (9 - dc.endY);
                  return (start + "-" + end).toUpperCase();
              }
              return node.move;
          }
```

---

### 2. 修改非 PGN 格式導出邏輯 (將標籤轉為註釋)

為了讓 DP Format 或其他格式能包含 Metadata,我們需要一個輔助函數來整合這些資訊。

```javascript
        // 輔助函數:獲取整合了 Metadata 的總評
        function getMetaRootComment() {
          let metaHeader = "";
          pgnMetadata.forEach(m => {
            if (m.value && m.value.trim() !== "" && m.name !== "Result") {
              metaHeader += `${m.name}:${m.value}\n`;
            }
          });
          let rootC = (historyFEN.c || "").trim();
          return metaHeader ? (metaHeader + (rootC ? "\n" + rootC : "")) : rootC;
        }

        // 輔助函數:處理最後一步的結果評論
        function appendResultToLastComment(lastNode) {
          let resSymbol = pgnMetadata.find(m => m.name === "Result")?.value || "*";
          let mapping = { "1-0": "紅勝", "0-1": "黑勝", "1/2-1/2": "平局" };
          let resText = mapping[resSymbol] || "";
          
          let existingC = (lastNode.c || "").trim();
          if (!resText) return existingC;
          return existingC ? (existingC + "\n" + resText) : resText;
        }
```

**然後修改 `generateDPFormat` 等函數(範例以 DP 為主):**
1. 將原本讀取 `historyFEN.c` 的地方改為讀取 `getMetaRootComment()`。
2. 在遍歷到最後一個節點時,調用 `appendResultToLastComment`。

---

### 3. 新增 PGN 導入解析邏輯 (`parsePGN`)

在 `<script>` 中加入以下解析器:

```javascript
        function importPGN(input) {
          // 1. 清空現有 Metadata
          pgnMetadata.forEach(m => m.value = (m.name === "Result" ? "*" : ""));

          // 2. 解析標籤 [Name "Value"]
          const tagRegex = /\[(\w+)\s+"([^"]*)"\]/g;
          let match;
          let lastIndex = 0;
          while ((match = tagRegex.exec(input)) !== null) {
            let key = match[1], val = match[2];
            let metaObj = pgnMetadata.find(m => m.name === key);
            if (metaObj) metaObj.value = val;
            else pgnMetadata.push({ name: key, value: val });
            lastIndex = tagRegex.lastIndex;
          }

          // 3. 提取棋譜主體 (移除所有標籤後的部分)
          let body = input.slice(lastIndex).trim();
          
          // 提取最後的結果標識 (1-0, 0-1, 1/2-1/2, *)
          const resMatch = body.match(/(1-0|0-1|1\/2-1\/2|\*)$/);
          if (resMatch) {
            let res = resMatch[1];
            let metaObj = pgnMetadata.find(m => m.name === "Result");
            if (metaObj) metaObj.value = res;
            body = body.slice(0, -res.length).trim();
          }

          // 4. 使用狀態機解析著法、註釋、分支
          function parseMoves(str) {
            let nodes = [];
            let i = 0;
            while (i < str.length) {
              let char = str[i];
              if (char === '{') { // 解析註釋
                let end = str.indexOf('}', i);
                let comment = str.slice(i + 1, end).trim();
                if (nodes.length > 0) nodes[nodes.length - 1].c = comment;
                else rootComment = comment; // 如果是第一個著法前的註釋
                i = end + 1;
              } else if (char === '(') { // 解析分支
                let count = 1, j = i + 1;
                while (count > 0 && j < str.length) {
                  if (str[j] === '(') count++;
                  if (str[j] === ')') count--;
                  j++;
                }
                let branchStr = str.slice(i + 1, j - 1);
                if (nodes.length > 0) nodes[nodes.length - 1].v.push(parseMoves(branchStr)[0]);
                i = j;
              } else if (/[0-9a-zA-Z\u4e00-\u9fa5]/.test(char)) { // 著法
                let end = str.search(/[\s\(\{\}]/, i);
                if (end === -1) end = str.length;
                let movePart = str.slice(i, end);
                // 排除步數序號如 "1." 或 "1..."
                if (!movePart.includes('.')) {
                  nodes.push({ move: movePart, c: '', v: [] });
                }
                i = end;
              } else { i++; }
            }
            return nodes;
          }

          let rootComment = "";
          // 這裡需要根據解析出的 nodes 重新構建 historyFEN 結構 (遞歸建立)
          // 由於 PGN 著法可能是中文或 ICCS,導入時需調用您現有的 movePiece 邏輯來驗證並生成 FEN
          // 此處簡化邏輯:
          const moveList = parseMoves(body);
          // (實作建議:遍歷 moveList,利用 executeMove(moveStr) 自動生成各節點的 FEN)
          return { moves: moveList, rootComment: rootComment };
        }
```

---

### 4. 修改 `handleConfirm` (支援 PGN 導入)

尋找按鈕的點擊事件(通常在 `renderExportTextUI` 裡面的 `btn-confirm`):

```javascript
          confirmBtn.addEventListener('click', () => {
            const input = textarea.value.trim();
            if (input.startsWith('[Game "Chinese Chess"]')) {
              // PGN 導入邏輯
              const result = importPGN(input);
              // 重置棋盤並遞歸載入 moveList...
              // (需要調用您的 applyHistory 邏輯)
              alert('PGN 導入成功');
              renderRecordUI();
            } else {
              // 原有的導入邏輯 (JSON 或 DP)
              // ...
            }
          });
```

### 總結修改點:
1. **ICCS 格式**:在 `generatePGNFormat` 的 `oddState === 1` 處強轉大寫並加 `-`。
2. **導入偵測**:在 `confirm` 時檢查 `[Game "Chinese Chess"]` 關鍵字。
3. **標籤處理**:導入時更新 `pgnMetadata`;導出非 PGN 時,將 `pgnMetadata` 內容(除 Result 外)拼接至 `historyFEN.c` 之前,並將 `Result` 轉換為中文字串附在最後一個節點的 `c` 欄位。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

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