Re: 將製作ejcees(中國象棋打譜程式)
實現導入東萍象棋棋譜,可包含分支:
https://ejsoon.vip/wp-content/uploads/2 ... 99771.html
https://ejsoon.vip/wp-content/uploads/2 ... 99771.html
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
如果第一步導入都會發生錯誤,則改為新建並往INITIAL_FEN導入,如果仍發生錯誤,則這次導入中止。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
代码: 全选
改進以下代碼:
一,當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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
- 附件
-
ejcees20260329withComment999774.7z- (42.75 KiB) 已下载 47 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
- 附件
-
ejcees20260329withComm999775.7z- (42.75 KiB) 已下载 49 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
加上一個判斷,如果是首個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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
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.
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
之前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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
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.
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
加上一個判斷,如果「不是分支的最後一個token」轉換失敗,則嘗試「新建局面」導入,如果在新建局面下仍然「不是分支的最後一個token」轉換失敗,則取消導入,恢復為導入之前的狀態。只有「分支的最後一個token」才可能成為上一步的評論。
同時,要首先通過英文棋譜來判斷是否能正確轉換,在英文棋譜的前四個字符中,第三個一定是[+-=」的其中一個,第四個一定是數字[1-9],前兩個一定是[RNBAKCPrnbakcp+-=1-9]的其中一個。
同時,要首先通過英文棋譜來判斷是否能正確轉換,在英文棋譜的前四個字符中,第三個一定是[+-=」的其中一個,第四個一定是數字[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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
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.
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
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/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
在线用户
正浏览此版面之用户: 没有注册用户 和 3 访客
