// ==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