代码: 全选
Here are the requested modifications to improve your Xiangqi application. All provided code segments are in English and address your 5 specific improvements.
### 1. Engine format export replacing `INITIAL_FEN` with `startpos`
Update the engine text generation in both the `performCopy` and `renderExportTextUI` functions.
**In `performCopy()`:**
```javascript
} else if (fmt === 'engine') {
let path = getGamePath(true);
let engineText = 'position fen ' + path.fen;
// Improvement 1: Check if FEN matches INITIAL_FEN board state
if (path.fen.split(' ')[0] === INITIAL_FEN.split(' ')[0]) {
engineText = 'position startpos';
}
let currNode = path;
```
**In `renderExportTextUI()`:**
```javascript
// Generate UCCI Engine Format String
let path = getGamePath(true);
let engineText = 'position fen ' + path.fen;
// Improvement 1: Check if FEN matches INITIAL_FEN board state
if (path.fen.split(' ')[0] === INITIAL_FEN.split(' ')[0]) {
engineText = 'position startpos';
}
let currNode = path;
```
---
### 2 & 3. PGN Metadata Modal Updates (Default button, logic refactor, and state saving)
Replace your entire `showPGNMetaModal()` function with this updated version:
```javascript
function showPGNMetaModal() {
const modal = document.createElement('div');
modal.className = 'pgn-meta-modal';
// Use local deep copy to prevent instant modification of the global variable
let localMetadata = JSON.parse(JSON.stringify(pgnMetadata));
const box = document.createElement('div');
box.className = 'pgn-meta-box';
// Container for Title and Default Button
const headerDiv = document.createElement('div');
headerDiv.style.display = 'flex';
headerDiv.style.justifyContent = 'space-between';
headerDiv.style.alignItems = 'center';
const title = document.createElement('h3');
title.textContent = 'PGN Metadata';
// Improvement 2: Added Default button
const defaultBtn = document.createElement('button');
defaultBtn.textContent = 'Default';
defaultBtn.style.padding = '4px 8px';
defaultBtn.style.cursor = 'pointer';
defaultBtn.style.marginRight = '30px'; // Leaves space for the close button
defaultBtn.onclick = () => {
localMetadata = [
{ name: 'Event', value: '' },
{ name: 'Site', value: '' },
{ name: 'Date', value: '' },
{ name: 'Round', value: '' },
{ name: 'RedTeam', value: '' },
{ name: 'Red', value: '' },
{ name: 'BlackTeam', value: '' },
{ name: 'Black', value: '' },
{ name: 'Result', value: '' },
{ name: 'Time', value: '' },
{ name: 'TimeControl', value: '' },
{ name: 'Opening', value: '' },
{ name: 'ECCO', value: '' },
{ name: 'Annotator', value: '' },
{ name: 'Termination', value: '' }
];
renderTable();
};
headerDiv.appendChild(title);
headerDiv.appendChild(defaultBtn);
// Improvement 3: Sync DOM inputs to the localMetadata array manually
function syncDOM() {
const rows = tbody.querySelectorAll('.pgn-meta-row');
localMetadata = [];
rows.forEach((tr) => {
const name = tr.querySelector('td:nth-child(2) input').value;
const valEl = tr.querySelector('td:nth-child(3) input, td:nth-child(3) select');
const value = valEl ? valEl.value : '';
localMetadata.push({ name, value });
});
}
const closeBtn = document.createElement('button');
closeBtn.className = 'close-btn';
closeBtn.style.top = '10px';
closeBtn.style.right = '10px';
closeBtn.onclick = () => {
syncDOM(); // Read the latest inputs before closing
// Only update and push to undoStack if there are actual changes
const isChanged = JSON.stringify(localMetadata) !== JSON.stringify(pgnMetadata);
if (isChanged) {
pgnMetadata = JSON.parse(JSON.stringify(localMetadata));
}
document.body.removeChild(modal);
if (isExportTextMode) {
const activeBtn = document.querySelector('.exp-fmt-btn.active');
const oddState = activeBtn ? parseInt(activeBtn.getAttribute('data-odd') || '0') : 0;
const exportArea = document.querySelector('.ejceescomment-edit');
if (exportArea) {
exportArea.value = generatePGNFormat(oddState);
}
}
if (isChanged) saveStateToUndo();
};
const table = document.createElement('table');
table.className = 'pgn-meta-table';
const thead = document.createElement('thead');
thead.innerHTML = `<tr><th>No.</th><th>Name</th><th>Value</th><th>Remove</th></tr>`;
const tbody = document.createElement('tbody');
table.appendChild(thead);
table.appendChild(tbody);
function renderTable() {
tbody.innerHTML = '';
const highlights = ['Event', 'Site', 'Date', 'Round', 'Red', 'Black', 'Result'];
localMetadata.forEach((item, index) => {
const tr = document.createElement('tr');
tr.className = 'pgn-meta-row';
if (pgnSelectedRowIndex === index) tr.classList.add('selected');
const tdNum = document.createElement('td');
tdNum.textContent = index + 1;
tdNum.style.cursor = 'pointer';
tdNum.onclick = () => {
syncDOM(); // Ensure current typings are saved before moving
if (pgnSelectedRowIndex === -1) {
pgnSelectedRowIndex = index;
} else if (pgnSelectedRowIndex === index) {
pgnSelectedRowIndex = -1;
} else {
const movedItem = localMetadata.splice(pgnSelectedRowIndex, 1)[0];
localMetadata.splice(index, 0, movedItem);
pgnSelectedRowIndex = -1;
}
renderTable();
};
tr.appendChild(tdNum);
const tdName = document.createElement('td');
const nameInput = document.createElement('input');
nameInput.className = 'pgn-meta-input';
if (highlights.includes(item.name)) nameInput.classList.add('pgn-meta-highlight');
nameInput.value = item.name;
tdName.appendChild(nameInput);
tr.appendChild(tdName);
const tdValue = document.createElement('td');
if (item.name === 'Result') {
const sel = document.createElement('select');
sel.className = 'pgn-meta-input';
['*', '1-0', '0-1', '1/2-1/2'].forEach((opt) => {
const option = document.createElement('option');
option.value = opt;
option.textContent = opt;
if (opt === item.value) option.selected = true;
sel.appendChild(option);
});
tdValue.appendChild(sel);
} else {
const valInput = document.createElement('input');
valInput.className = 'pgn-meta-input';
valInput.value = item.value;
tdValue.appendChild(valInput);
}
tr.appendChild(tdValue);
const tdDel = document.createElement('td');
tdDel.className = 'pgn-meta-trash';
tdDel.textContent = '🗑️';
tdDel.onclick = () => {
syncDOM();
localMetadata.splice(index, 1);
if (pgnSelectedRowIndex === index) pgnSelectedRowIndex = -1;
else if (pgnSelectedRowIndex > index) pgnSelectedRowIndex--;
renderTable();
};
tr.appendChild(tdDel);
tbody.appendChild(tr);
});
}
renderTable();
const addBtn = document.createElement('div');
addBtn.className = 'pgn-add-btn';
addBtn.textContent = '+';
addBtn.onclick = () => {
syncDOM();
localMetadata.push({ name: '', value: '' });
renderTable();
};
box.appendChild(closeBtn);
box.appendChild(headerDiv);
box.appendChild(table);
box.appendChild(addBtn);
modal.appendChild(box);
document.body.appendChild(modal);
}
```
---
### 4. Add `meta` to JSON structure and Undo/Redo states
Update the JSON generation, loading functionalities, and the history tracking to properly associate `pgnMetadata`.
**In `document.getElementById('tool-save').addEventListener('click', ...)`:**
```javascript
let copy;
if (isRangeMode) {
let rangeHistory = getGamePath();
copy = simplify(rangeHistory);
copy.fen = rangeHistory.fen;
isRangeMode = false;
rangeStart = null;
rangeEnd = null;
rangeClicks = 0;
clearRangeBorders();
renderNoteUI();
} else {
copy = simplify(historyFEN);
copy.fen = historyFEN.fen;
}
// Improvement 4: Append PGN metadata to the JSON payload
if (pgnMetadata && pgnMetadata.length > 0) {
copy.meta = pgnMetadata;
}
const blob = new Blob([JSON.stringify(copy)], {
```
**In `document.getElementById('file-input').addEventListener('change', ...)`:**
```javascript
const data = JSON.parse(event.target.result);
// Improvement 4: Read PGN metadata from the parsed JSON payload
if (data.meta && Array.isArray(data.meta)) {
pgnMetadata = JSON.parse(JSON.stringify(data.meta));
}
function expand(node, parentFen) {
```
**In `importExportedText()` -> (Handling JSON String Block):**
```javascript
// Handle Direct JSON structure
if (text.startsWith('{')) {
try {
const data = JSON.parse(text);
// Improvement 4: Load PGN metadata on JSON paste
if (data.meta && Array.isArray(data.meta)) {
pgnMetadata = JSON.parse(JSON.stringify(data.meta));
}
function expand(node, parentFen) {
```
**In `cloneGameState()` & `restoreState()`:**
```javascript
function cloneGameState() {
return {
historyFEN: structuredClone(historyFEN),
currentBranch: [...currentBranch],
currentStepIndex: currentStepIndex,
moveInterval: moveInterval,
moveSpeed: moveSpeed,
isRotateEnabled: isRotateEnabled,
isFlipEnabled: isFlipEnabled,
// Improvement 4: Capture current metadata into state history
pgnMetadata: JSON.parse(JSON.stringify(pgnMetadata))
};
}
function restoreState(state) {
const state_obj = JSON.parse(state);
historyFEN = structuredClone(state_obj.historyFEN);
currentBranch = [...state_obj.currentBranch];
currentStepIndex = state_obj.currentStepIndex;
moveInterval = parseFloat(state_obj.moveInterval);
moveSpeed = parseFloat(state_obj.moveSpeed);
isRotateEnabled = state_obj.isRotateEnabled;
isFlipEnabled = state_obj.isFlipEnabled;
// Improvement 4: Restore metadata from state history
if (state_obj.pgnMetadata) {
pgnMetadata = JSON.parse(JSON.stringify(state_obj.pgnMetadata));
}
```
---
### 5. Add Native PGN Parsing Capability
Update your `importExportedText()` function where the `[Game "Chinese Chess"]` check is defined:
**In `importExportedText()`:**
```javascript
// Import PGN format
if (text.startsWith('[Game "Chinese Chess"]')) {
// Extract Tags
const tagRegex = /\[([A-Za-z0-9_]+)\s+"([^"]*)"\]/g;
let match;
pgnMetadata = [];
let pgnFen = INITIAL_FEN;
while ((match = tagRegex.exec(text)) !== null) {
const tagName = match[1];
const tagValue = match[2];
if (tagName !== 'Game' && tagName !== 'Format' && tagName !== 'FEN') {
pgnMetadata.push({ name: tagName, value: tagValue });
}
if (tagName === 'FEN') pgnFen = tagValue;
}
// Extract notation body
let bodyStartIndex = text.lastIndexOf(']');
let bodyText = text.substring(bodyStartIndex + 1).trim();
bodyText = bodyText.replace(/(?:1-0|0-1|1\/2-1\/2|\*)$/, '').trim();
// Tokenize body for moves, comments, and variation markers
let tokens = [];
let currentToken = '';
let inComment = false;
for (let i = 0; i < bodyText.length; i++) {
let char = bodyText[i];
if (inComment) {
if (char === '}') {
inComment = false;
tokens.push({ type: 'comment', value: currentToken.trim() });
currentToken = '';
} else {
currentToken += char;
}
} else {
if (char === '{') {
if (currentToken.trim() !== '') tokens.push({ type: 'text', value: currentToken.trim() });
currentToken = '';
inComment = true;
} else if (char === '(' || char === ')') {
if (currentToken.trim() !== '') tokens.push({ type: 'text', value: currentToken.trim() });
currentToken = '';
tokens.push({ type: char });
} else {
currentToken += char;
}
}
}
if (currentToken.trim() !== '') tokens.push({ type: 'text', value: currentToken.trim() });
// Flatten into usable blocks
let flatTokens = [];
tokens.forEach(t => {
if (t.type === 'text') {
let parts = t.value.split(/\s+/);
parts.forEach(p => {
if (p !== '') flatTokens.push({ type: 'word', value: p });
});
} else {
flatTokens.push(t);
}
});
// Reconstruct Tree
let rootNode = { fen: pgnFen, move: null, lastMove: null, c: '', v: [] };
let currentNode = rootNode;
let lastAddedNode = null;
let isRedTurn = pgnFen.includes(' w');
let varStack = [];
let tIdx = 0;
if (flatTokens.length > 0 && flatTokens[0].type === 'comment') {
rootNode.c = flatTokens[0].value;
tIdx++;
}
for (; tIdx < flatTokens.length; tIdx++) {
let token = flatTokens[tIdx];
if (token.type === 'word') {
let word = token.value;
if (/^\d+\.+$/.test(word) || word === '...' || word === '…') continue;
let dc = null;
let moveEn = '';
let isEnglish = /^[a-zA-Z]/.test(word) || /^[+\-=1-9][a-zA-Z0-9]/.test(word);
if (/^[a-i][0-9][a-i][0-9]$/i.test(word) || /^[A-I][0-9][A-I][0-9]$/i.test(word)) {
word = word.toLowerCase();
let startX = word.charCodeAt(0) - 97;
let startY = 9 - parseInt(word.charAt(1), 10);
let endX = word.charCodeAt(2) - 97;
let endY = 9 - parseInt(word.charAt(3), 10);
dc = { startX, startY, endX, endY };
let vMap = buildVirtualMap(currentNode.fen);
let pId = vMap.get(`${startX},${startY}`);
if (pId) {
moveEn = getMoveNotation(pId, startX, startY, endX, endY, null, vMap);
} else {
moveEn = word;
}
} else {
moveEn = isEnglish ? word : NotationConverter.toEnglish(word, isRedTurn);
dc = deriveCoordsFromMove(currentNode.fen, moveEn, isRedTurn);
}
if (dc) {
let nextFen = simulateMove(currentNode.fen, dc);
let newNode = { fen: nextFen, move: moveEn, lastMove: dc, c: '', v: [], parent: currentNode };
currentNode.v.push(newNode);
lastAddedNode = newNode;
currentNode = newNode;
isRedTurn = !isRedTurn;
}
} else if (token.type === 'comment') {
if (lastAddedNode) {
if (lastAddedNode.c) lastAddedNode.c += '\n' + token.value;
else lastAddedNode.c = token.value;
} else {
if (rootNode.c) rootNode.c += '\n' + token.value;
else rootNode.c = token.value;
}
} else if (token.type === '(') {
varStack.push({ currentNode: currentNode, lastAddedNode: lastAddedNode, isRedTurn: isRedTurn });
if (lastAddedNode && lastAddedNode.parent) {
currentNode = lastAddedNode.parent;
isRedTurn = currentNode.fen.includes(' w');
}
} else if (token.type === ')') {
if (varStack.length > 0) {
let st = varStack.pop();
currentNode = st.currentNode;
lastAddedNode = st.lastAddedNode;
isRedTurn = st.isRedTurn;
}
}
}
// Strip temporary parent dependencies
function cleanParents(node) {
delete node.parent;
if (node.v) node.v.forEach(cleanParents);
}
cleanParents(rootNode);
historyFEN = rootNode;
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;
}
```