Lichess Auto Chess Player

Pro-level chess autoplay with Stockfish engine and human-like behavior

Size

14.6 KB

Version

2.1.3

Created

Feb 12, 2026

Updated

21 days ago

1// ==UserScript==
2// @name		Lichess Auto Chess Player
3// @description		Pro-level chess autoplay with Stockfish engine and human-like behavior
4// @version		2.1.3
5// @match		https://*.lichess.org/*
6// @icon		https://lichess1.org/assets/logo/lichess-favicon-32.png
7// @require		https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js
8// @require		https://cdn.jsdelivr.net/npm/stockfish@17.1.0/stockfish.js
9// @grant		GM.getValue
10// @grant		GM.setValue
11// ==/UserScript==
12(function() {
13    'use strict';
14
15    console.log('Lichess Pro Auto Chess Player loaded');
16
17    // Configuration
18    const CONFIG = {
19        enabled: true,
20        minThinkTime: 3000,  // Minimum thinking time in ms (3 seconds)
21        maxThinkTime: 10000, // Maximum thinking time in ms (10 seconds)
22        moveVariation: 0.4,  // 40% variation in move timing
23        enableMouseMovement: true,
24        stockfishDepth: 8,  // Analysis depth (higher = stronger, 18 is pro level)
25        skillLevel: 8,      // Stockfish skill level (0-20, 20 is maximum)
26    };
27
28    // Chess game instance and Stockfish engine
29    let chessGame = null;
30    let stockfishEngine = null;
31    let isAutoplayActive = false;
32    let isMyTurn = false;
33    let playerColor = null;
34    let engineReady = false;
35
36    // Initialize Stockfish engine
37    function initStockfish() {
38        return new Promise((resolve) => {
39            console.log('Initializing Stockfish engine...');
40            
41            if (typeof STOCKFISH === 'function') {
42                stockfishEngine = STOCKFISH();
43                
44                stockfishEngine.onmessage = function(event) {
45                    const message = event.data || event;
46                    console.log('Stockfish:', message);
47                    
48                    if (message === 'uciok') {
49                        engineReady = true;
50                        console.log('Stockfish engine ready!');
51                        resolve();
52                    }
53                };
54                
55                // Initialize UCI protocol
56                stockfishEngine.postMessage('uci');
57                stockfishEngine.postMessage(`setoption name Skill Level value ${CONFIG.skillLevel}`);
58                stockfishEngine.postMessage('setoption name MultiPV value 1');
59            } else {
60                console.error('Stockfish not available');
61                resolve();
62            }
63        });
64    }
65
66    // Utility: Random delay with human-like variation
67    function getHumanDelay() {
68        const base = CONFIG.minThinkTime + Math.random() * (CONFIG.maxThinkTime - CONFIG.minThinkTime);
69        const variation = base * CONFIG.moveVariation * (Math.random() - 0.5);
70        return Math.floor(base + variation);
71    }
72
73    // Utility: Sleep function
74    function sleep(ms) {
75        return new Promise(resolve => setTimeout(resolve, ms));
76    }
77
78    // Utility: Simulate human-like mouse movement
79    function simulateMouseMovement(element) {
80        if (!CONFIG.enableMouseMovement || !element) return;
81        
82        const rect = element.getBoundingClientRect();
83        const x = rect.left + rect.width / 2 + (Math.random() - 0.5) * 10;
84        const y = rect.top + rect.height / 2 + (Math.random() - 0.5) * 10;
85        
86        const mouseoverEvent = new MouseEvent('mouseover', {
87            bubbles: true,
88            cancelable: true,
89            clientX: x,
90            clientY: y
91        });
92        element.dispatchEvent(mouseoverEvent);
93    }
94
95    // Get current board state from Lichess
96    function getBoardState() {
97        try {
98            // Try to get FEN from the page
99            const fenElement = document.querySelector('[data-fen]');
100            if (fenElement) {
101                return fenElement.getAttribute('data-fen');
102            }
103
104            // Alternative: Try to extract from the board element
105            const mainBoard = document.querySelector('main.round');
106            if (mainBoard && mainBoard.dataset && mainBoard.dataset.fen) {
107                return mainBoard.dataset.fen;
108            }
109
110            // Try to get from Lichess global object
111            if (window.lichess && window.lichess.analysis) {
112                return window.lichess.analysis.node.fen;
113            }
114
115            return null;
116        } catch (error) {
117            console.error('Error getting board state:', error);
118            return null;
119        }
120    }
121
122    // Check if it's our turn
123    function checkIfMyTurn() {
124        try {
125            // Check which clock is running
126            const myColorClass = playerColor === 'white' ? '.rclock-white' : '.rclock-black';
127            const myClockElement = document.querySelector(myColorClass);
128            
129            if (myClockElement && myClockElement.classList.contains('running')) {
130                return true;
131            }
132
133            return false;
134        } catch (error) {
135            console.error('Error checking turn:', error);
136            return false;
137        }
138    }
139
140    // Determine player color
141    function determinePlayerColor() {
142        try {
143            const board = document.querySelector('.cg-wrap');
144            if (!board) return null;
145
146            if (board.classList.contains('orientation-white')) {
147                return 'white';
148            } else if (board.classList.contains('orientation-black')) {
149                return 'black';
150            }
151
152            return null;
153        } catch (error) {
154            console.error('Error determining color:', error);
155            return null;
156        }
157    }
158
159    // Calculate best move using Stockfish
160    function calculateBestMoveWithStockfish(fen) {
161        return new Promise((resolve) => {
162            if (!stockfishEngine || !engineReady) {
163                console.error('Stockfish engine not ready');
164                resolve(null);
165                return;
166            }
167
168            let bestMove = null;
169            
170            const messageHandler = function(event) {
171                const message = event.data || event;
172                
173                // Look for bestmove in the output
174                if (typeof message === 'string' && message.startsWith('bestmove')) {
175                    const parts = message.split(' ');
176                    if (parts.length >= 2) {
177                        bestMove = parts[1];
178                        console.log('Stockfish best move:', bestMove);
179                        stockfishEngine.onmessage = null;
180                        resolve(bestMove);
181                    }
182                }
183            };
184            
185            stockfishEngine.onmessage = messageHandler;
186            
187            // Send position and calculate
188            stockfishEngine.postMessage('ucinewgame');
189            stockfishEngine.postMessage(`position fen ${fen}`);
190            stockfishEngine.postMessage(`go depth ${CONFIG.stockfishDepth}`);
191            
192            // Timeout fallback
193            setTimeout(() => {
194                if (!bestMove) {
195                    console.log('Stockfish timeout, using fallback');
196                    stockfishEngine.onmessage = null;
197                    resolve(null);
198                }
199            }, 15000);
200        });
201    }
202
203    // Parse UCI move format (e.g., "e2e4") to chess.js format
204    function parseUCIMove(uciMove, fen) {
205        if (!uciMove || uciMove.length < 4) return null;
206        
207        try {
208            if (!chessGame) {
209                chessGame = new Chess(fen);
210            } else {
211                chessGame.load(fen);
212            }
213            
214            const from = uciMove.substring(0, 2);
215            const to = uciMove.substring(2, 4);
216            const promotion = uciMove.length > 4 ? uciMove[4] : undefined;
217            
218            const move = chessGame.move({
219                from: from,
220                to: to,
221                promotion: promotion
222            });
223            
224            chessGame.undo();
225            return move;
226        } catch (error) {
227            console.error('Error parsing UCI move:', error);
228            return null;
229        }
230    }
231
232    // Make a move on the board
233    async function makeMove(move) {
234        try {
235            console.log('Making move:', move.from, '->', move.to);
236
237            // Find the source square
238            const fromSquare = document.querySelector(`cg-board square[data-key="${move.from}"]`);
239            if (!fromSquare) {
240                console.error('Source square not found:', move.from);
241                return false;
242            }
243
244            // Simulate mouse movement to source
245            await sleep(100 + Math.random() * 200);
246            simulateMouseMovement(fromSquare);
247            await sleep(50 + Math.random() * 100);
248
249            // Click source square
250            fromSquare.click();
251            console.log('Clicked source square:', move.from);
252            
253            await sleep(200 + Math.random() * 300);
254
255            // Find the destination square
256            const toSquare = document.querySelector(`cg-board square[data-key="${move.to}"]`);
257            if (!toSquare) {
258                console.error('Destination square not found:', move.to);
259                return false;
260            }
261
262            // Simulate mouse movement to destination
263            simulateMouseMovement(toSquare);
264            await sleep(50 + Math.random() * 100);
265
266            // Click destination square
267            toSquare.click();
268            console.log('Clicked destination square:', move.to);
269
270            // Handle promotion if needed
271            if (move.promotion) {
272                await sleep(300);
273                const promotionPiece = document.querySelector(`[data-role="promotion"] piece[data-piece="${move.promotion}"]`);
274                if (promotionPiece) {
275                    promotionPiece.click();
276                    console.log('Selected promotion piece:', move.promotion);
277                }
278            }
279
280            return true;
281        } catch (error) {
282            console.error('Error making move:', error);
283            return false;
284        }
285    }
286
287    // Main autoplay loop
288    async function autoplayLoop() {
289        if (!isAutoplayActive) return;
290
291        try {
292            // Check if we're in an active game
293            const gameElement = document.querySelector('main.round');
294            if (!gameElement) {
295                console.log('No active game detected');
296                setTimeout(autoplayLoop, 2000);
297                return;
298            }
299
300            // Determine player color if not set
301            if (!playerColor) {
302                playerColor = determinePlayerColor();
303                console.log('Player color:', playerColor);
304            }
305
306            // Check if it's our turn
307            isMyTurn = checkIfMyTurn();
308            
309            if (!isMyTurn) {
310                console.log('Waiting for our turn...');
311                setTimeout(autoplayLoop, 1000);
312                return;
313            }
314
315            console.log('It\'s our turn! Calculating move...');
316
317            // Get current board state
318            const fen = getBoardState();
319            if (!fen) {
320                console.error('Could not get board state');
321                setTimeout(autoplayLoop, 2000);
322                return;
323            }
324
325            console.log('Current FEN:', fen);
326
327            // Calculate best move using Stockfish
328            const uciMove = await calculateBestMoveWithStockfish(fen);
329            if (!uciMove) {
330                console.log('No move calculated by Stockfish');
331                setTimeout(autoplayLoop, 2000);
332                return;
333            }
334
335            const bestMove = parseUCIMove(uciMove, fen);
336            if (!bestMove) {
337                console.log('Could not parse move');
338                setTimeout(autoplayLoop, 2000);
339                return;
340            }
341
342            // Wait with human-like delay
343            const thinkTime = getHumanDelay();
344            console.log(`Thinking for ${thinkTime}ms...`);
345            await sleep(thinkTime);
346
347            // Make the move
348            const moveSuccess = await makeMove(bestMove);
349            
350            if (moveSuccess) {
351                console.log('Move executed successfully');
352                isMyTurn = false;
353            } else {
354                console.error('Failed to execute move');
355            }
356
357            // Continue the loop
358            setTimeout(autoplayLoop, 1500);
359
360        } catch (error) {
361            console.error('Error in autoplay loop:', error);
362            setTimeout(autoplayLoop, 2000);
363        }
364    }
365
366    // Create UI toggle button
367    function createToggleButton() {
368        const button = document.createElement('button');
369        button.id = 'autoplay-toggle';
370        button.textContent = '🤖 Pro Auto: ON';
371        button.style.cssText = `
372            position: fixed;
373            top: 10px;
374            right: 10px;
375            z-index: 10000;
376            padding: 10px 15px;
377            background: #3893E8;
378            color: white;
379            border: none;
380            border-radius: 5px;
381            font-weight: bold;
382            cursor: pointer;
383            box-shadow: 0 2px 5px rgba(0,0,0,0.3);
384            font-size: 14px;
385        `;
386
387        button.addEventListener('click', () => {
388            isAutoplayActive = !isAutoplayActive;
389            button.textContent = isAutoplayActive ? '🤖 Pro Auto: ON' : '🤖 Pro Auto: OFF';
390            button.style.background = isAutoplayActive ? '#3893E8' : '#666';
391            
392            if (isAutoplayActive) {
393                console.log('Pro Autoplay enabled');
394                autoplayLoop();
395            } else {
396                console.log('Pro Autoplay disabled');
397            }
398        });
399
400        document.body.appendChild(button);
401        console.log('Toggle button created');
402    }
403
404    // Initialize the extension
405    async function init() {
406        console.log('Initializing Lichess Pro Auto Chess Player...');
407        
408        // Wait for page to be ready
409        if (document.readyState === 'loading') {
410            document.addEventListener('DOMContentLoaded', init);
411            return;
412        }
413
414        // Wait for body to be available
415        if (!document.body) {
416            setTimeout(init, 100);
417            return;
418        }
419
420        // Initialize Stockfish engine
421        await initStockfish();
422
423        // Create toggle button
424        try {
425            createToggleButton();
426        } catch (error) {
427            console.error('Error creating toggle button:', error);
428        }
429
430        // Start autoplay if enabled
431        if (CONFIG.enabled) {
432            isAutoplayActive = true;
433            setTimeout(autoplayLoop, 2000);
434        }
435
436        console.log('Lichess Pro Auto Chess Player initialized');
437    }
438
439    // Start the extension
440    if (document.readyState === 'loading') {
441        document.addEventListener('DOMContentLoaded', init);
442    } else {
443        init();
444    }
445
446})();