Chess Notation and Playback Feature Enhancements

Here are the required code modifications to implement all the requested features. Replace the respective blocks in your <script> section.

1. New Global Variables and Auto-Play Helper Methods

Add these declarations at the top of the <script> block, just below let isExportTextMode = false;.

JavaScript

        let isEditMode = false;
       let editUndoStack = [];
       let editRedoStack = [];
       let selectedPalettePiece = null;
       let isTile0Selected = false;
       let editMiniBoardState = 0;
       let preEditFEN = '';
       let editPickedPieceId = null;

       // AutoPlay & Range states
       let playIntervalId = null;
       let playSpeed = 1.8;
       let isAutoPlaying = false;
       let isRangeMode = false;
       let rangeStart = null;
       let rangeEnd = null;
       let rangeClicks = 0;

       function disableUIForPlay(disabled) {
           const panels = ['.ejceestool', '.ejceesstep', '.ejceessvg'];
           panels.forEach(sel => {
               const el = document.querySelector(sel);
               if (el) el.style.pointerEvents = disabled ? 'none' : '';
           });
       }

       function getStepElement(index) {
           if (index === 0) return document.getElementById('record-start');
           return document.getElementById(`step-record-${index}`);
       }

       function applyRangeBorders() {
           clearRangeBorders();
           if (rangeStart !== null) {
               const el = getStepElement(rangeStart);
               if (el) {
                   el.style.borderLeft = '2px solid lightgreen';
                   el.style.borderTop = '2px solid lightgreen';
               }
           }
           if (rangeEnd !== null) {
               const el = getStepElement(rangeEnd);
               if (el) {
                   el.style.borderRight = '2px solid lightcoral';
                   el.style.borderBottom = '2px solid lightcoral';
               }
           }
       }

       function clearRangeBorders() {
           document.querySelectorAll('.ejceesrcdstep, .ejceesrcdstart').forEach(el => {
               el.style.borderLeft = '';
               el.style.borderTop = '';
               el.style.borderRight = '';
               el.style.borderBottom = '';
           });
       }

       function startAutoPlay() {
           isAutoPlaying = true;
           renderNoteUI();
           disableUIForPlay(true);
           
           if (isRangeMode && rangeStart !== null) {
               if (currentStepIndex < rangeStart || currentStepIndex >= (rangeEnd !== null ? rangeEnd : rangeStart)) {
                   jumpToStep(rangeStart);
               }
           }

           playIntervalId = setInterval(() => {
               let targetEnd = (isRangeMode && rangeEnd !== null) ? rangeEnd : stepSlider.max;
               if (currentStepIndex >= targetEnd) {
                   stopAutoPlay();
                   return;
               }
               
               jumpToStep(currentStepIndex + 1);
               
               if (currentStepIndex >= targetEnd) {
                   stopAutoPlay();
               }
           }, playSpeed * 1000);
       }

       function stopAutoPlay() {
           isAutoPlaying = false;
           if (playIntervalId) {
               clearInterval(playIntervalId);
               playIntervalId = null;
           }
           disableUIForPlay(false);
           renderNoteUI();
           if (isRangeMode) {
               applyRangeBorders();
           }
       }

2. Update Notation generation for A and B

In the getMoveNotation function, locate if (colPieces.length > 1) { and replace the inner block with:

JavaScript

                if (colPieces.length > 1) {
                   if (['A', 'a', 'B', 'b'].includes(char)) {
                       prefix = name;
                       location = startCol;
                   } else {
                       // Normalize order: Front-to-Back
                       let sortedInCol = [...colPieces];
                       if (isRed) {
                           sortedInCol.sort((a, b) => a.y - b.y); // For Red, smaller Y is "Front"
                       } else {
                           sortedInCol.sort((a, b) => b.y - a.y); // For Black, larger Y is "Front"
                       }
                       const pIdx = sortedInCol.findIndex(p => p.id === pieceId);

                       // Determine Prefix (Front/Middle/Back or digits)
                       if (colPieces.length === 2) {
                           prefix = (pIdx === 0) ? '+' : '-';
                       } else if (colPieces.length === 3) {
                           prefix = (pIdx === 0) ? '+' : (pIdx === 1 ? '=' : '-');
                       } else {
                           prefix = (pIdx === 0) ? '+' : (pIdx === colPieces.length - 1 ? '-' : pIdx + 1);
                       }

                       // --- KEY FIX: Check for ambiguity across different columns ---
                       if (isPawn && multiPawnCols.length > 1 && prefix !== '=') {
                           location = startCol;
                       } else {
                           location = name;
                       }
                   }
               } else {
                   prefix = name;
                   location = startCol;
               }

3. Update notation parsing for A and B

In the deriveCoordsFromMove function, replace // Case 1: Standard Notation down to its if(candidate) check with:

JavaScript

                if (/[a-zA-Z]/.test(char1) && /[0-9]/.test(char2)) {
                   // Case 1: Standard Notation (e.g., "C2=5", "P3+1", "n8+7", "A4+5")
                   pieceType = isRed ? char1.toUpperCase() : char1.toLowerCase();
                   const startCol = parseInt(char2, 10);
                   startX = isRed ? (9 - startCol) : (startCol - 1);

                   const candidates = myPieces.filter(p => p.char === pieceType && p.x === startX);
                   if (candidates.length === 1) {
                       startY = candidates[0].y;
                   } else if (candidates.length > 1) {
                       // Using movable boundaries to disambiguate A/a/B/b
                       const dir = isRed ? (action === '+' ? -1 : 1) : (action === '+' ? 1 : -1);
                       const dy = (pieceType.toLowerCase() === 'b') ? 2 : 1;
                       for (const cand of candidates) {
                           let testEndY = cand.y + dir * dy;
                           if (pieceType === 'A') {
                               if (testEndY >= 7 && testEndY <= 9) { startY = cand.y; break; }
                           } else if (pieceType === 'a') {
                               if (testEndY >= 0 && testEndY <= 2) { startY = cand.y; break; }
                           } else if (pieceType === 'B') {
                               if (testEndY >= 5 && testEndY <= 9) { startY = cand.y; break; }
                           } else if (pieceType === 'b') {
                               if (testEndY >= 0 && testEndY <= 4) { startY = cand.y; break; }
                           }
                       }
                   }
               }

4. Step Jump Interception (Range Selection Click Flow)

In the jumpToStep function, add the range interception logic at the very beginning:

JavaScript

            function jumpToStep(index) {
               if (isEditingComment) return;

               if (isRangeMode && !isAutoPlaying) {
                   rangeClicks++;
                   if (rangeClicks === 1 || rangeClicks === 3) {
                       if (rangeClicks === 3) rangeClicks = 1;
                       rangeStart = index;
                       rangeEnd = null;
                   } else if (rangeClicks === 2) {
                       if (index < rangeStart) {
                           rangeEnd = rangeStart;
                           rangeStart = index;
                       } else {
                           rangeEnd = index;
                       }
                   }
                   applyRangeBorders();
                   return; // Intercept jump
               }

               const totalDepth = getPathDepth(historyFEN, currentBranch);
               // ... rest of the original jumpToStep code ...

5. Note UI & Autoplay / Speed UI Updates

Replace the entire renderNoteUI() function with this:

JavaScript

            function renderNoteUI() {
               const commentDiv = document.querySelector('.ejceescomment');
               const btnDiv = document.querySelector('.ejceestextbtn');
               const node = getNodeAtStep(currentStepIndex);

               if (!node) return;

               if (isEditingComment) {
                   commentDiv.innerHTML = `<textarea class="ejceescomment-edit" placeholder="Enter comments here...">${node.c || ''}</textarea>`;
                   btnDiv.innerHTML = `
                       <div class="ejceestextbtninner">
                           <div class="note-btn btn-confirm" title="Confirm">
                               <svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
                           </div>
                           <div class="note-btn btn-cancel" title="Cancel">
                               <svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
                           </div>
                       </div>
                   `;

                   const textarea = document.querySelector('.ejceescomment-edit');
                   textarea.focus();

                   document.querySelector('.btn-confirm').addEventListener('click', () => {
                       node.c = textarea.value;
                       isEditingComment = false;
                       renderNoteUI();
                       renderRecordUI();
                       saveStateToUndo();
                   });

                   document.querySelector('.btn-cancel').addEventListener('click', () => {
                       isEditingComment = false;
                       renderNoteUI();
                   });
               } else {
                   commentDiv.innerHTML = '';
                   commentDiv.textContent = node.c || '';

                   btnDiv.innerHTML = `
                       <div class="ejceestextbtninner">
                           <div class="note-btn" id="btn-del-move" title="Delete Move">
                               <svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
                           </div>
                           <div class="note-btn" id="btn-range" title="Range Selection" style="${isRangeMode ? 'background-color:#4CAF50;' : ''}">
                               <svg viewBox="0 0 24 24"><path fill="${isRangeMode ? '#fff' : '#ccc'}" d="M7 6h4v2H9v8h2v2H7V6zm10 0h-4v2h2v8h-2v2h4V6z"/></svg>
                           </div>
                           <div class="note-btn" id="btn-autoplay" title="Auto Play">
                               ${isAutoPlaying
                                   ? '<svg viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>'
                                   : '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>'
                               }
                           </div>
                           <div class="note-btn" id="btn-speed" title="Move Speed" style="width: auto; padding: 0 4px; font-weight: bold;">
                               ${playSpeed}s
                           </div>
                       </div>
                   `;

                   document.getElementById('btn-autoplay').addEventListener('click', () => {
                       if (isAutoPlaying) {
                           stopAutoPlay();
                       } else {
                           startAutoPlay();
                       }
                   });

                   document.getElementById('btn-del-move').addEventListener('click', deleteCurrentMoveAndAfter);

                   document.getElementById('btn-range').addEventListener('click', () => {
                       isRangeMode = !isRangeMode;
                       if (!isRangeMode) {
                           rangeStart = null;
                           rangeEnd = null;
                           rangeClicks = 0;
                           clearRangeBorders();
                       } else {
                           rangeStart = null;
                           rangeEnd = null;
                           rangeClicks = 0;
                           clearRangeBorders();
                       }
                       renderNoteUI();
                   });

                   document.getElementById('btn-speed').addEventListener('click', () => {
                       if (isAutoPlaying) return;
                       btnDiv.innerHTML = `
                           <div class="ejceestextbtninner" style="justify-content: flex-start; gap: 4px;">
                               <input type="text" id="speed-input" style="width: 36px; font-size: 12px; background: #222; color: #fff; border: 1px solid #555; text-align: center; outline: none;" value="${playSpeed}" maxlength="3">
                               <div class="note-btn speed-opt" data-val="1">1</div>
                               <div class="note-btn speed-opt" data-val="2">2</div>
                               <div class="note-btn speed-opt" data-val="3">3</div>
                               <div class="note-btn btn-confirm" id="speed-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="speed-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.querySelectorAll('.speed-opt').forEach(btn => {
                           btn.addEventListener('click', (e) => {
                               playSpeed = parseFloat(e.target.getAttribute('data-val'));
                               renderNoteUI();
                           });
                       });

                       document.getElementById('speed-confirm').addEventListener('click', () => {
                           let val = parseFloat(document.getElementById('speed-input').value);
                           if (isNaN(val) || val < 0.2 || val > 9) {
                               val = 1.8;
                           }
                           playSpeed = parseFloat(val.toFixed(1));
                           renderNoteUI();
                       });

                       document.getElementById('speed-cancel').addEventListener('click', () => {
                           renderNoteUI();
                       });
                   });
               }
               updateToolHighlights();
           }

6. Delete Logic adaptation for Range Mode

Replace the deleteCurrentMoveAndAfter() function:

JavaScript

            function deleteCurrentMoveAndAfter() {
               if (isRangeMode && rangeStart !== null) {
                   if (rangeStart === 0) return;
                   const parentNode = getNodeAtStep(rangeStart - 1);
                   if (!parentNode) return;

                   parentNode.v = [];
                   currentStepIndex = Math.max(0, rangeStart - 1);
                   stepSlider.value = currentStepIndex;

                   let forkIndex = -1;
                   let tempNode = historyFEN;
                   for (let j = 0; j < currentStepIndex; j++) {
                       if (tempNode.v && tempNode.v.length > 1) forkIndex++;
                       const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkIndex] || 0) : 0;
                       tempNode = tempNode.v[choice];
                   }
                   forkIndex++;
                   currentBranch = currentBranch.slice(0, forkIndex);

                   isRangeMode = false;
                   rangeStart = null;
                   rangeEnd = null;
                   rangeClicks = 0;
                   clearRangeBorders();

                   renderRecordUI();
                   renderNoteUI();
                   jumpToStep(currentStepIndex);
                   saveStateToUndo();
                   return;
               }

               if (currentStepIndex === 0) return;

               const parentNode = getNodeAtStep(currentStepIndex - 1);
               if (!parentNode) return;

               parentNode.v = [];
               currentStepIndex = Math.max(0, currentStepIndex - 1);
               stepSlider.value = currentStepIndex;
               let forkIndex = -1;
               let tempNode = historyFEN;
               for (let j = 0; j < currentStepIndex; j++) {
                   if (tempNode.v && tempNode.v.length > 1) forkIndex++;
                   const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkIndex] || 0) : 0;
                   tempNode = tempNode.v[choice];
               }
               forkIndex++;
               currentBranch = currentBranch.slice(0, forkIndex);

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

               saveStateToUndo();
           }

7. Tile0 Toggle Logic

In bindEditUIEvents(), replace the char === 'tile0' condition block with the following logic to toggle the shape on click:

JavaScript

                document.querySelectorAll('.edit-comment-item').forEach(item => {
                   item.addEventListener('click', () => {
                       const char = item.getAttribute('data-char');
                       if (char === 'tile0' || char === 'tile1') {
                           const newChar = char === 'tile0' ? 'tile1' : 'tile0';
                           item.setAttribute('data-char', newChar);
                           const useEl = item.querySelector('use');
                           if (useEl) {
                               useEl.setAttribute('href', `#${newChar}`);
                               useEl.setAttributeNS('http://www.w3.org/1999/xlink', 'href', `#${newChar}`);
                           }
                       } else if (char === 'K' || char === 'k') {
                           currentTurn = (char === 'K') ? 'w' : 'b';
                           saveEditState();
                       }
                       updateEditUI();
                   });
               });

8. UI Slider and Controls Lock (Range Mode)

Replace the slider, minus, plus, and start event bindings with these implementations:

JavaScript

            stepSlider.addEventListener('input', (e) => {
               if (isEditingComment || isExportTextMode || isEditMode || isRangeMode) {
                   e.preventDefault();
                   e.target.value = currentStepIndex;
                   return;
               }
               jumpToStep(parseInt(e.target.value, 10));
           });

           document.querySelector('.ejceesstepminus').addEventListener('click', () => {
               if (isEditingComment || isExportTextMode || isEditMode || isRangeMode) return;
               jumpToStep(currentStepIndex - 1);
           });

           document.querySelector('.ejceesstepplus').addEventListener('click', () => {
               if (isEditingComment || isExportTextMode || isEditMode || isRangeMode) return;
               jumpToStep(currentStepIndex + 1);
           });

           document.querySelector('.ejceesrcdstart').addEventListener('click', ejceesrcdstartclick);
           function ejceesrcdstartclick() {
               if (isEditingComment || isExportTextMode || isEditMode) return;
               jumpToStep(0);
           }

9. Top Menu Actions mapped to Range Mode (New, Save, Export)

Inside document.addEventListener('DOMContentLoaded', () => { ... }), update the tools (tool-new, tool-save, tool-exp-txt, tool-exp-svg) to handle extracting the active Range Selection.

Tool New: Replace toolNewBtn click listener with:

JavaScript

            const toolNewBtn = document.getElementById('tool-new');
           if (toolNewBtn) {
               toolNewBtn.addEventListener('click', () => {
                   if (isExportTextMode) return;
                   if (isRangeMode && rangeStart !== null && rangeEnd !== null) {
                       const startNode = getNodeAtStep(rangeStart);
                       const startFen = startNode.fen;
                       let newHistory = { fen: startFen, move: null, lastMove: null, c: "", v: [] };
                       let curr = newHistory;
                       for (let i = rangeStart + 1; i <= rangeEnd; i++) {
                           const node = getNodeAtStep(i);
                           let newNode = { fen: node.fen, move: node.move, lastMove: node.lastMove, c: node.c, v: [] };
                           curr.v.push(newNode);
                           curr = newNode;
                       }
                       historyFEN = newHistory;
                       currentBranch = [];
                       currentStepIndex = 0;
                       isRangeMode = false;
                       rangeStart = null; rangeEnd = null; rangeClicks = 0;
                       clearRangeBorders();
                       stepSlider.max = getPathDepth(historyFEN, currentBranch) - 1;
                       stepSlider.value = 0;
                       renderRecordUI();
                       renderNoteUI();
                       loadFEN(startFen);
                       applyLastMoveVisuals(null);
                       saveStateToUndo();
                       return;
                   }

                   historyFEN = {
                       fen: INITIAL_FEN,
                       move: null,
                       lastMove: null,
                       c: "",
                       v: []
                   };
                   currentBranch = [];
                   currentStepIndex = 0;
                   stepSlider.max = 0;
                   stepSlider.value = 0;
                   renderRecordUI();
                   renderNoteUI();
                   highlightActiveStep(0);
                   loadFEN(INITIAL_FEN);
                   applyLastMoveVisuals(null);
                   saveStateToUndo();
               });
           }

Tool Save: Replace tool-save listener with:

JavaScript

            document.getElementById('tool-save').addEventListener('click', () => {
               if (isExportTextMode) return;
               function simplify(node) {
                   let obj = node.move ? { m: node.move } : {};
                   if (node.c) obj.c = node.c;
                   if (node.v && node.v.length > 0) {
                       obj.v = node.v.map(child => simplify(child));
                   }
                   return obj;
               }

               let copy;
               if (isRangeMode && rangeStart !== null && rangeEnd !== null) {
                   const startNode = getNodeAtStep(rangeStart);
                   let rangeHistory = { fen: startNode.fen, move: null, lastMove: null, c: "", v: [] };
                   let curr = rangeHistory;
                   for (let i = rangeStart + 1; i <= rangeEnd; i++) {
                       const node = getNodeAtStep(i);
                       let newNode = { fen: node.fen, move: node.move, lastMove: node.lastMove, c: node.c, v: [] };
                       curr.v.push(newNode);
                       curr = newNode;
                   }
                   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;
               }

               const blob = new Blob([JSON.stringify(copy)], { type: 'text/plain' });
               const a = document.createElement('a');
               const now = new Date();
               const timestamp = now.getFullYear().toString().padStart(4, '0') +
                   (now.getMonth() + 1).toString().padStart(2, '0') +
                   now.getDate().toString().padStart(2, '0') +
                   now.getHours().toString().padStart(2, '0') +
                   now.getMinutes().toString().padStart(2, '0') +
                   now.getSeconds().toString().padStart(2, '0');
               a.href = URL.createObjectURL(blob);
               a.download = `ejcees_json_${timestamp}.txt`;
               a.click();
           });

Export Path Filter: Replace the getGamePath function entirely:

JavaScript

            function getGamePath() {
               let path = [];
               let startIdx = (isRangeMode && rangeStart !== null) ? rangeStart : 0;
               let endIdx = (isRangeMode && rangeEnd !== null) ? rangeEnd : getPathDepth(historyFEN, currentBranch) - 1;
               
               let step = 0;
               let currentNode = historyFEN;
               let branchPtr = 0;

               while (currentNode && currentNode.v && currentNode.v.length > 0) {
                   const choice = (currentNode.v.length > 1) ? (currentBranch[branchPtr++] || 0) : 0;
                   currentNode = currentNode.v[choice];
                   step++;
                   if (step > startIdx && step <= endIdx) {
                       path.push(currentNode);
                   }
                   if (step === endIdx) break;
               }
               return path;
           }

           function generateExportText(isEnglish) {
               let path = getGamePath();
               let startIdx = (isRangeMode && rangeStart !== null) ? rangeStart : 0;
               let fen = getNodeAtStep(startIdx).fen;
               let text = fen + '\n';
               let isRedTurn = fen.split(' ')[1] === 'w';
               
               let moveIdx = 0;
               let moveNum = 1;
               // ... rest of the original generateExportText

Tool Export Listeners: Replace their respective listeners to clear the range style upon clicking.

JavaScript

            document.getElementById('tool-exp-txt').addEventListener('click', () => {
               if (isExportTextMode || isEditingComment) return;
               isExportTextMode = true;
               updateToolHighlights();
               renderExportTextUI();
               if (isRangeMode) {
                   isRangeMode = false;
                   clearRangeBorders();
               }
           });

           document.getElementById('tool-exp-svg').addEventListener('click', () => {
               if (isRangeMode) {
                   isRangeMode = false;
                   clearRangeBorders();
                   renderNoteUI();
               }
           });