Highlight squares

14/04/2025, 16:51:53

// ==UserScript==
// @name        Highlight squares
// @namespace   Violentmonkey Scripts
// @match       https://chess.ytdraws.win/*
// @grant       none
// @version     1.0
// @author      -
// @description 14/04/2025, 16:51:53
// @license CC-NC-SA
// ==/UserScript==
// == Pathfinding & Attack Visualization Script ==

(function() { // Encapsulate to avoid polluting global scope too much

    // --- Configuration ---
    const ATTACKED_SQUARE_COLOR = 'rgba(255, 0, 0, 0.25)'; // Red, semi-transparent
    const PATH_COLOR = 'rgba(0, 0, 255, 0.3)'; // Blue, semi-transparent
    const PATH_STEP_INTERVAL_MS = 50; // How often to check if we can take the next step (doesn't override game cooldown)
    const MOVE_COOLDOWN_THRESHOLD = 220; // From original game code, ms

    // --- State Variables ---
    let visualizeAttacksEnabled = false;
    let attackedSquares = new Set(); // Stores "x,y" strings of attacked squares
    let pathfindingData = {
        active: false,
        pieceType: 0,
        startX: -1,
        startY: -1,
        targetX: -1,
        targetY: -1,
        currentPath: [], // Array of [x, y] steps
        pathIndex: 0,
        intervalId: null
    };

    // --- Attack Calculation ---

    /**
     * Calculates all squares attacked by enemy pieces.
     * Assumes generateLegalMoves works correctly for all piece types.
     * @returns {Set<string>} A set of "x,y" strings representing attacked squares.
     */
    function calculateAllEnemyAttacks() {
        const attacked = new Set();
        if (typeof board === 'undefined' || typeof teams === 'undefined' || typeof selfId === 'undefined' || typeof generateLegalMoves !== 'function') {
            return attacked;
        }

        for (let x = 0; x < boardW; x++) {
            for (let y = 0; y < boardH; y++) {
                const team = teams[x]?.[y];
                const piece = board[x]?.[y];
                // Check if it's an enemy piece (not ours, not neutral, not empty)
                if (piece && team && team !== selfId && team !== 0) {
                    try {
                        const moves = generateLegalMoves(x, y, board, teams);
                        for (const move of moves) {
                            attacked.add(`${move[0]},${move[1]}`);
                        }
                    } catch (e) {
                        // console.warn(`Error generating moves for enemy at ${x},${y}:`, e);
                    }
                }
            }
        }
        return attacked;
    }

    // --- Pathfinding (BFS) ---

    /**
     * Finds the shortest path using Breadth-First Search.
     * @param {number} startX
     * @param {number} startY
     * @param {number} targetX
     * @param {number} targetY
     * @param {number} pieceType The type of piece trying to move (for generateLegalMoves)
     * @returns {Array<Array<number>> | null} Path as array of [x,y] steps, or null if no path.
     */
    function findPathBFS(startX, startY, targetX, targetY, pieceType) {
        console.log(`Pathfinding: Type ${pieceType} from ${startX},${startY} to ${targetX},${targetY}`);
        if (typeof board === 'undefined' || typeof teams === 'undefined' || typeof selfId === 'undefined' || typeof generateLegalMoves !== 'function') {
            console.error("Pathfinding failed: Missing game variables or functions.");
            return null;
        }

        const queue = [[startX, startY]];
        const visited = new Set([`${startX},${startY}`]);
        const parentMap = {}; // Store "childX,childY": [parentX, parentY]

        while (queue.length > 0) {
            const [currX, currY] = queue.shift();

            // Target found?
            if (currX === targetX && currY === targetY) {
                // Reconstruct path
                const path = [];
                let step = [targetX, targetY];
                while (step[0] !== startX || step[1] !== startY) {
                    path.push(step);
                    const parentKey = `${step[0]},${step[1]}`;
                    if (!parentMap[parentKey]) break; // Should not happen if target found
                    step = parentMap[parentKey];
                }
                path.push([startX, startY]); // Add start
                console.log("Path found:", path.reverse()); // Reverse to get start -> end
                return path;
            }

            // Get legal moves *as if* the piece were at currX, currY
            // We need a way to call generateLegalMoves assuming the piece *type*
            // is at currX, currY, temporarily ignoring the actual board state there
            // for planning purposes, *except* for blocking friendly pieces.
            // This requires modifying generateLegalMoves or having a variant.
            // --- SIMPLIFICATION for this example: ---
            // We'll call generateLegalMoves normally. This means it might fail if
            // the intermediate square is occupied inappropriately for the real piece.
            // A more robust solution needs a planning-specific move generator.
            // We also *simulate* the piece being there to check moves.
            const originalPiece = board[currX]?.[currY];
            const originalTeam = teams[currX]?.[currY];
            board[currX][currY] = pieceType; // Temporarily place piece for move generation
            teams[currX][currY] = selfId;
            let legalMoves = [];
            try {
                 legalMoves = generateLegalMoves(currX, currY, board, teams);
            } catch (e) { /* handle error */ }
            // Restore original board state
            board[currX][currY] = originalPiece === undefined ? 0 : originalPiece;
            teams[currX][currY] = originalTeam === undefined ? 0 : originalTeam;
            // --- End Simplification ---


            for (const move of legalMoves) {
                const [nextX, nextY] = move;
                const nextKey = `${nextX},${nextY}`;

                // Check bounds and if already visited
                if (nextX < 0 || nextX >= boardW || nextY < 0 || nextY >= boardH || visited.has(nextKey)) {
                    continue;
                }

                // Check if destination is blocked by a friendly piece (crucial!)
                // Allow moving *to* the target square even if occupied (capture/overwrite)
                if (teams[nextX]?.[nextY] === selfId && !(nextX === targetX && nextY === targetY)) {
                    continue;
                }

                // Mark visited, store parent, enqueue
                visited.add(nextKey);
                parentMap[nextKey] = [currX, currY];
                queue.push([nextX, nextY]);
            }
        }

        console.log("Pathfinding failed: No path found.");
        return null; // No path found
    }

    // --- Path Execution ---

    function executePathStep() {
        if (!pathfindingData.active || typeof send !== 'function' || typeof curMoveCooldown === 'undefined') {
            stopPathfinding();
            return;
        }

        // Check game cooldown
        if (curMoveCooldown > MOVE_COOLDOWN_THRESHOLD) {
            return; // Wait for cooldown
        }

        // Check if path is complete
        if (pathfindingData.pathIndex >= pathfindingData.currentPath.length - 1) {
            console.log("Path complete.");
            stopPathfinding();
            return;
        }

        const currentStep = pathfindingData.currentPath[pathfindingData.pathIndex];
        const nextStep = pathfindingData.currentPath[pathfindingData.pathIndex + 1];

        console.log(`Executing path step: [${currentStep[0]}, ${currentStep[1]}] -> [${nextStep[0]}, ${nextStep[1]}]`);

        try {
            // Send the move for the *next* step
            const buf = new Uint16Array(4);
            buf[0] = currentStep[0]; // From
            buf[1] = currentStep[1];
            buf[2] = nextStep[0];   // To
            buf[3] = nextStep[1];
            send(buf);

            // Advance path index (will be used in next interval check)
            pathfindingData.pathIndex++;
             // We assume the 'send' action triggers the game's cooldown mechanism.
             // Our check of `curMoveCooldown` at the start handles waiting.

        } catch (e) {
            console.error("Error sending path step move:", e);
            stopPathfinding();
        }
    }

    function startPathfinding(targetX, targetY) {
        stopPathfinding(); // Stop any previous path

        if (typeof selectedSquareX === 'undefined' || typeof selectedSquareY === 'undefined') {
            console.log("Pathfinding: No piece selected.");
            return;
        }
        if (selectedSquareX === targetX && selectedSquareY === targetY) {
            console.log("Pathfinding: Target is the same as start.");
            return;
        }
         const pieceType = board[selectedSquareX]?.[selectedSquareY];
         if (!pieceType) {
             console.error("Pathfinding: Cannot determine selected piece type.");
             return;
         }


        const path = findPathBFS(selectedSquareX, selectedSquareY, targetX, targetY, pieceType);

        if (path && path.length > 1) { // Need at least start and one step
            pathfindingData = {
                active: true,
                pieceType: pieceType,
                startX: selectedSquareX,
                startY: selectedSquareY,
                targetX: targetX,
                targetY: targetY,
                currentPath: path,
                pathIndex: 0, // Start at the beginning of the path
                intervalId: setInterval(executePathStep, PATH_STEP_INTERVAL_MS)
            };
            console.log("Pathfinding started.");
            // Deselect piece visually maybe? Or keep selected? User choice.
            // selectedSquareX = selectedSquareY = undefined; // Optional deselect
        } else {
            console.log("Pathfinding: No valid path found or path too short.");
            // Optionally provide user feedback (e.g., flash screen red)
        }
    }

    function stopPathfinding() {
        if (pathfindingData.intervalId) {
            clearInterval(pathfindingData.intervalId);
        }
        pathfindingData = { active: false, intervalId: null, currentPath: [], pathIndex: 0 }; // Reset state
        console.log("Pathfinding stopped.");
    }

    // --- Drawing ---

    function drawAttackedSquares() {
        if (!visualizeAttacksEnabled || typeof ctx === 'undefined' || typeof camera === 'undefined') return;

        // Recalculate attacks (can be optimized)
        attackedSquares = calculateAllEnemyAttacks();

        if (attackedSquares.size === 0) return;

        const originalTransform = ctx.getTransform(); // Save original transform
        ctx.translate(canvas.width / 2, canvas.height / 2);
        ctx.scale(camera.scale, camera.scale);
        ctx.translate(camera.x, camera.y);

        ctx.fillStyle = ATTACKED_SQUARE_COLOR;

        // Get visible bounds (using canvasPos - slightly adapted)
        const topLeftView = canvasPos ? canvasPos({ x: 0, y: 0 }) : { x: -camera.x * camera.scale, y: -camera.y * camera.scale }; // Fallback if canvasPos missing
        const bottomRightView = canvasPos ? canvasPos({ x: innerWidth, y: innerHeight }) : { x: (innerWidth / camera.scale) - camera.x, y: (innerHeight / camera.scale) - camera.y }; // Fallback

        const startCol = Math.max(0, Math.floor(topLeftView.x / squareSize) - 1);
        const endCol = Math.min(boardW, Math.ceil(bottomRightView.x / squareSize) + 1);
        const startRow = Math.max(0, Math.floor(topLeftView.y / squareSize) - 1);
        const endRow = Math.min(boardH, Math.ceil(bottomRightView.y / squareSize) + 1);


        attackedSquares.forEach(key => {
            const [x, y] = key.split(',').map(Number);
            // Only draw if within rough view bounds
            if (x >= startCol && x < endCol && y >= startRow && y < endRow) {
                 ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
            }
        });

        ctx.setTransform(originalTransform); // Restore original transform
    }

     function drawCurrentPath() {
        if (!pathfindingData.active || pathfindingData.currentPath.length === 0 || typeof ctx === 'undefined' || typeof camera === 'undefined') return;

        const originalTransform = ctx.getTransform();
        ctx.translate(canvas.width / 2, canvas.height / 2);
        ctx.scale(camera.scale, camera.scale);
        ctx.translate(camera.x, camera.y);

        ctx.fillStyle = PATH_COLOR;
        ctx.strokeStyle = 'rgba(0, 0, 150, 0.5)';
        ctx.lineWidth = 3 / camera.scale; // Make line width scale invariant

        // Draw path lines/squares
        for (let i = pathfindingData.pathIndex; i < pathfindingData.currentPath.length; i++) {
             const [x, y] = pathfindingData.currentPath[i];
             // Draw square highlight
             ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);

             // Draw line to next segment (optional)
             if (i < pathfindingData.currentPath.length - 1) {
                const [nextX, nextY] = pathfindingData.currentPath[i+1];
                ctx.beginPath();
                ctx.moveTo(x * squareSize + squareSize / 2, y * squareSize + squareSize / 2);
                ctx.lineTo(nextX * squareSize + squareSize / 2, nextY * squareSize + squareSize / 2);
                ctx.stroke();
             }
        }
        ctx.setTransform(originalTransform);
    }


    // --- Integration with Game Loop & Events ---

    // Monkey-patch the render function (use MutationObserver if render is complex/obfuscated)
    if (typeof render === 'function') {
        const originalRender = render;
        window.render = function(...args) {
            originalRender.apply(this, args); // Call original render first
            // Add our drawing functions
            try {
                drawAttackedSquares();
                drawCurrentPath();
            } catch (e) {
                 console.error("Error in visualization drawing:", e);
                 // Stop visuals if they error out?
                 visualizeAttacksEnabled = false;
                 stopPathfinding();
            }
        }
        console.log("Visualization hooks added to render function.");
    } else {
        console.error("Could not find global 'render' function to hook into.");
    }

    // Monkey-patch mousedown (or add event listener if preferred)
     if (typeof onmousedown === 'function') {
         const originalMouseDown = onmousedown;
         window.onmousedown = function(e) {
             // Stop any ongoing pathfinding if user clicks anywhere
             if (pathfindingData.active) {
                  console.log("User click interrupted pathfinding.");
                  stopPathfinding();
                  // Let the original handler decide if a new selection/move happens
             }

            // Calculate potential target square BEFORE calling original handler
             let targetX, targetY;
             let mousePosDown; // Store mouse pos for pathfinding check
             try {
                 const t = ctx.getTransform(); // Need context for transform
                 ctx.translate(canvas.width/2, canvas.height/2);
                 ctx.scale(camera.scale, camera.scale);
                 ctx.translate(camera.x, camera.y);
                 mousePosDown = canvasPos({ x: e.clientX, y: e.clientY }); // Use clientX/Y
                 ctx.setTransform(t); // Restore immediately
                 targetX = Math.floor(mousePosDown.x / squareSize);
                 targetY = Math.floor(mousePosDown.y / squareSize);
             } catch (err) {
                // console.error("Could not calculate target square on mousedown:", err);
                originalMouseDown.call(this, e); // Still call original
                return;
            }

            // --- Pathfinding Check ---
            let isImmediateLegalMove = false;
            if (typeof selectedSquareX !== 'undefined' && typeof selectedSquareY !== 'undefined' && typeof legalMoves !== 'undefined' && Array.isArray(legalMoves)) {
                for (let i = 0; i < legalMoves.length; i++) {
                    if (legalMoves[i][0] === targetX && legalMoves[i][1] === targetY) {
                        isImmediateLegalMove = true;
                        break;
                    }
                }
            }

            // Call the original handler *first* to handle selection/deselection/immediate moves
            originalMouseDown.call(this, e);

            // --- Initiate Pathfinding AFTER original handler ---
            // Check if:
            // 1. A piece *is still* selected (original handler didn't deselect or move successfully).
            // 2. The click was NOT an immediate legal move for the originally selected piece.
            // 3. The click is within board bounds.
            // 4. Pathfinding isn't already active (should have been stopped above, but double check).
            if (typeof selectedSquareX !== 'undefined' && typeof selectedSquareY !== 'undefined' &&
                !isImmediateLegalMove &&
                targetX >= 0 && targetX < boardW && targetY >= 0 && targetY < boardH &&
                !pathfindingData.active)
            {
                // Check cooldown just before starting pathfinding
                if (curMoveCooldown <= MOVE_COOLDOWN_THRESHOLD) {
                    console.log("Initiating pathfinding to", targetX, targetY);
                    startPathfinding(targetX, targetY);
                } else {
                    console.log("Cannot start pathfinding, move cooldown active.");
                }
            }
         }
         console.log("Pathfinding hook added to onmousedown function.");
     } else {
        console.error("Could not find global 'onmousedown' function to hook into.");
     }


    // --- Global Control Functions ---
    window.toggleAttackVisualization = function(enable = !visualizeAttacksEnabled) {
        visualizeAttacksEnabled = enable;
        console.log("Attack Visualization " + (visualizeAttacksEnabled ? "Enabled" : "Disabled"));
        if (!visualizeAttacksEnabled) {
            attackedSquares.clear(); // Clear stored squares when disabled
            // Request a redraw if possible (difficult without direct access to game loop flags)
        }
    }

    window.cancelCurrentPath = function() {
        stopPathfinding();
    }

    console.log("Pathfinding and Attack Visualization script loaded.");
    console.log("Use toggleAttackVisualization() to turn red squares on/off.");
    console.log("Click an invalid square after selecting a piece to pathfind.");
    console.log("Use cancelCurrentPath() to stop active pathfinding.");
    toggleAttackVisualization()

})(); // End IIFE