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

分享和讨论Javascript相关的话题
回复
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5428
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

改進:
在pgn-meta-modal窗口中,增加一個「Review」,指對局終的點評。

例如:
[Game "Chinese Chess"]
[Result "0-1"]
[Rotate "true"]
[Format "Chinese"]

{吳可欣贏網友一盤棋}
1. 炮八平五 馬2進3
2. 兵七進一 車1平2
3. 馬八進七 卒7進1
4. 車九平八 馬8進7
5. 馬二進一 車9進1
6. 炮二平四{估計是這步棋使局勢急轉直下,因為它導致紅方右車完全出不來}
馬7進8
7. 炮四進三 卒7進1
8. 兵三進一 馬8進6
9. 馬七進六 車9平4
{吳姐說希望他踩中兵}
10. 馬六進五 馬3進5
11. 炮五進四 車4進3
12. 兵三進一 車4平5
{吳姐說差點走馬6進8}
13. 車一平二 砲8進4
14. 炮五平四 車5進2
15. 仕四進五 馬6進4
16. 車八進六 馬4進3
17. 帥五平四 砲8平6
18. 前炮平六 砲6退1
19. 炮六退五 車5平6
20. 帥四平五 車6平4
21. 帥五平四 車4進2
22. 車二進四 車4退2
23. 車二平四 砲2平6
24. 車八進三 砲6進3
{紅方認負}
0-1
{吳可欣正常發揮,拿下對手}

則「紅方認負」是最後一步棋的評論,而「吳可欣正常發揮,拿下對手」是Review」。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。


gemini pro:

代码: 全选

Here are the modifications needed to support the post-game "Review" feature. All code and comments are provided in English as requested.

### 1. Update Constructor
Add the `Review` field to the default PGN metadata arrays so it appears in the `pgn-meta-modal`.

**Find this part in `constructor(container, mode)`:**
```javascript
    this.pgnMetadata = [
      { name: 'Event', value: '' },
      { name: 'Site', value: '' },
      // ... (other fields)
      { name: 'Annotator', value: '' },
      { name: 'Termination', value: '' }
    ];
    this.defaultPgnKeys = [
      'Event',
      'Site',
      // ... (other fields)
      'Annotator',
      'Termination'
    ];
```

**Change it to:**
```javascript
    this.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: 'Variation', value: '' },
      { name: 'ECCO', value: '' },
      { name: 'Annotator', value: '' },
      { name: 'Termination', value: '' },
      { name: 'Review', value: '' } // Added Review field
    ];
    this.defaultPgnKeys = [
      'Event',
      'Site',
      'Date',
      'Round',
      'RedTeam',
      'Red',
      'BlackTeam',
      'Black',
      'Result',
      'Time',
      'TimeControl',
      'Opening',
      'Variation',
      'ECCO',
      'Annotator',
      'Termination',
      'Review' // Added Review field
    ];
```

---

### 2. Update PGN Parser
Modify the PGN import logic to extract the trailing review comment that appears *after* the game result.

**Find this part in `importExportedText(text, importedFen = null)`:**
```javascript
      // Extract notation body
      let bodyStartIndex = text.lastIndexOf(']');
      let bodyText = text.substring(bodyStartIndex + 1).trim();
      bodyText = bodyText.replace(/(?:1-0|0-1|1\/2-1\/2|\*)$/, '').trim();
```

**Change it to:**
```javascript
      // Extract notation body
      let bodyStartIndex = text.lastIndexOf(']');
      let bodyText = text.substring(bodyStartIndex + 1).trim();

      // Extract trailing Review comment if it exists after the result
      const reviewRegex = /(?:1-0|0-1|1\/2-1\/2|\*)\s*\{([^}]*)\}\s*$/;
      const reviewMatch = bodyText.match(reviewRegex);
      if (reviewMatch) {
        const existingReview = this.pgnMetadata.find(m => m.name === 'Review');
        if (existingReview) {
          existingReview.value = reviewMatch[1].trim();
        } else {
          this.pgnMetadata.push({ name: 'Review', value: reviewMatch[1].trim() });
        }
        // Remove the result and review part from bodyText
        bodyText = bodyText.replace(reviewRegex, '').trim();
      }

      bodyText = bodyText.replace(/(?:1-0|0-1|1\/2-1\/2|\*)$/, '').trim();
```

---

### 3. Update PGN Generator
Modify the PGN generation logic to prevent `Review` from being printed as a standard tag at the top, and instead append it in brackets at the very end of the file.

**Find the beginning of `generatePGNFormat(oddState)`:**
```javascript
  generatePGNFormat(oddState) {
    let text = '[Game "Chinese Chess"]\n';
    let userResult = '';

    this.pgnMetadata.forEach(meta => {
      let val = meta.value ? meta.value.trim() : '';
      let isResultEmpty = meta.name === 'Result' && val === '*';
      if (val !== '' && !isResultEmpty) {
        text += `[${meta.name} "${val}"]\n`;
        if (meta.name === 'Result') userResult = val;
      }
    });
```

**Change it to:**
```javascript
  generatePGNFormat(oddState) {
    let text = '[Game "Chinese Chess"]\n';
    let userResult = '';
    let reviewText = '';

    this.pgnMetadata.forEach(meta => {
      let val = meta.value ? meta.value.trim() : '';
      let isResultEmpty = meta.name === 'Result' && val === '*';
      if (val !== '' && !isResultEmpty) {
        if (meta.name === 'Review') {
          reviewText = val; // Store review text to append at the end
        } else {
          text += `[${meta.name} "${val}"]\n`;
          if (meta.name === 'Result') userResult = val;
        }
      }
    });
```

**Then, find the end of `generatePGNFormat(oddState)`:**
```javascript
    let startIsRed = this.historyFEN.fen.split(' ')[1] === 'w';
    let initMoveNum = parseInt(this.historyFEN.fen.split(' ')[5], 10) || 1;
    text += processPGNSequence(this.historyFEN, initMoveNum, startIsRed);
    if (!text.endsWith('\n')) text += '\n';
    return text + finalTerminator;
  }
```

**Change it to:**
```javascript
    let startIsRed = this.historyFEN.fen.split(' ')[1] === 'w';
    let initMoveNum = parseInt(this.historyFEN.fen.split(' ')[5], 10) || 1;
    text += processPGNSequence(this.historyFEN, initMoveNum, startIsRed);
    if (!text.endsWith('\n')) text += '\n';
    text += finalTerminator;

    // Append Review comment at the very end if it exists
    if (reviewText !== '') {
      text += `\n{${reviewText}}`;
    }

    return text;
  }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5428
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

東萍分支導入出錯:

代码: 全选

      for (let i = 0; i < lines.length; i++) {
        let line = lines[i];
        if (!line) continue;

        if (!/^\d+\./.test(line)) {
          // Rule 3: Lines not starting with "^\d+\." are comments or branch declarations
          let circles = line.match(/[①-⑳]|\(\d+\)/g) || [];
          let isBranchDecl = false;

          // Rule 4 & 5: Find the branch insertion point (second occurrence)
          for (let c of circles) {
            console.log('(circles, branchMap[c] ,c)', circles, branchMap[c] ,c);
            if (seenCircles.has(c) && branchMap[c]) {
              currNode = branchMap[c].parentNode;
              currentFen = branchMap[c].fen;
              vMap = this.buildVirtualMap(currentFen);
              currentCommentNode = currNode;
              isBranchDecl = true;
              delete branchMap[c]; // Only care about the first two occurrences
              break; // Switch context once per line
            }
          }

          // Accumulate comments
          if (!isBranchDecl) {
            if (!currentCommentNode.c) currentCommentNode.c = '';
            currentCommentNode.c += (currentCommentNode.c ? '\n' : '') + line;
          }
        } else {
          // Rule 1 & 2: Line starting with "^\d+\.", strip the number, then split by spaces
          let cleanLine = line.replace(/^\d+\./, '').trim();
          let tokens = cleanLine.split(/[\s,,!!??;;]+/);

          for (let token of tokens) {
            // Ignore standalone ellipses
            if (token === '...' || token === '…') continue;

            let circles = line.match(/[①-⑳]|\(\d+\)/g) || [];
            // Strip circled numbers and ellipses to get the pure move string
            let moveStr = token.replace(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳…\.]|\(\d+\)/g, '');

            console.log(moveStr, currentFen);
            if (moveStr.length >= 4) {
              let dc = null;
              let moveEn = '';
              let isRed = currentFen.includes(' w');

              if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
                let startX = moveStr.charCodeAt(0) - 97;
                let startY = 9 - parseInt(moveStr.charAt(1), 10);
                let endX = moveStr.charCodeAt(2) - 97;
                let endY = 9 - parseInt(moveStr.charAt(3), 10);
                dc = {
                  startX,
                  startY,
                  endX,
                  endY
                };

                let pId = vMap.get(`${startX},${startY}`);
                if (!pId) {
                  hasError = true;
                  continue;
                }
                moveEn = this.coordsToEngine(startX, startY, endX, endY);
              } else {
                let isEnglish = /^[a-zA-Z]/.test(moveStr) || /^[+\-=1-9][a-zA-Z0-9]/.test(moveStr);
                moveEn = isEnglish ? moveStr : this.NotationConverter.toEnglish(moveStr, isRed);
                if (/^[RNBAKCPrnbakcp+\-=1-9]{2}[+\-=][1-9]/.test(moveEn)) {
                  dc = this.deriveCoordsFromPgnMove(currentFen, moveEn, isRed);
                } else {
                  dc = moveEn;
                }
              }

              if (moveEn === dc) {
                console.log('is here currentFen', currentFen);
                if (!currentCommentNode.c) {
                  currentCommentNode.c = '';
                }
                // Append the token, separating with a space if a comment already exists
                currentCommentNode.c += (currentCommentNode.c ? ' ' : '') + token;
                continue; // Skip node creation and continue parsing
              }

              if (!dc) {
                const cleanInitial = this.INITIAL_FEN.replace(/ - - 0 1$/, '');
                if (importedFen !== cleanInitial && importedFen !== this.INITIAL_FEN) {
                  this.importExportedText(text, this.INITIAL_FEN);
                  return;
                } else {
                  alert('Parsed partially due to an invalid move notation.');
                  this.renderRecordUI();
                  this.renderNoteUI();
                  this.updateToolHighlights();
                  return;
                }
              }

              // Rule 4: The variation is attached to the parent of the move
              lastMoveParent = currNode;
              lastMoveParentFen = currentFen;

              let nextFen = this.simulateMove(currentFen, dc);
              let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);

              if (childIdx === -1) {
                let newNode = {
                  fen: nextFen,
                  move: this.coordsToEngine(dc.startX, dc.startY, dc.endX, dc.endY),
                  c: '',
                  v: []
                };
                currNode.v.push(newNode);
                childIdx = currNode.v.length - 1;
              }

              // If this.this creates a fork on the main path, update this.currentBranch
              if (currNode.v.length > 1 && currNode === attachNode) {
                this.currentBranch[forkCount] = childIdx;
                forkCount++;
                attachNode = currNode.v[childIdx]; // Follow the first parsed line as the main path
              }

              currNode = currNode.v[childIdx];
              currentFen = nextFen;
              currentCommentNode = currNode;

              let pId = vMap.get(`${dc.startX},${dc.startY}`);
              vMap.delete(`${dc.startX},${dc.startY}`);
              if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
            } else {
              // lastMoveParent = currNode;
              // lastMoveParentFen = currentFen;
            }

            // Rule 4, 7 & 8: Process multiple circled numbers wherever they occur
            for (let c of circles) {
              if (!seenCircles.has(c)) {
                console.log('first c', c);
                seenCircles.add(c);
                // Map the first occurrence to the parent node of the currently parsed move
                console.log('lastMoveParent, lastMoveParentFen', lastMoveParent, lastMoveParentFen);
                branchMap[c] = {
                  parentNode: lastMoveParent,
                  fen: lastMoveParentFen
                };
              }
            }
          }
        }
      }



當我用以上的js程式導入以下文本:
「
吳可欣翻車現場

表面上看,吳姐似乎是因為失誤翻車的,實際上走到最後已經是必敗無疑了
1. 兵七進一 卒7進1
2. 炮二平三 砲8平5
3. 馬八進七 馬8進7
4. 相七進五 馬7進6
5. 仕六進五 車9平8
6. 馬二進一 馬2進3
吳姐說,只要你堅持一直做一件事情,你就一定會成功的。但是呢,還要學會總結。
7. 車九平六 砲2平1
吳姐說,今天的翻車已經結束了,再也不能讓你們看到我翻車的名場面了,這個不允許啊
8. 車一進一  車1平2
9. 車一平四 馬6進5
10. 馬七進五 砲5進4
吳姐說,這個棋我以前是不是輸給王子涵啊?有點像…

吳姐很想車六進三
11. 兵一進一
AI也是這樣想的
11. ………… 車2進6
吳姐說,這個人壞滴很!
12. 車四進三
吳姐說,這個棋我們應該還是要猛起來,我們一兇,對面就慫了
12. ………… 砲1進4 ②
13. 馬一進二
吳姐說這棋應該已經算明白了
13. ………… 砲1退1
14. 兵七進一 砲1平8
15. 兵七進一 砲8進4
16. 兵七進一 車8進8
吳姐說剛才是算明白了,但是女人是善變的
17. 車六進一 車2平3
18. 炮八平七 車8平7 ④
黑方應該吃兵,否則紅方帥坐出就會反先
19. 帥五平六 


②
12. ………… 象7進5
24層-59分
13. 馬一進二 士6進5
14. 炮三平二 車8平7

④
18. ………… 車3退4
25層195分
19. 車六進二 砲5退2
20. 車六平五 車3平2
21. 炮七平八 車8平7
22. 車四平二 砲8平9
」

如果去掉注釋:
              // lastMoveParent = currNode;
              // lastMoveParentFen = currentFen;
④就不能導入,如果注釋的話,②就無法正確導入。

請說明原因,並提出修改辦法。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5428
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

grok出馬:

代码: 全选

**問題診斷正確**。

目前程式碼的邏輯在這一行有明顯缺陷:

```js
lastMoveParent = currNode;           // 這一步執行得太晚
```

當分支第一次出現在**本步走法之後**(如 `12. ... 砲1進4①`),`lastMoveParent` 還沒來得及更新,就已經用舊值記錄到 `branchMap` 了,導致後續第二次出現圈號時跳錯位置。

---

### 正確的語意應該是:

> **圈號①出現在哪一步的後面,就代表該分支是從「這一步」之後產生的變招。**

因此,`branchMap[c]` 應該記錄的是**剛剛處理完的這一步**(`currNode`),而不是它的父節點。

### 建議修正方式(推薦)

把圈號處理的時機**移到建立新節點之後**,並且明確記錄「當前走完的節點」作為分支插入點。

#### 修改後的核心片段:

```js
for (let token of tokens) {
    if (token === '...' || token === '…') continue;

    let circles = line.match(/[①-⑳]|\(\d+\)/g) || [];
    let moveStr = token.replace(/[①-⑳…\.]|\(\d+\)/g, '');

    // ... 原有的走法解析、建立節點的程式碼 ...

    if (moveStr.length >= 4) {
        // ... 所有建立新節點的邏輯保持不變 ...

        // === 重點修改:成功走一步後再處理圈號 ===
        if (dc && moveEn !== dc) {   // 成功建立走法的情況
            lastMoveParent = currNode;           // ← 現在 currNode 已經是新建立的這一步
            lastMoveParentFen = currentFen;

            // Rule 4,7,8:處理圈號
            for (let c of circles) {
                if (!seenCircles.has(c)) {
                    seenCircles.add(c);
                    branchMap[c] = {
                        parentNode: lastMoveParent,   // 這一步就是分支的父節點
                        fen: lastMoveParentFen
                    };
                    console.log(`[Branch] First occurrence of ${c} attached to move: ${moveEn}`);
                }
            }
        }
    } 
    else {
        // 無法解析的 token 當註解處理...
    }

    // === 移除原本放在 for token 迴圈最後的圈號處理 ===
}
```

---

### 額外建議優化

1. **把 `circles` 從 `line` 改成從 `token` 提取**(更精準):
   ```js
   let circles = token.match(/[①-⑳]|\(\d+\)/g) || [];
   ```

2. **增加對「... ①」這種情況的支援**(省略號後直接標分支):
   ```js
   // 在處理 token 前先檢查整行是否有圈號但沒有實質走法
   if (circles.length && cleanLine.includes('...')) {
       // 直接把分支掛在 currNode(前一步)
       for (let c of circles) {
           if (!seenCircles.has(c)) {
               seenCircles.add(c);
               branchMap[c] = { parentNode: currNode, fen: currentFen };
           }
       }
   }
   ```

3. **第二次出現時的處理**(建議加上防護):
   ```js
   if (seenCircles.has(c) && branchMap[c]) {
       currNode = branchMap[c].parentNode;
       currentFen = branchMap[c].fen;
       vMap = this.buildVirtualMap(currentFen);
       currentCommentNode = currNode;
       delete branchMap[c];
       isBranchDecl = true;
       // 可選:break; 改成 continue; 讓一行可以有多個分支切換
   }
   ```

---

### 為什麼原本會錯?

- 原程式把 `lastMoveParent = currNode` 放在**走法處理前面**。
- 當這行是 `12. ... 砲1進4①` 時,`currNode` 還是第11步,導致分支被錯誤掛在第11步。

修正後,只要**成功走完一步**,就把 `currNode` 當作分支父節點,就符合「圈號出現在哪步後面,分支就從哪步開始」的直覺。

---

需要我幫你把**完整修正後的迴圈**重新整理出來嗎?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5428
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

ejceesobj202605021716

修復了東萍多分支導入問題。

還得是gemini pro啊!!!
附件
ejceesobj202605021716.js
(305.48 KiB) 尚未被下载
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5428
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

mini模式的ctrl+右,不太對。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

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