遊戲走法搜尋與判斷函式

Here are the modifications to add the getAllLegalMoves function and integrate it into the three use cases you specified. All code and comments are in English.

1. Update isValidMove to accept a custom board map

First, modify the isValidMove signature so it doesn't hardcode the global tileMap. This allows our new search function to safely evaluate hypothetical boards if needed (though we mostly use the current board here, it's best practice).

Find the isValidMove function (around line 1182) and modify its first few lines:

JavaScript

        // Main validation function
       // --- Modified: Added boardMap parameter (defaults to tileMap) ---
       function isValidMove(pieceId, startX, startY, endX, endY, boardMap = tileMap) {
           if (!checkMoveGeometry(pieceId, startX, startY, endX, endY, boardMap)) return false;

           const simulatedMap = new Map(boardMap);
           simulatedMap.delete(`${startX},${startY}`);
           simulatedMap.set(`${endX},${endY}`, pieceId);
           // ... (keep the rest of the function unchanged)

2. Add the getAllLegalMoves Function

Insert the new function right below the isValidMove function. This function iterates through all pieces of the active player and checks all 90 squares for legal moves.

Add this right below isValidMove:

JavaScript

        // --- New Function: Search for all legal moves for a given player ---
       function getAllLegalMoves(isRedTurn, boardMap = tileMap) {
           const legalMoves = [];
           for (const [pos, id] of boardMap.entries()) {
               const pieceIsRed = id[0] === id[0].toUpperCase();
               if (pieceIsRed === isRedTurn) {
                   const [sx, sy] = pos.split(',').map(Number);
                   for (let tx = 0; tx <= 8; tx++) {
                       for (let ty = 0; ty <= 9; ty++) {
                           // Skip if target is the same as start
                           if (sx === tx && sy === ty) continue;
                           
                           // Skip if target is occupied by a friendly piece
                           const targetId = boardMap.get(`${tx},${ty}`);
                           if (targetId && isSameTeam(id, targetId)) continue;
                           
                           // Validate the move (implicitly prevents King capturing and facing Kings)
                           if (isValidMove(id, sx, sy, tx, ty, boardMap)) {
                               legalMoves.push({ startX: sx, startY: sy, endX: tx, endY: ty });
                           }
                       }
                   }
               }
           }
           return legalMoves;
       }

3. Use Case 1: Game End Detection (Checkmate / Stalemate)

After a move animation completes, we use the new function to determine if the next player has any legal moves left.

Find the movePieceWithAnimation function. Locate the onComplete logic block (around line 1250) and add the check:

JavaScript

                    if (onComplete) {
                       onComplete();
                   } else {
                       if (eatenId) {
                           halfMoveClock = 0;
                       } else {
                           halfMoveClock++;
                       }
                       if (currentTurn === 'b') {
                           fullMoveNumber++;
                       }
                       currentTurn = currentTurn === 'w' ? 'b' : 'w';
                       pushHistory(moveText, moveData);
                       applyLastMoveVisuals(moveData);
                       restoreOutputText();
                       
                       // --- Added Logic: Checkmate / Stalemate Detection ---
                       const nextTurnIsRed = (currentTurn === 'w');
                       const legalMoves = getAllLegalMoves(nextTurnIsRed, tileMap);
                       
                       if (legalMoves.length === 0) {
                           let isCheck = false;
                           let myKingPos = null;
                           
                           // Find the King of the player who has no moves
                           for (const [pos, id] of tileMap.entries()) {
                               if ((nextTurnIsRed && id[0] === 'K') || (!nextTurnIsRed && id[0] === 'k')) {
                                   myKingPos = pos.split(',').map(Number);
                                   break;
                               }
                           }
                           
                           // Determine if the King is currently under attack
                           if (myKingPos && isSquareAttacked(myKingPos[0], myKingPos[1], !nextTurnIsRed, tileMap)) {
                               isCheck = true;
                           }
                           
                           const endMsg = isCheck ? "Checkmate!" : "Stalemate!";
                           setOutputText(endMsg, false);
                           setTimeout(() => alert("Game Over: " + endMsg), 50);
                       }
                       // ----------------------------------------------------
                   }
                   updateToolHighlights();

4. Use Case 2: Edit Mode Validation Optimization

Replace the old, nested loop inside exitEditModeConfirm with our new clean function.

Find the exitEditModeConfirm() function (around line 1475) and replace the hasValidMove block:

JavaScript

            // ... (keep the King check / checkKingsFacing logic)
           const isRedTurn = currentTurn === 'w';
           const oppKingPos = isRedTurn ? bk : rk;
           const [okX, okY] = oppKingPos.split(',').map(Number);
           if (isSquareAttacked(okX, okY, !isRedTurn, tileMap)) {
               alert("Invalid state: Active player can directly capture the opponent's King.");
               setOutputText("Validation failed: Opponent King in check.", true);
               return;
           }

           // --- Modified: Use getAllLegalMoves for stalemate validation ---
           const legalMoves = getAllLegalMoves(isRedTurn, tileMap);
           if (legalMoves.length === 0) {
               alert("Invalid state: Active player has no valid moves (Stalemate/Checkmate).");
               setOutputText("Validation failed: No valid moves.", true);
               return;
           }
           // ---------------------------------------------------------------

           isEditMode = false;
           // ... (keep the rest)

5. Use Case 3: Engine Format Searchmoves Toggle

Modify the logic inside the Export Text UI so that clicking "Engine Format" twice triggers the searchmoves text.

Find the renderExportTextUI() function (around line 2100). Replace the formatBtns.forEach click event listener:

JavaScript

            formatBtns.forEach(btn => {
               btn.setAttribute('data-odd', '0');
               btn.addEventListener('click', () => {
                   const wasActive = btn.classList.contains('active');

                   // Update active styling and reset others
                   formatBtns.forEach(b => {
                       b.classList.remove('active');
                       if (!wasActive && b !== btn) {
                           b.setAttribute('data-odd', '0'); // Reset state of inactive buttons
                       }
                   });
                   btn.classList.add('active');

                   const fmt = btn.getAttribute('data-fmt');
                   
                   // If the button was just activated, display default text
                   if (!wasActive) {
                       btn.setAttribute('data-odd', '0');
                       textarea.value = formats[fmt];
                   } else {
                       // Toggle logic for already active buttons
                       if (fmt === 'cn') {
                           if (btn.getAttribute('data-odd') === '0') {
                               btn.setAttribute('data-odd', '1');
                               textarea.value = cnTextWidthComment;
                           } else {
                               btn.setAttribute('data-odd', '0');
                               textarea.value = formats[fmt];
                           }
                       } else if (fmt === 'engine') {
                           // --- Modified: Append searchmoves for engine format ---
                           if (btn.getAttribute('data-odd') === '0') {
                               btn.setAttribute('data-odd', '1');
                               const isRed = currentTurn === 'w';
                               const moves = getAllLegalMoves(isRed, tileMap);
                               
                               // Format array of moves into standard coordinates
                               const searchMovesStr = moves.map(m => {
                                   return String.fromCharCode(97 + m.startX) + (9 - m.startY) +
                                          String.fromCharCode(97 + m.endX) + (9 - m.endY);
                               }).join(' ');
                               
                               textarea.value = formats[fmt] + (searchMovesStr ? "\nsearchmoves " + searchMovesStr : "");
                           } else {
                               btn.setAttribute('data-odd', '0');
                               textarea.value = formats[fmt];
                           }
                           // ------------------------------------------------------
                       } else {
                           // Any other formats just stay the same on repeat clicks
                           textarea.value = formats[fmt];
                       }
                   }
                   
                   textarea.blur();
                   setTimeout(() => {
                       textarea.focus();
                       textarea.select();
                   }, 12);
               });
           });