Chinese Chess Board Editor Enhancements

Here are the modifications to implement the requested improvements. All changes are contained within the <script> block.

1. Update importExportedText for Character Replacements and FEN Fallback

Find the importExportedText(text) function and modify the beginning of it to handle the character replacements and the FEN fallback:

JavaScript

            function importExportedText(text) {
               text = text.trim();
               
               // --- Improvement 1: Replace specific Chinese characters and fix ' r' to ' w' ---
               text = text.replace(/车/g, '車')
                          .replace(/马/g, '馬')
                          .replace(/帅/g, '帥')
                          .replace(/将/g, '將')
                          .replace(/进/g, '進');
               text = text.replace(/ r(\n|$)/g, ' w$1');
               
               if (!text) {
                   renderRecordUI();
                   renderNoteUI();
                   updateToolHighlights();
                   return;
               }

               // Treat as JSON structure
               if (text.startsWith('{')) {
                   // ... (keep existing JSON try-catch block) ...
                   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();
                       return;
                   } catch (e) {
                       console.log("JSON parse failed, falling back to text format.");
                   }
               }

               // --- Improvement 2: FEN fallback to INITIAL_FEN if not found ---
               const fenMatch = text.match(/([a-zA-Z0-9/]+ [wb])/);
               let baseFen = INITIAL_FEN;
               if (fenMatch) {
                   baseFen = fenMatch[1];
               }
               
               let lines = text.split('\n');
               let movesList = [];
               let currentIsRed = baseFen.includes(' w');
               // ... (keep the rest of importExportedText unchanged) ...

2. Implement "Edit Board" State (Improvement 3)

Add these new state variables at the top of your <script> block (near let isExportTextMode = false;):

JavaScript

        let isEditMode = false;
       let editUndoStack = [];
       let editRedoStack = [];
       let selectedPalettePiece = null;
       let isTile1Selected = true;
       let editMiniBoardState = 0; // 0 = INITIAL_FEN, 1 = Kings only
       let preEditFEN = '';
       let editPickedPieceId = null;

Next, replace the svgElement.addEventListener('click', ...) with the following logic to intercept clicks during Edit Mode:

JavaScript

            // --- 4. Click Event Handler ---
           svgElement.addEventListener('click', (event) => {
               if (isAnimating || isEditingComment || isExportTextMode) return;

               const pt = svgElement.createSVGPoint();
               pt.x = event.clientX;
               pt.y = event.clientY;
               const cursorPt = pt.matrixTransform(svgElement.getScreenCTM().inverse());

               const clickX = Math.floor(cursorPt.x);
               const clickY = Math.floor(cursorPt.y);

               if (clickX < 0 || clickX >= 432 || clickY < 0 || clickY >= 480) return;

               const gridX_vis = Math.floor(clickX / 48);
               const gridY_vis = Math.floor(clickY / 48);
               const logicalPt = getLogicalCoords(gridX_vis, gridY_vis);
               const gridX = logicalPt.x;
               const gridY = logicalPt.y;
               const clickedId = tileMap.get(`${gridX},${gridY}`);

               let outputText = `(${clickX},${clickY}), (${gridX},${gridY})`;
               if (clickedId) outputText += ` - ID: ${clickedId}`;
               outputElement.textContent = outputText;

               // --- Handle Edit Mode Clicks ---
               if (isEditMode) {
                   handleEditBoardClick(gridX, gridY, clickedId);
                   return;
               }

               // ... (Keep existing normal play click logic here) ...
               if (!pickedPieceId) {
                   // ... (Normal logic) ...

Now, add the main logic for Edit Mode right above // --- 5. Step Controls & Tools Events ---:

JavaScript

            // --- Edit Mode Core Logic ---
           function getEditIconHTML(char, cssClass, widthPct) {
               const tileId = pieceToHrefMap[char] || 'tile1';
               const origTile = document.getElementById(tileId);
               const defsContent = origTile ? origTile.outerHTML : '';
               return `
               <svg class="${cssClass}" data-char="${char}" viewBox="0 0 48 48" style="width: ${widthPct}; aspect-ratio: 1; cursor:pointer; box-sizing: border-box; border: 2px solid transparent; float: left;">
                   <defs>${defsContent}</defs>
                   <use id="edit-${char}" href="#${tileId}" fill="none" stroke="none" transform="translate(24,24) scale(1,1) rotate(0)"></use>
               </svg>`;
           }

           function enterEditMode() {
               isEditMode = true;
               preEditFEN = boardToFEN();
               editUndoStack = [preEditFEN];
               editRedoStack = [];
               isTile1Selected = true;
               selectedPalettePiece = null;
               editMiniBoardState = 0;
               editPickedPieceId = null;

               // Clear ongoing states
               if (pickedPieceId) setPickup(pickedPieceId, false);
               pickedPieceId = null;
               const pieces = etboard.querySelectorAll('use');
               pieces.forEach(el => {
                   el.removeAttribute('currentmove');
                   el.setAttribute('stroke', 'none');
                   el.removeAttribute('stroke-width');
               });
               const dot = document.getElementById('ejceesstartdot');
               if (dot) {
                   dot.setAttribute('cx', initialDotCx);
                   dot.setAttribute('cy', initialDotCy);
               }

               // Inject Palette into ejceesrecord
               const recordContainer = document.querySelector('.ejceesrecord');
               recordContainer.innerHTML = '';
               const row1 = ['R', 'N', 'n', 'r'];
               const row2 = ['C', 'P', 'p', 'c'];
               const row3 = ['B', 'A', 'a', 'b'];
               let paletteHTML = '<div style="display:flex; justify-content:space-around; width:100%; margin-bottom:10px;">' + row1.map(c => getEditIconHTML(c, 'edit-palette-item', '11.11%')).join('') + '</div>';
               paletteHTML += '<div style="display:flex; justify-content:space-around; width:100%; margin-bottom:10px;">' + row2.map(c => getEditIconHTML(c, 'edit-palette-item', '11.11%')).join('') + '</div>';
               paletteHTML += '<div style="display:flex; justify-content:space-around; width:100%; margin-bottom:10px;">' + row3.map(c => getEditIconHTML(c, 'edit-palette-item', '11.11%')).join('') + '</div>';
               recordContainer.innerHTML = paletteHTML;

               // Inject Comment tools
               const commentContainer = document.querySelector('.ejceescomment');
               let commentHTML = '<div style="display:flex; justify-content:space-around; align-items:center; width:100%; margin-bottom:10px;">';
               commentHTML += getEditIconHTML('tile1', 'edit-comment-item', '16.66%');
               commentHTML += `<div class="ejceespbmini-container" style="width:16.66%; aspect-ratio:432/480; cursor:pointer; box-sizing:border-box; border:2px solid transparent;"></div>`;
               commentHTML += '</div>';
               commentHTML += '<div style="display:flex; justify-content:space-around; align-items:center; width:100%; margin-bottom:10px;">';
               commentHTML += getEditIconHTML('K', 'edit-comment-item', '16.66%');
               commentHTML += getEditIconHTML('k', 'edit-comment-item', '16.66%');
               commentHTML += '</div>';
               commentContainer.innerHTML = commentHTML;

               // Clone board into mini board
               const miniContainer = document.querySelector('.ejceespbmini-container');
               const cloneSVG = document.querySelector('.ejceespb').cloneNode(true);
               cloneSVG.setAttribute('class', 'ejceespbmini');
               cloneSVG.style.width = '100%';
               cloneSVG.style.height = '100%';
               miniContainer.appendChild(cloneSVG);
               updateMiniBoard();

               // Buttons
               const btnDiv = document.querySelector('.ejceestextbtn');
               btnDiv.innerHTML = `
                   <div class="ejceestextbtninner">
                       <div class="note-btn btn-confirm" id="edit-confirm" title="Confirm">
                           <svg viewBox="0 0 24 24"><path fill="#4CAF50" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
                       </div>
                       <div class="note-btn btn-cancel" id="edit-cancel" title="Cancel">
                           <svg viewBox="0 0 24 24"><path fill="#F44336" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
                       </div>
                   </div>
               `;

               document.getElementById('edit-confirm').addEventListener('click', exitEditModeConfirm);
               document.getElementById('edit-cancel').addEventListener('click', exitEditModeCancel);

               bindEditUIEvents();
               updateEditUI();
               updateToolHighlights();
           }

           function bindEditUIEvents() {
               document.querySelectorAll('.edit-palette-item').forEach(item => {
                   item.addEventListener('click', () => {
                       if (item.classList.contains('disabled')) return;
                       const char = item.getAttribute('data-char');
                       selectedPalettePiece = (selectedPalettePiece === char) ? null : char;
                       updateEditUI();
                   });
               });

               document.querySelectorAll('.edit-comment-item').forEach(item => {
                   item.addEventListener('click', () => {
                       const char = item.getAttribute('data-char');
                       if (char === 'tile1') {
                           isTile1Selected = !isTile1Selected;
                       } else if (char === 'K' || char === 'k') {
                           currentTurn = (char === 'K') ? 'w' : 'b';
                           saveEditState();
                       }
                       updateEditUI();
                   });
               });

               document.querySelector('.ejceespbmini-container').addEventListener('click', () => {
                   editMiniBoardState = 1 - editMiniBoardState;
                   const newFen = editMiniBoardState === 0 ? INITIAL_FEN : '4k4/9/9/9/9/9/9/9/9/4K4 w';
                   const savedTurn = currentTurn;
                   loadFEN(newFen);
                   currentTurn = savedTurn;
                   updateMiniBoard();
                   saveEditState();
                   updateEditUI();
               });
           }

           function updateEditUI() {
               const counts = {};
               for (const id of tileMap.values()) counts[id[0]] = (counts[id[0]] || 0) + 1;

               document.querySelectorAll('.edit-palette-item').forEach(item => {
                   const char = item.getAttribute('data-char');
                   const max = (char === 'p' || char === 'P') ? 5 : 2;
                   if ((counts[char] || 0) >= max) {
                       item.style.opacity = '0.36';
                       item.classList.add('disabled');
                       if (selectedPalettePiece === char) selectedPalettePiece = null;
                   } else {
                       item.style.opacity = '1';
                       item.classList.remove('disabled');
                   }
                   item.style.borderColor = (selectedPalettePiece === char) ? 'red' : 'transparent';
               });

               document.querySelectorAll('.edit-comment-item').forEach(item => {
                   const char = item.getAttribute('data-char');
                   if (char === 'tile1') item.style.borderColor = isTile1Selected ? 'red' : 'transparent';
                   else if (char === 'K') item.style.borderColor = (currentTurn === 'w') ? 'red' : 'transparent';
                   else if (char === 'k') item.style.borderColor = (currentTurn === 'b') ? 'red' : 'transparent';
               });
           }

           function updateMiniBoard() {
               const miniBoard = document.querySelector('.ejceespbmini');
               if (!miniBoard) return;
               const targetFen = editMiniBoardState === 1 ? '4k4/9/9/9/9/9/9/9/9/4K4' : INITIAL_FEN.split(' ')[0];
               
               const pieces = miniBoard.querySelectorAll('use');
               pieces.forEach(el => {
                   const href = el.getAttribute('href') || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
                   if (/^#(tile([2-9]|1[0-5]))$/.test(href)) el.remove();
               });

               const etboardMini = miniBoard.querySelector('.etboard') || miniBoard.querySelector('g:last-child');
               targetFen.split('/').forEach((row, y) => {
                   let x = 0;
                   for (let i = 0; i < row.length; i++) {
                       const char = row[i];
                       if (/[0-9]/.test(char)) x += parseInt(char, 10);
                       else {
                           const useEl = document.createElementNS('http://www.w3.org/2000/svg', 'use');
                           useEl.setAttribute('href', `#${pieceToHrefMap[char]}`);
                           useEl.setAttribute('transform', `translate(${x * 48 + 24}, ${y * 48 + 24})`);
                           etboardMini.appendChild(useEl);
                           x++;
                       }
                   }
               });
           }

           function isValidEditPlacement(char, x, y) {
               if (char === 'K') return x >= 3 && x <= 5 && y >= 7 && y <= 9;
               if (char === 'k') return x >= 3 && x <= 5 && y >= 0 && y <= 2;
               if (char === 'A') return x >= 3 && x <= 5 && y >= 7 && y <= 9 && Math.abs(x - 4) === Math.abs(y - 8);
               if (char === 'a') return x >= 3 && x <= 5 && y >= 0 && y <= 2 && Math.abs(x - 4) === Math.abs(y - 1);
               if (char === 'B') return y >= 5 && y <= 9 && y % 2 !== 0 && ((y === 5 || y === 9) ? (x === 2 || x === 6) : (x === 0 || x === 4 || x === 8));
               if (char === 'b') return y >= 0 && y <= 4 && y % 2 === 0 && ((y === 0 || y === 4) ? (x === 2 || x === 6) : (x === 0 || x === 4 || x === 8));
               return true;
           }

           function handleEditBoardClick(x, y, clickedId) {
               if (!isTile1Selected) {
                   if (!selectedPalettePiece) {
                       if (clickedId && clickedId[0] !== 'K' && clickedId[0] !== 'k') {
                           removeEditPiece(clickedId);
                           saveEditState();
                           updateEditUI();
                       }
                   } else {
                       if (clickedId) {
                           if (clickedId[0] !== 'K' && clickedId[0] !== 'k' && isValidEditPlacement(selectedPalettePiece, x, y)) {
                               removeEditPiece(clickedId);
                               placeEditPiece(selectedPalettePiece, x, y);
                               saveEditState();
                               updateEditUI();
                           }
                       } else {
                           if (isValidEditPlacement(selectedPalettePiece, x, y)) {
                               placeEditPiece(selectedPalettePiece, x, y);
                               saveEditState();
                               updateEditUI();
                           }
                       }
                   }
               } else {
                   if (editPickedPieceId) {
                       if (clickedId === editPickedPieceId) {
                           setPickup(editPickedPieceId, false);
                           editPickedPieceId = null;
                       } else if (clickedId) {
                           setPickup(editPickedPieceId, false);
                           editPickedPieceId = clickedId;
                           setPickup(editPickedPieceId, true);
                       } else {
                           if (isValidEditPlacement(editPickedPieceId[0], x, y)) {
                               const pos = piecePos.get(editPickedPieceId);
                               tileMap.delete(`${pos.x},${pos.y}`);
                               tileMap.set(`${x},${y}`, editPickedPieceId);
                               piecePos.set(editPickedPieceId, {x, y});
                               const pieceEl = document.getElementById(editPickedPieceId);
                               const vis = getVisualCoords(x, y);
                               pieceEl.setAttribute('transform', `translate(${vis.x*48+24}, ${vis.y*48+24})`);
                               setPickup(editPickedPieceId, false);
                               editPickedPieceId = null;
                               saveEditState();
                               updateEditUI();
                           }
                       }
                   } else {
                       if (clickedId) {
                           editPickedPieceId = clickedId;
                           setPickup(editPickedPieceId, true);
                       } else {
                           if (selectedPalettePiece && isValidEditPlacement(selectedPalettePiece, x, y)) {
                               placeEditPiece(selectedPalettePiece, x, y);
                               saveEditState();
                               updateEditUI();
                           }
                       }
                   }
               }
           }

           function removeEditPiece(id) {
               const el = document.getElementById(id);
               if (el) el.remove();
               const pos = piecePos.get(id);
               if (pos) tileMap.delete(`${pos.x},${pos.y}`);
               piecePos.delete(id);
           }

           function placeEditPiece(char, x, y) {
               let idx = 0;
               while (piecePos.has(`${char}${idx}`)) idx++;
               const newId = `${char}${idx}`;
               const useEl = document.createElementNS('http://www.w3.org/2000/svg', 'use');
               useEl.setAttribute('id', newId);
               useEl.setAttribute('href', `#${pieceToHrefMap[char]}`);
               const vis = getVisualCoords(x, y);
               useEl.setAttribute('transform', `translate(${vis.x * 48 + 24}, ${vis.y * 48 + 24})`);
               document.querySelector('.etboard').appendChild(useEl);
               tileMap.set(`${x},${y}`, newId);
               piecePos.set(newId, {x, y});
           }

           function saveEditState() {
               const fen = boardToFEN();
               editUndoStack.push(fen);
               if (editUndoStack.length > 36) editUndoStack.shift();
               editRedoStack = [];
               updateToolHighlights();
           }

           function undoEdit() {
               if (editUndoStack.length <= 1) return;
               if (editPickedPieceId) { setPickup(editPickedPieceId, false); editPickedPieceId = null; }
               editRedoStack.push(editUndoStack.pop());
               const fen = editUndoStack[editUndoStack.length - 1];
               loadFEN(fen);
               updateEditUI();
               updateToolHighlights();
           }

           function redoEdit() {
               if (editRedoStack.length === 0) return;
               if (editPickedPieceId) { setPickup(editPickedPieceId, false); editPickedPieceId = null; }
               const fen = editRedoStack.pop();
               editUndoStack.push(fen);
               loadFEN(fen);
               updateEditUI();
               updateToolHighlights();
           }

           function exitEditModeCancel() {
               isEditMode = false;
               loadFEN(preEditFEN);
               renderRecordUI();
               renderNoteUI();
               updateToolHighlights();
           }

           function exitEditModeConfirm() {
               isEditMode = false;
               const finalFEN = boardToFEN();
               historyFEN = { fen: finalFEN, move: null, lastMove: null, c: "", v: [] };
               currentBranch = [];
               currentStepIndex = 0;
               stepSlider.max = 0;
               stepSlider.value = 0;
               saveStateToUndo();
               renderRecordUI();
               renderNoteUI();
               updateToolHighlights();
           }

3. Update Controls and Buttons

Modify updateToolHighlights() to disable non-relevant buttons in Edit Mode and fix undo/redo integration:

JavaScript

            function updateToolHighlights() {
               if (isEditMode) {
                   const editHighlights = {
                       'tool-new': false, 'tool-open': false, 'tool-edit': false, 'tool-save': false,
                       'tool-undo': editUndoStack.length > 1,
                       'tool-redo': editRedoStack.length > 0,
                       'tool-rotate': true, 'tool-flip': true,
                       'tool-exp-txt': false, 'tool-exp-svg': false
                   };
                   for (const [id, shouldEnable] of Object.entries(editHighlights)) {
                       const btn = document.getElementById(id);
                       if (btn) btn.classList.toggle('enable', shouldEnable);
                   }
                   return;
               }

               const isModified = currentStepIndex > 0 || historyFEN.fen !== INITIAL_FEN;
               const highlights = {
                   'tool-new': isModified && !isExportTextMode,
                   'tool-open': !isExportTextMode,
                   'tool-edit': !isEditingComment && !isExportTextMode,
                   // ... (rest of the highlights remain identical) ...

Finally, wire up the Edit Tool button, edit undo/redo, and keybinds near your existing event listeners:

JavaScript

            // Existing edit button listener integration
           document.getElementById('tool-edit').addEventListener('click', () => {
               if (isExportTextMode || isEditingComment) return;
               if (!isEditMode) enterEditMode();
           });

           // Modify undo/redo listeners to check for edit mode
           document.getElementById('tool-undo').addEventListener('click', () => {
               if (isExportTextMode) return;
               isEditMode ? undoEdit() : undo();
           });
           document.getElementById('tool-redo').addEventListener('click', () => {
               if (isExportTextMode) return;
               isEditMode ? redoEdit() : redo();
           });

           // Replace existing keydown block
           document.addEventListener('keydown', (e) => {
               if (isExportTextMode) return;
               if (e.ctrlKey && e.key === 'z') {
                   e.preventDefault();
                   isEditMode ? undoEdit() : undo();
               }
               if (e.ctrlKey && e.key === 'y') {
                   e.preventDefault();
                   isEditMode ? redoEdit() : redo();
               }
           });