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

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

帖子 ejsoon »

打開保存的svg動畫,如果是旋轉過的,則記譜會發生錯誤。
比如這個:
viewtopic.php?p=8592

導出svg時應記下旋轉或鏡像,導入時轉換。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

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

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

帖子 ejsoon »

但是為什麼自動播放時,rotate和flip又不停切換?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

為何rotate和flip老是跳,在自動播放時。

代码: 全选

{"v":[{"m":"C2=5","v":[{"m":"n8+7","v":[{"m":"N2+3","v":[{"m":"r9=8","v":[{"m":"P7+1","v":[{"m":"p7+1","v":[{"m":"R1+1","v":[{"m":"n2+3","v":[{"m":"N8+7","v":[{"m":"c2+4"}]},{"m":"R1=4","v":[{"m":"b3+5","v":[{"m":"N8+7","v":[{"m":"a4+5","v":[{"m":"C8=9","v":[{"m":"c2+4","v":[{"m":"R9=8","v":[{"m":"r1=2","v":[{"m":"N7+6","v":[{"m":"c2=7","v":[{"m":"R8+9","v":[{"m":"n3-2"}]}]}]}]}]}]}]}]},{"m":"c2+4","v":[{"m":"C8=9","v":[{"m":"r1=2","v":[{"m":"R9=8","v":[{"m":"c8+4","v":[{"m":"N7+6","v":[{"m":"c2=7","v":[{"m":"R8+9","v":[{"m":"c7+3","v":[{"m":"A4+5","v":[{"m":"n3-2","v":[{"m":"R4=2","v":[{"m":"c7=9","v":[{"m":"N3+4","v":[{"m":"p7+1","v":[{"m":"N4+6","v":[{"m":"r8+1","v":[{"m":"C9+4","v":[{"m":"r8=4","v":[{"m":"C5=6","v":[{"m":"c8-1","v":[{"m":"C9-2","v":[{"m":"c8=4","v":[{"m":"C9=6","v":[{"m":"r4=6","v":[{"m":"R2=1","v":[{"m":"c9=8"}]}]}]}]}]},{"m":"R2=1","v":[{"m":"c9=7","v":[{"m":"R1-1","v":[{"m":"c7-3","v":[{"m":"C9-2","v":[{"m":"n2+1"},{"m":"r4=6"},{"m":"c8=4","v":[{"m":"C9=6","v":[{"m":"r4=6","v":[{"m":"N6+5","v":[{"m":"a4+5","v":[{"m":"R1=3","v":[{"m":"n2+1","v":[{"m":"-C=9","v":[{"m":"r6=8"}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]},{"m":"c8+2","v":[{"m":"N7+6","v":[{"m":"c2=7","v":[{"m":"R8+9","v":[{"m":"c7+3","v":[{"m":"A4+5","v":[{"m":"n3-2","v":[{"m":"R4=2"}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}]}],"fen":"rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w"}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

代码: 全选

                moveInterval = initialHistory.moveInterval ? parseFloat(initialHistory.moveInterval) : moveInterval;
                moveSpeed = initialHistory.moveSpeed ? parseFloat(initialHistory.moveSpeed) : moveSpeed;
                isRotateEnabled = initialHistory.isRotateEnabled || isRotateEnabled;
                isFlipEnabled = initialHistory.isFlipEnabled || isFlipEnabled;
解決跳動問題。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

刪掉各種console。
附件
ejcees20260319resize99942.7z
(35.5 KiB) 已下载 66 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

當導入文本時,如果沒有fen,將整合到currentStep當前局面,如果有則新建局面。

增加引擎坐標轉換。

svg動畫記錄rotate和flip。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

如果historyFEN的根節點含有評論的話,ejceesrcdstart的右下角也要顯示評論圖標,跟「.ejceesrcdstep.has-comment::after」一樣。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

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

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

帖子 ejsoon »

導入文本時,加上「toggle」位置按鈕,切換成引擎坐標表示法,如「a0b0 b2e2」。

之前貼上文本時,如果沒有局面信息,則設為默認初始局面。現在改為如果沒有局面信息,則把後續的走子都加入當前的currentStep中。

沒有局面信息,可以接收以下三種情況:a0b0,車九平八,1. 車九平八 。如果有局面信息,則首先判斷它跟當前局面是否一樣,如果不一樣,往前洄溯父節點,直到找到一樣的。如果直至根節點都不一樣,則新建一個。

合並局面之後,分支要切換至新增的分支,當前局面跳到所增加分支的節點上。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

改進:
一,如果historyFEN的根節點含有評論的話,ejceesrcdstart的右下角也要顯示評論圖標,跟「.ejceesrcdstep.has-comment::after」一樣。

二,當導出svg單圖或動畫時,應記錄當前的rotate和flip狀態,把它寫入.ejceespbanimate的屬性ejceesrotate="1"和ejceesflip="1"。

當打開svg時,在把from和to轉成坐標之前,要先根據ejceesrotate和ejceesflip來轉換。

三,導入文本時,在右邊兩個下載鏈接後面,加上「toggle」按鈕(顯示為一個72*48px的svg圖片),點擊時會把左邊的文本切換成引擎坐標表示法,格式為「
position fen rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w moves a0b0 b2e2」。再次點擊則切換回來。

之前貼上文本時,如果沒有fen局面信息,則設為默認初始局面。現在改為如果沒有fen局面信息,則把後續的走子都加入當前的currentStep中。

在導入文本時,可以接收:
1. 車九平八 車1平2(多行,每行兩步,行首有數字)

現在增加以下兩種情況:
a0b0(單行,空格分隔。字母表示横坐標從左到右,數字表示縱坐標從下到上,比如c0e2轉成坐標就是2,9到4,7)
車九平八(單行,空格分隔)

如果含有fen局面信息,則首先判斷它跟當前局面是否一樣,如果不一樣,往前洄溯父節點,直到找到一樣的。如果直至根節點都不一樣,則新建局面。

合並局面之後,當前分支要切換至新增的分支,當前局面跳到所增加的分支的父節點上。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

grok

代码: 全选

```html
<!-- MODIFICATION 1: CSS RULE FOR ROOT COMMENT ICON -->
<style>
/* ... existing styles ... */

/* NEW: Root start banner comment indicator (same visual as steps) */
.ejceesrcdstart.has-comment::after {
    content: '';
    position: absolute;
    bottom: 4px;
    right: 4px;
    width: 12px;
    height: 12px;
    background: url('data:image/svg+xml;utf8,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27><path fill=%27%23ffd700%27 d=%27M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z%27/></svg>') no-repeat center/contain;
    pointer-events: none;
}
</style>
```

```javascript
// MODIFICATION 1: Update renderRecordUI to add .has-comment to start banner if root has comment
function renderRecordUI() {
    // ... existing code until startDiv creation ...
    const startDiv = document.createElement('div');
    startDiv.id = 'record-start';
    startDiv.classList.add('ejceesrcdstart');
    startDiv.onclick = ejceesrcdstartclick;

    // NEW: Check root comment
    if (historyFEN.c && historyFEN.c.trim() !== '') {
        startDiv.classList.add('has-comment');
    }

    if (historyFEN.fen === INITIAL_FEN) {
        startDiv.textContent = '棋局開始,紅方先行';
    } else if (historyFEN.fen.includes(' w')) {
        startDiv.textContent = '特定局面,輪到紅方';
    } else {
        startDiv.textContent = '特定局面,輪到黑方';
    }
    // ... rest unchanged ...
}
```

```javascript
// MODIFICATION 2: Export SVG - add rotate/flip attributes
// Inside document.getElementById('tool-exp-svg').addEventListener('click', () => { ... }
const cloneSvg = document.querySelector('.ejceespb').cloneNode(true);

// NEW: Record current view state on the animation group
const etdropClone = cloneSvg.querySelector('.etdrop') || cloneSvg.querySelector('.ejceespbanimate');
if (etdropClone) {
    etdropClone.setAttribute('ejceesrotate', isRotateEnabled ? '1' : '0');
    etdropClone.setAttribute('ejceesflip', isFlipEnabled ? '1' : '0');
}

// ... rest of export code unchanged ...
```

```javascript
// MODIFICATION 2: Import SVG - read and apply rotate/flip BEFORE coordinate conversion
function importExportedSVG(svgText) {
    // ... existing parser code ...

    const etdropEl = svgDoc.querySelector('.etdrop') || svgDoc.querySelector('.ejceespbanimate');
    const importedRotate = etdropEl && etdropEl.getAttribute('ejceesrotate') === '1';
    const importedFlip = etdropEl && etdropEl.getAttribute('ejceesflip') === '1';

    // NEW: Helper to convert visual coords back using imported view state
    function visualToLogical(vx, vy) {
        let lx = vx;
        let ly = vy;
        if (importedRotate) {
            lx = 8 - lx;
            ly = 9 - ly;
        }
        if (importedFlip) lx = 8 - lx;
        return { x: lx, y: ly };
    }

    // ... later in the step loop, replace raw Math.round parsing with:
    const fParts = fromStr.split(',');
    const tParts = toStr.split(',');
    const startVisX = Math.round((parseFloat(fParts[0]) - 24) / 48);
    const startVisY = Math.round((parseFloat(fParts[1]) - 24) / 48);
    const endVisX = Math.round((parseFloat(tParts[0]) - 24) / 48);
    const endVisY = Math.round((parseFloat(tParts[1]) - 24) / 48);

    const startLog = visualToLogical(startVisX, startVisY);
    const endLog = visualToLogical(endVisX, endVisY);

    // Use startLog.x / startLog.y etc. for the rest of the logic
    // ... rest of import unchanged ...
}
```

```javascript
// MODIFICATION 3: Updated renderExportTextUI with toggle button (72x48 SVG)
function renderExportTextUI() {
    // ... existing cnText / enText generation ...

    recordContainer.innerHTML = `<textarea class="ejceescomment-edit" id="export-textarea" style="width:100%; height:100%; ...">${cnText}</textarea>`;

    // NEW: Toggle button (72*48 px SVG icon)
    const toggleSVG = `
        <svg width="72" height="48" viewBox="0 0 72 48" xmlns="http://www.w3.org/2000/svg">
            <rect x="4" y="8" width="64" height="32" rx="4" fill="#333" stroke="#fff" stroke-width="3"/>
            <text x="36" y="28" text-anchor="middle" fill="#fff" font-family="monospace" font-size="11" font-weight="bold">ENGINE</text>
            <path d="M12 20 L20 28 L12 36" fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round"/>
            <path d="M52 20 L60 28 L52 36" fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round"/>
        </svg>
    `;

    commentDiv.innerHTML = `
        <div style="display:flex; flex-direction:column; gap:10px; padding:10px;">
            <a href="${URL.createObjectURL(cnBlob)}" download="ejcees_fen_${timestamp}.txt" style="color:#66b2ff;">fen (${cnBlob.size} bytes)</a>
            <a href="${URL.createObjectURL(enBlob)}" download="ejcees_en_fen_${timestamp}.txt" style="color:#66b2ff;">en_fen (${enBlob.size} bytes)</a>
            <div id="toggle-engine" style="cursor:pointer; width:72px; height:48px; margin:8px auto;">${toggleSVG}</div>
        </div>
    `;

    // ... existing confirm/cancel buttons ...

    // NEW: Toggle handler
    let isEngineMode = false;
    document.getElementById('toggle-engine').addEventListener('click', () => {
        isEngineMode = !isEngineMode;
        const textarea = document.getElementById('export-textarea');
        if (isEngineMode) {
            textarea.value = generateEngineText();
        } else {
            textarea.value = cnText; // restore original Chinese
        }
    });
}
```

```javascript
// MODIFICATION 3: NEW helper to generate engine coordinate text
function generateEngineText() {
    let path = getGamePath();
    let fen = path.fen;
    let movesStr = '';
    let currentNode = path;
    while (currentNode.v && currentNode.v.length) {
        const m = currentNode.v[0].move || '';
        // Convert to engine coord (a0b0 style) using deriveCoordsFromMove logic
        const dc = deriveCoordsFromMove(currentNode.fen, m);
        if (dc) {
            const startFile = String.fromCharCode(97 + dc.startX); // a-i
            const startRank = (9 - dc.startY).toString(); // 0-9 bottom-up
            const endFile = String.fromCharCode(97 + dc.endX);
            const endRank = (9 - dc.endY).toString();
            movesStr += ` ${startFile}${startRank}${endFile}${endRank}`;
        }
        currentNode = currentNode.v[0];
    }
    return `position fen ${fen} moves${movesStr}`;
}
```

```javascript
// MODIFICATION 3: Enhanced importExportedText (handles new formats + FEN matching)
function importExportedText(text) {
    text = text.trim();
    if (!text) return;

    // NEW: Detect engine coord lines (single line with a0b0 style)
    const lines = text.split('\n').map(l => l.trim()).filter(l => l);
    let baseFen = INITIAL_FEN;
    let moveList = [];

    // Try to extract FEN if present
    const fenMatch = text.match(/fen\s+([a-zA-Z0-9/]+\s+[wb])/i);
    if (fenMatch) {
        baseFen = fenMatch[1].trim();
    }

    // NEW: FEN matching logic - find common ancestor or create new branch
    let targetNode = historyFEN;
    let foundMatch = false;
    let parentPath = [];
    let current = historyFEN;
    while (current) {
        if (current.fen === baseFen) {
            targetNode = current;
            foundMatch = true;
            break;
        }
        if (current.v && current.v.length) {
            parentPath.push(current);
            current = current.v[currentBranch[parentPath.length - 1] || 0];
        } else {
            break;
        }
    }
    if (!foundMatch) {
        // Create new root branch
        targetNode = { fen: baseFen, move: null, lastMove: null, c: "", v: [] };
        historyFEN.v.push(targetNode); // attach as new branch from root
        currentBranch = [historyFEN.v.length - 1];
    } else {
        // Use matched node as insertion point
        currentBranch = [...currentBranch.slice(0, parentPath.length)];
    }

    // Parse moves (support Chinese, engine coord, and mixed)
    for (let line of lines) {
        if (/^\d+\./.test(line)) {
            // Traditional multi-line Chinese
            const tokens = line.replace(/^\d+\.\s*/, '').trim().split(/\s+/);
            for (let token of tokens) {
                if (token === '...' || !token) continue;
                let enMove = NotationConverter.toEnglish(token, baseFen.includes(' w'));
                moveList.push(enMove);
            }
        } else if (/^[a-i][0-9][a-i][0-9](?:\s+[a-i][0-9][a-i][0-9])*$/.test(line)) {
            // NEW: Engine coord format (a0b0 c2d4 ...)
            const coords = line.match(/[a-i][0-9][a-i][0-9]/g) || [];
            for (let pair of coords) {
                const sx = pair.charCodeAt(0) - 97;
                const sy = 9 - parseInt(pair[1]);
                const ex = pair.charCodeAt(2) - 97;
                const ey = 9 - parseInt(pair[3]);
                // Convert to internal move string (will be normalized later)
                moveList.push(`${String.fromCharCode(97 + sx)}${9 - sy}${String.fromCharCode(97 + ex)}${9 - ey}`);
            }
        } else if (/[\u4e00-\u9fa5]/.test(line)) {
            // Chinese single-line
            const tokens = line.split(/\s+/);
            for (let token of tokens) {
                if (NotationConverter.enPiece[token[0]] || token.length > 1) {
                    moveList.push(NotationConverter.toEnglish(token, baseFen.includes(' w')));
                }
            }
        }
    }

    // Apply moves to targetNode (append to current branch)
    let currentNode = targetNode;
    let currentFen = baseFen;
    for (let m of moveList) {
        const dc = deriveCoordsFromMove(currentFen, m);
        if (!dc) continue;
        const nextFen = simulateMove(currentFen, dc);
        const newNode = { fen: nextFen, move: m, lastMove: dc, c: "", v: [] };
        currentNode.v.push(newNode);
        currentNode = newNode;
        currentFen = nextFen;
    }

    // Switch to the newly added branch
    if (targetNode.v.length > 0) {
        currentBranch.push(targetNode.v.length - 1);
    }
    currentStepIndex = getPathDepth(historyFEN, currentBranch) - 1;
    renderRecordUI();
    renderNoteUI();
    jumpToStep(currentStepIndex);
    saveStateToUndo();
    updateToolHighlights();
}
```

**All other parts of the original file remain unchanged.** These are the exact modifications required for the three improvements.
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

gemini

https://ejsoon.vip/wp-content/uploads/2 ... Recor.html

代码: 全选

 Conversation with Gemini
You said

改進:

一,如果historyFEN的根節點含有評論的話,ejceesrcdstart的右下角也要顯示評論圖標,跟「.ejceesrcdstep.has-comment::after」一樣。


二,當導出svg單圖或動畫時,應記錄當前的rotate和flip狀態,把它寫入.ejceespbanimate的屬性ejceesrotate="1"和ejceesflip="1"。


當打開svg時,在把from和to轉成坐標之前,要先根據ejceesrotate和ejceesflip來轉換。


三,導入文本時,在右邊兩個下載鏈接後面,加上「toggle」按鈕(顯示為一個72*48px的svg圖片),點擊時會把左邊的文本切換成引擎坐標表示法,格式為「

position fen rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w moves a0b0 b2e2」。再次點擊則切換回來。


之前貼上文本時,如果沒有fen局面信息,則設為默認初始局面。現在改為如果沒有fen局面信息,則把後續的走子都加入當前的currentStep中。


在導入文本時,可以接收:

1. 車九平八 車1平2(多行,每行兩步,行首有數字)


現在增加以下兩種情況:

a0b0(單行,空格分隔。字母表示横坐標從左到右,數字表示縱坐標從下到上,比如c0e2轉成坐標就是2,9到4,7)

車九平八(單行,空格分隔)


如果含有fen局面信息,則首先判斷它跟當前局面是否一樣,如果不一樣,往前洄溯父節點,直到找到一樣的。如果直至根節點都不一樣,則新建局面。


合並局面之後,當前分支要切換至新增的分支,當前局面跳到所增加的分支的父節點上。


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

Here are the modifications to address your requirements. All instructions and code comments are in English.
1. CSS Modifications

Update the .ejceesrcdstart class to add position: relative; and add the new .ejceesrcdstart.has-comment::after rule so it can display the comment icon exactly like .ejceesrcdstep.

Find this block in your <style> section:
CSS

    .ejceesrcdstart {
        width: 100%;
        padding: 8px;
        box-sizing: border-box;
        text-align: center;
        font-weight: bold;
        border-bottom: 1px solid #444;
        color: #aaa;
        font-size: 14px;
        cursor: pointer;
    }

Replace it with the following:
CSS

    .ejceesrcdstart {
        width: 100%;
        padding: 8px;
        box-sizing: border-box;
        text-align: center;
        font-weight: bold;
        border-bottom: 1px solid #444;
        color: #aaa;
        font-size: 14px;
        cursor: pointer;
        position: relative; /* Added for absolute positioning of the marker */
    }

    .ejceesrcdstart.has-comment::after {
        content: '';
        position: absolute;
        bottom: 0;
        right: 0;
        width: 12px;
        height: 12px;
        background: url('data:image/svg+xml;utf8,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27><path fill=%27%23ffd700%27 d=%27M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z%27/></svg>') no-repeat center/contain;
        pointer-events: none;
    }

2. JavaScript Modifications
A. Add Comment Icon to Start Node

In the renderRecordUI() function, add logic to check if the root historyFEN has a comment and apply the class.

Find this part inside renderRecordUI():
JavaScript

            if (historyFEN.fen === INITIAL_FEN) {
                startDiv.textContent = '棋局開始,紅方先行';
            } else if (historyFEN.fen.includes(' w')) {
                startDiv.textContent = '特定局面,輪到紅方';
            } else {
                startDiv.textContent = '特定局面,輪到黑方';
            }
            recordContainer.innerHTML = '';
            recordContainer.appendChild(startDiv);

Add the comment check right before appending:
JavaScript

            if (historyFEN.fen === INITIAL_FEN) {
                startDiv.textContent = '棋局開始,紅方先行';
            } else if (historyFEN.fen.includes(' w')) {
                startDiv.textContent = '特定局面,輪到紅方';
            } else {
                startDiv.textContent = '特定局面,輪到黑方';
            }
            
            // Check if root has comment
            if (historyFEN.c && historyFEN.c.trim() !== '') {
                startDiv.classList.add('has-comment');
            }

            recordContainer.innerHTML = '';
            recordContainer.appendChild(startDiv);

B. Allow getMoveNotation to Accept a Custom Map

We need getMoveNotation to accept a currentMap parameter to evaluate abstract logical states during text import without moving actual DOM elements.

Find the definition:
JavaScript

function getMoveNotation(pieceId, startX, startY, endX, endY, capturedId) {

Replace it with:
JavaScript

function getMoveNotation(pieceId, startX, startY, endX, endY, capturedId, currentMap = tileMap) {

Inside that function, find this loop:
JavaScript

            for (const [pos, id] of tileMap.entries()) {

Replace it with:
JavaScript

            for (const [pos, id] of currentMap.entries()) {

C. Modify Export/Import SVG for Rotate/Flip

During SVG Export, record the ejceesrotate and ejceesflip attributes.

Inside the document.getElementById('tool-exp-svg').addEventListener('click', ...) callback, find this block:
JavaScript

            // 2. Remove specific elements
            if (isAnimation) {

Right before if (isAnimation) {, add these lines:
JavaScript

            // Record current view states onto the SVG
            cloneSvg.setAttribute('ejceesrotate', isRotateEnabled ? '1' : '0');
            cloneSvg.setAttribute('ejceesflip', isFlipEnabled ? '1' : '0');

Then, update importExportedSVG to reverse these view states when parsing coordinates.
Find this function:
JavaScript

        function importExportedSVG(svgText) {
            const parser = new DOMParser();
            const svgDoc = parser.parseFromString(svgText, "image/svg+xml");

Modify the beginning of the function and coordinate extraction logic like so:
JavaScript

        function importExportedSVG(svgText) {
            const parser = new DOMParser();
            const svgDoc = parser.parseFromString(svgText, "image/svg+xml");

            // Extract the saved view mode from the exported SVG
            const isRotated = svgDoc.documentElement.getAttribute('ejceesrotate') === '1';
            const isFlipped = svgDoc.documentElement.getAttribute('ejceesflip') === '1';

            // Helper to reverse export-time visual coordinates back to logical coordinates
            function parseExportVisCoords(px, py) {
                let visX = Math.round((px - 24) / 48);
                let visY = Math.round((py - 24) / 48);
                let lX = visX; let lY = visY;
                if (isRotated) { lX = 8 - lX; lY = 9 - lY; }
                if (isFlipped) { lX = 8 - lX; }
                return { x: lX, y: lY };
            }

            // 1. Get step0 for initial comment
            let initialComment;
            const step0 = svgDoc.getElementById('step0');
            if (svgDoc.documentElement.hasAttribute('data-comment')) {
                initialComment = b64DecodeUnicode(svgDoc.documentElement.getAttribute('data-comment'));
            } else {
                initialComment = step0 && step0.getAttribute('data-comment') ? b64DecodeUnicode(step0.getAttribute('data-comment')) : "";
            }

            // 2. Reconstruct Initial FEN from SVG DOM positions
            const etboard = svgDoc.querySelector('.etboard');
            const uses = etboard.querySelectorAll('use');
            const boardMap = Array(10).fill(null).map(() => Array(9).fill(''));

            let firstMovePieceId = null;
            const step1 = svgDoc.getElementById('step1');
            if (step1 && step1.parentElement) {
                firstMovePieceId = step1.parentElement.getAttribute('id');
            }

            uses.forEach(use => {
                const href = use.getAttribute('href') || use.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
                if (/^#(tile([2-9]|1[0-5]))$/.test(href)) {
                    const transform = use.getAttribute('transform');
                    const match = transform && transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
                    if (match) {
                        const px = parseFloat(match[1]);
                        const py = parseFloat(match[2]);
                        
                        // Apply reversal for initial piece layout
                        const pt = parseExportVisCoords(px, py);
                        const x = pt.x;
                        const y = pt.y;
                        
                        const id = use.getAttribute('id');
                        if (id && !id.endsWith('f') && x >= 0 && x < 9 && y >= 0 && y < 10) {
                            boardMap[y][x] = id[0];
                        }
                    }
                }
            });

            let initialFenBoard = "";
            for (let y = 0; y < 10; y++) {
                let emptyCount = 0;
                for (let x = 0; x < 9; x++) {
                    if (boardMap[y][x] === '') {
                        emptyCount++;
                    } else {
                        if (emptyCount > 0) {
                            initialFenBoard += emptyCount;
                            emptyCount = 0;
                        }
                        initialFenBoard += boardMap[y][x];
                    }
                }
                if (emptyCount > 0) initialFenBoard += emptyCount;
                if (y < 9) initialFenBoard += "/";
            }

            let initialTurn = 'w';
            if (firstMovePieceId && firstMovePieceId[0] === firstMovePieceId[0].toLowerCase()) {
                initialTurn = 'b';
            }
            const baseFen = initialFenBoard + " " + initialTurn;

            let newHistoryFEN = {
                fen: baseFen,
                move: null,
                lastMove: null,
                c: initialComment,
                v: []
            };
            let currentNode = newHistoryFEN;
            let currentFen = baseFen;
            let stepIndex = 1;

            loadFEN(baseFen);

            // 3. Reconstruct moves by analyzing animateTransform values
            while (true) {
                const stepAnim = svgDoc.getElementById(`step${stepIndex}`);
                if (!stepAnim) break;

                const commentStr = stepAnim.getAttribute('data-comment');
                const comment = commentStr ? b64DecodeUnicode(commentStr) : "";

                const fromStr = stepAnim.getAttribute('from');
                const toStr = stepAnim.getAttribute('to');
                const pId = stepAnim.parentElement.getAttribute('id');

                const fParts = fromStr.split(',');
                const tParts = toStr.split(',');
                
                // Apply reversal for animations
                const startPt = parseExportVisCoords(parseFloat(fParts[0]), parseFloat(fParts[1]));
                const endPt = parseExportVisCoords(parseFloat(tParts[0]), parseFloat(tParts[1]));
                
                const startX = startPt.x;
                const startY = startPt.y;
                const endX = endPt.x;
                const endY = endPt.y;

                const capturedId = tileMap.get(`${endX},${endY}`) || null;
                const moveNotation = getMoveNotation(pId, startX, startY, endX, endY, capturedId);
                const dc = { startX, startY, endX, endY };
                const nextFen = simulateMove(currentFen, dc);

                tileMap.delete(`${startX},${startY}`);
                tileMap.set(`${endX},${endY}`, pId);

                let newNode = {
                    fen: nextFen,
                    move: moveNotation,
                    lastMove: dc,
                    c: comment,
                    v: []
                };
                currentNode.v.push(newNode);
                currentNode = newNode;
                currentFen = nextFen;
                stepIndex++;
            }

            historyFEN = newHistoryFEN;
            initBranch();
            currentStepIndex = 0;
            renderRecordUI();
            renderNoteUI();
            jumpToStep(0);
            saveStateToUndo();
            updateToolHighlights();
        }

D. Enhanced Text Export UI & Format Toggle

Update renderExportTextUI() to include the engine format logic and the toggle button SVG.

Find function renderExportTextUI() { and replace it entirely with:
JavaScript

        function renderExportTextUI() {
            const recordContainer = document.querySelector('.ejceesrecord');
            const commentDiv = document.querySelector('.ejceescomment');
            const btnDiv = document.querySelector('.ejceestextbtn');

            const cnText = generateExportText(false);
            const enText = generateExportText(true);

            // Generate UCCI Engine Format String
            let path = getGamePath();
            let engineText = "position fen " + path.fen + " moves";
            let currNode = path;
            while(currNode && currNode.v && currNode.v.length > 0) {
                currNode = currNode.v[0];
                let dc = currNode.lastMove;
                if(dc) {
                    // Convert to coordinates like a0b0
                    engineText += " " + String.fromCharCode(97 + dc.startX) + (9 - dc.startY) + String.fromCharCode(97 + dc.endX) + (9 - dc.endY);
                }
            }

            let isEngineFormat = false;

            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;">${cnText}</textarea>`;

            const cnBlob = new Blob([cnText], { type: 'text/plain' });
            const enBlob = new Blob([enText], { type: 'text/plain' });

            const now = new Date();
            const timestamp =
                now.getFullYear().toString().padStart(4, '0') +
                (now.getMonth() + 1).toString().padStart(2, '0') +
                now.getDate().toString().padStart(2, '0') +
                now.getHours().toString().padStart(2, '0') +
                now.getMinutes().toString().padStart(2, '0') +
                now.getSeconds().toString().padStart(2, '0');

            // Insert Toggle Button alongside the links
            commentDiv.innerHTML = `<div style="display:flex; flex-direction:column; gap:10px; padding:10px;">
                    <a href="${URL.createObjectURL(cnBlob)}" download="ejcees_fen_${timestamp}.txt" style="color:#66b2ff; text-decoration:none;">fen (${cnBlob.size} bytes)</a>
                    <a href="${URL.createObjectURL(enBlob)}" download="ejcees_en_fen_${timestamp}.txt" style="color:#66b2ff; text-decoration:none;">en_fen (${enBlob.size} bytes)</a>
                    <div id="exp-toggle" title="Toggle Format" style="width: 72px; height: 48px; border-radius: 4px; display: flex; justify-content: center; align-items: center; cursor: pointer; background-color: #555; margin-top: 10px;">
                        <svg viewBox="0 0 24 24" style="width:32px;height:32px;fill:#fff;"><path d="M12 4l-4 4h3v7h2V8h3l-4-4zm0 16l4-4h-3V9h-2v7H8l4 4z"/></svg>
                    </div>
                </div>`;

            btnDiv.innerHTML = `
                    <div class="ejceestextbtninner">
                        <div class="note-btn btn-confirm" id="exp-confirm" title="Confirm">
                            <svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
                        </div>
                        <div class="note-btn btn-cancel" id="exp-cancel" title="Cancel">
                            <svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
                        </div>
                    </div>
                `;

            document.getElementById('exp-cancel').addEventListener('click', () => {
                isExportTextMode = false;
                renderRecordUI();
                renderNoteUI();
                updateToolHighlights();
            });

            document.getElementById('exp-confirm').addEventListener('click', () => {
                const text = document.getElementById('export-textarea').value;
                isExportTextMode = false;
                importExportedText(text);
            });

            document.getElementById('exp-toggle').addEventListener('click', () => {
                isEngineFormat = !isEngineFormat;
                document.getElementById('export-textarea').value = isEngineFormat ? engineText : cnText;
            });
        }

E. New Virtual Map Helper and Enhanced Text Parsing Logic

Add a helper function to create logic maps during FEN manipulation, and rewrite importExportedText() to fulfill the branch matching/attachment rules and engine format parsing.

Find function importExportedText(text) { and replace it and its contents entirely with:
JavaScript

        // Helper to evaluate notations without touching the actual DOM layout
        function buildVirtualMap(fen) {
            const map = new Map();
            const counts = {};
            const rows = fen.split(' ')[0].split('/');
            rows.forEach((row, y) => {
                let x = 0;
                for (let i = 0; i < row.length; i++) {
                    const char = row[i];
                    if (/[0-9]/.test(char)) x += parseInt(char, 10);
                    else {
                        counts[char] = counts[char] || 0;
                        map.set(`${x},${y}`, `${char}${counts[char]++}`);
                        x++;
                    }
                }
            });
            return map;
        }

        function importExportedText(text) {
            text = text.trim();
            if (!text) {
                renderRecordUI();
                renderNoteUI();
                updateToolHighlights();
                return;
            }

            // Normalize Chinese notations
            text = text.replace(/车/g, '車').replace(/马/g, '馬').replace(/帅/g, '帥').replace(/将/g, '將').replace(/后/g, '後').replace(/进/g, '進');
            text = text.replace(/ r(\n|\s|$)/g, ' w$1');

            // Handle Direct JSON structure
            if (text.startsWith('{')) {
                try {
                    const data = JSON.parse(text);
                    function expand(node, parentFen) {
                        let dc, simfen, newNode;
                        if (node.m) {
                            dc = deriveCoordsFromMove(parentFen, node.m);
                            simfen = simulateMove(parentFen, dc);
                            newNode = { fen: simfen, move: node.m, lastMove: dc, c: node.c || "", v: [] };
                        } else {
                            newNode = { fen: parentFen, c: node.c || "", v: [] };
                        }
                        if (node.v) { newNode.v = node.v.map(child => expand(child, newNode.fen)); }
                        return newNode;
                    }
                    historyFEN = expand(data, data.fen);
                    initBranch();
                    currentStepIndex = 0;
                    renderRecordUI();
                    renderNoteUI();
                    jumpToStep(0);
                    saveStateToUndo();
                    updateToolHighlights();
                    return;
                } catch (e) {
                    console.log("JSON parse failed, processing as text notation.");
                }
            }

            // 1. Locate explicitly provided FEN if present
            let importedFen = null;
            const fenMatch = text.match(/(?:position fen )?([a-zA-Z0-9/]+ [wb])/);
            let movesStr = text;

            if (fenMatch) {
                importedFen = fenMatch[1];
                // Strip the FEN string declaration so it isn't parsed as moves later
                movesStr = movesStr.replace(/position fen [a-zA-Z0-9/]+ [wb](\s+moves)?/, '');
                if (movesStr === text) {
                    movesStr = movesStr.replace(fenMatch[1], '');
                }
            }

            // 2. Identify the Attach Point (Parent Node)
            let attachIndex = currentStepIndex;
            let attachNode = null;

            if (importedFen) {
                // If FEN exists, trace backwards to find a matching state
                for (let i = currentStepIndex; i >= 0; i--) {
                    const node = getNodeAtStep(i);
                    if (node && node.fen === importedFen) {
                        attachIndex = i;
                        attachNode = node;
                        break;
                    }
                }
                // No match down to root -> Overwrite everything
                if (!attachNode) {
                    attachIndex = 0;
                    attachNode = { fen: importedFen, move: null, lastMove: null, c: "", v: [] };
                    historyFEN = attachNode;
                    currentBranch = [];
                }
            } else {
                // No FEN -> Default attach point is the current active step
                attachNode = getNodeAtStep(attachIndex);
                importedFen = attachNode.fen;
            }

            // 3. Process move sequence text
            // Remove line numbers (e.g., "1. " or " 23. ") and ellipsis "..."
            movesStr = movesStr.replace(/^\d+\.\s*/gm, ' ').replace(/\s\d+\.\s/g, ' ').replace(/\.\.\./g, ' ');
            let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');

            let currentFen = importedFen;
            let vMap = buildVirtualMap(currentFen);
            let currNode = attachNode;
            let attachDepth = attachIndex;
            
            // Truncate currentBranch up to the attachIndex to prepare for the new branch selection
            currentBranch.length = attachDepth;

            let hasError = false;

            for (let i = 0; i < tokens.length; i++) {
                let token = tokens[i];
                let dc = null;
                let moveEn = "";
                let isRed = currentFen.includes(' w');

                // Check for UCCI Engine Coordinate Format (e.g., "a0b0" or "h7e7")
                if (/^[a-i][0-9][a-i][0-9]$/.test(token)) {
                    let startX = token.charCodeAt(0) - 97;
                    let startY = 9 - parseInt(token.charAt(1), 10);
                    let endX = token.charCodeAt(2) - 97;
                    let endY = 9 - parseInt(token.charAt(3), 10);
                    dc = { startX, startY, endX, endY };

                    let pId = vMap.get(`${startX},${startY}`);
                    if (!pId) { hasError = true; break; }

                    // Generate UI notation dynamically from coordinates
                    moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                } else {
                    // Standard English or Chinese notation
                    let isEnglish = /^[a-zA-Z]/.test(token) || /^[+\-=1-9][a-zA-Z0-9]/.test(token);
                    moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
                    dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                    if (!dc) { hasError = true; break; }
                }

                let nextFen = simulateMove(currentFen, dc);

                // Check if this move already exists as a branch from the current node
                let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
                if (childIdx === -1) {
                    // Create new branch
                    let newNode = { fen: nextFen, move: moveEn, lastMove: dc, c: "", v: [] };
                    currNode.v.push(newNode);
                    childIdx = currNode.v.length - 1;
                }

                // Force selection onto this path
                currentBranch[attachDepth] = childIdx;
                currNode = currNode.v[childIdx];
                currentFen = nextFen;
                attachDepth++;

                // Update virtual map for next iterations
                let pId = vMap.get(`${dc.startX},${dc.startY}`);
                vMap.delete(`${dc.startX},${dc.startY}`);
                if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
            }

            if (hasError && tokens.length > 0) {
                alert("Parsed partially due to an invalid move notation.");
            }
            
            // Pad currentBranch with 0s if the newly attached path has existing single children further down
            while (currNode && currNode.v && currNode.v.length > 0) {
                currentBranch[attachDepth] = 0;
                currNode = currNode.v[0];
                attachDepth++;
            }

            // Jump to the parent node of the newly added branch (attach point)
            currentStepIndex = attachIndex;
            renderRecordUI();
            renderNoteUI();
            jumpToStep(currentStepIndex);
            
            saveStateToUndo();
            updateToolHighlights();
        }


Gemini isn’t human. It can make mistakes, so double-check it.
附件
geminiEnhancingRecor.7z
(72.37 KiB) 已下载 62 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

代码: 全选

 Conversation with Gemini
You said

中國象棋局面fen字符串後面的- - 0 1是什麼意義?
Gemini said

在中国象棋的 FEN (Forsyth-Edwards Notation) 格式中,局面字符串通常由六个部分组成,中间用空格隔开。你提到的 - - 0 1 对应的是 FEN 字串的最后四个字段。

虽然中国象棋的 FEN 标准在不同的软件(如象棋巫师、XQBoard)中可能略有差异,但通用的含义如下:
1. 第一个 -:王著(或者特定规则下的标记)

在标准的国际象棋 FEN 中,这里是易位权限。但在中国象棋中:

    由于中国象棋没有“易位”,这个位置通常用 - 占位。

    在某些变体规则中,它可能用来记录某些特殊权限,但 99% 的情况下你看到的都是 -。

2. 第二个 -:吃过路兵位置

这也是从国际象棋 FEN 格式继承过来的字段。

    中国象棋没有“吃过路兵”的规则,所以这里固定为 -。

3. 数字 0:半回合数 (Half-move Clock)

这个数字记录了自上一次“吃子”或“进兵”以来,双方合计走了多少步(每个单步算 1)。

    用途: 主要用于判断“和棋”。例如,在某些规则下,如果连续 60 步(或双方各 60 回合,视具体规则而定)没有任何吃子或兵卒移动,可以判和。

    重置: 只要场上有任何棋子被吃,或者有兵(卒)向前推进,这个计数器就会归零。

4. 数字 1:全回合数 (Full-move Number)

这个数字表示当前是第几个回合。

    规则: 游戏开始时为 1。

    增加: 每当黑方走完一步棋,这个数字就会增加 1(即红黑各走一招,算一个完整回合)。

总结举例

一个典型的中国象棋 FEN 可能长这样:
rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1
字段	示例内容	含义
棋盘布局	rnbak...	从第 10 路到第 1 路的棋子分布
轮到谁走	w (或 r)	w 代表红方(先手),b 代表黑方
易位/特殊	-	无意义,占位符
过路兵	-	无意义,占位符
半回合数	0	距离上次吃子或动兵的单步数
全回合数	1	当前比赛进行的总回合数

如果你是在编写程序或处理棋谱,通常只需要关注 局面布局 和 轮到谁走,后面的四个参数在大多数推演中可以默认设为 0 1(如果不需要严格判断长打或和棋规则的话)。

需要我帮你解析一个具体的 FEN 字符串,或者将其转换成棋盘吗?
You said

如果易位或吃了過路兵,要怎麼記?
Gemini said

这是一个非常敏锐的问题!简单直接的回答是:在中国象棋(Xiangqi)的 FEN 标准中,由于规则本身不包含“易位”和“吃过路兵”,这两个字段永远记为 -。

如果你在处理 FEN 字符串时,想要强行记录这两项,通常是因为你在处理国际象棋(Chess),或者你正在设计一种结合了这两项规则的象棋变体。

以下是按照国际标准(主要参考国际象棋 FEN)记录这两个字段的方法:
1. 易位權限 (Castling Availability)

在国际象棋中,这个字段用来记录王和车是否还有权利进行易位。

    记录规则: 使用字母缩写。

        K: 白方王翼易位(短易位)

        Q: 白方后翼易位(长易位)

        k: 黑方王翼易位

        q: 黑方后翼易位

    记录方式:

        如果双方都有双向易位权,记为 KQkq。

        如果只有白方可以短易位,黑方可以长易位,记为 Kq。

        如果没有任何易位权(即你看到的中国象棋情况),记为 -。

2. 吃過路兵位置 (En Passant Target Square)

当一方的兵(Pawn)第一次移动两格时,它“跳过”的那个格子就是“过路兵目标格”。

    记录规则: 使用棋盘坐标(例如 e3 或 c6)。

    记录方式:

        如果上一回合白方把兵从 e2 走到了 e4,那么黑方在下一招可以吃掉它,此时 FEN 记为 e3(即兵跳过的位置)。

        如果上一招没有出现兵走两格的情况,或者该规则不适用,则记为 -。

為什麼中國象棋總是 - -?

中国象棋的规则决定了这两个字段没有发挥空间:

    没有易位: 帅(将)和仕(士)只能在九宫格内移动,没有类似国际象棋中王与车交换位置的动作。

    兵(卒)只能走一格: 中国象棋的兵在过河前和过河后,每次都只能向前或横向走一格。既然没有“首步走两格”的规则,自然也就没有“吃过路兵”的情况。

總結

即使你使用的是支持多种棋类的引擎(如 Stockfish),当你切换到中国象棋模式时:

    易位字段: 永远是 -。

    过路兵字段: 永远是 -。

如果你在尝试编写一个支持这些规则的自定义象棋游戏,你可以借用国际象棋的记法。例如,若红方可以“易位”,你可以用大写字母 R 表示。但请注意,这就不再是标准的中国象棋 FEN 了。

你想了解如何将这些规则融合进象棋变体的编程实现吗?
You said

如果我把這兩個-符號改為0或1,用於記錄中國象棋當前盤面是否旋轉或鏡像,是否可行?
Gemini said

从技术实现的角度来看,这完全可行,但从标准兼容性的角度来看,这属于自定义扩展(Private Extension)。

在编程和数据存储中,你可以自由定义 FEN 字符串的含义,只要你的解析器(Parser)和渲染器(Renderer)能够理解这些规则即可。

以下是关于这种做法的详细评估和建议:
1. 为什么这在技术上是可行的?

FEN 本质上是一个以空格分隔的字符串。解析引擎通常会按空格拆分:

    第 1-2 段(布局和走子方)是核心,几乎所有引擎都会读取。

    第 3-6 段(包括你提到的两个 -)在很多简单的中国象棋程序中甚至会被直接忽略。

如果你将其改为 0 或 1:

    记录旋转:0 代表红方在下,1 代表黑方在下(视角翻转)。

    记录镜像:0 代表原始,1 代表左右镜像。

这种做法非常节省空间,比单独开一个 JSON 字段记录“是否旋转”要紧凑得多。
2. 潜在的兼容性风险(副作用)

如果你打算让你的 FEN 字符串与其他主流象棋软件(如象棋巫师、东萍棋路、或者是 Stockfish/Lichess 的象棋变体)交互,可能会遇到以下问题:

    解析错误:某些严谨的解析器会检查第三、四位是否为 -。如果发现是数字,可能会报错,或者误以为这是某种奇怪的国际象棋变体。

    信息丢失:如果你把带 1 的 FEN 导入到标准软件中,标准软件会忽略这个信息。当你再次从该软件导出时,你的“旋转/镜像”信息可能会被重新重置为 -。

3. 更专业的建议:使用 "Extended FEN"

如果你希望在不破坏标准的前提下记录额外信息,通常有两种更推荐的做法:
方案 A:在末尾追加(推荐)

不要修改中间的 - -,保持其占位符的功能,而是在 FEN 的最后增加第七个字段。

    ... w - - 0 1 <rotate_flag> <mirror_flag>

    例如:rnbakabnr/... w - - 0 1 1 0(末尾的 1 0 代表旋转但不镜像)

方案 B:利用“轮到谁走”字段

中国象棋的 FEN 中,走子方通常是 w(红)或 b(黑)。有些开发者会使用大写/小写以外的标记,但这种改动较大,不建议使用。
4. 总结与实施建议
方案	优点	缺点
直接改 - 为数字	最直接,字符串长度不变。	破坏了 FEN 格式的语义(占位符变成了数据)。
在末尾添加新字段	扩展性强,不会干扰前六项的逻辑。	字符串变长,非自定义程序可能无法识别。

我的建议:
如果你这个 FEN 只是在你自己开发的系统内部流动(比如你的前端和后端之间),直接改 - 为 0/1 是非常聪明的做法,因为这利用了死字段。

你会如何定义这两个数字? 例如:

    第三位:0=不旋转,1=旋转 180°

    第四位:0=不镜像,1=左右镜像

需要我帮你写一段 Python 或 JavaScript 代码,用来解析这种带自定义标记的 FEN 字符串吗?


Gemini isn’t human. It can make mistakes, so double-check it.
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

導入之後,並未切換分支,其餘已經做好:
附件
ejcees20260322aabb99945.7z
(36.47 KiB) 已下载 61 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

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