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

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

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

帖子 ejsoon »

ejceesobj202604291303

解決pgn格式對於C4-C3的導入問題,解決Ecs不能退出兩個「pgn設置窗口」和「導出svg窗口」的問題。
附件
ejceesobj202604291303.js
(298.43 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

https://www.xqdao.com/qipu/show/68358/

這裡有WXF格式示例:

代码: 全选

[Game "Chinese Chess"]
[FEN "5k3/9/3P2P2/3P2P2/3P5/9/9/9/9/3K5 b - - 0 1"]
[Format "WXF"]
1.      K6.5
2. Pc+1 K5.6
3. Pa+1 K6.5
4. Pd.7 K5.6
5. P7.6 K6.5
6. Pd.5 K5.6
7. Pd.5 K6.5
8. Pd.6 K5.6
9. P5.6 K6.5
10. Pc.7 K5.6
11. P7.6 K6.5
12. Pd+1 K5.6
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

改進:「this.historyFEN」的move將改為「engine format」,即把「C2=5」改為「h2e2」。去掉lastMove,因為move已經是在描述棋子移動的起始和終點坐標。

代码: 全选

改進:
1.「this.historyFEN」的move將改為「engine format」,即把「C2=5」改為「h2e2」。去掉lastMove,因為move已經是在描述棋子移動的起始和終點坐標。

2.為「this.historyFEN」的根節點加上r和f,表示rotate和flip,值為0或1。當this.isRotateEnabled和this.isFlipEnabled發生改變時,將同時改變「this.historyFEN」的根節點的r和f值。取消改變fen後面的「- -」。

3.在保存JSON格式時,如果是選取範圍,則需要為這個範圍的根節點加上r和f屬性。

4.在導出文本時,把PGN格式移至第一位,跟DP格式交換位置。如果this.isRotateEnabled或this.isFlipEnabled為true,則加上標籤[rotate "true"]或[flip "true"]。

5.導出文本的核心邏輯將要重寫。

把engine format轉換為「PGN 中文」的邏輯改為:

如果有兩個兵在同一列,其餘的兵都不在同一列,則這兩個兵將稱為「前兵」和「後兵」;
如果有三個兵在同一列,其餘的兵都不在同一列,則這三個兵將稱為「前兵」、「中兵」和「後兵」;
如果有四五個兵在同一列,則從前到後稱它們為「一兵、二兵、三兵、四兵、五兵」;
如果有兩三個兵在同一列,另兩三個兵也同在另一列,則從最右列的前面開始數,數完再到左列,稱它們為「一兵、二兵、三兵、四兵、五兵」。

「PGN WXF」的邏輯將改為:

在中文的基礎上,把「車馬炮相士帥兵」都轉為英文字母「RNCBAKP」,紅方和黑方都是大寫字母。「進平退」轉成「+.-」。如果有「前馬、前炮、前兵」等,代表棋子的字母始終在第一位,把「前中後」換成「+.-」,比如「前車進一」轉成「R++1」。當存在「一兵、二兵、三兵、四兵、五兵」時,將把「一二三四五」轉成「abcde」,P始終在第一位,比如「三兵平一」轉成「Pc.1」。

「PGN ICCS」的邏輯不變,即把engine format的字母變大寫,再在起始和終點坐標之間加上「-」連接,比如「h2e2」轉成「H2-e2」。

其餘三種格式的邏輯不變,但是注意現在是engine format為this.historyFEN的核心,所以邏輯都要重寫。

6.在ejceesrecord中顯示的著法將是PGN中文,而不再是DP format。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
附件
ejceesobj202604300954.js
(299.52 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

gemini pro愛啃硬的,吃硬不吃軟:


代码: 全选

Here are the required modifications to implement the new engine format, rotate/flip root logic, text export features, and the updated PGN Chinese/WXF notations.

### 1. Update `constructor` (Initialization and FEN format)
Modify `this.INITIAL_FEN` and add the default export mode:
```javascript
    // In constructor()
    this.INITIAL_FEN = 'rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w 0 1'; // Removed "- -"
    this.pieceToHrefMap = { ... }; // Unchanged
    
    // ...
    this.historyFEN = {
      fen: this.INITIAL_FEN,
      move: null,
      r: 0,
      f: 0,
      c: '',
      v: []
    };
    
    // Update default export format to PGN
    this.lastExportFormat = 'pgn';
```

### 2. Add New Coordinate Helper Methods & Notation Engine
Add these new methods to the `Ejcees` class. You can completely delete the old `getMoveNotation` method, as `getNotation` replaces it:
```javascript
  engineToCoords(engineMove) {
    if (!engineMove || engineMove.length !== 4) return null;
    return {
      startX: engineMove.charCodeAt(0) - 97,
      startY: 9 - parseInt(engineMove.charAt(1), 10),
      endX: engineMove.charCodeAt(2) - 97,
      endY: 9 - parseInt(engineMove.charAt(3), 10)
    };
  }

  coordsToEngine(startX, startY, endX, endY) {
    return String.fromCharCode(97 + startX) + (9 - startY) +
           String.fromCharCode(97 + endX) + (9 - endY);
  }

  getNotation(fen, engineMove, format) {
    if (format === 'iccs') {
      return engineMove.substring(0, 2).toUpperCase() + '-' + engineMove.substring(2, 4).toLowerCase();
    }
    let coords = this.engineToCoords(engineMove);
    if (!coords) return engineMove;
    let { startX, startY, endX, endY } = coords;

    let vMap = this.buildVirtualMap(fen);
    let pId = vMap.get(`${startX},${startY}`);
    if (!pId) return engineMove;

    let char = pId[0];
    let isRed = char === char.toUpperCase();
    let isPawn = char.toLowerCase() === 'p';

    let samePieces = [];
    for (let [pos, id] of vMap.entries()) {
      if (id[0] === char) {
        let [x, y] = pos.split(',').map(Number);
        samePieces.push({x, y});
      }
    }

    let prefixIndex = -1; // -1: none, 0: Front, 1: Middle, 2: Back, 11-15: 1st-5th
    let colPieces = samePieces.filter(p => p.x === startX);
    let multiCols = [];

    if (isPawn) {
      for (let c = 0; c <= 8; c++) {
        if (samePieces.filter(p => p.x === c).length > 1) multiCols.push(c);
      }
      if (multiCols.length > 1) {
        let allMultiPawns = [];
        for (let c of multiCols) allMultiPawns.push(...samePieces.filter(p => p.x === c));
        allMultiPawns.sort((a, b) => {
          if (isRed) {
            if (b.x !== a.x) return b.x - a.x;
            return a.y - b.y;
          } else {
            if (a.x !== b.x) return a.x - b.x;
            return b.y - a.y;
          }
        });
        prefixIndex = 11 + allMultiPawns.findIndex(p => p.x === startX && p.y === startY);
      } else if (colPieces.length >= 4) {
        colPieces.sort((a, b) => isRed ? a.y - b.y : b.y - a.y);
        prefixIndex = 11 + colPieces.findIndex(p => p.y === startY);
      } else if (colPieces.length === 3) {
        colPieces.sort((a, b) => isRed ? a.y - b.y : b.y - a.y);
        prefixIndex = colPieces.findIndex(p => p.y === startY);
      } else if (colPieces.length === 2) {
        colPieces.sort((a, b) => isRed ? a.y - b.y : b.y - a.y);
        prefixIndex = colPieces.findIndex(p => p.y === startY) === 0 ? 0 : 2;
      }
    } else if (colPieces.length > 1 && !['a','A','b','B'].includes(char)) {
      colPieces.sort((a, b) => isRed ? a.y - b.y : b.y - a.y);
      if (colPieces.length === 3) {
        prefixIndex = colPieces.findIndex(p => p.y === startY);
      } else {
        prefixIndex = colPieces.findIndex(p => p.y === startY) === 0 ? 0 : 2;
      }
    }

    let isAdvancing = isRed ? startY > endY : startY < endY;
    let isWXF = format === 'wxf';
    let actionStr = startY === endY ? (isWXF ? '.' : '平') : (isAdvancing ? (isWXF ? '+' : '進') : (isWXF ? '-' : '退'));

    let startFile = isRed ? 9 - startX : startX + 1;
    let endFile = isRed ? 9 - endX : endX + 1;

    let valStr;
    if (startY === endY) {
      valStr = isWXF ? endFile.toString() : this.NotationConverter[isRed ? 'cnNumRed' : 'cnNumBlk'][endFile];
    } else {
      let isStraight = !['n','N','b','B','a','A'].includes(char);
      let valNum = isStraight ? Math.abs(startY - endY) : endFile;
      valStr = isWXF ? valNum.toString() : this.NotationConverter[isRed ? 'cnNumRed' : 'cnNumBlk'][valNum];
    }

    let pieceStr = isWXF ? char.toUpperCase() : this.NotationConverter[isRed ? 'cnPieceRed' : 'cnPieceBlk'][char.toUpperCase()];

    if (isWXF) {
      let wxfPrefix = '';
      if (prefixIndex === 0) wxfPrefix = '+';
      else if (prefixIndex === 1) wxfPrefix = '=';
      else if (prefixIndex === 2) wxfPrefix = '-';
      else if (prefixIndex >= 11) wxfPrefix = String.fromCharCode(97 + prefixIndex - 11);

      if (wxfPrefix) return pieceStr + wxfPrefix + actionStr + valStr;
      return pieceStr + startFile + actionStr + valStr;
    } else {
      let cnPrefix = '';
      if (prefixIndex === 0) cnPrefix = '前';
      else if (prefixIndex === 1) cnPrefix = '中';
      else if (prefixIndex === 2) cnPrefix = '後';
      else if (prefixIndex >= 11) cnPrefix = ["一","二","三","四","五"][prefixIndex - 11];

      if (cnPrefix) return cnPrefix + (prefixIndex >= 11 ? "兵" : pieceStr) + actionStr + valStr;
      return pieceStr + this.NotationConverter[isRed ? 'cnNumRed' : 'cnNumBlk'][startFile] + actionStr + valStr;
    }
  }
```

### 3. Update `boardToFEN` & History Pushing
Modify `boardToFEN` to remove `- -` logic, and update `pushHistory` to use the engine format.
```javascript
  boardToFEN() {
    let fen = '';
    for (let y = 0; y <= 9; y++) {
      let emptyCount = 0;
      for (let x = 0; x <= 8; x++) {
        const pieceId = this.tileMap.get(`${x},${y}`);
        const pieceEl = this.container.querySelector('#' + pieceId);
        if (pieceId && pieceEl) {
          const href = pieceEl.getAttribute('href') || pieceEl.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
          if (/^#(tile([2-9]|1[0-5]))$/.test(href)) {
            if (emptyCount > 0) {
              fen += emptyCount;
              emptyCount = 0;
            }
            fen += pieceId[0];
          } else {
            emptyCount++;
          }
        } else {
          emptyCount++;
        }
      }
      if (emptyCount > 0) fen += emptyCount;
      if (y < 9) fen += '/';
    }
    fen += ` ${this.currentTurn}`;
    fen += ` ${this.halfMoveClock}`;
    fen += ` ${this.fullMoveNumber}`;
    return fen;
  }

  pushHistory(engineMove) {
    if (!engineMove) return;
    const newFEN = this.boardToFEN();
    const parentNode = this.getNodeAtStep(this.currentStepIndex);

    if (parentNode) {
      let branchIndex = parentNode.v.findIndex(child => child.move === engineMove && child.fen === newFEN);

      if (branchIndex === -1) {
        parentNode.v.push({
          fen: newFEN,
          move: engineMove,
          c: '',
          v: []
        });
        branchIndex = parentNode.v.length - 1;
      }

      if (parentNode.v.length > 1) {
        let forkCount = 0;
        let curr = this.historyFEN;
        for (let i = 0; i < this.currentStepIndex; i++) {
          if (curr.v && curr.v.length > 1) forkCount++;
          const choice = curr.v.length > 1 ? this.currentBranch[forkCount - 1] || 0 : 0;
          curr = curr.v[choice];
        }
        this.updateBranchPath(forkCount, branchIndex);
      }
    }

    this.currentStepIndex++;
    const totalDepth = this.getPathDepth(this.historyFEN, this.currentBranch);
    this.stepSlider.max = Math.max(0, totalDepth - 1);
    this.stepSlider.value = this.currentStepIndex;

    this.renderRecordUI();
    const cnMove = this.getNotation(parentNode.fen, engineMove, 'cn');
    const outputElement = this.container.querySelector('.ejceesoutput');
    outputElement.textContent += ' | ' + cnMove;
    this.renderNoteUI();
    this.saveStateToUndo();
  }
```

Update the final callback inside `movePieceWithAnimation` (replace `const moveText = this.getMoveNotation(...)` logic):
```javascript
        // Inside movePieceWithAnimation completion block...
        
        // Update logical data only
        const moveData = {
          startX: startLogical.x,
          startY: startLogical.y,
          endX: targetLogicalX,
          endY: targetLogicalY
        };
        const engineMove = this.coordsToEngine(startLogical.x, startLogical.y, targetLogicalX, targetLogicalY);

        this.tileMap.delete(`${startLogical.x},${startLogical.y}`);
        this.tileMap.set(`${targetLogicalX},${targetLogicalY}`, pieceId);
        this.piecePos.set(pieceId, { x: targetLogicalX, y: targetLogicalY });

        // ...
        
        if (onComplete) {
          onComplete();
        } else {
          if (eatenId) {
            this.halfMoveClock = 0;
          } else {
            this.halfMoveClock++;
          }
          if (this.currentTurn === 'b') {
            this.fullMoveNumber++;
          }
          this.currentTurn = this.currentTurn === 'w' ? 'b' : 'w';
          this.pushHistory(engineMove);
          this.applyLastMoveVisuals(moveData);
          this.restoreOutputText();
          // ... Checkmate detection ...
```

### 4. Replace `lastMove` dependencies
Update `applyLastMoveVisuals`, `restoreOutputText`, and loop logic:
```javascript
  // Inside restoreOutputText()
      const node = this.getNodeAtStep(this.currentStepIndex);
      if (node && node.move) {
        promptText = ' ' + node.move;
        if (this.isAutoPlaying) {
          promptText = '(Auto-playing) ' + promptText;
        }
      } else {
// ...

  // Update applyLastMoveVisuals signature to parse node.move
  applyLastMoveVisuals(moveDataOrEngineMove) {
    // Determine if it's already coordinate data or an engine string
    let lastMove = moveDataOrEngineMove;
    if (typeof lastMove === 'string') {
        lastMove = this.engineToCoords(lastMove);
    }
// ...

  // In getGamePath(limitToCurrentStep = false)
    let newHistory = {
      fen: this.historyFEN.fen,
      move: null,
      r: this.historyFEN.r,
      f: this.historyFEN.f,
      c: this.historyFEN.c,
      v: []
    };
    // ...
```

### 5. Rotation/Flip Actions & Save Logic
```javascript
  // In bindEvents() update tool-rotate
    this.container.querySelector('#tool-rotate').addEventListener('click', () => {
      if (this.isExportTextMode || this.isAutoPlaying) return;
      this.isRotateEnabled = !this.isRotateEnabled;
      if (this.historyFEN) this.historyFEN.r = this.isRotateEnabled ? 1 : 0;
      this.reapplyVisualPositions();
      if (this.isEditMode) this.updateMiniBoard();
      this.saveStateToUndo();
      this.updateToolHighlights();
    });

  // In bindEvents() update tool-flip
    this.container.querySelector('#tool-flip').addEventListener('click', () => {
      if (this.isExportTextMode || this.isAutoPlaying) return;
      this.isFlipEnabled = !this.isFlipEnabled;
      if (this.historyFEN) this.historyFEN.f = this.isFlipEnabled ? 1 : 0;
      this.reapplyVisualPositions();
      this.saveStateToUndo();
      this.updateToolHighlights();
    });

  // In tool-save logic
            let copy;
            if (this.isRangeMode) {
                let rangeHistory = this.getGamePath();
                copy = simplify(rangeHistory);
                copy.fen = rangeHistory.fen;
                copy.r = this.isRotateEnabled ? 1 : 0;
                copy.f = this.isFlipEnabled ? 1 : 0;
                // ...
            } else {
                copy = simplify(this.historyFEN);
                copy.fen = this.historyFEN.fen;
                copy.r = this.historyFEN.r || 0;
                copy.f = this.historyFEN.f || 0;
            }
```

### 6. Replace Export UI & Formatting Logic
Update `renderExportTextUI`:
```javascript
    // In renderExportTextUI
    commentDiv.innerHTML = `<div class="exp-fmt-outer">
                <div class="exp-fmt-btn ${this.lastExportFormat === 'pgn' ? 'active' : ''}" data-fmt="pgn" data-odd="${this.formatOddStates['pgn']}">PGN Format</div>
                <div class="exp-fmt-btn ${this.lastExportFormat === 'cn' ? 'active' : ''}" data-fmt="cn" data-odd="${this.formatOddStates['cn']}">DP Format</div>
                <div class="exp-fmt-btn ${this.lastExportFormat === 'engine' ? 'active' : ''}" data-fmt="engine" data-odd="${this.formatOddStates['engine']}">Engine Format</div>
                <div class="exp-fmt-btn ${this.lastExportFormat === 'url' ? 'active' : ''}" data-fmt="url" data-odd="${this.formatOddStates['url']}">URL Parameter</div>
               </div>`;
```

Rewrite Text Generators to use `getNotation`:
```javascript
  generateTextWithComment(isEnglish = false) {
    const pathRoot = this.getGamePath();
    const fen = pathRoot.fen;
    let text = pathRoot.fen !== this.INITIAL_FEN ? pathRoot.fen + '\n' : '';
    if (pathRoot.c && pathRoot.c.trim() !== '') text += pathRoot.c.trim() + '\n';

    let isRedTurn = fen.split(' ')[1] === 'w';
    let moveNum = 1;
    let curr = pathRoot;
    let needsEllipsis = !isRedTurn;
    let currentFen = fen;

    while (curr.v && curr.v.length > 0) {
      let nextNode = curr.v[0];
      const moveStr = this.getNotation(currentFen, nextNode.move, 'cn');

      if (isRedTurn) {
        text += `${moveNum}. ${moveStr}`;
        if (nextNode.c && nextNode.c.trim() !== '') {
          text += `\n${nextNode.c.trim()}\n`;
          needsEllipsis = true;
        } else {
          text += ' ';
          needsEllipsis = false;
        }
        isRedTurn = false;
      } else {
        if (needsEllipsis) text += `${moveNum}. ... ${moveStr}\n`;
        else text += `${moveStr}\n`;
        
        if (nextNode.c && nextNode.c.trim() !== '') text += `${nextNode.c.trim()}\n`;
        moveNum++;
        isRedTurn = true;
        needsEllipsis = false;
      }
      currentFen = nextNode.fen;
      curr = nextNode;
    }
    return text.trim();
  }

  generateExportText(isEnglish) {
    let path = this.getGamePath();
    let fen = path.fen;
    let text = fen !== this.INITIAL_FEN ? fen + '\n' : '';
    let isRedTurn = fen.split(' ')[1] === 'w';
    let moveNum = 1;
    let currentFen = fen;

    while (path.v.length) {
      let line = `${moveNum}. `;
      if (!isRedTurn) {
        let nextNode = path.v[0];
        line += `... ${this.getNotation(currentFen, nextNode.move, 'cn')}`;
        currentFen = nextNode.fen;
        path = nextNode;
        isRedTurn = true;
      } else {
        let nextNode = path.v[0];
        line += `${this.getNotation(currentFen, nextNode.move, 'cn')}`;
        currentFen = nextNode.fen;
        path = nextNode;
        if (path.v.length) {
          let blkNode = path.v[0];
          line += ` ${this.getNotation(currentFen, blkNode.move, 'cn')}`;
          currentFen = blkNode.fen;
          path = blkNode;
        }
      }
      text += line + '\n';
      moveNum++;
    }
    return text.trim();
  }

  generateDPFormat(isEnglish = false) {
    let text = this.historyFEN.fen !== this.INITIAL_FEN ? this.historyFEN.fen + '\n' : '';
    if (this.historyFEN.c && this.historyFEN.c.trim() !== '') text += this.historyFEN.c.trim() + '\n';

    let branchQueue = [];
    let branchCounter = 1;
    const getMarker = num => num <= 20 ? String.fromCharCode(0x245f + num) : `(${num})`;

    const processSequence = (startNode, absDepth, moveNum, isRed, pathArr, skipFirstQueue) => {
      let seqText = '';
      let curr = startNode;
      let lineBuffer = '';
      let isFirstIteration = true; 
      let currentFen = curr.fen;

      while (curr.v && curr.v.length > 0) {
        let choiceIdx = (pathArr && pathArr[absDepth] !== undefined) ? pathArr[absDepth] : 0;
        if (choiceIdx >= curr.v.length) choiceIdx = 0;

        let nextNode = curr.v[choiceIdx];
        let moveStr = this.getNotation(currentFen, nextNode.move, 'cn');

        let markers = '';
        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++;
        }
        currentFen = nextNode.fen;
        curr = nextNode;
        absDepth++;
      }
      if (lineBuffer !== '') seqText += lineBuffer + '\n';
      return seqText.trim();
    };

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

    while (branchQueue.length > 0) {
      let b = branchQueue.shift();
      text += `\n\n${b.marker}\n`;
      let branchPath = Array(b.absDepth).fill(0);
      branchPath.push(b.choiceIdx);
      text += processSequence(b.parentNode, b.absDepth, b.moveNum, b.isRedTurn, branchPath, true);
    }
    return text;
  }

  generatePGNFormat(oddState) {
    let text = '[Game "Chinese Chess"]\n';
    let userResult = '';
    
    if (this.historyFEN.r === 1 || this.isRotateEnabled) text += `[Rotate "true"]\n`;
    if (this.historyFEN.f === 1 || this.isFlipEnabled) text += `[Flip "true"]\n`;

    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;
      }
    });

    if (this.historyFEN.fen.split(' ')[0] !== this.INITIAL_FEN.split(' ')[0]) {
      text += `[FEN "${this.historyFEN.fen}"]\n`;
    }

    const formatMap = { 0: 'Chinese', 1: 'ICCS', 2: 'WXF' };
    text += `[Format "${formatMap[oddState]}"]\n\n`;
    if (this.historyFEN.c && this.historyFEN.c.trim() !== '') text += `{${this.historyFEN.c.trim()}}\n`;

    let finalTerminator = userResult !== '' ? userResult : '*';

    const getMoveStr = (node, parentFen) => {
      if (oddState === 0) return this.getNotation(parentFen, node.move, 'cn');
      if (oddState === 2) return this.getNotation(parentFen, node.move, 'wxf');
      if (oddState === 1) return this.getNotation(parentFen, node.move, 'iccs');
      return node.move;
    };

    const processPGNSequence = (startNode, moveNum, isRed) => {
      let seqText = '';
      let curr = startNode;
      let currentFen = curr.fen;

      while (curr.v && curr.v.length > 0) {
        let nextNode = curr.v[0];
        let moveStr = getMoveStr(nextNode, currentFen);
        let currentMoveText = isRed ? `${moveNum}. ${moveStr}` : (curr === startNode ? `${moveNum}. ... ${moveStr}\n` : `${moveStr}\n`);

        seqText += (seqText.length > 0 && !seqText.endsWith('\n') && !isRed ? ' ' : '') + currentMoveText;
        if (nextNode.c && nextNode.c.trim() !== '') seqText += `{${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, currentFen);
            let innerText = isRed ? `${moveNum}. ${bMoveStr}` : `${moveNum}. ... ${bMoveStr}`;
            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';
        }
        currentFen = nextNode.fen;
        curr = nextNode;
        if (!isRed) moveNum++;
        isRed = !isRed;
      }
      return seqText.trim();
    };

    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;
  }
```

### 7. Record UI Update
Ensure the sidebar history renders strictly using the `cn` format parsed correctly from `getNotation`:
```javascript
  addMoveToRecordUI(engineMove, index) {
    const recordContainer = this.container.querySelector('.ejceesrecord');
    const node = this.getNodeAtStep(index);
    if (!node) return;
    const parentNode = this.getNodeAtStep(index - 1);
    
    // Convert engineMove to Chinese PGN for display
    const cnMove = this.getNotation(parentNode.fen, engineMove, 'cn');
    
    const thisFen = node.fen;
    const turnAfterMove = thisFen.split(' ')[1];
    const whoMoved = turnAfterMove === 'b' ? 'w' : 'b'; 

    let lastOuter = recordContainer.lastElementChild;
    if (!lastOuter || !lastOuter.classList.contains('ejceesrcdstepouter')) {
      lastOuter = null;
    }
    let outer = lastOuter;

    if (whoMoved === 'w') {
      outer = document.createElement('div');
      outer.className = 'ejceesrcdstepouter';
      recordContainer.appendChild(outer);
    } else {
      if (!outer || outer.children.length >= 2) {
        outer = document.createElement('div');
        outer.className = 'ejceesrcdstepouter';
        recordContainer.appendChild(outer);

        const spacer = document.createElement('div');
        spacer.className = 'ejceesrcdstep';
        spacer.style.pointerEvents = 'none'; 
        outer.appendChild(spacer);
      }
    }

    const stepDiv = document.createElement('div');
    stepDiv.className = 'ejceesrcdstep';
    stepDiv.id = `step-record-${index}`;
    stepDiv.dataset.index = index;

    if (node.c && node.c.trim() !== '') stepDiv.classList.add('has-comment');

    const marker = document.createElement('div');
    marker.className = 'branch-marker';

    if (parentNode && parentNode.v.length > 1) {
      let forkIndex = -1;
      let tempNode = this.historyFEN;
      for (let j = 0; j < index - 1; j++) {
        if (tempNode.v && tempNode.v.length > 1) forkIndex++;
        const choice = tempNode.v && tempNode.v.length > 1 ? this.currentBranch[forkIndex] || 0 : 0;
        tempNode = tempNode.v[choice];
      }
      forkIndex++; 

      const branchMark = document.createElement('span');
      branchMark.className = 'branch-marker';
      const currentChoice = this.currentBranch[forkIndex] !== undefined ? this.currentBranch[forkIndex] : 0;
      branchMark.innerText = `${currentChoice + 1}/${parentNode.v.length}`;

      branchMark.onclick = e => {
        e.stopPropagation();
        this.showBranchMenu(branchMark, parentNode, forkIndex, index);
      };
      stepDiv.appendChild(branchMark);
    }

    const textSpan = document.createElement('span');
    textSpan.className = 'ejceesrcdstepcontent';
    textSpan.textContent = cnMove; // Updated to PGN Chinese notation logic
    stepDiv.appendChild(textSpan);

    stepDiv.addEventListener('click', () => {
      if (this.isEditingComment || this.isExportTextMode || this.isEditMode || this.isAutoPlaying) return;
      this.jumpToStep(index);
      this.saveStateToUndo();
      this.updateToolHighlights();
    });

    outer.appendChild(stepDiv);
    recordContainer.scrollTop = recordContainer.scrollHeight;
    this.highlightActiveStep(index);
  }
```

### 8. Import JSON fixes
Update `importExportedText` and file reading to restore `r` and `f` root properties properly:
```javascript
        // Inside #file-input event listener
        this.historyFEN = expand(data, data.fen);
        this.historyFEN.r = data.r || 0;
        this.historyFEN.f = data.f || 0;
        this.isRotateEnabled = this.historyFEN.r === 1;
        this.isFlipEnabled = this.historyFEN.f === 1;
        this.reapplyVisualPositions();

        // In importExportedText, JSON parse
        this.historyFEN = expand(data, data.fen);
        this.historyFEN.r = data.r || 0;
        this.historyFEN.f = data.f || 0;
        this.isRotateEnabled = this.historyFEN.r === 1;
        this.isFlipEnabled = this.historyFEN.f === 1;
        this.reapplyVisualPositions();
```

*(Note: Completely remove `this.updateHistoryFENFlags` function, as modifying FEN properties natively solves this more elegantly at the root).*
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

當黑方走棋時,為什麼會變成「undefined8平5」?

gemini pro:

代码: 全选

This happens because the `this.NotationConverter.cnPieceBlk` object uses lowercase letters as keys (e.g., `'c': '砲'`), but the previous code forced the character to uppercase (`char.toUpperCase()`) before looking it up. This caused it to search for `'C'` in the black pieces list, which returned `undefined`.

Here is the fix for the `getNotation` method.

### Update `getNotation`
Find this specific line inside your `getNotation` method:
```javascript
let pieceStr = isWXF ? char.toUpperCase() : this.NotationConverter[isRed ? 'cnPieceRed' : 'cnPieceBlk'][char.toUpperCase()];
```

And change it to this:
```javascript
let pieceStr = isWXF ? char.toUpperCase() : this.NotationConverter[isRed ? 'cnPieceRed' : 'cnPieceBlk'][char];
```

By simply passing `char` (which is naturally uppercase for Red and lowercase for Black based on `pId[0]`), it will correctly look up `'C'` in `cnPieceRed` and `'c'` in `cnPieceBlk`.
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

當點擊tool-exp-txt時出現以下錯誤訊息:

代码: 全选

can't access property "startX", child.lastMove is undefined
    encodeUrlParam file:///home/ejsoon/Documents/work/ejcees/obj/ejceesobj.js:6597
    generateUrlHash file:///home/ejsoon/Documents/work/ejcees/obj/ejceesobj.js:6621
    renderExportTextUI file:///home/ejsoon/Documents/work/ejcees/obj/ejceesobj.js:6763
    bindEvents file:///home/ejsoon/Documents/work/ejcees/obj/ejceesobj.js:3099
查明原因。

代码: 全选

這是一個典型的 JavaScript 運行時錯誤,通常發生在 Ejcees 類別(Chess 棋盤組件)的 `renderExportTextUI` 函數試圖處理數據時。

### 錯誤原因分析
錯誤訊息 `can't access property "startX", child.lastMove is undefined` 指出:
當你點擊 **tool-exp-txt** (導出文本) 時,程式碼試圖讀取 `child.lastMove.startX`。然而,此時的 `child` 對象中並沒有 `lastMove` 這個屬性(它是 `undefined`),導致程式崩潰。

這通常發生在以下情境:
1.  **棋譜處於起始狀態**:如果你在棋盤剛初始化、尚未下任何棋步時點擊導出,歷史紀錄中可能只有根節點而沒有「最後一步」。
2.  **變著(Branch)數據不完整**:如果棋譜中包含變著,而導出邏輯在遞迴遍歷分支時,遇到了沒有移動紀錄的空節點。

---

### 解決方案

你需要檢查 `ejcees.js` 中 `renderExportTextUI` 函數或其調用的 `generateUrlHash` 邏輯。以下是修復建議:

#### 1. 增加安全檢查(Optional Chaining)
在報錯的程式碼位置,將 `child.lastMove.startX` 修改為安全存取模式。

**修改前:**
```javascript
let x = child.lastMove.startX; // 這裡會報錯
```

**修改後(推薦):**
```javascript
// 使用 ?. 語法,如果 lastMove 不存在則返回 undefined 而不崩潰
let x = child.lastMove?.startX; 

// 或者使用傳統判斷
let x = (child.lastMove && child.lastMove.startX) ? child.lastMove.startX : 0;
```

#### 2. 修改 `generateUrlHash` 邏輯
如果是因為起始點沒有移動數據,請在生成 URL Hash 之前檢查 `historyFEN` 的狀態:

```javascript
generateUrlHash() {
    // 檢查是否有移動記錄,如果沒有,應給予預設值或跳過
    if (!this.historyFEN || !this.historyFEN.v || this.historyFEN.v.length === 0) {
        return ""; 
    }
    // ... 原有的 encode 邏輯
}
```

#### 3. 檢查 `renderExportTextUI`
確保在切換到導出模式時,`lastExportFormat` 正確初始化。如果 `isExportTextMode` 為 `true` 但數據棧為空,應直接顯示起始 FEN 而非嘗試解析移動路徑。

---

### 如何快速測試修復?
1.  在瀏覽器開發者工具(F12)的 **Console** 面板中,輸入 `ejceesInstance.historyFEN`(假設你的實例名是這個)查看結構。
2.  手動下兩步棋後再點擊導出。如果下棋後錯誤消失,則確定是「起始空紀錄處理不當」的問題。

**修正建議總結:**
請搜尋程式碼中的 `encodeUrlParam` 區塊,找到存取 `lastMove` 的地方,確保在存取 `startX`, `startY`, `endX`, `endY` 之前,先確認 `lastMove` 對象確實存在。
附件
ejceesobj202604301104.js
(296.48 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

改寫整個deriveCoordsFromMove(),現在node.m已經改成了「h2e2」這樣的機器碼,已經等同於坐標。


所有的代碼和注釋都要使用英文。


gemini thinking(pro在睡覺):

代码: 全选

根據您的需求,我已經將 `deriveCoordsFromMove()` 函數完全改寫為處理「h2e2」這類機器碼(ICCS/UCI 格式)的邏輯。由於移動代碼現在直接包含坐標信息,我們不再需要解析 FEN 字符串來尋找棋子位置,這大幅簡化了代碼。

以下是改寫後的函數,代碼與注釋均使用英文:

```javascript
  /**
   * Derives start and end coordinates from a machine-encoded move string (UCI/ICCS format).
   * Example: "h2e2" -> startX: 7, startY: 7, endX: 4, endY: 7
   * * Mapping logic:
   * - Files (columns) 'a'-'i' map to indices 0-8.
   * - Ranks (rows) '0'-'9' map to indices 9-0 (where 0 is the top-most rank in the internal board array).
   * * @param {string} fen - The FEN string of the current position (kept for signature compatibility).
   * @param {string} move - The machine move code (e.g., "h2e2").
   * @returns {Object|null} An object containing {startX, startY, endX, endY}, or null if the move is invalid.
   */
  deriveCoordsFromMove(fen, move) {
    // 1. Basic validation: ensure move is a string with at least 4 characters
    if (!move || typeof move !== 'string' || move.length < 4) {
      console.warn('deriveCoordsFromMove: Invalid move string provided.', move);
      return null;
    }

    // 2. Extract and convert column/file characters ('a' to 'i') to 0-8
    // charCodeAt(0) - 97: 'a' is 97, 'b' is 98, etc.
    const startX = move.charCodeAt(0) - 97;
    const endX = move.charCodeAt(2) - 97;

    // 3. Extract and convert rank characters ('0' to '9')
    // In standard ICCS for Xiangqi:
    // '0' is the bottom-most rank (array index 9)
    // '9' is the top-most rank (array index 0)
    const startRankChar = move.charAt(1);
    const endRankChar = move.charAt(3);

    const startY = 9 - parseInt(startRankChar, 10);
    const endY = 9 - parseInt(endRankChar, 10);

    // 4. Boundary validation for a 9x10 Xiangqi board
    const isOutOfBounds = (x, y) => (x < 0 || x > 8 || y < 0 || y > 9);

    if (isOutOfBounds(startX, startY) || isOutOfBounds(endX, endY)) {
      console.error(`deriveCoordsFromMove: Coordinates out of bounds for move "${move}"`, { startX, startY, endX, endY });
      return null;
    }

    // 5. Return the coordinate object
    return {
      startX,
      startY,
      endX,
      endY
    };
  }
```

### 針對您提到的錯誤訊息修正建議:
報錯 `can't access property "startX", child.lastMove is undefined` 是因為程式碼在讀取 `child.lastMove.startX` 前,沒有檢查 `lastMove` 是否存在。

建議在 `encodeUrlParam` 或 `generateUrlHash` 等調用處增加「安全存取」檢查(Optional Chaining):

```javascript
// 修改前:
let sx = child.lastMove.startX;

// 修改後 (使用 ?. 語法):
let sx = child.lastMove?.startX;

// 或者判斷是否存在:
if (child.lastMove) {
    // 執行涉及 startX 的邏輯
}
```

這能確保當棋局處於起始點(沒有 `lastMove`)或解析失敗時,程式不會崩潰。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

檢查生成分支的代碼,現在中文都變成了undefined。


gemini thinking:

代码: 全选

這是一個典型的數據格式不匹配問題。自從您將 `node.m`(或 `node.move`)改為像 「h2e2」 這樣的機器碼後,原本的 `NotationConverter.toChinese()` 函數因為無法解析英文字母坐標(它只能解析像 `C2=5` 這樣的 PGN 格式),所以會返回 `undefined`。

在生成分支(Branch)的 UI 代碼中,需要改用 `this.getNotation()`,因為該函數能根據棋盤狀態將機器碼轉換為正確的中文著法。

以下是需要修正的兩個主要地方:

### 1. 修正 Mini Mode 的分支顯示 (`renderNoteUI`)

在 `renderNoteUI` 函數中,遍歷分支生成連結的部分:

**修改前:**
```javascript
// In renderNoteUI() loop for branches
moveLink.innerText = this.NotationConverter.toChinese(child.move);
```

**修改後 (使用英文注釋):**
```javascript
// Use getNotation to convert machine code (e.g., "h2e2") to Chinese notation
moveLink.innerText = this.getNotation(parentNode.fen, child.move, 'cn');
```

---

### 2. 修正分支菜單的顯示 (`showBranchMenu`)

如果您在正常模式下點擊步驟右側的分支標記(如 `1/2`),彈出的菜單也需要修正:

**修改前:**
```javascript
// In showBranchMenu()
item.textContent = this.NotationConverter.toChinese(child.move);
```

**修改後 (使用英文注釋):**
```javascript
// Ensure coordinate-based moves are correctly translated using the board context
item.textContent = this.getNotation(parentNode.fen, child.move, 'cn');
```

---

### 為什麼這樣改?

* **`NotationConverter.toChinese()`**: 是一個簡單的字符串替換工具。當它看到 "h2e2" 時,它試圖在映射表中尋找 "h",但找不到對應的棋子名稱,因此返回空或 `undefined`。
* **`this.getNotation(fen, move, 'cn')`**: 這是類內部的核心方法。它會調用 `deriveCoordsFromMove` 解析 "h2e2" 得到坐標,查看該坐標上的棋子(例如「炮」),並計算其移動方向(「平」、「進」、「退」),最後生成正確的「炮二平五」。

### 額外檢查:`renderRecordUI`

請確保 `renderRecordUI` 內部獲取 `cnMove` 的邏輯也是一致的:

```javascript
// Correct logic in renderRecordUI
const cnMove = this.getNotation(parentNode.fen, node.move, 'cn');
```

這樣修改後,無論 `node.m` 是舊版的 PGN 格式還是新版的坐標機器碼,分支顯示都能恢復正常的中文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

對所上傳的js程式作出解讀或修改:
一,在這個程式中,有兩種中文棋譜的表示方法,一是PGN中文,二是DP中文。

這兩種表示法在多個兵處於同一縱線時有所區別,PGN中文會是「五兵進一」,而DP中文會是「後三進一」。

分別找到它們跟engine format的轉換方法,如果沒有,則需要增加。

代码: 全选

對所上傳的js程式作出解讀或修改:
一,在這個程式中,有兩種中文棋譜的表示方法,一是PGN中文,二是DP中文。

這兩種表示法在多個兵處於同一縱線時有所區別,PGN中文會是「五兵進一」,而DP中文會是「後三進一」。

分別找到它們跟engine format的轉換方法,如果沒有,則需要增加。

二,在mini模式中,對於分支的表示,應用pgn中文,但是現在還沒有調整過來,錯誤的表現為:
<div id="mini-branch-container" class="mini-branch-container" style="display: block;"><div class="branch-move-link active">n7+8</div><div class="branch-move-link">馬九退undefined</div></div>
請查找原因並更正。

三,現在點擊tool-exp-svg,動畫不會動,查找原因。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
附件
ejceesobj202604301326.js
(297.62 KiB) 已下载 6 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

gemini pro英文太好了:

代码: 全选

Here are the reasons for the issues and the exact modifications needed to fix them. All code and explanations are provided in English.

### 1. PGN vs. DP Chinese Pawn Notation Conversion

**Reason:** 
When multiple pawns are on the same file, PGN format uses the pawn's order (e.g., `5P+1` converts directly to `五兵進一`), which is naturally handled by your current code. However, DP notation completely omits the piece name (e.g., `後三進一`), which translates to `-3+1` in English. Because it lacks a piece character (`P`/`p`), the color (`isRed`) becomes ambiguous, and the engine format becomes invalid. 

**Modification:**
Update both `toEnglish` and `toChinese` inside `NotationConverter` to detect the DP format, dynamically insert the Pawn character (`P`/`p`) for the engine, and hide it when converting back to Chinese. This also fixes the `undefined` bug by adding a fallback for unmatched characters.

```javascript
    NotationConverter: {
      // ... (Keep existing cnPieceRed, cnPieceBlk, etc. mappings) ...

      toEnglish: function (cn, isRed) {
        let en = '';
        
        // Detect DP Chinese Pawn notation which omits the piece (e.g., '後三進一')
        // Length is 4, starts with a positional char, and the second char is a number.
        let isDPPawn = cn.length === 4 && 
                       (cn[0] === '前' || cn[0] === '後' || cn[0] === '中' || cn[0] === '二' || cn[0] === '四') && 
                       this.enNum[cn[1]] && !this.enPiece[cn[1]];

        for (let i = 0; i < cn.length; i++) {
          let char = cn[i];
          if (this.enPiece[char]) {
            en += isRed ? this.enPiece[char] : this.enPiece[char].toLowerCase();
          } else if (this.enNum[char]) {
            en += this.enNum[char];
          } else if (this.enAct[char]) {
            en += this.enAct[char];
          } else {
            en += char;
          }
        }

        // Add missing pawn piece for engine format compatibility (e.g., '-3+1' -> '-P3+1')
        if (isDPPawn && en.length >= 2) {
          en = en[0] + (isRed ? 'P' : 'p') + en.substring(1);
        }
        
        return en;
      },

      toChinese: function (en) {
        if (!en) return '';

        let pieceMatch = en.match(/[a-zA-Z]/);
        let isRed = true;
        if (pieceMatch) {
          let pieceChar = pieceMatch[0];
          isRed = pieceChar === pieceChar.toUpperCase();
        }

        // Detect DP Engine format (e.g., '-P3+1' or '+p3+1')
        let isDPEnglish = en.length >= 4 && 
                          (en[0] === '+' || en[0] === '-' || en[0] === '=') && 
                          en[1].toLowerCase() === 'p' && /[0-9]/.test(en[2]);

        let cn = '';
        for (let i = 0; i < en.length; i++) {
          let char = en[i];

          // Skip pawn character for DP format so '-P3+1' becomes '後三進一' instead of '後兵三進一'
          if (isDPEnglish && i === 1) {
            continue;
          }

          if (/[a-zA-Z]/.test(char)) {
            let p = isRed ? this.cnPieceRed[char.toUpperCase()] : this.cnPieceBlk[char.toLowerCase()];
            cn += p !== undefined ? p : char; // Fix: Fallback prevents 'undefined' string
          } else if (/[0-9]/.test(char)) {
            let n = isRed ? this.cnNumRed[char] : this.cnNumBlk[char];
            cn += n !== undefined ? n : char; // Fix: Fallback prevents 'undefined' string
          } else if (/[+\-=]/.test(char)) {
            if (i === 0) {
              if (char === '+') cn += '前';
              else if (char === '-') cn += '後';
              else if (char === '=') cn += '中';
            } else {
              if (char === '+') cn += '進';
              else if (char === '-') cn += '退';
              else if (char === '=') cn += '平';
            }
          } else {
            cn += char;
          }
        }
        return cn;
      }
    }
```

---

### 2. Mini Mode Branch Formatting Bug

**Reason:**
There are two distinct errors causing `<div class="branch-move-link active">n7+8</div><div class="branch-move-link">馬九退undefined</div>`:
1. The **active** branch remains `n7+8` because `this.NotationConverter.toChinese()` is omitted when mapping the active state's text.
2. The **inactive** branch shows `馬九退undefined` because an incomplete/malformed move string (like `N9-undefined`) was generated upstream and passed into the converter. Because English letters like 'u' do not exist in `cnPieceRed`, the array lookup returns `undefined`. (The fallback added in Task 1 will now safely convert this to `馬九退undefined` instead of crashing, but the root cause is upstream).

**Modification:**
Locate the function responsible for rendering the `#mini-branch-container` (typically `updateMiniBranches` or within `renderDOM`) and enforce the Chinese translation for *all* branches while sanitizing missing targets.

```javascript
  // Look for the code block rendering the mini-branch-container and update it like this:
  
  // Ensure the move string is valid before translating.
  let safeMove = branch.move.replace(/undefined/g, ''); 
  
  // ALWAYS wrap the move in toChinese(), even if it's the active branch
  let translatedMove = this.NotationConverter.toChinese(safeMove);

  // Example of corrected render logic:
  let branchHTML = `<div class="branch-move-link ${branch.isActive ? 'active' : ''}">${translatedMove}</div>`;
```

---

### 3. SVG Animation (`tool-exp-svg`) Fails on Click

**Reason:**
When the `<svg id="etani">` element is cloned to be exported and previewed in `.ejcees-export-modal`, it creates a **duplicate ID** in the DOM. SVGs relying on SMIL animations (`<animate>`, `<use href="#tileX">`, or `<defs mode="0">`) break because the browser will only target the very first `id="etani"` it finds (the original board), freezing the copied version. Furthermore, SMIL `<animate>` tags copy their current completed state and need to be reset to `0s`.

**Modification:**
When handling the `tool-exp-svg` click event, strip or append a unique suffix to the cloned SVG's IDs before injecting it into `.ejcees-export-preview`.

```javascript
  // In your bindEvents() or export handling function:
  
  document.getElementById('tool-exp-svg').addEventListener('click', () => {
    let originalSvg = document.getElementById('etani');
    
    // 1. Clone the SVG
    let clonedSvg = originalSvg.cloneNode(true);
    
    // 2. Prevent ID collision to restore SMIL animation targeting
    clonedSvg.id = 'etani-export'; 
    
    // 3. Restart SMIL animations natively for the cloned node
    // Using setTimeout ensures the DOM registers the node before triggering timeline reset
    setTimeout(() => {
      let previewSvg = document.querySelector('.ejcees-export-preview svg');
      if (previewSvg && typeof previewSvg.setCurrentTime === 'function') {
        previewSvg.setCurrentTime(0); 
      }
    }, 50);

    // 4. Inject into Modal
    document.querySelector('.ejcees-export-preview').innerHTML = '';
    document.querySelector('.ejcees-export-preview').appendChild(clonedSvg);
  });
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

對這個方法的 if (isPawn) {...}增加判斷邏輯。

如果format === 'dp',則:

代码: 全选

  getNotation(fen, engineMove, format) {
    if (format === 'iccs') {
      return engineMove.substring(0, 2).toUpperCase() + '-' + engineMove.substring(2, 4).toLowerCase();
    }
    let coords = this.engineToCoords(engineMove);
    if (!coords) return engineMove;
    let { startX, startY, endX, endY } = coords;

    let vMap = this.buildVirtualMap(fen);
    let pId = vMap.get(`${startX},${startY}`);
    if (!pId) return engineMove;

    let char = pId[0];
    let isRed = char === char.toUpperCase();
    let isPawn = char.toLowerCase() === 'p';

    let samePieces = [];
    for (let [pos, id] of vMap.entries()) {
      if (id[0] === char) {
        let [x, y] = pos.split(',').map(Number);
        samePieces.push({ x, y });
      }
    }

    let prefixIndex = -1; // -1: none, 0: Front, 1: Middle, 2: Back, 11-15: 1st-5th
    let colPieces = samePieces.filter(p => p.x === startX);
    let multiCols = [];

    if (isPawn) {
      for (let c = 0; c <= 8; c++) {
        if (samePieces.filter(p => p.x === c).length > 1) multiCols.push(c);
      }
      if (multiCols.length > 1) {
        let allMultiPawns = [];
        for (let c of multiCols) allMultiPawns.push(...samePieces.filter(p => p.x === c));
        allMultiPawns.sort((a, b) => {
          if (isRed) {
            if (b.x !== a.x) return b.x - a.x;
            return a.y - b.y;
          } else {
            if (a.x !== b.x) return a.x - b.x;
            return b.y - a.y;
          }
        });
        prefixIndex = 11 + allMultiPawns.findIndex(p => p.x === startX && p.y === startY);
      } else if (colPieces.length >= 4) {
        colPieces.sort((a, b) => (isRed ? a.y - b.y : b.y - a.y));
        prefixIndex = 11 + colPieces.findIndex(p => p.y === startY);
      } else if (colPieces.length === 3) {
        colPieces.sort((a, b) => (isRed ? a.y - b.y : b.y - a.y));
        prefixIndex = colPieces.findIndex(p => p.y === startY);
      } else if (colPieces.length === 2) {
        colPieces.sort((a, b) => (isRed ? a.y - b.y : b.y - a.y));
        prefixIndex = colPieces.findIndex(p => p.y === startY) === 0 ? 0 : 2;
      }
    } else if (colPieces.length > 1 && !['a', 'A', 'b', 'B'].includes(char)) {
      colPieces.sort((a, b) => (isRed ? a.y - b.y : b.y - a.y));
      if (colPieces.length === 3) {
        prefixIndex = colPieces.findIndex(p => p.y === startY);
      } else {
        prefixIndex = colPieces.findIndex(p => p.y === startY) === 0 ? 0 : 2;
      }
    }

    let isAdvancing = isRed ? startY > endY : startY < endY;
    let isWXF = format === 'wxf';
    let actionStr = startY === endY ? (isWXF ? '.' : '平') : isAdvancing ? (isWXF ? '+' : '進') : isWXF ? '-' : '退';

    let startFile = isRed ? 9 - startX : startX + 1;
    let endFile = isRed ? 9 - endX : endX + 1;

    let valStr;
    if (startY === endY) {
      valStr = isWXF ? endFile.toString() : this.NotationConverter[isRed ? 'cnNumRed' : 'cnNumBlk'][endFile];
    } else {
      let isStraight = !['n', 'N', 'b', 'B', 'a', 'A'].includes(char);
      let valNum = isStraight ? Math.abs(startY - endY) : endFile;
      valStr = isWXF ? valNum.toString() : this.NotationConverter[isRed ? 'cnNumRed' : 'cnNumBlk'][valNum];
    }

    let pieceStr = isWXF ? char.toUpperCase() : this.NotationConverter[isRed ? 'cnPieceRed' : 'cnPieceBlk'][char];

    if (isWXF) {
      let wxfPrefix = '';
      if (prefixIndex === 0) wxfPrefix = '+';
      else if (prefixIndex === 1) wxfPrefix = '=';
      else if (prefixIndex === 2) wxfPrefix = '-';
      else if (prefixIndex >= 11) wxfPrefix = String.fromCharCode(97 + prefixIndex - 11);

      if (wxfPrefix) return pieceStr + wxfPrefix + actionStr + valStr;
      return pieceStr + startFile + actionStr + valStr;
    } else {
      let cnPrefix = '';
      if (prefixIndex === 0) cnPrefix = '前';
      else if (prefixIndex === 1) cnPrefix = '中';
      else if (prefixIndex === 2) cnPrefix = '後';
      else if (prefixIndex >= 11) cnPrefix = ['一', '二', '三', '四', '五'][prefixIndex - 11];

      if (cnPrefix) return cnPrefix + (prefixIndex >= 11 ? '兵' : pieceStr) + actionStr + valStr;
      return pieceStr + this.NotationConverter[isRed ? 'cnNumRed' : 'cnNumBlk'][startFile] + actionStr + valStr;
    }
  }

對這個方法的    if (isPawn) {...}增加判斷邏輯。

如果format === 'dp',則:

如果所移動的兵有另一個兵跟它在同一列,其餘兵不在同一列,則結果為「前兵」或「後兵」;
如果所移動的兵有另兩個兵跟它在同一列,其餘兵不在同一列,則結果為「前兵」、「中兵」或「後兵」;
如果所移動的兵有另一個兵跟它在同一列,其餘有兵同在另一列,則結果為「前N」或「後N」;
如果所移動的兵有另兩個兵跟它在同一列,其餘有兵同在另一列,則結果為「前N」、「中兵」或「後N」;
如果所移動的兵有另三個或四個兵跟它在同一列,則結果為「前兵」、「二兵」、「三兵」、「四兵」或「後兵」;

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
附件
ejceesobj202604301418.js
(297.51 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

gemini thinking:

代码: 全选

To implement the specific "dp" notation rules for pawns in Xiangqi, you need to update the `if (isPawn)` block to categorize the pawns based on column distribution and update the final string construction logic to handle the "Front/Back + File Number" (`前N`/`後N`) case.

Here is the modified code for those sections:

### 1. Updated `if (isPawn)` logic
Replace the existing `if (isPawn) { ... }` block with the following:

```javascript
    if (isPawn) {
      // Count columns that contain more than one pawn
      for (let c = 0; c <= 8; c++) {
        if (samePieces.filter(p => p.x === c).length > 1) multiCols.push(c);
      }

      if (format === 'dp') {
        colPieces.sort((a, b) => (isRed ? a.y - b.y : b.y - a.y));
        let idx = colPieces.findIndex(p => p.y === startY);
        let numInCol = colPieces.length;

        if (numInCol >= 4) {
          // Rule 5: 4 or 5 pawns in the same column
          if (idx === numInCol - 1) prefixIndex = 2; // "Back" (後)
          else if (idx === 0) prefixIndex = 0;      // "Front" (前)
          else prefixIndex = 11 + idx;              // "2nd", "3rd", "4th" (二, 三, 四)
        } else if (multiCols.length > 1) {
          // Rules 3 & 4: Multiple columns have multiple pawns
          if (numInCol === 2) {
            prefixIndex = (idx === 0 ? 20 : 22);    // "Front N" (前N) or "Back N" (後N)
          } else if (numInCol === 3) {
            if (idx === 0) prefixIndex = 20;        // "Front N" (前N)
            else if (idx === 1) prefixIndex = 1;    // "Middle" (中)
            else prefixIndex = 22;                  // "Back N" (後N)
          }
        } else if (numInCol > 1) {
          // Rules 1 & 2: Only one column has multiple pawns
          if (numInCol === 2) {
            prefixIndex = (idx === 0 ? 0 : 2);      // "Front" (前) or "Back" (後)
          } else if (numInCol === 3) {
            prefixIndex = idx;                      // 0:Front, 1:Middle, 2:Back
          }
        }
      } else {
        // Standard logic for other formats
        if (multiCols.length > 1) {
          let allMultiPawns = [];
          for (let c of multiCols) allMultiPawns.push(...samePieces.filter(p => p.x === c));
          allMultiPawns.sort((a, b) => {
            if (isRed) {
              if (b.x !== a.x) return b.x - a.x;
              return a.y - b.y;
            } else {
              if (a.x !== b.x) return a.x - b.x;
              return b.y - a.y;
            }
          });
          prefixIndex = 11 + allMultiPawns.findIndex(p => p.x === startX && p.y === startY);
        } else if (colPieces.length >= 4) {
          colPieces.sort((a, b) => (isRed ? a.y - b.y : b.y - a.y));
          prefixIndex = 11 + colPieces.findIndex(p => p.y === startY);
        } else if (colPieces.length === 3) {
          colPieces.sort((a, b) => (isRed ? a.y - b.y : b.y - a.y));
          prefixIndex = colPieces.findIndex(p => p.y === startY);
        } else if (colPieces.length === 2) {
          colPieces.sort((a, b) => (isRed ? a.y - b.y : b.y - a.y));
          prefixIndex = colPieces.findIndex(p => p.y === startY) === 0 ? 0 : 2;
        }
      }
    }
```

---

### 2. Updated Output Logic
Since the "dp" format uses a file number instead of the piece name (e.g., `前三` instead of `前兵`) in certain cases, update the final `else` block (the Chinese notation part):

```javascript
    } else {
      let cnPrefix = '';
      if (prefixIndex === 0) cnPrefix = '前';
      else if (prefixIndex === 1) cnPrefix = '中';
      else if (prefixIndex === 2) cnPrefix = '後';
      else if (prefixIndex === 20) cnPrefix = '前'; // Front for "Front N"
      else if (prefixIndex === 22) cnPrefix = '後'; // Back for "Back N"
      else if (prefixIndex >= 11) cnPrefix = ['一', '二', '三', '四', '五'][prefixIndex - 11];

      if (cnPrefix) {
        let midPart;
        if (format === 'dp' && (prefixIndex === 20 || prefixIndex === 22)) {
          // For Rule 3 & 4: Prefix + File Number (e.g., 前三)
          midPart = this.NotationConverter[isRed ? 'cnNumRed' : 'cnNumBlk'][startFile];
        } else {
          // Standard: Prefix + Piece Name (e.g., 前兵 or 二兵)
          midPart = (prefixIndex >= 11 || (format === 'dp' && isPawn)) ? '兵' : pieceStr;
        }
        return cnPrefix + midPart + actionStr + valStr;
      }
      return pieceStr + this.NotationConverter[isRed ? 'cnNumRed' : 'cnNumBlk'][startFile] + actionStr + valStr;
    }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

ejceesobj202604301913

以機器碼(h2e2)作為內碼驅動。區分了東萍格式和PGN格式。
附件
ejceesobj202604301913.js
(299.54 KiB) 已下载 3 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

說明如何把dc轉換成engine format,並把上方的代碼中的moveEn通過dc轉換成engine format。

代码: 全选

              console.log('2 dc', dc);
              if (childIdx === -1) {
                let newNode = {
                  fen: nextFen,
                  move: moveEn,
                  c: '',
                  v: []
                };
                currNode.v.push(newNode);
                childIdx = currNode.v.length - 1;
              }
說明如何把dc轉換成engine format,並把上方的代碼中的moveEn通過dc轉換成engine format。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
附件
ejceesobj202604301959.js
(299.68 KiB) 已下载 3 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5398
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 176 次
被圈友点赞: 204 次
联系:

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

帖子 ejsoon »

gemini pro輕輕鬆鬆:

代码: 全选

### How to Convert `dc` to Engine Format

The engine format (UCCI/ICCS) represents chess moves using a 4-character string where:
* **Columns (X-axis):** Mapped from `0-8` to letters `a-i` (using ASCII, where `97` is 'a').
* **Rows (Y-axis):** Mapped from `0-9` to numbers `9-0` (inverted, so `9 - Y`).

To convert the `dc` object (`{startX, startY, endX, endY}`) into this format, you can either use the class's built-in helper method `this.coordsToEngine(dc.startX, dc.startY, dc.endX, dc.endY)` or manually map it using `String.fromCharCode(97 + x) + (9 - y)`.

---

### Modified Code Snippet

Here is the modified code around the section you provided. We reassign `moveEn` to the engine format immediately before it is used to check for existing branches and creating the new node.

```javascript
              // Convert the derived coordinates (dc) to engine format
              moveEn = this.coordsToEngine(dc.startX, dc.startY, dc.endX, dc.endY);
              
              let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);

              console.log('2 dc', dc);
              if (childIdx === -1) {
                let newNode = {
                  fen: nextFen,
                  move: moveEn, // Now safely stored as engine format (e.g., 'h2e2')
                  c: '',
                  v: []
                };
                currNode.v.push(newNode);
                childIdx = currNode.v.length - 1;
              }
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

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