Pro-level chess autoplay with Stockfish engine and human-like behavior
Size
15.3 KB
Version
2.1.1
Created
Nov 2, 2025
Updated
13 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.1
5// @match https://*.lichess.org/*
6// @icon https://lichess1.org/assets/logo/lichess-favicon-32.png
7// @require https://cdn.jsdelivr.net/npm/stockfish@16.0.0/src/stockfish.js
8// @require https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.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: 18, // Analysis depth (higher = stronger, 18 is pro level)
25 skillLevel: 20, // 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 const turnIndicator = document.querySelector('.rclock-turn');
126 if (!turnIndicator) return false;
127
128 const myColorClass = playerColor === 'white' ? '.rclock-bottom' : '.rclock-top';
129 const myClockElement = document.querySelector(myColorClass);
130
131 if (myClockElement && myClockElement.classList.contains('rclock-turn')) {
132 return true;
133 }
134
135 // Alternative check: Look for the turn class on the board
136 const board = document.querySelector('cg-board');
137 if (board && board.parentElement) {
138 const wrapper = board.closest('.cg-wrap');
139 if (wrapper) {
140 const orientation = wrapper.classList.contains('orientation-white') ? 'white' : 'black';
141 const turnElement = document.querySelector('.turn');
142 if (turnElement) {
143 const turnText = turnElement.textContent.toLowerCase();
144 return turnText.includes(orientation);
145 }
146 }
147 }
148
149 return false;
150 } catch (error) {
151 console.error('Error checking turn:', error);
152 return false;
153 }
154 }
155
156 // Determine player color
157 function determinePlayerColor() {
158 try {
159 const board = document.querySelector('.cg-wrap');
160 if (!board) return null;
161
162 if (board.classList.contains('orientation-white')) {
163 return 'white';
164 } else if (board.classList.contains('orientation-black')) {
165 return 'black';
166 }
167
168 return null;
169 } catch (error) {
170 console.error('Error determining color:', error);
171 return null;
172 }
173 }
174
175 // Calculate best move using Stockfish
176 function calculateBestMoveWithStockfish(fen) {
177 return new Promise((resolve) => {
178 if (!stockfishEngine || !engineReady) {
179 console.error('Stockfish engine not ready');
180 resolve(null);
181 return;
182 }
183
184 let bestMove = null;
185
186 const messageHandler = function(event) {
187 const message = event.data || event;
188
189 // Look for bestmove in the output
190 if (typeof message === 'string' && message.startsWith('bestmove')) {
191 const parts = message.split(' ');
192 if (parts.length >= 2) {
193 bestMove = parts[1];
194 console.log('Stockfish best move:', bestMove);
195 stockfishEngine.onmessage = null;
196 resolve(bestMove);
197 }
198 }
199 };
200
201 stockfishEngine.onmessage = messageHandler;
202
203 // Send position and calculate
204 stockfishEngine.postMessage('ucinewgame');
205 stockfishEngine.postMessage(`position fen ${fen}`);
206 stockfishEngine.postMessage(`go depth ${CONFIG.stockfishDepth}`);
207
208 // Timeout fallback
209 setTimeout(() => {
210 if (!bestMove) {
211 console.log('Stockfish timeout, using fallback');
212 stockfishEngine.onmessage = null;
213 resolve(null);
214 }
215 }, 15000);
216 });
217 }
218
219 // Parse UCI move format (e.g., "e2e4") to chess.js format
220 function parseUCIMove(uciMove, fen) {
221 if (!uciMove || uciMove.length < 4) return null;
222
223 try {
224 if (!chessGame) {
225 chessGame = new Chess(fen);
226 } else {
227 chessGame.load(fen);
228 }
229
230 const from = uciMove.substring(0, 2);
231 const to = uciMove.substring(2, 4);
232 const promotion = uciMove.length > 4 ? uciMove[4] : undefined;
233
234 const move = chessGame.move({
235 from: from,
236 to: to,
237 promotion: promotion
238 });
239
240 chessGame.undo();
241 return move;
242 } catch (error) {
243 console.error('Error parsing UCI move:', error);
244 return null;
245 }
246 }
247
248 // Make a move on the board
249 async function makeMove(move) {
250 try {
251 console.log('Making move:', move.from, '->', move.to);
252
253 // Find the source square
254 const fromSquare = document.querySelector(`cg-board square[data-key="${move.from}"]`);
255 if (!fromSquare) {
256 console.error('Source square not found:', move.from);
257 return false;
258 }
259
260 // Simulate mouse movement to source
261 await sleep(100 + Math.random() * 200);
262 simulateMouseMovement(fromSquare);
263 await sleep(50 + Math.random() * 100);
264
265 // Click source square
266 fromSquare.click();
267 console.log('Clicked source square:', move.from);
268
269 await sleep(200 + Math.random() * 300);
270
271 // Find the destination square
272 const toSquare = document.querySelector(`cg-board square[data-key="${move.to}"]`);
273 if (!toSquare) {
274 console.error('Destination square not found:', move.to);
275 return false;
276 }
277
278 // Simulate mouse movement to destination
279 simulateMouseMovement(toSquare);
280 await sleep(50 + Math.random() * 100);
281
282 // Click destination square
283 toSquare.click();
284 console.log('Clicked destination square:', move.to);
285
286 // Handle promotion if needed
287 if (move.promotion) {
288 await sleep(300);
289 const promotionPiece = document.querySelector(`[data-role="promotion"] piece[data-piece="${move.promotion}"]`);
290 if (promotionPiece) {
291 promotionPiece.click();
292 console.log('Selected promotion piece:', move.promotion);
293 }
294 }
295
296 return true;
297 } catch (error) {
298 console.error('Error making move:', error);
299 return false;
300 }
301 }
302
303 // Main autoplay loop
304 async function autoplayLoop() {
305 if (!isAutoplayActive) return;
306
307 try {
308 // Check if we're in an active game
309 const gameElement = document.querySelector('main.round');
310 if (!gameElement) {
311 console.log('No active game detected');
312 setTimeout(autoplayLoop, 2000);
313 return;
314 }
315
316 // Determine player color if not set
317 if (!playerColor) {
318 playerColor = determinePlayerColor();
319 console.log('Player color:', playerColor);
320 }
321
322 // Check if it's our turn
323 isMyTurn = checkIfMyTurn();
324
325 if (!isMyTurn) {
326 console.log('Waiting for our turn...');
327 setTimeout(autoplayLoop, 1000);
328 return;
329 }
330
331 console.log('It\'s our turn! Calculating move...');
332
333 // Get current board state
334 const fen = getBoardState();
335 if (!fen) {
336 console.error('Could not get board state');
337 setTimeout(autoplayLoop, 2000);
338 return;
339 }
340
341 console.log('Current FEN:', fen);
342
343 // Calculate best move using Stockfish
344 const uciMove = await calculateBestMoveWithStockfish(fen);
345 if (!uciMove) {
346 console.log('No move calculated by Stockfish');
347 setTimeout(autoplayLoop, 2000);
348 return;
349 }
350
351 const bestMove = parseUCIMove(uciMove, fen);
352 if (!bestMove) {
353 console.log('Could not parse move');
354 setTimeout(autoplayLoop, 2000);
355 return;
356 }
357
358 // Wait with human-like delay
359 const thinkTime = getHumanDelay();
360 console.log(`Thinking for ${thinkTime}ms...`);
361 await sleep(thinkTime);
362
363 // Make the move
364 const moveSuccess = await makeMove(bestMove);
365
366 if (moveSuccess) {
367 console.log('Move executed successfully');
368 isMyTurn = false;
369 } else {
370 console.error('Failed to execute move');
371 }
372
373 // Continue the loop
374 setTimeout(autoplayLoop, 1500);
375
376 } catch (error) {
377 console.error('Error in autoplay loop:', error);
378 setTimeout(autoplayLoop, 2000);
379 }
380 }
381
382 // Create UI toggle button
383 function createToggleButton() {
384 const button = document.createElement('button');
385 button.id = 'autoplay-toggle';
386 button.textContent = '🤖 Pro Auto: ON';
387 button.style.cssText = `
388 position: fixed;
389 top: 10px;
390 right: 10px;
391 z-index: 10000;
392 padding: 10px 15px;
393 background: #3893E8;
394 color: white;
395 border: none;
396 border-radius: 5px;
397 font-weight: bold;
398 cursor: pointer;
399 box-shadow: 0 2px 5px rgba(0,0,0,0.3);
400 font-size: 14px;
401 `;
402
403 button.addEventListener('click', () => {
404 isAutoplayActive = !isAutoplayActive;
405 button.textContent = isAutoplayActive ? '🤖 Pro Auto: ON' : '🤖 Pro Auto: OFF';
406 button.style.background = isAutoplayActive ? '#3893E8' : '#666';
407
408 if (isAutoplayActive) {
409 console.log('Pro Autoplay enabled');
410 autoplayLoop();
411 } else {
412 console.log('Pro Autoplay disabled');
413 }
414 });
415
416 document.body.appendChild(button);
417 console.log('Toggle button created');
418 }
419
420 // Initialize the extension
421 async function init() {
422 console.log('Initializing Lichess Pro Auto Chess Player...');
423
424 // Wait for page to be ready
425 if (document.readyState === 'loading') {
426 document.addEventListener('DOMContentLoaded', init);
427 return;
428 }
429
430 // Wait for body to be available
431 if (!document.body) {
432 setTimeout(init, 100);
433 return;
434 }
435
436 // Initialize Stockfish engine
437 await initStockfish();
438
439 // Create toggle button
440 try {
441 createToggleButton();
442 } catch (error) {
443 console.error('Error creating toggle button:', error);
444 }
445
446 // Start autoplay if enabled
447 if (CONFIG.enabled) {
448 isAutoplayActive = true;
449 setTimeout(autoplayLoop, 2000);
450 }
451
452 console.log('Lichess Pro Auto Chess Player initialized');
453 }
454
455 // Start the extension
456 if (document.readyState === 'loading') {
457 document.addEventListener('DOMContentLoaded', init);
458 } else {
459 init();
460 }
461
462})();