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