Automated moves

14/04/2025, 16:54:06

// ==UserScript==
// @name        Automated moves
// @namespace   Violentmonkey Scripts
// @match       https://chess.ytdraws.win/*
// @grant       none
// @version     1.0
// @author      -
// @description 14/04/2025, 16:54:06
// @license     CC0
// ==/UserScript==


let basicBotInterval = null;

// --- Helper Functions (Existing + New) ---

/**
 * Calculates Manhattan distance between two points.
 * @param {number} x1
 * @param {number} y1
 * * @param {number} x2
 * @param {number} y2
 * @returns {number}
 */
function manhattanDistance(x1, y1, x2, y2) {
    return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}

/**
 * Finds all coordinates of the player's pieces of a specific type.
 * @param {number} pieceType - The type of piece (1-6).
 * @returns {Array<Array<number>>} An array of [x, y] coordinates.
 */
function findMyPiecesOfType(pieceType) {
    const pieces = [];
    if (typeof selfId === 'undefined' || selfId === -1) return pieces;

    for (let x = 0; x < boardW; x++) {
        for (let y = 0; y < boardH; y++) {
            if (board[x][y] === pieceType && teams[x][y] === selfId) {
                pieces.push([x, y]);
            }
        }
    }
    return pieces;
}

/**
 * Finds all coordinates of the player's King(s).
 * @returns {Array<Array<number>>} An array of [x, y] coordinates.
 */
function findMyKings() {
    return findMyPiecesOfType(6); // 6 is King
}

/**
 * Checks if the player only has King(s) left.
 * @returns {boolean} True if only Kings remain, false otherwise.
 */
function isOnlyKingLeft() {
    if (typeof selfId === 'undefined' || selfId === -1) return false;
    for (let x = 0; x < boardW; x++) {
        for (let y = 0; y < boardH; y++) {
            // If we find a piece belonging to us that is NOT a King (type 6)
            if (teams[x][y] === selfId && board[x][y] !== 6 && board[x][y] !== 0) {
                return false; // Found a non-king piece
            }
        }
    }
    return true; // No non-king pieces found
}

/**
 * Checks if a specific square is attacked by any enemy piece.
 * (Same as before - potentially inefficient)
 * @param {number} targetX - The x-coordinate of the square to check.
 * @param {number} targetY - The y-coordinate of the square to check.
 * @returns {boolean} True if the square is under attack, false otherwise.
 */
function isSquareAttacked(targetX, targetY) {
    if (typeof selfId === 'undefined' || selfId === -1) return false;
    if (typeof generateLegalMoves === 'undefined') return false; // Safety check

    for (let x = 0; x < boardW; x++) {
        for (let y = 0; y < boardH; y++) {
            if (board[x][y] !== 0 && teams[x][y] !== selfId && teams[x][y] !== 0) {
                try {
                    const enemyMoves = generateLegalMoves(x, y, board, teams);
                    for (const move of enemyMoves) {
                        if (move[0] === targetX && move[1] === targetY) {
                            return true;
                        }
                    }
                } catch (e) {
                    // console.error(`Error generating moves for enemy at ${x},${y} when checking attack on ${targetX},${targetY}:`, e);
                }
            }
        }
    }
    return false;
}

/**
 * Finds the coordinates of the nearest neutral (Team 0) piece.
 * @param {number} fromX - The starting X coordinate.
 * @param {number} fromY - The starting Y coordinate.
 * @returns {Array<number> | null} Coordinates [x, y] of the nearest neutral piece, or null if none found.
 */
function findNearestNeutralPiece(fromX, fromY) {
    let nearestPos = null;
    let minDistance = Infinity;

    for (let x = 0; x < boardW; x++) {
        for (let y = 0; y < boardH; y++) {
            // Team 0 is assumed to be neutral/white/unclaimed
            if (teams[x][y] === 0 && board[x][y] !== 0) {
                const dist = manhattanDistance(fromX, fromY, x, y);
                if (dist < minDistance) {
                    minDistance = dist;
                    nearestPos = [x, y];
                }
            }
        }
    }
    return nearestPos;
}


/**
 * Checks if a player's piece is "idle" (not attacking anything and not under attack).
 * @param {number} pieceX The piece's X coordinate.
 * @param {number} pieceY The piece's Y coordinate.
 * @param {Array<Array<number>>} legalMoves The pre-calculated legal moves for this piece.
 * @returns {boolean} True if the piece is idle, false otherwise.
 */
function isPieceIdle(pieceX, pieceY, legalMoves) {
     // 1. Check if under attack
    if (isSquareAttacked(pieceX, pieceY)) {
        return false;
    }

    // 2. Check if any legal move is a capture
    for (const move of legalMoves) {
        const [toX, toY] = move;
        // Is the destination occupied by an enemy (not empty, not ours, not neutral)?
        if (board[toX][toY] !== 0 && teams[toX][toY] !== selfId && teams[toX][toY] !== 0) {
            return false; // This piece can attack
        }
    }

    // If not attacked and cannot attack, it's idle
    return true;
}

/**
 * Finds the best move from a list of legal moves towards a target coordinate.
 * Handles Knights (type 2) separately to mitigate looping issues.
 * "Best" minimizes Manhattan distance, with tie-breaking for Knights.
 * @param {number} pieceType The type (1-6) of the moving piece.
 * @param {number} currentX The current X of the moving piece.
 * @param {number} currentY The current Y of the moving piece.
 * @param {Array<Array<number>>} legalMoves List of [toX, toY] moves.
 * @param {number} targetX Target X coordinate.
 * @param {number} targetY Target Y coordinate.
 * @returns {Array<number> | null} The best move [toX, toY] or null if no suitable legal moves.
 */
function findBestMoveTowards(pieceType, currentX, currentY, legalMoves, targetX, targetY) {
    let bestMove = null;
    let minDistance = Infinity;
    let madeProgress = false; // Did any move reduce distance?

    const currentTargetDist = manhattanDistance(currentX, currentY, targetX, targetY);

    // --- Knight-Specific Logic (Type 2) ---
    if (pieceType === 2) {
        let potentialMoves = [];
        for (const move of legalMoves) {
            const [toX, toY] = move;
            // Skip moves onto own pieces
            if (teams[toX]?.[toY] === selfId) continue;

            const newDist = manhattanDistance(toX, toY, targetX, targetY);
            potentialMoves.push({ move: move, dist: newDist });
        }

        // Sort potential moves: prioritize smaller distance, then break ties
        potentialMoves.sort((a, b) => {
            if (a.dist !== b.dist) {
                return a.dist - b.dist; // Closer is better
            } else {
                 // Tie-breaker: Prioritize move that reduces the LARGER axis difference more
                 const [ax, ay] = a.move;
                 const [bx, by] = b.move;
                 const aDiffX = Math.abs(ax - targetX);
                 const aDiffY = Math.abs(ay - targetY);
                 const bDiffX = Math.abs(bx - targetX);
                 const bDiffY = Math.abs(by - targetY);
                 const currentDiffX = Math.abs(currentX - targetX);
                 const currentDiffY = Math.abs(currentY - targetY);

                 if (currentDiffX > currentDiffY) { // Further in X direction
                     return aDiffX - bDiffX; // Smaller X difference is better
                 } else if (currentDiffY > currentDiffX) { // Further in Y direction
                     return aDiffY - bDiffY; // Smaller Y difference is better
                 } else {
                     return 0; // No preference if equidistant on axes
                 }
            }
        });

        // Return the best valid move found
        if (potentialMoves.length > 0) {
            bestMove = potentialMoves[0].move;
             // Basic loop avoidance: if best move doesn't decrease distance, maybe pick 2nd best if it does?
             if(potentialMoves.length > 1 && potentialMoves[0].dist >= currentTargetDist && potentialMoves[1].dist < currentTargetDist) {
                bestMove = potentialMoves[1].move;
                // console.log(`Knight: Avoiding non-progress move, taking 2nd best.`);
             }
        }
        return bestMove;

    } else {
        // --- Standard Logic (Non-Knights) ---
        for (const move of legalMoves) {
            const [toX, toY] = move;
            // Skip moves onto own pieces
            if (teams[toX]?.[toY] === selfId) continue;

            const dist = manhattanDistance(toX, toY, targetX, targetY);

            if (dist < minDistance) {
                minDistance = dist;
                bestMove = move;
                if (dist < currentTargetDist) madeProgress = true;
            }
        }
        // If the best move doesn't actually get closer, maybe reconsider?
        // For non-knights, usually direct path is fine, but this could be added if needed.
         // If best move doesn't make progress, but other moves *do*, prefer one that does?
         if(bestMove && !madeProgress) {
            let progressMove = null;
            let progressMinDist = Infinity;
             for (const move of legalMoves) {
                 const [toX, toY] = move;
                 if (teams[toX]?.[toY] === selfId) continue;
                 const dist = manhattanDistance(toX, toY, targetX, targetY);
                 if (dist < currentTargetDist && dist < progressMinDist) { // Found a move that makes progress
                     progressMinDist = dist;
                     progressMove = move;
                 }
             }
             if(progressMove) {
                // console.log(`Non-Knight: Prioritizing progress move over non-progress best move.`);
                bestMove = progressMove;
             }
         }

        return bestMove;
    }
}

/**
 * Checks if moving a high-value piece (Rook/Queen) to a square is acceptably safe.
 * Currently just checks if the destination square is attacked.
 * @param {number} toX Destination X.
 * @param {number} toY Destination Y.
 * @returns {boolean} True if the move is considered safe enough, false otherwise.
 */
function isHunterMoveSafe(toX, toY) {
    // For now, "safe enough" means the destination isn't directly attacked.
    // Could be expanded (e.g., allow if capturing equally valuable piece).
    return !isSquareAttacked(toX, toY);
}

/**
 * Counts the number of Rooks (4) and Queens (5) the player controls.
 * @returns {number} The total count of Rooks and Queens.
 */
function countMyHunterPieces() {
    let count = 0;
    if (typeof selfId === 'undefined' || selfId === -1) return 0;

    for (let x = 0; x < boardW; x++) {
        for (let y = 0; y < boardH; y++) {
            if (teams[x][y] === selfId && (board[x][y] === 4 || board[x][y] === 5)) {
                count++;
            }
        }
    }
    return count;
}

/**
 * Finds the coordinates of the nearest enemy King (piece type 6).
 * @param {number} fromX - The starting X coordinate.
 * @param {number} fromY - The starting Y coordinate.
 * @returns {Array<number> | null} Coordinates [x, y] of the nearest enemy king, or null if none found.
 */
function findNearestEnemyKing(fromX, fromY) {
    let nearestPos = null;
    let minDistance = Infinity;

    for (let x = 0; x < boardW; x++) {
        for (let y = 0; y < boardH; y++) {
            // Check for King (6) that is NOT ours and NOT neutral (0)
            if (board[x][y] === 6 && teams[x][y] !== selfId && teams[x][y] !== 0) {
                const dist = manhattanDistance(fromX, fromY, x, y);
                if (dist < minDistance) {
                    minDistance = dist;
                    nearestPos = [x, y];
                }
            }
        }
    }
    return nearestPos;
}


/**
 * Finds the coordinates of the nearest *prioritized* neutral (Team 0) piece.
 * Priority: Queens/Rooks > Other Pieces. Distance breaks ties within priority.
 * @param {number} fromX - The starting X coordinate.
 * @param {number} fromY - The starting Y coordinate.
 * @returns {Array<number> | null} Coordinates [x, y] of the best neutral target, or null if none found.
 */
function findPrioritizedNearestNeutralPiece(fromX, fromY) {
    let bestHighPriorityTarget = { pos: null, dist: Infinity }; // Rooks (4), Queens (5)
    let bestLowPriorityTarget = { pos: null, dist: Infinity };  // Others (1, 2, 3, 6)

    for (let x = 0; x < boardW; x++) {
        for (let y = 0; y < boardH; y++) {
            // Team 0 is assumed to be neutral/white/unclaimed
            if (teams[x][y] === 0 && board[x][y] !== 0) {
                const pieceType = board[x][y];
                const dist = manhattanDistance(fromX, fromY, x, y);

                // Check if it's a high-priority target (Rook or Queen)
                if (pieceType === 4 || pieceType === 5) {
                    if (dist < bestHighPriorityTarget.dist) {
                        bestHighPriorityTarget.dist = dist;
                        bestHighPriorityTarget.pos = [x, y];
                    }
                } else { // Low priority target
                    if (dist < bestLowPriorityTarget.dist) {
                        bestLowPriorityTarget.dist = dist;
                        bestLowPriorityTarget.pos = [x, y];
                    }
                }
            }
        }
    }

    // Return the high-priority target if found, otherwise the low-priority one
    if (bestHighPriorityTarget.pos) {
        // console.log(`Prioritized Neutral Target: High Prio [${bestHighPriorityTarget.pos}] at dist ${bestHighPriorityTarget.dist}`);
        return bestHighPriorityTarget.pos;
    } else if (bestLowPriorityTarget.pos) {
        // console.log(`Prioritized Neutral Target: Low Prio [${bestLowPriorityTarget.pos}] at dist ${bestLowPriorityTarget.dist}`);
        return bestLowPriorityTarget.pos;
    }

    return null; // No neutral pieces found
}


/**
 * The main logic function for the bot, run periodically.
 */
function basicBotTurnLogic() {
    // --- Pre-computation Checks ---
    if (gameOver || typeof selfId === 'undefined' || selfId === -1 || typeof board === 'undefined' || typeof teams === 'undefined' || typeof generateLegalMoves === 'undefined' || typeof send === 'undefined' || typeof boardW === 'undefined' || typeof curMoveCooldown === 'undefined') {
        return; // Not ready
    }
    if (curMoveCooldown > 220) {
        return; // On cooldown
    }

    let actionTakenThisTurn = false; // Flag to prevent multiple actions
    const pieceMovesCache = {}; // Cache moves for potential reuse

    // --- Priority 1: King Safety ---
    const myKings = findMyKings();
    for (const kingPos of myKings) {
        const [kingX, kingY] = kingPos;
        if (isSquareAttacked(kingX, kingY)) {
            console.log(`BasicBot: King at ${kingX},${kingY} is under attack! Finding safe move.`);
            try {
                const kingMoves = generateLegalMoves(kingX, kingY, board, teams);
                let safeMove = null;
                let desperationMove = null;
                for (const move of kingMoves) { // Find best escape
                    const [toX, toY] = move;
                    if (teams[toX]?.[toY] === selfId) continue;
                    if (!isSquareAttacked(toX, toY)) { safeMove = move; break; }
                    else if (!desperationMove) { desperationMove = move; }
                }
                if (safeMove) { // Execute safe move
                    console.log(`BasicBot: Moving King from ${kingX},${kingY} to SAFE square ${safeMove[0]},${safeMove[1]}.`);
                    send(new Uint16Array([kingX, kingY, safeMove[0], safeMove[1]]));
                    actionTakenThisTurn = true;
                } else if (desperationMove) { // Execute desperation move
                    console.warn(`BasicBot: No safe escape! Moving King from ${kingX},${kingY} to potentially UNSAFE square ${desperationMove[0]},${desperationMove[1]}.`);
                    send(new Uint16Array([kingX, kingY, desperationMove[0], desperationMove[1]]));
                    actionTakenThisTurn = true;
                } else { console.error(`BasicBot: King at ${kingX},${kingY} attacked, NO legal moves!`); }
            } catch (e) { console.error(`BasicBot: Error processing King escape @ ${kingX},${kingY}:`, e); }
            if (actionTakenThisTurn) return; // End turn
        }
    }

    // --- Priority 2: Standard Capture Opportunity ---
    if (actionTakenThisTurn) return;
    for (let x = 0; x < boardW; x++) {
        for (let y = 0; y < boardH; y++) {
            if (teams[x]?.[y] === selfId && board[x][y] !== 0 && board[x][y] !== 6) { // Our piece, not King
                const pieceType = board[x][y];
                try {
                    const legalMoves = pieceMovesCache[`${x},${y}`] || generateLegalMoves(x, y, board, teams);
                    if (!pieceMovesCache[`${x},${y}`]) pieceMovesCache[`${x},${y}`] = legalMoves;

                    for (const move of legalMoves) {
                        const [toX, toY] = move;
                        const targetPiece = board[toX]?.[toY];
                        const targetTeam = teams[toX]?.[toY];

                        // Is it a capture of an enemy piece?
                        if (targetPiece !== 0 && targetTeam !== selfId && targetTeam !== 0) {
                            // ** Hunter Safety Check for Capture **
                            if (pieceType === 4 || pieceType === 5) { // Is the capturing piece a Rook or Queen?
                                if (!isHunterMoveSafe(toX, toY)) {
                                     console.log(`BasicBot: Hunter (${pieceType}) at ${x},${y} avoiding UNSAFE capture at ${toX},${toY}.`);
                                     continue; // Skip this specific capture attempt
                                }
                            }

                            // If safe (or not a hunter), proceed with capture
                            console.log(`BasicBot: Capture! Moving ${pieceType} from ${x},${y} to ${toX},${toY}`);
                            send(new Uint16Array([x, y, toX, toY]));
                            actionTakenThisTurn = true;
                            break; // Exit inner loop (capture found)
                        }
                    }
                } catch (e) { /* Handle error */ }
            }
            if (actionTakenThisTurn) break; // Exit outer loop
        }
        if (actionTakenThisTurn) break; // Exit loops if standard capture made
    }
    if (actionTakenThisTurn) return;


    // --- Priority 3: Lone King Moves Towards Prioritized Neutral (If Safe) ---
    if (actionTakenThisTurn) return;
    const onlyKingLeft = isOnlyKingLeft();
    if (onlyKingLeft && myKings.length > 0) {
        const [kingX, kingY] = myKings[0];
        if (!isSquareAttacked(kingX, kingY)) { // Only if King is currently safe
            const nearestNeutral = findPrioritizedNearestNeutralPiece(kingX, kingY);
            if (nearestNeutral) {
                const [targetX, targetY] = nearestNeutral;
                const targetType = board[targetX]?.[targetY] || '?';
                try {
                    const kingMoves = pieceMovesCache[`${kingX},${kingY}`] || generateLegalMoves(kingX, kingY, board, teams);
                    // Use the modified findBestMoveTowards (though King is not type 2)
                    const bestMove = findBestMoveTowards(6, kingX, kingY, kingMoves, targetX, targetY);
                    if (bestMove) {
                        const [toX, toY] = bestMove;
                        if (!isSquareAttacked(toX, toY)) { // Check destination safety
                            console.log(`BasicBot: Lone King moving from ${kingX},${kingY} to SAFE square ${toX},${toY} towards neutral type ${targetType}.`);
                            send(new Uint16Array([kingX, kingY, toX, toY]));
                            actionTakenThisTurn = true;
                        } else { /* console.log(`BasicBot: Lone King best move to ${toX},${toY} is UNSAFE. Holding.`); */ }
                    }
                } catch (e) { console.error(`BasicBot: Error processing Lone King move towards neutral:`, e); }
            }
        }
    }
    if (actionTakenThisTurn) return;

    // --- Priority 4: Idle Pieces Move Towards Prioritized Neutral ---
    if (actionTakenThisTurn) return;
    if (onlyKingLeft) return; // Don't run if only king left

    for (let x = 0; x < boardW; x++) {
        for (let y = 0; y < boardH; y++) {
            if (teams[x]?.[y] === selfId && board[x][y] !== 0 && board[x][y] !== 6) { // Our piece, not King
                 const pieceType = board[x][y];
                try {
                    const legalMoves = pieceMovesCache[`${x},${y}`] || generateLegalMoves(x, y, board, teams);
                    if (!pieceMovesCache[`${x},${y}`]) pieceMovesCache[`${x},${y}`] = legalMoves;

                    if (isPieceIdle(x, y, legalMoves)) { // Check if idle first
                        const nearestNeutral = findPrioritizedNearestNeutralPiece(x, y);
                        if (nearestNeutral) {
                            const [targetX, targetY] = nearestNeutral;
                            const targetType = board[targetX]?.[targetY] || '?';
                            // Use the modified findBestMoveTowards
                            const bestMove = findBestMoveTowards(pieceType, x, y, legalMoves, targetX, targetY);
                            if (bestMove) {
                                const [toX, toY] = bestMove;

                                // ** Hunter Safety Check for Idle Move **
                                if (pieceType === 4 || pieceType === 5) { // Is the moving piece a Rook or Queen?
                                    if (!isHunterMoveSafe(toX, toY)) {
                                        console.log(`BasicBot: Hunter (${pieceType}) at ${x},${y} avoiding move to UNSAFE idle target square ${toX},${toY}.`);
                                        continue; // Skip moving this piece this turn, maybe another idle piece can move.
                                    }
                                }

                                // If safe (or not a hunter), proceed with move
                                console.log(`BasicBot: Moving idle piece ${pieceType} from ${x},${y} to ${toX},${toY} towards neutral type ${targetType}.`);
                                send(new Uint16Array([x, y, toX, toY]));
                                actionTakenThisTurn = true;
                                break; // Exit inner loop (idle piece moved)
                            }
                        }
                    }
                } catch (e) { /* Handle error */ }
            }
            if (actionTakenThisTurn) break; // Exit outer loop
        }
        if (actionTakenThisTurn) break; // Exit loops if idle piece moved
    }

    // if (!actionTakenThisTurn) console.log("BasicBot: No actions taken this cycle.");
}


// --- startBasicBot and stopBasicBot functions remain the same ---

/**
 * Starts the bot's periodic execution.
 */
function startBasicBot(intervalMs = 300) {
    if (basicBotInterval) { console.log("BasicBot: Already running."); return; }
    if (typeof selfId === 'undefined' || selfId === -1) {
         console.warn("BasicBot: Cannot start, game not fully initialized (selfId unknown). Try again shortly.");
         setTimeout(() => startBasicBot(intervalMs), 2000); return;
    }
    console.log(`BasicBot: Starting bot with ${intervalMs}ms interval. R/Q safety enabled.`);
    if (basicBotInterval) clearInterval(basicBotInterval);
    basicBotInterval = setInterval(basicBotTurnLogic, intervalMs);
}

/**
 * Stops the bot's periodic execution.
 */
function stopBasicBot() {
    if (basicBotInterval) { console.log("BasicBot: Stopping bot."); clearInterval(basicBotInterval); basicBotInterval = null; }
    else { console.log("BasicBot: Bot is not running."); }
}

startBasicBot()