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

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

帖子 ejsoon »

實現導入東萍象棋棋譜,可包含分支:
https://ejsoon.vip/wp-content/uploads/2 ... 99771.html
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

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

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

帖子 ejsoon »

如果第一步導入都會發生錯誤,則改為新建並往INITIAL_FEN導入,如果仍發生錯誤,則這次導入中止。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

代码: 全选

改進以下代碼:

一,當path的根節點存在path.c且不為空時,在「fen + '\n'」的後面加上「path.c + \n」。

二,當任何節點存在path.c且不為空時,在它下方加「\n + path.c + \n」。如果輪到紅方,則加完後,接下來黑方會加入 「...」省略號。

例如之前是「1. 炮二平五   馬8進7」,現在就變為「
1. 炮二平五
對於炮二平五的評論。
1. ... 馬8進7
」

給出完整的generateCnTextWidthComment函式,所有代碼和注釋都要使用英文。

function generateCnTextWidthComment() {
            let path = getGamePath();
            let fen = path.fen;
            let text = fen + '\n';
            let isRedTurn = fen.split(' ')[1] === 'w';

            let moveIdx = 0;
            let moveNum = 1;

            while (path.v.length) {
                let line = `${moveNum}. `;
                if (!isRedTurn && moveIdx === 0) {
                    path = path.v[0];
                    let blkMove = isEnglish ? path.move : NotationConverter.toChinese(path.move);
                    line += `... ${blkMove}`;
                    moveIdx++;
                } else {
                    path = path.v[0];
                    let redMove = isEnglish ? path.move : NotationConverter.toChinese(path.move);
                    line += `${redMove}`;
                    moveIdx++;

                    if (path.v.length) {
                        path = path.v[0];
                        let blkMove = isEnglish ? path.move : NotationConverter.toChinese(path.move);
                        line += ` ${blkMove}`;
                        moveIdx++;
                    }
                }
                text += line + '\n';
                moveNum++;
            }
            return text.trim();
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

gemini thinking:

代码: 全选

/**
 * Generates game notation text including FEN, moves, and comments.
 * Handles automatic ellipsis (...) insertion when a comment interrupts a turn.
 */
function generateCnTextWidthComment() {
    const pathRoot = getGamePath();
    const fen = pathRoot.fen;
    let text = fen + '\n';

    // Requirement 1: Add root comment if it exists and is not empty
    if (pathRoot.c && pathRoot.c.trim() !== '') {
        text += pathRoot.c.trim() + '\n';
    }

    let isRedTurn = fen.split(' ')[1] === 'w';
    let moveNum = 1; // Standard starting move number
    let currentNode = pathRoot;
    
    // Tracks if the previous move was interrupted by a comment
    // or if the game started on a Black move.
    let needsEllipsis = !isRedTurn;

    while (currentNode.v && currentNode.v.length > 0) {
        // Move to the next node in the main variation
        currentNode = currentNode.v[0];
        
        // Convert move notation based on language setting
        const moveStr = typeof isEnglish !== 'undefined' && isEnglish 
            ? currentNode.move 
            : NotationConverter.toChinese(currentNode.move);

        if (isRedTurn) {
            // RED TURN PROCESSING
            text += `${moveNum}. ${moveStr}`;

            // Requirement 2: Check for comment after Red move
            if (currentNode.c && currentNode.c.trim() !== '') {
                text += `\n${currentNode.c.trim()}\n`;
                needsEllipsis = true; // Black will need "..." on the next line
            } else {
                text += " "; // Add space to separate from potential Black move
                needsEllipsis = false;
            }
            
            isRedTurn = false; // Toggle turn to Black
        } else {
            // BLACK TURN PROCESSING
            if (needsEllipsis) {
                // If interrupted by Red's comment or starting mid-turn
                text += `${moveNum}. ... ${moveStr}\n`;
            } else {
                // Standard flow: Black follows Red on the same line
                text += `${moveStr}\n`;
            }

            // Requirement 2: Check for comment after Black move
            if (currentNode.c && currentNode.c.trim() !== '') {
                text += `${currentNode.c.trim()}\n`;
            }

            moveNum++;         // Increment move number after Black finishes
            isRedTurn = true;  // Toggle turn to Red
            needsEllipsis = false; // Reset ellipsis flag
        }
    }

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

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

帖子 ejsoon »

ejcees20260329withComment999774

這下帶序號的帶評論了:

https://ejsoon.vip/wp-content/uploads/2 ... 99774.html
附件
ejcees20260329withComment999774.7z
(42.75 KiB) 已下载 47 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

ejcees20260329withComm999775

改對評論的單雙次按出結果:

https://ejsoon.vip/wp-content/uploads/2 ... 99775.html
附件
ejcees20260329withComm999775.7z
(42.75 KiB) 已下载 49 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

加上一個判斷,如果是首個token就轉換失敗,則把局面改為「新建局面」導入,如果在新建局面下首個token仍然轉換失敗,則取消導入,恢復為導入之前的狀態。

代码: 全选

改進renderExportTextUI函式:

        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 URL format directly if pasted
            if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
                let hashPart = text.includes('#') ? text.split('#')[1] : text;
                if (hashPart) {
                    try {
                        let parsedTree = parseUrlHash(hashPart);
                        if (parsedTree && parsedTree.fen) {
                            historyFEN = parsedTree;
                            initBranch();
                            currentStepIndex = 0;
                            let fenArr = historyFEN.fen.split(' ');
                            if (fenArr.length >= 4) {
                                isRotateEnabled = fenArr[2] === '1';
                                isFlipEnabled = fenArr[3] === '1';
                                reapplyVisualPositions();
                            }
                            renderRecordUI();
                            renderNoteUI();
                            jumpToStep(0);
                            saveStateToUndo();
                            updateToolHighlights();
                            return;
                        }
                    } catch (e) {
                        console.log("Failed to parse as URL parameter, falling back to text notation.", e);
                    }
                }
            }

            // 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);
                    let fenArr = historyFEN.fen.split(' ');
                    if (fenArr.length === 6) {
                        isRotateEnabled = fenArr[2] === '1';
                        isFlipEnabled = fenArr[3] === '1';
                        reapplyVisualPositions();
                    }
                    initBranch(); // initBranch correctly builds currentBranch based on forks
                    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 fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
            const fenMatch = text.match(fenRegex);
            let movesStr = text;

            if (fenMatch) {
                importedFen = fenMatch[0];
                // Strip the FEN string declaration so it isn't parsed as moves later
                const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
                movesStr = movesStr.replace(cleanRegex, '').trim();
            }

            // 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
            // Rule 1: Split text by \n and trim to form an array.
            let lines = movesStr.split('\n').map(l => l.trim());
            // Check if any line matches the "^\d+\." pattern to determine the parsing mode.
            let isComplexFormat = lines.some(l => /^\d+\./.test(l));

            let currentFen = importedFen;
            let vMap = buildVirtualMap(currentFen);
            let currNode = attachNode;
            let currentCommentNode = attachNode;

            // Calculate how many forks exist from root up to attachIndex
            let forkCount = 0;
            let tempNode = historyFEN;
            for (let i = 0; i < attachIndex; i++) {
                if (tempNode.v && tempNode.v.length > 1) {
                    forkCount++;
                }
                const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
                tempNode = tempNode.v[choice];
            }

            // Truncate currentBranch to discard choices beyond the attach point
            currentBranch = currentBranch.slice(0, forkCount);

            let hasError = false;

            if (isComplexFormat) {
                let branchMap = {};
                let seenCircles = new Set();
                let lastMoveParent = attachNode;
                let lastMoveParentFen = importedFen;

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

                    if (!/^\d+\./.test(line)) {
                        // Rule 3: Lines not starting with "^\d+\." are comments or branch declarations
                        let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
                        let isBranchDecl = false;

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

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

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

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

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

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

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

                                if (!dc) {
                                    // If coordinates cannot be derived, treat the token as a comment
                                    // for the current node (which represents the previous step)
                                    if (!currentCommentNode.c) {
                                        currentCommentNode.c = "";
                                    }
                                    // Append the token, separating with a space if a comment already exists
                                    currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
                                    continue; // Skip node creation and continue parsing
                                }

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

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

                                if (childIdx === -1) {
                                    let newNode = {
                                        fen: nextFen,
                                        move: moveEn,
                                        lastMove: dc,
                                        c: "",
                                        v: []
                                    };
                                    currNode.v.push(newNode);
                                    childIdx = currNode.v.length - 1;
                                }

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

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

                                let pId = vMap.get(`${dc.startX},${dc.startY}`);
                                vMap.delete(`${dc.startX},${dc.startY}`);
                                if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
                            }

                            // Rule 4, 7 & 8: Process multiple circled numbers wherever they occur
                            for (let c of circles) {
                                if (!seenCircles.has(c)) {
                                    seenCircles.add(c);
                                    // Map the first occurrence to the parent node of the currently parsed move
                                    branchMap[c] = {
                                        parentNode: lastMoveParent,
                                        fen: lastMoveParentFen
                                    };
                                }
                            }
                        }
                    }
                }
            } else {
                // Fallback for simple flat formats (no "^\d+\." format detected)
                let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
                let tokensStartIdx = 0;
                for (let i = 0; i < tokens.length; i++) {
                    if (/^1\./.test(tokens[i])) {
                        tokensStartIdx = i;
                        break;
                    }
                }

                for (let i = tokensStartIdx; i < tokens.length; i++) {
                    let token = tokens[i].replace(/^\d+\.\s*/, '').substring(0, 4);
                    let dc = null;
                    let moveEn = "";
                    let isRed = currentFen.includes(' w');

                    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;
                        }
                        moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                    } else {
                        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);
                    let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);

                    if (childIdx === -1) {
                        let newNode = {
                            fen: nextFen,
                            move: moveEn,
                            lastMove: dc,
                            c: "",
                            v: []
                        };
                        currNode.v.push(newNode);
                        childIdx = currNode.v.length - 1;
                    }

                    if (currNode.v.length > 1) {
                        currentBranch[forkCount] = childIdx;
                        forkCount++;
                    }

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

                    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) {
                alert("Parsed partially due to an invalid move notation.");
            }

            // Fill default choices (0) for any remaining branches down the tree
            let followNode = attachNode;
            let followIdx = 0;
            while (followNode && followIdx < currentBranch.length) {
                if (followNode.v && followNode.v.length > 1) {
                    followNode = followNode.v[currentBranch[followIdx]];
                } else if (followNode.v && followNode.v.length === 1) {
                    followNode = followNode.v[0];
                } else {
                    break;
                }
                followIdx++;
            }
            while (followNode && followNode.v && followNode.v.length > 0) {
                if (followNode.v.length > 1) {
                    currentBranch.push(0);
                }
                followNode = followNode.v[0];
            }

            currentStepIndex = attachIndex;
            renderRecordUI();
            renderNoteUI();
            jumpToStep(currentStepIndex);

            saveStateToUndo();
            updateToolHighlights();
        }

在這一段:「

                                    dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                                }

                                if (!dc) {
                                    // If coordinates cannot be derived, treat the token as a comment
                                    // for the current node (which represents the previous step)
                                    if (!currentCommentNode.c) {
                                        currentCommentNode.c = "";
                                    }
                                    // Append the token, separating with a space if a comment already exists
                                    currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
                                    continue; // Skip node creation and continue parsing
                                }
」

它的意義為,如果用deriveCoordsFromMove轉換失敗,則token將成為上一步的評論。

現在加上一個判斷,如果是首個token就轉換失敗,則把局面改為「新建局面」導入,如果在新建局面下首個token仍然轉換失敗,則取消導入,恢復為導入之前的狀態。

新建局面主要影響的是這三個變量:

                historyFEN = {
                    fen: INITIAL_FEN,
                    move: null,
                    lastMove: null,
                    c: "",
                    v: []
                };
            currentBranch = [];
            currentStepIndex = 0;

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

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

帖子 ejsoon »

Here is the complete and improved importExportedText function.

To achieve your goal, I have added a state backup at the very beginning of the function. I also introduced an isFirstMoveAttempt flag to track if we are evaluating the first valid move token. If the first token fails to parse, it attempts to parse it against the INITIAL_FEN. If that succeeds, it resets the tree to the initial board state; if it fails again, it restores the backed-up state and cancels the import entirely.

All comments and code are written in English as requested.

代码: 全选

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

            // Backup current state in case the import fails completely on the first move
            const backupHistoryFEN = JSON.parse(JSON.stringify(historyFEN));
            const backupBranch = [...currentBranch];
            const backupStepIndex = currentStepIndex;

            // 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 URL format directly if pasted
            if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
                let hashPart = text.includes('#') ? text.split('#')[1] : text;
                if (hashPart) {
                    try {
                        let parsedTree = parseUrlHash(hashPart);
                        if (parsedTree && parsedTree.fen) {
                            historyFEN = parsedTree;
                            initBranch();
                            currentStepIndex = 0;
                            let fenArr = historyFEN.fen.split(' ');
                            if (fenArr.length >= 4) {
                                isRotateEnabled = fenArr[2] === '1';
                                isFlipEnabled = fenArr[3] === '1';
                                reapplyVisualPositions();
                            }
                            renderRecordUI();
                            renderNoteUI();
                            jumpToStep(0);
                            saveStateToUndo();
                            updateToolHighlights();
                            return;
                        }
                    } catch (e) {
                        console.log("Failed to parse as URL parameter, falling back to text notation.", e);
                    }
                }
            }

            // 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);
                    let fenArr = historyFEN.fen.split(' ');
                    if (fenArr.length === 6) {
                        isRotateEnabled = fenArr[2] === '1';
                        isFlipEnabled = fenArr[3] === '1';
                        reapplyVisualPositions();
                    }
                    initBranch(); // initBranch correctly builds currentBranch based on forks
                    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 fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
            const fenMatch = text.match(fenRegex);
            let movesStr = text;

            if (fenMatch) {
                importedFen = fenMatch[0];
                // Strip the FEN string declaration so it isn't parsed as moves later
                const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
                movesStr = movesStr.replace(cleanRegex, '').trim();
            }

            // 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
            // Rule 1: Split text by \n and trim to form an array.
            let lines = movesStr.split('\n').map(l => l.trim());
            // Check if any line matches the "^\d+\." pattern to determine the parsing mode.
            let isComplexFormat = lines.some(l => /^\d+\./.test(l));

            let currentFen = importedFen;
            let vMap = buildVirtualMap(currentFen);
            let currNode = attachNode;
            let currentCommentNode = attachNode;

            // Calculate how many forks exist from root up to attachIndex
            let forkCount = 0;
            let tempNode = historyFEN;
            for (let i = 0; i < attachIndex; i++) {
                if (tempNode.v && tempNode.v.length > 1) {
                    forkCount++;
                }
                const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
                tempNode = tempNode.v[choice];
            }

            // Truncate currentBranch to discard choices beyond the attach point
            currentBranch = currentBranch.slice(0, forkCount);

            let hasError = false;
            let isFirstMoveAttempt = true; // Flag to track the first move parsing attempt

            if (isComplexFormat) {
                let branchMap = {};
                let seenCircles = new Set();
                let lastMoveParent = attachNode;
                let lastMoveParentFen = importedFen;

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

                    if (!/^\d+\./.test(line)) {
                        // Rule 3: Lines not starting with "^\d+\." are comments or branch declarations
                        let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
                        let isBranchDecl = false;

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

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

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

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

                            if (moveStr.length >= 4) {
                                let dc = null;
                                let moveEn = "";
                                let isRed = currentFen.includes(' w');
                                let isEnglish = /^[a-zA-Z]/.test(moveStr) || /^[+\-=1-9][a-zA-Z0-9]/.test(moveStr);

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

                                    let pId = vMap.get(`${startX},${startY}`);
                                    if (!pId) {
                                        hasError = true;
                                        continue;
                                    }
                                    moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                                } else {
                                    moveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, isRed);
                                    dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                                }

                                if (!dc) {
                                    if (isFirstMoveAttempt) {
                                        // If the very first move fails, try it with INITIAL_FEN
                                        let testIsRed = INITIAL_FEN.includes(' w');
                                        let testMoveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, testIsRed);
                                        let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);
                                        
                                        if (!testDc) {
                                            // Fails even on INITIAL_FEN, abort import completely
                                            historyFEN = backupHistoryFEN;
                                            currentBranch = backupBranch;
                                            currentStepIndex = backupStepIndex;
                                            console.warn("Import aborted: Failed to parse the first move token.");
                                            return; 
                                        } else {
                                            // Success with INITIAL_FEN. Reset the game to "New Game" state.
                                            historyFEN = {
                                                fen: INITIAL_FEN,
                                                move: null,
                                                lastMove: null,
                                                c: "",
                                                v: []
                                            };
                                            currentBranch = [];
                                            currentStepIndex = 0;
                                            attachIndex = 0;
                                            
                                            // Re-initialize parsing traversal variables
                                            attachNode = historyFEN;
                                            currNode = historyFEN;
                                            currentCommentNode = historyFEN;
                                            currentFen = INITIAL_FEN;
                                            vMap = buildVirtualMap(currentFen);
                                            forkCount = 0;
                                            lastMoveParent = historyFEN;
                                            lastMoveParentFen = INITIAL_FEN;

                                            // Apply the successful test data
                                            dc = testDc;
                                            isRed = testIsRed;
                                            moveEn = testMoveEn;
                                        }
                                    } else {
                                        // If coordinates cannot be derived and it's not the first move, treat as a comment
                                        if (!currentCommentNode.c) {
                                            currentCommentNode.c = "";
                                        }
                                        currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
                                        continue; // Skip node creation and continue parsing
                                    }
                                }
                                
                                // Flag that we have successfully processed at least one valid move
                                isFirstMoveAttempt = false;

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

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

                                if (childIdx === -1) {
                                    let newNode = {
                                        fen: nextFen,
                                        move: moveEn,
                                        lastMove: dc,
                                        c: "",
                                        v: []
                                    };
                                    currNode.v.push(newNode);
                                    childIdx = currNode.v.length - 1;
                                }

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

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

                                let pId = vMap.get(`${dc.startX},${dc.startY}`);
                                vMap.delete(`${dc.startX},${dc.startY}`);
                                if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
                            }

                            // Rule 4, 7 & 8: Process multiple circled numbers wherever they occur
                            for (let c of circles) {
                                if (!seenCircles.has(c)) {
                                    seenCircles.add(c);
                                    // Map the first occurrence to the parent node of the currently parsed move
                                    branchMap[c] = {
                                        parentNode: lastMoveParent,
                                        fen: lastMoveParentFen
                                    };
                                }
                            }
                        }
                    }
                }
            } else {
                // Fallback for simple flat formats (no "^\d+\." format detected)
                let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
                let tokensStartIdx = 0;
                for (let i = 0; i < tokens.length; i++) {
                    if (/^1\./.test(tokens[i])) {
                        tokensStartIdx = i;
                        break;
                    }
                }

                for (let i = tokensStartIdx; i < tokens.length; i++) {
                    let token = tokens[i].replace(/^\d+\.\s*/, '').substring(0, 4);
                    let dc = null;
                    let moveEn = "";
                    let isRed = currentFen.includes(' w');
                    let isEnglish = /^[a-zA-Z]/.test(token) || /^[+\-=1-9][a-zA-Z0-9]/.test(token);

                    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;
                        }
                        moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                    } else {
                        moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
                        dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                        
                        if (!dc) {
                            if (isFirstMoveAttempt) {
                                // Try with INITIAL_FEN
                                let testIsRed = INITIAL_FEN.includes(' w');
                                let testMoveEn = isEnglish ? token : NotationConverter.toEnglish(token, testIsRed);
                                let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);
                                
                                if (!testDc) {
                                    // Fails completely, restore state
                                    historyFEN = backupHistoryFEN;
                                    currentBranch = backupBranch;
                                    currentStepIndex = backupStepIndex;
                                    console.warn("Import aborted: Failed to parse the first move token in simple format.");
                                    return;
                                } else {
                                    // Reset state to New Game
                                    historyFEN = {
                                        fen: INITIAL_FEN,
                                        move: null,
                                        lastMove: null,
                                        c: "",
                                        v: []
                                    };
                                    currentBranch = [];
                                    currentStepIndex = 0;
                                    
                                    attachNode = historyFEN;
                                    currNode = historyFEN;
                                    currentFen = INITIAL_FEN;
                                    vMap = buildVirtualMap(currentFen);
                                    forkCount = 0;
                                    
                                    dc = testDc;
                                    isRed = testIsRed;
                                    moveEn = testMoveEn;
                                }
                            } else {
                                hasError = true;
                                break;
                            }
                        }
                    }

                    isFirstMoveAttempt = false;

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

                    if (childIdx === -1) {
                        let newNode = {
                            fen: nextFen,
                            move: moveEn,
                            lastMove: dc,
                            c: "",
                            v: []
                        };
                        currNode.v.push(newNode);
                        childIdx = currNode.v.length - 1;
                    }

                    if (currNode.v.length > 1) {
                        currentBranch[forkCount] = childIdx;
                        forkCount++;
                    }

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

                    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) {
                alert("Parsed partially due to an invalid move notation.");
            }

            // Fill default choices (0) for any remaining branches down the tree
            let followNode = attachNode;
            let followIdx = 0;
            while (followNode && followIdx < currentBranch.length) {
                if (followNode.v && followNode.v.length > 1) {
                    followNode = followNode.v[currentBranch[followIdx]];
                } else if (followNode.v && followNode.v.length === 1) {
                    followNode = followNode.v[0];
                } else {
                    break;
                }
                followIdx++;
            }
            while (followNode && followNode.v && followNode.v.length > 0) {
                if (followNode.v.length > 1) {
                    currentBranch.push(0);
                }
                followNode = followNode.v[0];
            }

            currentStepIndex = attachIndex;
            renderRecordUI();
            renderNoteUI();
            jumpToStep(currentStepIndex);

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

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

帖子 ejsoon »

之前isFirstMoveAttempt的意義為「第一個token就轉換失敗」,現在要改為「不是最後一個token轉換失敗」。名稱可能要從isFirstMoveAttempt改成其它的單字。

代码: 全选

對以下的代碼作改進:
        function importExportedText(text) {
            text = text.trim();
            if (!text) {
                renderRecordUI();
                renderNoteUI();
                updateToolHighlights();
                return;
            }

            // Backup current state in case the import fails completely on the first move
            const backupHistoryFEN = JSON.parse(JSON.stringify(historyFEN));
            const backupBranch = [...currentBranch];
            const backupStepIndex = currentStepIndex;

            // 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 URL format directly if pasted
            if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
                let hashPart = text.includes('#') ? text.split('#')[1] : text;
                if (hashPart) {
                    try {
                        let parsedTree = parseUrlHash(hashPart);
                        if (parsedTree && parsedTree.fen) {
                            historyFEN = parsedTree;
                            initBranch();
                            currentStepIndex = 0;
                            let fenArr = historyFEN.fen.split(' ');
                            if (fenArr.length >= 4) {
                                isRotateEnabled = fenArr[2] === '1';
                                isFlipEnabled = fenArr[3] === '1';
                                reapplyVisualPositions();
                            }
                            renderRecordUI();
                            renderNoteUI();
                            jumpToStep(0);
                            saveStateToUndo();
                            updateToolHighlights();
                            return;
                        }
                    } catch (e) {
                        console.log("Failed to parse as URL parameter, falling back to text notation.", e);
                    }
                }
            }

            // 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);
                    let fenArr = historyFEN.fen.split(' ');
                    if (fenArr.length === 6) {
                        isRotateEnabled = fenArr[2] === '1';
                        isFlipEnabled = fenArr[3] === '1';
                        reapplyVisualPositions();
                    }
                    initBranch(); // initBranch correctly builds currentBranch based on forks
                    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 fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
            const fenMatch = text.match(fenRegex);
            let movesStr = text;

            if (fenMatch) {
                importedFen = fenMatch[0];
                // Strip the FEN string declaration so it isn't parsed as moves later
                const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
                movesStr = movesStr.replace(cleanRegex, '').trim();
            }

            // 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
            // Rule 1: Split text by \n and trim to form an array.
            let lines = movesStr.split('\n').map(l => l.trim());
            // Check if any line matches the "^\d+\." pattern to determine the parsing mode.
            let isComplexFormat = lines.some(l => /^\d+\./.test(l));

            let currentFen = importedFen;
            let vMap = buildVirtualMap(currentFen);
            let currNode = attachNode;
            let currentCommentNode = attachNode;

            // Calculate how many forks exist from root up to attachIndex
            let forkCount = 0;
            let tempNode = historyFEN;
            for (let i = 0; i < attachIndex; i++) {
                if (tempNode.v && tempNode.v.length > 1) {
                    forkCount++;
                }
                const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
                tempNode = tempNode.v[choice];
            }

            // Truncate currentBranch to discard choices beyond the attach point
            currentBranch = currentBranch.slice(0, forkCount);

            let hasError = false;
            let isFirstMoveAttempt = true; // Flag to track the first move parsing attempt

            if (isComplexFormat) {
                let branchMap = {};
                let seenCircles = new Set();
                let lastMoveParent = attachNode;
                let lastMoveParentFen = importedFen;

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

                    if (!/^\d+\./.test(line)) {
                        // Rule 3: Lines not starting with "^\d+\." are comments or branch declarations
                        let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
                        let isBranchDecl = false;

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

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

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

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

                            if (moveStr.length >= 4) {
                                let dc = null;
                                let moveEn = "";
                                let isRed = currentFen.includes(' w');
                                let isEnglish = /^[a-zA-Z]/.test(moveStr) || /^[+\-=1-9][a-zA-Z0-9]/.test(moveStr);

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

                                    let pId = vMap.get(`${startX},${startY}`);
                                    if (!pId) {
                                        hasError = true;
                                        continue;
                                    }
                                    moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                                } else {
                                    moveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, isRed);
                                    dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                                }

                                if (!dc) {
                                    if (isFirstMoveAttempt) {
                                        // If the very first move fails, try it with INITIAL_FEN
                                        let testIsRed = INITIAL_FEN.includes(' w');
                                        let testMoveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, testIsRed);
                                        let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);

                                        if (!testDc) {
                                            // Fails even on INITIAL_FEN, abort import completely
                                            historyFEN = backupHistoryFEN;
                                            currentBranch = backupBranch;
                                            currentStepIndex = backupStepIndex;
                                            console.warn("Import aborted: Failed to parse the first move token.");
                                            return; 
                                        } else {
                                            // Success with INITIAL_FEN. Reset the game to "New Game" state.
                                            historyFEN = {
                                                fen: INITIAL_FEN,
                                                move: null,
                                                lastMove: null,
                                                c: "",
                                                v: []
                                            };
                                            currentBranch = [];
                                            currentStepIndex = 0;
                                            attachIndex = 0;

                                            // add root comment
                                            for (let j = 0; j < lines.length; j++) {
                                                let line = lines[j];
                                                if (!/^\d+\./.test(line)) {
                                                    historyFEN.c += (historyFEN.c === "" ? "" : "\n") + line;
                                                } else {
                                                    break;
                                                }
                                            }

                                            // Re-initialize parsing traversal variables
                                            attachNode = historyFEN;
                                            currNode = historyFEN;
                                            currentCommentNode = historyFEN;
                                            currentFen = INITIAL_FEN;
                                            vMap = buildVirtualMap(currentFen);
                                            forkCount = 0;
                                            lastMoveParent = historyFEN;
                                            lastMoveParentFen = INITIAL_FEN;

                                            // Apply the successful test data
                                            dc = testDc;
                                            isRed = testIsRed;
                                            moveEn = testMoveEn;
                                        }
                                    } else {
                                        // If coordinates cannot be derived and it's not the first move, treat as a comment
                                        if (!currentCommentNode.c) {
                                            currentCommentNode.c = "";
                                        }
                                        currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
                                        continue; // Skip node creation and continue parsing
                                    }
                                }

                                // Flag that we have successfully processed at least one valid move
                                isFirstMoveAttempt = false;

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

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

                                if (childIdx === -1) {
                                    let newNode = {
                                        fen: nextFen,
                                        move: moveEn,
                                        lastMove: dc,
                                        c: "",
                                        v: []
                                    };
                                    currNode.v.push(newNode);
                                    childIdx = currNode.v.length - 1;
                                }

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

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

                                let pId = vMap.get(`${dc.startX},${dc.startY}`);
                                vMap.delete(`${dc.startX},${dc.startY}`);
                                if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
                            }

                            // Rule 4, 7 & 8: Process multiple circled numbers wherever they occur
                            for (let c of circles) {
                                if (!seenCircles.has(c)) {
                                    seenCircles.add(c);
                                    // Map the first occurrence to the parent node of the currently parsed move
                                    branchMap[c] = {
                                        parentNode: lastMoveParent,
                                        fen: lastMoveParentFen
                                    };
                                }
                            }
                        }
                    }
                }
            } else {
                // Fallback for simple flat formats (no "^\d+\." format detected)
                let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
                let tokensStartIdx = 0;
                for (let i = 0; i < tokens.length; i++) {
                    if (/^1\./.test(tokens[i])) {
                        tokensStartIdx = i;
                        break;
                    }
                }

                for (let i = tokensStartIdx; i < tokens.length; i++) {
                    let token = tokens[i].replace(/^\d+\.\s*/, '').substring(0, 4);
                    let dc = null;
                    let moveEn = "";
                    let isRed = currentFen.includes(' w');
                    let isEnglish = /^[a-zA-Z]/.test(token) || /^[+\-=1-9][a-zA-Z0-9]/.test(token);

                    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;
                        }
                        moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                    } else {
                        moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
                        dc = deriveCoordsFromMove(currentFen, moveEn, isRed);

                        if (!dc) {
                            if (isFirstMoveAttempt) {
                                // Try with INITIAL_FEN
                                let testIsRed = INITIAL_FEN.includes(' w');
                                let testMoveEn = isEnglish ? token : NotationConverter.toEnglish(token, testIsRed);
                                let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);

                                if (!testDc) {
                                    // Fails completely, restore state
                                    historyFEN = backupHistoryFEN;
                                    currentBranch = backupBranch;
                                    currentStepIndex = backupStepIndex;
                                    console.warn("Import aborted: Failed to parse the first move token in simple format.");
                                    return;
                                } else {
                                    // Reset state to New Game
                                    historyFEN = {
                                        fen: INITIAL_FEN,
                                        move: null,
                                        lastMove: null,
                                        c: "",
                                        v: []
                                    };
                                    currentBranch = [];
                                    currentStepIndex = 0;

                                    attachNode = historyFEN;
                                    currNode = historyFEN;
                                    currentFen = INITIAL_FEN;
                                    vMap = buildVirtualMap(currentFen);
                                    forkCount = 0;

                                    dc = testDc;
                                    isRed = testIsRed;
                                    moveEn = testMoveEn;
                                }
                            } else {
                                hasError = true;
                                break;
                            }
                        }
                    }

                    isFirstMoveAttempt = false;

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

                    if (childIdx === -1) {
                        let newNode = {
                            fen: nextFen,
                            move: moveEn,
                            lastMove: dc,
                            c: "",
                            v: []
                        };
                        currNode.v.push(newNode);
                        childIdx = currNode.v.length - 1;
                    }

                    if (currNode.v.length > 1) {
                        currentBranch[forkCount] = childIdx;
                        forkCount++;
                    }

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

                    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) {
                alert("Parsed partially due to an invalid move notation.");
            }

            // Fill default choices (0) for any remaining branches down the tree
            let followNode = attachNode;
            let followIdx = 0;
            while (followNode && followIdx < currentBranch.length) {
                if (followNode.v && followNode.v.length > 1) {
                    followNode = followNode.v[currentBranch[followIdx]];
                } else if (followNode.v && followNode.v.length === 1) {
                    followNode = followNode.v[0];
                } else {
                    break;
                }
                followIdx++;
            }
            while (followNode && followNode.v && followNode.v.length > 0) {
                if (followNode.v.length > 1) {
                    currentBranch.push(0);
                }
                followNode = followNode.v[0];
            }

            currentStepIndex = attachIndex;
            renderRecordUI();
            renderNoteUI();
            jumpToStep(currentStepIndex);

            saveStateToUndo();
            updateToolHighlights();
        }


之前isFirstMoveAttempt的意義為「第一個token就轉換失敗」,現在要改為「不是最後一個token轉換失敗」。名稱可能要從isFirstMoveAttempt改成其它的單字。

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

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

帖子 ejsoon »

gemini thinking:

To improve the logic as requested, I have renamed isFirstMoveAttempt to isNotLastMove (or specifically checked against the loop index) to ensure that a failure in the middle of a sequence triggers the INITIAL_FEN fallback, while a failure at the very end can still be treated as a trailing comment.

All code and comments have been translated into English.

代码: 全选

/**
 * Imports game notation from a string. 
 * Supports URL hashes, JSON trees, FEN strings, and text notations (Complex or Simple).
 */
function importExportedText(text) {
    text = text.trim();
    if (!text) {
        renderRecordUI();
        renderNoteUI();
        updateToolHighlights();
        return;
    }

    // Backup current state in case the import fails or needs to be reverted
    const backupHistoryFEN = JSON.parse(JSON.stringify(historyFEN));
    const backupBranch = [...currentBranch];
    const backupStepIndex = currentStepIndex;

    // Normalize Chinese characters to standard Traditional Chinese used in logic
    text = text.replace(/车/g, '車').replace(/马/g, '馬').replace(/帅/g, '帥')
               .replace(/将/g, '將').replace(/后/g, '後').replace(/进/g, '進');
    // Standardize turn indicator
    text = text.replace(/ r(\n|\s|$)/g, ' w$1');

    // Handle URL format directly if pasted (contains hash or specific protocol prefixes)
    if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
        let hashPart = text.includes('#') ? text.split('#')[1] : text;
        if (hashPart) {
            try {
                let parsedTree = parseUrlHash(hashPart);
                if (parsedTree && parsedTree.fen) {
                    historyFEN = parsedTree;
                    initBranch();
                    currentStepIndex = 0;
                    let fenArr = historyFEN.fen.split(' ');
                    if (fenArr.length >= 4) {
                        isRotateEnabled = fenArr[2] === '1';
                        isFlipEnabled = fenArr[3] === '1';
                        reapplyVisualPositions();
                    }
                    renderRecordUI();
                    renderNoteUI();
                    jumpToStep(0);
                    saveStateToUndo();
                    updateToolHighlights();
                    return;
                }
            } catch (e) {
                console.log("Failed to parse as URL parameter, falling back to text notation.", e);
            }
        }
    }

    // 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);
            let fenArr = historyFEN.fen.split(' ');
            if (fenArr.length === 6) {
                isRotateEnabled = fenArr[2] === '1';
                isFlipEnabled = fenArr[3] === '1';
                reapplyVisualPositions();
            }
            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 fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
    const fenMatch = text.match(fenRegex);
    let movesStr = text;

    if (fenMatch) {
        importedFen = fenMatch[0];
        // Remove FEN string and keywords so they aren't parsed as moves
        const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
        movesStr = movesStr.replace(cleanRegex, '').trim();
    }

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

    if (importedFen) {
        // Trace backwards to find a matching state in history
        for (let i = currentStepIndex; i >= 0; i--) {
            const node = getNodeAtStep(i);
            if (node && node.fen === importedFen) {
                attachIndex = i;
                attachNode = node;
                break;
            }
        }
        // No match found -> Overwrite history from this FEN
        if (!attachNode) {
            attachIndex = 0;
            attachNode = {
                fen: importedFen,
                move: null,
                lastMove: null,
                c: "",
                v: []
            };
            historyFEN = attachNode;
            currentBranch = [];
        }
    } else {
        // No FEN found -> Attach to current step
        attachNode = getNodeAtStep(attachIndex);
        importedFen = attachNode.fen;
    }

    // 3. Process move sequence text
    let lines = movesStr.split('\n').map(l => l.trim()).filter(l => l.length > 0);
    let isComplexFormat = lines.some(l => /^\d+\./.test(l));

    let currentFen = importedFen;
    let vMap = buildVirtualMap(currentFen);
    let currNode = attachNode;
    let currentCommentNode = attachNode;

    // Calculate existing fork choices up to attachIndex
    let forkCount = 0;
    let tempNode = historyFEN;
    for (let i = 0; i < attachIndex; i++) {
        if (tempNode.v && tempNode.v.length > 1) {
            forkCount++;
        }
        const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
        tempNode = tempNode.v[choice];
    }
    currentBranch = currentBranch.slice(0, forkCount);

    let hasError = false;

    if (isComplexFormat) {
        let branchMap = {};
        let seenCircles = new Set();
        let lastMoveParent = attachNode;
        let lastMoveParentFen = importedFen;

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

            if (!/^\d+\./.test(line)) {
                // Line is a comment or branch declaration
                let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
                let isBranchDecl = false;

                for (let c of circles) {
                    if (seenCircles.has(c) && branchMap[c]) {
                        currNode = branchMap[c].parentNode;
                        currentFen = branchMap[c].fen;
                        vMap = buildVirtualMap(currentFen);
                        currentCommentNode = currNode;
                        isBranchDecl = true;
                        delete branchMap[c]; 
                        break; 
                    }
                }

                if (!isBranchDecl) {
                    if (!currentCommentNode.c) currentCommentNode.c = "";
                    currentCommentNode.c += (currentCommentNode.c ? "\n" : "") + line;
                }
            } else {
                let cleanLine = line.replace(/^\d+\./, '').trim();
                let tokens = cleanLine.split(/\s+/).filter(t => t.length > 0);

                for (let j = 0; j < tokens.length; j++) {
                    let token = tokens[j];
                    if (token === '...' || token === '…') continue;

                    let circles = token.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
                    let moveStr = token.replace(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳…\.]/g, '');

                    // Logic: If this is NOT the last token in the entire text, we consider it a candidate for reset
                    const isNotLastMoveToken = (i < lines.length - 1) || (j < tokens.length - 1);

                    if (moveStr.length >= 4) {
                        let dc = null;
                        let moveEn = "";
                        let isRed = currentFen.includes(' w');
                        let isEnglish = /^[a-zA-Z]/.test(moveStr) || /^[+\-=1-9][a-zA-Z0-9]/.test(moveStr);

                        if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
                            let startX = moveStr.charCodeAt(0) - 97;
                            let startY = 9 - parseInt(moveStr.charAt(1), 10);
                            let endX = moveStr.charCodeAt(2) - 97;
                            let endY = 9 - parseInt(moveStr.charAt(3), 10);
                            dc = { startX, startY, endX, endY };
                            let pId = vMap.get(`${startX},${startY}`);
                            if (!pId) dc = null; // Mark as failure
                            else moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                        } else {
                            moveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, isRed);
                            dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                        }

                        if (!dc) {
                            // Failure logic: If it's not the last token, try restarting from Initial Position
                            if (isNotLastMoveToken) {
                                let testIsRed = INITIAL_FEN.includes(' w');
                                let testMoveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, testIsRed);
                                let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);

                                if (!testDc) {
                                    // Complete failure
                                    historyFEN = backupHistoryFEN;
                                    currentBranch = backupBranch;
                                    currentStepIndex = backupStepIndex;
                                    console.warn("Import aborted: Critical failure in move sequence.");
                                    return; 
                                } else {
                                    // Success with INITIAL_FEN. Reset game state.
                                    historyFEN = { fen: INITIAL_FEN, move: null, lastMove: null, c: "", v: [] };
                                    currentBranch = [];
                                    currentStepIndex = 0;
                                    attachIndex = 0;
                                    // Re-parse leading lines as root comments
                                    for (let k = 0; k < lines.length; k++) {
                                        if (!/^\d+\./.test(lines[k])) historyFEN.c += (historyFEN.c ? "\n" : "") + lines[k];
                                        else break;
                                    }
                                    attachNode = currNode = currentCommentNode = lastMoveParent = historyFEN;
                                    currentFen = lastMoveParentFen = INITIAL_FEN;
                                    vMap = buildVirtualMap(currentFen);
                                    forkCount = 0;
                                    dc = testDc; isRed = testIsRed; moveEn = testMoveEn;
                                }
                            } else {
                                // Last token failed, treat as comment
                                currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
                                continue;
                            }
                        }

                        lastMoveParent = currNode;
                        lastMoveParentFen = currentFen;

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

                        if (childIdx === -1) {
                            currNode.v.push({ fen: nextFen, move: moveEn, lastMove: dc, c: "", v: [] });
                            childIdx = currNode.v.length - 1;
                        }

                        if (currNode.v.length > 1 && currNode === attachNode) {
                            currentBranch[forkCount] = childIdx;
                            forkCount++;
                            attachNode = currNode.v[childIdx];
                        }

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

                        let pId = vMap.get(`${dc.startX},${dc.startY}`);
                        vMap.delete(`${dc.startX},${dc.startY}`);
                        if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
                    }

                    for (let c of circles) {
                        if (!seenCircles.has(c)) {
                            seenCircles.add(c);
                            branchMap[c] = { parentNode: lastMoveParent, fen: lastMoveParentFen };
                        }
                    }
                }
            }
        }
    } else {
        // Fallback for Simple/Flat formats
        let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
        let tokensStartIdx = 0;
        for (let i = 0; i < tokens.length; i++) {
            if (/^1\./.test(tokens[i])) { tokensStartIdx = i; break; }
        }

        for (let i = tokensStartIdx; i < tokens.length; i++) {
            let token = tokens[i].replace(/^\d+\.\s*/, '').substring(0, 4);
            const isNotLastMoveToken = i < tokens.length - 1;

            let dc = null;
            let moveEn = "";
            let isRed = currentFen.includes(' w');
            let isEnglish = /^[a-zA-Z]/.test(token) || /^[+\-=1-9][a-zA-Z0-9]/.test(token);

            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) dc = null;
                else moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
            } else {
                moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
                dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
            }

            if (!dc) {
                if (isNotLastMoveToken) {
                    let testIsRed = INITIAL_FEN.includes(' w');
                    let testMoveEn = isEnglish ? token : NotationConverter.toEnglish(token, testIsRed);
                    let testDc = deriveCoordsFromMove(INITIAL_FEN, testMoveEn, testIsRed);

                    if (!testDc) {
                        historyFEN = backupHistoryFEN;
                        currentBranch = backupBranch;
                        currentStepIndex = backupStepIndex;
                        console.warn("Import aborted: Failure in simple format move sequence.");
                        return;
                    } else {
                        historyFEN = { fen: INITIAL_FEN, move: null, lastMove: null, c: "", v: [] };
                        currentBranch = [];
                        currentStepIndex = 0;
                        attachNode = currNode = historyFEN;
                        currentFen = INITIAL_FEN;
                        vMap = buildVirtualMap(currentFen);
                        forkCount = 0;
                        dc = testDc; isRed = testIsRed; moveEn = testMoveEn;
                    }
                } else {
                    hasError = true;
                    break;
                }
            }

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

            if (childIdx === -1) {
                currNode.v.push({ fen: nextFen, move: moveEn, lastMove: dc, c: "", v: [] });
                childIdx = currNode.v.length - 1;
            }

            if (currNode.v.length > 1) {
                currentBranch[forkCount] = childIdx;
                forkCount++;
            }

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

            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) alert("Parsed partially due to an invalid move notation.");

    // Resolve default choices for remaining branches down the tree
    let followNode = attachNode;
    let followIdx = 0;
    while (followNode && followIdx < currentBranch.length) {
        if (followNode.v && followNode.v.length > 0) {
            let idx = currentBranch[followIdx] || 0;
            followNode = followNode.v[idx] || followNode.v[0];
        } else break;
        followIdx++;
    }
    while (followNode && followNode.v && followNode.v.length > 0) {
        if (followNode.v.length > 1) currentBranch.push(0);
        followNode = followNode.v[0];
    }

    currentStepIndex = attachIndex;
    renderRecordUI();
    renderNoteUI();
    jumpToStep(currentStepIndex);
    saveStateToUndo();
    updateToolHighlights();
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5786
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

加上一個判斷,如果「不是分支的最後一個token」轉換失敗,則嘗試「新建局面」導入,如果在新建局面下仍然「不是分支的最後一個token」轉換失敗,則取消導入,恢復為導入之前的狀態。只有「分支的最後一個token」才可能成為上一步的評論。

同時,要首先通過英文棋譜來判斷是否能正確轉換,在英文棋譜的前四個字符中,第三個一定是[+-=」的其中一個,第四個一定是數字[1-9],前兩個一定是[RNBAKCPrnbakcp+-=1-9]的其中一個。

代码: 全选

改進importExportedText函式:
```
        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 URL format directly if pasted
            if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
                let hashPart = text.includes('#') ? text.split('#')[1] : text;
                if (hashPart) {
                    try {
                        let parsedTree = parseUrlHash(hashPart);
                        if (parsedTree && parsedTree.fen) {
                            historyFEN = parsedTree;
                            initBranch();
                            currentStepIndex = 0;
                            let fenArr = historyFEN.fen.split(' ');
                            if (fenArr.length >= 4) {
                                isRotateEnabled = fenArr[2] === '1';
                                isFlipEnabled = fenArr[3] === '1';
                                reapplyVisualPositions();
                            }
                            renderRecordUI();
                            renderNoteUI();
                            jumpToStep(0);
                            saveStateToUndo();
                            updateToolHighlights();
                            return;
                        }
                    } catch (e) {
                        console.log("Failed to parse as URL parameter, falling back to text notation.", e);
                    }
                }
            }

            // 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);
                    let fenArr = historyFEN.fen.split(' ');
                    if (fenArr.length === 6) {
                        isRotateEnabled = fenArr[2] === '1';
                        isFlipEnabled = fenArr[3] === '1';
                        reapplyVisualPositions();
                    }
                    initBranch(); // initBranch correctly builds currentBranch based on forks
                    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 fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
            const fenMatch = text.match(fenRegex);
            let movesStr = text;

            if (fenMatch) {
                importedFen = fenMatch[0];
                // Strip the FEN string declaration so it isn't parsed as moves later
                const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
                movesStr = movesStr.replace(cleanRegex, '').trim();
            }

            // 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
            // Rule 1: Split text by \n and trim to form an array.
            let lines = movesStr.split('\n').map(l => l.trim());
            // Check if any line matches the "^\d+\." pattern to determine the parsing mode.
            let isComplexFormat = lines.some(l => /^\d+\./.test(l));

            let currentFen = importedFen;
            let vMap = buildVirtualMap(currentFen);
            let currNode = attachNode;
            let currentCommentNode = attachNode;

            // Calculate how many forks exist from root up to attachIndex
            let forkCount = 0;
            let tempNode = historyFEN;
            for (let i = 0; i < attachIndex; i++) {
                if (tempNode.v && tempNode.v.length > 1) {
                    forkCount++;
                }
                const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
                tempNode = tempNode.v[choice];
            }

            // Truncate currentBranch to discard choices beyond the attach point
            currentBranch = currentBranch.slice(0, forkCount);

            let hasError = false;

            if (isComplexFormat) {
                let branchMap = {};
                let seenCircles = new Set();
                let lastMoveParent = attachNode;
                let lastMoveParentFen = importedFen;

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

                    if (!/^\d+\./.test(line)) {
                        // Rule 3: Lines not starting with "^\d+\." are comments or branch declarations
                        let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
                        let isBranchDecl = false;

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

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

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

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

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

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

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

                                if (!dc) {
                                    // If coordinates cannot be derived, treat the token as a comment
                                    // for the current node (which represents the previous step)
                                    if (!currentCommentNode.c) {
                                        currentCommentNode.c = "";
                                    }
                                    // Append the token, separating with a space if a comment already exists
                                    currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
                                    continue; // Skip node creation and continue parsing
                                }

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

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

                                if (childIdx === -1) {
                                    let newNode = {
                                        fen: nextFen,
                                        move: moveEn,
                                        lastMove: dc,
                                        c: "",
                                        v: []
                                    };
                                    currNode.v.push(newNode);
                                    childIdx = currNode.v.length - 1;
                                }

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

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

                                let pId = vMap.get(`${dc.startX},${dc.startY}`);
                                vMap.delete(`${dc.startX},${dc.startY}`);
                                if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
                            }

                            // Rule 4, 7 & 8: Process multiple circled numbers wherever they occur
                            for (let c of circles) {
                                if (!seenCircles.has(c)) {
                                    seenCircles.add(c);
                                    // Map the first occurrence to the parent node of the currently parsed move
                                    branchMap[c] = {
                                        parentNode: lastMoveParent,
                                        fen: lastMoveParentFen
                                    };
                                }
                            }
                        }
                    }
                }
            } else {
                // Fallback for simple flat formats (no "^\d+\." format detected)
                let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
                let tokensStartIdx = 0;
                for (let i = 0; i < tokens.length; i++) {
                    if (/^1\./.test(tokens[i])) {
                        tokensStartIdx = i;
                        break;
                    }
                }

                for (let i = tokensStartIdx; i < tokens.length; i++) {
                    let token = tokens[i].replace(/^\d+\.\s*/, '').substring(0, 4);
                    let dc = null;
                    let moveEn = "";
                    let isRed = currentFen.includes(' w');

                    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;
                        }
                        moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                    } else {
                        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);
                    let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);

                    if (childIdx === -1) {
                        let newNode = {
                            fen: nextFen,
                            move: moveEn,
                            lastMove: dc,
                            c: "",
                            v: []
                        };
                        currNode.v.push(newNode);
                        childIdx = currNode.v.length - 1;
                    }

                    if (currNode.v.length > 1) {
                        currentBranch[forkCount] = childIdx;
                        forkCount++;
                    }

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

                    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) {
                alert("Parsed partially due to an invalid move notation.");
            }

            // Fill default choices (0) for any remaining branches down the tree
            let followNode = attachNode;
            let followIdx = 0;
            while (followNode && followIdx < currentBranch.length) {
                if (followNode.v && followNode.v.length > 1) {
                    followNode = followNode.v[currentBranch[followIdx]];
                } else if (followNode.v && followNode.v.length === 1) {
                    followNode = followNode.v[0];
                } else {
                    break;
                }
                followIdx++;
            }
            while (followNode && followNode.v && followNode.v.length > 0) {
                if (followNode.v.length > 1) {
                    currentBranch.push(0);
                }
                followNode = followNode.v[0];
            }

            currentStepIndex = attachIndex;
            renderRecordUI();
            renderNoteUI();
            jumpToStep(currentStepIndex);

            saveStateToUndo();
            updateToolHighlights();
        }
```

在這一段:「

                                    dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                                }

                                if (!dc) {
                                    // If coordinates cannot be derived, treat the token as a comment
                                    // for the current node (which represents the previous step)
                                    if (!currentCommentNode.c) {
                                        currentCommentNode.c = "";
                                    }
                                    // Append the token, separating with a space if a comment already exists
                                    currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
                                    continue; // Skip node creation and continue parsing
                                }
」

它的意義為,如果用deriveCoordsFromMove轉換失敗,則token將成為上一步的評論。

現在加上一個判斷,如果「不是分支的最後一個token」轉換失敗,則嘗試「新建局面」導入,如果在新建局面下仍然「不是分支的最後一個token」轉換失敗,則取消導入,恢復為導入之前的狀態。只有「分支的最後一個token」才可能成為上一步的評論。

新建局面主要影響的是這三個變量:

                historyFEN = {
                    fen: INITIAL_FEN,
                    move: null,
                    lastMove: null,
                    c: "",
                    v: []
                };
            currentBranch = [];
            currentStepIndex = 0;

下面舉例說明什麼是「分支的最後一個token」:
```
标题: 15 兑子抢先 丝丝入扣
分类: 象棋谱大全-现代棋书专集
赛事: 中局大全
轮次: 战术精华
结果: 黑方胜
日期: 0000.00.00
评论: 讲评: 郭有志
作者: 文件作者: 郭有志
备注:  
棋局类型: 全局
棋局性质: 慢棋
棋谱主人: 象棋谱大全
棋谱价值: 1
浏览次数: 7930
来源网站: www.dpxq.com
 
初始局面:
         黑方         
                   
 ┌-[车][象][士][将][士]-┬-[车]-┐ 
 │ │ │ │\│/│ │ │ │ 
 ├─┼─┼─┼─※─┼─┼─┼─┤ 
 │ │ │ │/│\│ │ │ │ 
 ├─┼-[马]-┼-[象]-┼-[炮]-┼─┤ 
 │ │ │ │ │ │ │ │ │ 
 [卒]-┼-[卒]-┼-[卒]-┼─┼─┼-[卒] 
 │ │ │ │ │ │ │ │ │ 
 ├─┴─┴─┴─┴─┴─┴─┴─┤ 
 │               │ 
 ├─┬-(兵)-┬─┬─┬-(车)-┬─┤ 
 │ │ │ │ │ │ │ │ │ 
 (兵)-┼-[炮]-┼-(兵)-┼─┼─┼-(兵) 
 │ │ │ │ │ │ │ │ │ 
 ├-(炮)(炮)-┼-(相)-┼-(马)-┼─┤ 
 │ │ │ │\│/│ │ │ │ 
 ├─┼─┼─┼─※─┼─┼─┼─┤ 
 │ │ │ │/│\│ │ │ │ 
 └-(车)-┴-(士)(帅)(士)(相)-┴─┘ 
                   
         红方         
 
 
【主变: 黑胜】
    1.  炮7进5
    兑子抢先,车占兵林,力在其中。
    1.  …………    炮七平三   
    2.  车8进6    车三平五 ①
    3.  车2进4
    高车,护卒攻车。
    3.  …………    仕六进五   
    4.  卒5进1    车五平六   
    5.  马3进5
    跃马,否则红有车6进2,再平7制马之棋。
    5.  …………    车八平六   
    6.  士6进5
    必行之着!
    6.  …………    炮八平七 ②
    7.  马5进7
    马跃进河口,下着有卒5进1。
    7.  …………    前车平三   
    8.  马7退6    车六进三   
    9.  炮3平5
    黑方运子推进,着法丝丝入扣。炮打中兵,确立优势。
    9.  …………    帅五平六   
    10. 卒5进1
    强冲中卒,好!红车不敢吃之。
    10. …………    相五退七 ③
    11. 车2平8    炮七平五 ④
    12. 炮5平1    车六进五   
    13. 前车平5    炮五进二   
    14. 车8平5    炮五进三 ⑤
    弃炮无奈。
    15. 后车退2    炮三平五   
    16. 炮1平9    黑胜势!!
 
变①接主变第2回合
    2.  …………    兵五进一   
    3.  车2进6    兵九进一   
    4.  炮3平9    炮三平一   
    5.  车8退3    黑优。
 
变②接主变第6回合
    6.  …………    兵五进一   
    7.  马5进7    兵五进一   
    8.  马7进6    前车平四   
    9.  车2平5    黑优!
 
变③接主变第10回合
    10. …………    车三平五   
    11. 炮5进2    车六平二 ⑥
    12. 炮5退3    黑优!!
 
变④接主变第11回合
    11. …………    车三平五   
    12. 炮5平1    车六平二   
    13. 车8进2    兵一进一   
    14. 炮1退1    兵七进一   
    15. 炮1进4    相七进九   
    16. 卒3进1
 
变⑤接主变第14回合
    14. …………    炮五平四   
    15. 炮1进3    相七进五   
    16. 后车平2
 
变⑥接变③第11回合
    11. …………    车五平六   
    12. 车8平4    车六退一   
    13. 炮5平9
 
棋谱由 http://www.dpxq.com/ 生成
```
在上面的棋譜文本中,「黑胜势!!」、「黑优。」、「黑优!」、「黑优!!」、「卒3进1」、「后车平2」、「炮5平9」都是分支的最後一個token。其中,「黑胜势!!」、「黑优。」、「黑优!」、「黑优!!」將成為上一步的評論。

同時,要首先通過英文棋譜來判斷是否能正確轉換,在英文棋譜的前四個字符中,第三個一定是[+-=」的其中一個,第四個一定是數字[1-9],前兩個一定是[RNBAKCPrnbakcp+-=1-9]的其中一個。

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

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

帖子 ejsoon »

chatgpt能用否:

代码: 全选

function importExportedText(text) {
    // Backup current state (for rollback)
    const backup = {
        historyFEN: JSON.parse(JSON.stringify(historyFEN)),
        currentBranch: [...currentBranch],
        currentStepIndex
    };

    function restoreBackup() {
        historyFEN = backup.historyFEN;
        currentBranch = backup.currentBranch;
        currentStepIndex = backup.currentStepIndex;
    }

    // Check if token looks like valid English notation
    function isLikelyEnglishMove(str) {
        if (str.length < 4) return false;
        const s = str.substring(0, 4);
        return /^[RNBAKCPrnbakcp+\-=1-9]{2}[+\-=][1-9]$/.test(s);
    }

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

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

    // --- URL format ---
    if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
        let hashPart = text.includes('#') ? text.split('#')[1] : text;
        if (hashPart) {
            try {
                let parsedTree = parseUrlHash(hashPart);
                if (parsedTree && parsedTree.fen) {
                    historyFEN = parsedTree;
                    initBranch();
                    currentStepIndex = 0;

                    let fenArr = historyFEN.fen.split(' ');
                    if (fenArr.length >= 4) {
                        isRotateEnabled = fenArr[2] === '1';
                        isFlipEnabled = fenArr[3] === '1';
                        reapplyVisualPositions();
                    }

                    renderRecordUI();
                    renderNoteUI();
                    jumpToStep(0);
                    saveStateToUndo();
                    updateToolHighlights();
                    return;
                }
            } catch (e) {}
        }
    }

    // --- JSON format ---
    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) {}
    }

    // --- FEN detection ---
    let importedFen = null;
    const fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
    const fenMatch = text.match(fenRegex);

    let movesStr = text;

    if (fenMatch) {
        importedFen = fenMatch[0];
        movesStr = movesStr.replace(fenRegex, '').trim();
    }

    let attachNode;
    let attachIndex = currentStepIndex;

    if (importedFen) {
        attachNode = {
            fen: importedFen,
            move: null,
            lastMove: null,
            c: "",
            v: []
        };
        historyFEN = attachNode;
        currentBranch = [];
        attachIndex = 0;
    } else {
        attachNode = getNodeAtStep(attachIndex);
        importedFen = attachNode.fen;
    }

    let lines = movesStr.split('\n').map(l => l.trim());
    let isComplex = lines.some(l => /^\d+\./.test(l));

    let currentFen = importedFen;
    let vMap = buildVirtualMap(currentFen);
    let currNode = attachNode;
    let currentCommentNode = attachNode;

    function tryReparseFromInitial(token, isLastToken) {
        if (isLastToken) return false;

        historyFEN = {
            fen: INITIAL_FEN,
            move: null,
            lastMove: null,
            c: "",
            v: []
        };
        currentBranch = [];
        currentStepIndex = 0;

        currentFen = INITIAL_FEN;
        vMap = buildVirtualMap(currentFen);
        currNode = historyFEN;

        let isRed = currentFen.includes(' w');
        let moveEn = isLikelyEnglishMove(token)
            ? token
            : NotationConverter.toEnglish(token, isRed);

        let dc = deriveCoordsFromMove(currentFen, moveEn, isRed);

        return !!dc;
    }

    let hasError = false;

    if (isComplex) {
        for (let line of lines) {
            if (!line) continue;

            if (!/^\d+\./.test(line)) {
                currentCommentNode.c += (currentCommentNode.c ? "\n" : "") + line;
                continue;
            }

            let tokens = line.replace(/^\d+\./, '').trim().split(/\s+/);

            for (let i = 0; i < tokens.length; i++) {
                let token = tokens[i];
                let isLastToken = (i === tokens.length - 1);

                if (token === '...' || token === '…') continue;

                let moveStr = token.replace(/[①-⑳…\.]/g, '');

                let dc = null;
                let moveEn = "";
                let isRed = currentFen.includes(' w');

                if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
                    let sx = moveStr.charCodeAt(0) - 97;
                    let sy = 9 - parseInt(moveStr[1]);
                    let ex = moveStr.charCodeAt(2) - 97;
                    let ey = 9 - parseInt(moveStr[3]);

                    dc = { startX: sx, startY: sy, endX: ex, endY: ey };

                } else {
                    moveEn = isLikelyEnglishMove(moveStr)
                        ? moveStr
                        : NotationConverter.toEnglish(moveStr, isRed);

                    dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                }

                // --- NEW CORE LOGIC ---
                if (!dc) {
                    if (!isLastToken) {
                        // Try reset
                        if (!tryReparseFromInitial(token, false)) {
                            restoreBackup();
                            alert("Import failed: invalid move.");
                            return;
                        }
                        continue;
                    } else {
                        // Only last token becomes comment
                        currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
                        continue;
                    }
                }

                let nextFen = simulateMove(currentFen, dc);

                let newNode = {
                    fen: nextFen,
                    move: moveEn,
                    lastMove: dc,
                    c: "",
                    v: []
                };

                currNode.v.push(newNode);

                currNode = newNode;
                currentFen = nextFen;
                currentCommentNode = currNode;

                let pId = vMap.get(`${dc.startX},${dc.startY}`);
                vMap.delete(`${dc.startX},${dc.startY}`);
                if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
            }
        }
    }

    renderRecordUI();
    renderNoteUI();
    jumpToStep(currentStepIndex);

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

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

帖子 ejsoon »

gemini thinking:

This improved version of the importExportedText function includes a state-saving mechanism for rollbacks, logic to detect English notation according to your rules, and a retry strategy that resets the board to the initial position if a mid-sequence move is invalid.

代码: 全选

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

    // Backup current state for potential rollback
    const backupState = {
        historyFEN: JSON.parse(JSON.stringify(historyFEN)),
        currentBranch: [...currentBranch],
        currentStepIndex: currentStepIndex
    };

    const restoreBackup = () => {
        historyFEN = backupState.historyFEN;
        currentBranch = backupState.currentBranch;
        currentStepIndex = backupState.currentStepIndex;
        renderRecordUI();
        renderNoteUI();
        jumpToStep(currentStepIndex);
        updateToolHighlights();
    };

    // 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 URL format directly
    if (text.includes('#') || text.startsWith('i;') || text.startsWith('i+') || text.startsWith('i@') || /^([a-zA-Z0-9]+,)+[wb]/.test(text)) {
        let hashPart = text.includes('#') ? text.split('#')[1] : text;
        if (hashPart) {
            try {
                let parsedTree = parseUrlHash(hashPart);
                if (parsedTree && parsedTree.fen) {
                    historyFEN = parsedTree;
                    initBranch();
                    currentStepIndex = 0;
                    let fenArr = historyFEN.fen.split(' ');
                    if (fenArr.length >= 4) {
                        isRotateEnabled = fenArr[2] === '1';
                        isFlipEnabled = fenArr[3] === '1';
                        reapplyVisualPositions();
                    }
                    renderRecordUI();
                    renderNoteUI();
                    jumpToStep(0);
                    saveStateToUndo();
                    updateToolHighlights();
                    return;
                }
            } catch (e) {
                console.log("URL parse failed, falling back.");
            }
        }
    }

    // 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);
            let fenArr = historyFEN.fen.split(' ');
            if (fenArr.length === 6) {
                isRotateEnabled = fenArr[2] === '1';
                isFlipEnabled = fenArr[3] === '1';
                reapplyVisualPositions();
            }
            initBranch();
            currentStepIndex = 0;
            renderRecordUI();
            renderNoteUI();
            jumpToStep(0);
            saveStateToUndo();
            updateToolHighlights();
            return;
        } catch (e) {
            console.log("JSON parse failed.");
        }
    }

    // Parse logic for text notations (Simple/Complex)
    let hasAttemptedReset = false;

    function performImport(isRetry = false) {
        let currentFen = isRetry ? INITIAL_FEN : (importedFen || getNodeAtStep(currentStepIndex).fen);
        
        // If we are retrying from Initial FEN, we reset the history root
        if (isRetry) {
            historyFEN = { fen: INITIAL_FEN, move: null, lastMove: null, c: "", v: [] };
            currentBranch = [];
            currentStepIndex = 0;
        }

        let vMap = buildVirtualMap(currentFen);
        let currNode = isRetry ? historyFEN : getNodeAtStep(currentStepIndex);
        let attachNode = currNode;
        let currentCommentNode = currNode;
        let attachIndex = currentStepIndex;

        // Calculate forkCount for the current branch path
        let forkCount = 0;
        let tempNode = historyFEN;
        for (let i = 0; i < attachIndex; i++) {
            if (tempNode.v && tempNode.v.length > 1) forkCount++;
            const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkCount - 1] || 0) : 0;
            tempNode = tempNode.v[choice];
        }
        currentBranch = currentBranch.slice(0, forkCount);

        let lines = movesStr.split('\n').map(l => l.trim()).filter(l => l.length > 0);
        let isComplexFormat = lines.some(l => /^\d+\./.test(l));

        if (isComplexFormat) {
            let branchMap = {};
            let seenCircles = new Set();
            let lastMoveParent = attachNode;
            let lastMoveParentFen = currentFen;

            for (let i = 0; i < lines.length; i++) {
                let line = lines[i];
                if (!/^\d+\./.test(line)) {
                    // Treat non-move lines as comments
                    let circles = line.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
                    let isBranchDecl = false;
                    for (let c of circles) {
                        if (seenCircles.has(c) && branchMap[c]) {
                            currNode = branchMap[c].parentNode;
                            currentFen = branchMap[c].fen;
                            vMap = buildVirtualMap(currentFen);
                            currentCommentNode = currNode;
                            isBranchDecl = true;
                            delete branchMap[c];
                            break;
                        }
                    }
                    if (!isBranchDecl) {
                        currentCommentNode.c = (currentCommentNode.c ? currentCommentNode.c + "\n" : "") + line;
                    }
                } else {
                    let cleanLine = line.replace(/^\d+\./, '').trim();
                    let tokens = cleanLine.split(/\s+/).filter(t => t !== '...' && t !== '…');
                    
                    for (let j = 0; j < tokens.length; j++) {
                        let token = tokens[j];
                        let isLastTokenOfBranch = (j === tokens.length - 1);
                        let circles = token.match(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/g) || [];
                        let moveStr = token.replace(/[①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳…\.]/g, '');

                        if (moveStr.length < 4) {
                            if (isLastTokenOfBranch) {
                                currentCommentNode.c = (currentCommentNode.c ? currentCommentNode.c + " " : "") + token;
                                continue;
                            } else if (!hasAttemptedReset) {
                                hasAttemptedReset = true;
                                return performImport(true);
                            } else {
                                restoreBackup();
                                return;
                            }
                        }

                        let dc = null;
                        let moveEn = "";
                        let isRed = currentFen.includes(' w');

                        // Rule: Check for English Notation pattern
                        // Piece code (2 chars) + Operator [+-=] + Number [1-9]
                        let isEnglish = /^[RNBAKCPrnbakcp+\-=1-9]{2}[+\-=][1-9]/.test(moveStr);

                        if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
                            let startX = moveStr.charCodeAt(0) - 97;
                            let startY = 9 - parseInt(moveStr.charAt(1), 10);
                            let endX = moveStr.charCodeAt(2) - 97;
                            let endY = 9 - parseInt(moveStr.charAt(3), 10);
                            dc = { startX, startY, endX, endY };
                            let pId = vMap.get(`${startX},${startY}`);
                            if (pId) moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                        } else {
                            moveEn = isEnglish ? moveStr : NotationConverter.toEnglish(moveStr, isRed);
                            dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                        }

                        if (!dc) {
                            if (isLastTokenOfBranch) {
                                currentCommentNode.c = (currentCommentNode.c ? currentCommentNode.c + " " : "") + token;
                                continue;
                            } else if (!hasAttemptedReset) {
                                hasAttemptedReset = true;
                                return performImport(true);
                            } else {
                                restoreBackup();
                                return;
                            }
                        }

                        // Success: Build the move node
                        lastMoveParent = currNode;
                        lastMoveParentFen = currentFen;
                        let nextFen = simulateMove(currentFen, dc);
                        let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);

                        if (childIdx === -1) {
                            let newNode = { fen: nextFen, move: moveEn, lastMove: dc, c: "", v: [] };
                            currNode.v.push(newNode);
                            childIdx = currNode.v.length - 1;
                        }

                        if (currNode.v.length > 1 && currNode === attachNode) {
                            currentBranch[forkCount] = childIdx;
                            forkCount++;
                            attachNode = currNode.v[childIdx];
                        }

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

                        // Update Virtual Map
                        let pId = vMap.get(`${dc.startX},${dc.startY}`);
                        vMap.delete(`${dc.startX},${dc.startY}`);
                        if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);

                        // Map circles for branch declarations
                        for (let c of circles) {
                            if (!seenCircles.has(c)) {
                                seenCircles.add(c);
                                branchMap[c] = { parentNode: lastMoveParent, fen: lastMoveParentFen };
                            }
                        }
                    }
                }
            }
        } else {
            // Simple format parsing
            let tokens = movesStr.split(/\s+/).filter(t => t.length > 0 && t !== 'moves');
            for (let i = 0; i < tokens.length; i++) {
                let token = tokens[i].replace(/^\d+\.\s*/, '');
                let isLastToken = (i === tokens.length - 1);
                let moveStr = token.substring(0, 4);
                let isRed = currentFen.includes(' w');
                let dc = null, moveEn = "";

                if (/^[a-i][0-9][a-i][0-9]$/.test(moveStr)) {
                    let startX = moveStr.charCodeAt(0) - 97;
                    let startY = 9 - parseInt(moveStr.charAt(1), 10);
                    let endX = moveStr.charCodeAt(2) - 97;
                    let endY = 9 - parseInt(moveStr.charAt(3), 10);
                    dc = { startX, startY, endX, endY };
                    let pId = vMap.get(`${startX},${startY}`);
                    if (pId) moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
                } else {
                    let isEnglish = /^[RNBAKCPrnbakcp+\-=1-9]{2}[+\-=][1-9]/.test(token);
                    moveEn = isEnglish ? token : NotationConverter.toEnglish(token, isRed);
                    dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                }

                if (!dc) {
                    if (isLastToken) {
                        currNode.c = (currNode.c ? currNode.c + " " : "") + token;
                        continue;
                    } else if (!hasAttemptedReset) {
                        hasAttemptedReset = true;
                        return performImport(true);
                    } else {
                        restoreBackup();
                        return;
                    }
                }

                let nextFen = simulateMove(currentFen, dc);
                let childIdx = currNode.v.findIndex(c => c.move === moveEn && c.fen === nextFen);
                if (childIdx === -1) {
                    let newNode = { fen: nextFen, move: moveEn, lastMove: dc, c: "", v: [] };
                    currNode.v.push(newNode);
                    childIdx = currNode.v.length - 1;
                }
                if (currNode.v.length > 1) {
                    currentBranch[forkCount] = childIdx;
                    forkCount++;
                }
                currNode = currNode.v[childIdx];
                currentFen = nextFen;
                let pId = vMap.get(`${dc.startX},${dc.startY}`);
                vMap.delete(`${dc.startX},${dc.startY}`);
                if (pId) vMap.set(`${dc.endX},${dc.endY}`, pId);
            }
        }

        // Finalize Import
        currentStepIndex = attachIndex;
        renderRecordUI();
        renderNoteUI();
        jumpToStep(currentStepIndex);
        saveStateToUndo();
        updateToolHighlights();
    }

    // Identify FEN in text
    let importedFen = null;
    const fenRegex = /\b(?:[rnbakcpRNBAKCP1-9]{1,9}\/){9}[rnbakcpRNBAKCP1-9]{1,9} [wb](?:(?: -| \d+){4})?\b/;
    const fenMatch = text.match(fenRegex);
    let movesStr = text;

    if (fenMatch) {
        importedFen = fenMatch[0];
        const cleanRegex = new RegExp(`position fen | moves |${fenRegex.source}`, 'g');
        movesStr = movesStr.replace(cleanRegex, '').trim();
    }

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

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

帖子 ejsoon »

gemini pro一直不吃,愁死我了。

代码: 全选

改進importExportedText函式,察看上傳的function.js。

在這一段:
```
                                    dc = deriveCoordsFromMove(currentFen, moveEn, isRed);
                                }

                                if (!dc) {
                                    // If coordinates cannot be derived, treat the token as a comment
                                    // for the current node (which represents the previous step)
                                    if (!currentCommentNode.c) {
                                        currentCommentNode.c = "";
                                    }
                                    // Append the token, separating with a space if a comment already exists
                                    currentCommentNode.c += (currentCommentNode.c ? " " : "") + token;
                                    continue; // Skip node creation and continue parsing
                                }
```

它的意義為,如果用deriveCoordsFromMove轉換失敗,則token將成為上一步的評論。

現在加上一個判斷,如果「不是分支的最後一個token」轉換失敗,則嘗試「新建局面」導入,如果在新建局面下仍然「不是分支的最後一個token」轉換失敗,則取消導入,恢復為導入之前的狀態。只有「分支的最後一個token」才可能成為上一步的評論。

新建局面主要影響的是這三個變量:
```
                historyFEN = {
                    fen: INITIAL_FEN,
                    move: null,
                    lastMove: null,
                    c: "",
                    v: []
                };
            currentBranch = [];
            currentStepIndex = 0;
```
下面舉例說明什麼是「分支的最後一個token」,察看上傳的dpqp.txt。

在上面的棋譜文本中,「黑胜势!!」、「黑优。」、「黑优!」、「黑优!!」、「卒3进1」、「后车平2」、「炮5平9」都是分支的最後一個token。其中,「黑胜势!!」、「黑优。」、「黑优!」、「黑优!!」將成為上一步的評論。

同時,要首先通過英文棋譜來判斷是否能正確轉換,在英文棋譜的前四個字符中,第三個一定是[+-=」的其中一個,第四個一定是數字[1-9],前兩個一定是[RNBAKCPrnbakcp+-=1-9]的其中一個。

給出完整的importExportedText函式,所有代碼和注釋都要使用英文。
附件
tmp2.txt
(2.36 KiB) 已下载 49 次
dpqp.txt
(3.79 KiB) 已下载 45 次
function.txt
(18.59 KiB) 已下载 43 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

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