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.
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)
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;
}
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();
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)
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);
});
});