diff --git a/BetterMint/html/options.html b/BetterMint/html/options.html index fd7a583..08118e4 100644 --- a/BetterMint/html/options.html +++ b/BetterMint/html/options.html @@ -2,7 +2,7 @@ - Mint V2 Internal + Mint V2.0 @@ -224,7 +224,7 @@ +
+ + +
+ +
+ + +
+
+
Activation Key:
+ +
+ +
+ + +
+
+ + +
+
+
Opening Move Speed (% Faster):
+ +
+
+ +
+ + +
+
+
Panic Threshold (seconds):
+ +
+
Auto Move Delay:
@@ -558,7 +615,7 @@

Features (not updated)

Public

- by BetterMint, HotaVN, lucaskvasirr, Webcubed, & thedemons + by BetterMint, HotaVN, lucaskvasirr, Webcubed, & thedemons. Updated by .baconater69.
diff --git a/BetterMint/js/Mint.js b/BetterMint/js/Mint.js index b1e4b22..5850572 100644 --- a/BetterMint/js/Mint.js +++ b/BetterMint/js/Mint.js @@ -126,8 +126,46 @@ var enumOptions = { MoveAnalysis: "option-move-analysis", DepthBar: "option-depth-bar", EvaluationBar: "option-evaluation-bar", + AutoQueue: "option-auto-queue", + PanicMode: "option-panic-mode", + PanicTime: "option-panic-time", + Notifications: "option-notifications", + FastOpeningMoves: "option-fast-opening-moves", + FastOpeningSpeed: "option-fast-opening-speed", + InstantPremove: "option-instant-premove", + InstantPremoveKey: "option-instant-premove-key", }; +// helper to parse remaining time for current player +function getRemainingPlayerTimeSeconds() { + const selectors = [ + '.clock-component.clock-bottom', + '.clock-button-component.clock-bottom', + '.clock.clock-bottom', + '.clock-bottom', + ]; + for (const sel of selectors) { + const el = document.querySelector(sel); + if (el && el.textContent) { + const text = el.textContent.trim(); + // Extract time like 1:23, 10:15, 0:05, 1:02:15 + const match = text.match(/\d{1,2}(:\d{2}){1,2}/); + if (!match) continue; + const parts = match[0].split(':').map(Number); + let seconds = 0; + if (parts.length === 3) { + seconds = parts[0] * 3600 + parts[1] * 60 + parts[2]; + } else if (parts.length === 2) { + seconds = parts[0] * 60 + parts[1]; + } else if (parts.length === 1) { + seconds = parts[0]; + } + return seconds; + } + } + return null; +} + var BetterMintmaster; var Config = undefined; var context = undefined; @@ -179,7 +217,7 @@ class GameController { console.log("WHITE'S FIRST MOVE - INITIATING PRE-MOVES"); } } - + this.UpdateEngine(false); }); // check if a new game has started @@ -193,6 +231,8 @@ class GameController { BetterMintmaster.engine.moveCounter = 0; BetterMintmaster.engine.hasShownLimitMessage = false; BetterMintmaster.engine.isPreMoveSequence = true; + } else if (event.data === "gameOver") { + this.handleGameOver(); } }); let checkEventOne = false; @@ -221,7 +261,62 @@ class GameController { this.evalBar.classList.add("evaluation-bar-flipped"); else this.evalBar.classList.remove("evaluation-bar-flipped"); } + // handle auto queue toggle dynamically + this.setupAutoQueueObserver(); + }); + + // Setup auto-queue observer initially if enabled + this.setupAutoQueueObserver(); + } + + // Observe DOM for the "New" game button and click it when Auto Queue is enabled + setupAutoQueueObserver() { + if (this.autoQueueObserver) { + // If observer exists but AutoQueue is disabled, disconnect it. + if (!getValueConfig(enumOptions.AutoQueue)) { + this.autoQueueObserver.disconnect(); + this.autoQueueObserver = null; + } + return; + } + + if (!getValueConfig(enumOptions.AutoQueue)) return; + + this.autoQueueObserver = new MutationObserver(() => { + const buttons = document.querySelectorAll('button.game-over-buttons-button'); + for (const btn of buttons) { + const span = btn.querySelector('span'); + if (span && span.textContent.trim().startsWith('New')) { + btn.click(); + break; + } + } }); + + this.autoQueueObserver.observe(document.body, { childList: true, subtree: true }); + } + + handleGameOver() { + if (getValueConfig(enumOptions.AutoQueue)) { + const maxAttempts = 60; // Poll for 30 seconds + let attempts = 0; + const intervalId = setInterval(() => { + attempts++; + if (attempts > maxAttempts) { + clearInterval(intervalId); + return; + } + + const buttons = document.querySelectorAll('.game-over-buttons-button'); + for (const button of buttons) { + if (button.textContent && button.textContent.trim().startsWith('New')) { + button.click(); + clearInterval(intervalId); + return; + } + } + }, 500); + } } UpdateExtensionOptions() { if (getValueConfig(enumOptions.EvaluationBar) && this.evalBar == null) @@ -233,12 +328,8 @@ class GameController { this.evalBar.remove(); this.evalBar = null; } - if (getValueConfig(enumOptions.DepthBar) && this.depthBar == null) - this.CreateAnalysisTools(); - else if (!getValueConfig(enumOptions.DepthBar) && this.depthBar != null) { - this.depthBar.parentElement.remove(); - this.depthBar = null; - } + // Start or stop auto queue observer based on updated option + this.setupAutoQueueObserver(); if (!getValueConfig(enumOptions.ShowHints)) { this.RemoveCurrentMarkings(); } @@ -467,6 +558,75 @@ class GameController { } } +function getPieceFromFen(fen, square) { + const fenBoard = fen.split(' ')[0]; + const ranks = fenBoard.split('/'); + + const file = square.charCodeAt(0) - 'a'.charCodeAt(0); + const rank = 8 - parseInt(square.charAt(1), 10); + + if (rank < 0 || rank > 7 || file < 0 || file > 7) { + return null; // Invalid square + } + + const rankStr = ranks[rank]; + let fileIdx = 0; + for (let i = 0; i < rankStr.length; i++) { + const char = rankStr.charAt(i); + if (/\d/.test(char)) { // it's a number for empty squares + fileIdx += parseInt(char, 10); + } else { + if (fileIdx === file) { + return char; + } + fileIdx++; + } + } + return null; // Should be an empty square +} + +function getVerboseMove(move, fen) { + const piece = getPieceFromFen(fen, move.from); + // if (!piece) return move.move; // fallback - let's be optimistic + + const pieceNames = { + 'p': 'pawn', 'n': 'knight', 'b': 'bishop', 'r': 'rook', 'q': 'queen', 'k': 'king' + }; + + const pieceName = pieceNames[piece.toLowerCase()]; + + let verboseMove = `${pieceName} to ${move.to}`; + + const capturedPiece = getPieceFromFen(fen, move.to); + if (capturedPiece) { + const capturedPieceName = pieceNames[capturedPiece.toLowerCase()]; + verboseMove = `${pieceName} takes ${capturedPieceName} on ${move.to}`; + } + + // basic castling detection + if (piece.toLowerCase() === 'k') { + const fromFile = move.from.charCodeAt(0); + const toFile = move.to.charCodeAt(0); + if (Math.abs(fromFile - toFile) > 1) { + if (toFile > fromFile) return "castles kingside"; + else return "castles queenside"; + } + } + + // en passant detection + if (piece.toLowerCase() === 'p' && !capturedPiece && move.from.charAt(0) !== move.to.charAt(0)) { + verboseMove = `pawn takes on ${move.to}`; + } + + if (move.promotion) { + const promotionPieceName = pieceNames[move.promotion.toLowerCase()]; + verboseMove += ` promoting to a ${promotionPieceName}`; + } + + return verboseMove; +} + + class StockfishEngine { constructor(BetterMintmaster) { let stockfishJsURL; @@ -582,11 +742,22 @@ class StockfishEngine { go() { this.onReady(() => { this.stopEvaluation(() => { - // Prevent overlapping evaluations if (this.isEvaluating) return; - console.assert(!this.isEvaluating, "Duplicated Stockfish go command"); this.isEvaluating = true; - this.send(`go depth ${this.depth}`); + + // Panic mode logic + let cmd = null; + if (getValueConfig(enumOptions.PanicMode) && getValueConfig(enumOptions.LegitAutoMove)) { + const remaining = getRemainingPlayerTimeSeconds(); + const threshold = parseInt(getValueConfig(enumOptions.PanicTime)); + if (remaining !== null && remaining <= threshold) { + cmd = `go depth 1`; + } + } + if (!cmd) { + cmd = `go depth ${this.depth}`; + } + this.send(cmd); }); }); } @@ -643,7 +814,7 @@ class StockfishEngine { callback(); } } - + onStockfishResponse() { if (this.isRequestedStop) { this.isRequestedStop = false; @@ -704,7 +875,7 @@ class StockfishEngine { ProcessMessage(event) { this.ready = false; let line = event && typeof event === "object" ? event.data : event; - + if (line === "uciok") { this.loaded = true; this.BetterMintmaster.onEngineLoaded(); @@ -735,7 +906,7 @@ class StockfishEngine { let pvMatch = line.match(/^info .*?pv ([a-h][1-8][a-h][1-8][qrbn]?(?: [a-h][1-8][a-h][1-8][qrbn]?)*)(?: .*)?/); let multipvMatch = line.match(/^info .*?multipv (\d+)/); let bestMoveMatch = line.match(/^bestmove ([a-h][1-8][a-h][1-8][qrbn]?)(?: ponder ([a-h][1-8][a-h][1-8][qrbn]?))?/); - + if (depthMatch && scoreMatch && pvMatch) { let depth = parseInt(depthMatch[1]); let seldepth = seldepthMatch ? parseInt(seldepthMatch[1]) : null; @@ -744,10 +915,10 @@ class StockfishEngine { let score = parseInt(scoreMatch[2]); let multipv = multipvMatch ? parseInt(multipvMatch[1]) : 1; let pv = pvMatch[1]; - + let cpScore = scoreType === "cp" ? score : null; let mateScore = scoreType === "mate" ? score : null; - + if (!this.isRequestedStop) { let move = new TopMove(pv, depth, cpScore, mateScore, multipv); this.onTopMoves(move, false); @@ -765,7 +936,7 @@ class StockfishEngine { const bestMove = bestMoveMatch[1]; const ponderMove = bestMoveMatch[2]; const index = this.topMoves.findIndex((object) => object.move === bestMove); - + if (index < 0) { console.warn(`The engine returned the best move "${bestMove}" but it's not in the top move list.`); let bestMoveOnTop = new TopMove( @@ -782,7 +953,7 @@ class StockfishEngine { this.isRequestedStop = false; } } - } + } executeReadyCallbacks() { while (this.readyCallbacks.length > 0) { @@ -930,18 +1101,31 @@ class StockfishEngine { } } } - if (bestMoveSelected && this.topMoves.length > 0) { + if (this.BetterMintmaster && this.BetterMintmaster.instantPremoveActive && bestMoveSelected) { + const bestMove = this.topMoves[0]; + const legalMoves = this.BetterMintmaster.game.controller.getLegalMoves(); + const moveData = legalMoves.find(m=>m.from===bestMove.from && m.to===bestMove.to); + if (moveData) { + moveData.userGenerated=true; + if(bestMove.promotion) moveData.promotion=bestMove.promotion; + this.BetterMintmaster.instantPremoveActive=false; + this.BetterMintmaster.game.controller.move(moveData); + return; + } + } + + if (bestMoveSelected) { const bestMove = this.topMoves[0]; const currentFEN = this.BetterMintmaster.game.controller.getFEN(); const currentTurn = currentFEN.split(" ")[1]; // 'w' or 'b' const playingAs = this.BetterMintmaster.game.controller.getPlayingAs(); - + if (getValueConfig(enumOptions.Premove) && getValueConfig(enumOptions.LegitAutoMove)) { // [FIX] Execute pre-moves if: // - It's player's turn AND // - Haven't reached move limit if ( - ((playingAs === 1 && currentTurn === 'w') || + ((playingAs === 1 && currentTurn === 'w') || (playingAs === 2 && currentTurn === 'b')) && this.moveCounter < getValueConfig(enumOptions.MaxPreMoves) && // Use move counter instead of premove depth !this.hasShownLimitMessage @@ -950,16 +1134,16 @@ class StockfishEngine { const moveData = legalMoves.find( move => move.from === bestMove.from && move.to === bestMove.to ); - + if (moveData) { moveData.userGenerated = true; - + if (bestMove.promotion !== null) { moveData.promotion = bestMove.promotion; } - + this.moveCounter++; // Increment move counter - + // Calculate pre-move execution time let pre_move_time = getValueConfig(enumOptions.PreMoveTime) + @@ -968,11 +1152,11 @@ class StockfishEngine { ) % getValueConfig(enumOptions.PreMoveTimeRandomDiv)) * getValueConfig(enumOptions.PreMoveTimeRandomMulti); - + setTimeout(() => { this.BetterMintmaster.game.controller.move(moveData); - - if (window.toaster) { + + if (getValueConfig(enumOptions.Notifications) && window.toaster) { window.toaster.add({ id: "auto-move-counter", duration: 2000, @@ -987,9 +1171,9 @@ class StockfishEngine { } }); } - + if (this.moveCounter >= getValueConfig(enumOptions.MaxPreMoves)) { - if (window.toaster) { + if (getValueConfig(enumOptions.Notifications) && window.toaster) { window.toaster.add({ id: "auto-move-limit", duration: 2000, // Reduced from 3000 @@ -1009,22 +1193,22 @@ class StockfishEngine { }, pre_move_time); // Execute with calculated delay } } - + // Check for mate in 3 or less - MOVED INSIDE THE PREMOVE CHECK if (bestMove.mate !== null && bestMove.mate > 0 && bestMove.mate <= getValueConfig(enumOptions.MateFinderValue)) { const legalMoves = this.BetterMintmaster.game.controller.getLegalMoves(); const moveData = legalMoves.find( move => move.from === bestMove.from && move.to === bestMove.to ); - + if (moveData) { moveData.userGenerated = true; - + if (bestMove.promotion !== null) { moveData.promotion = bestMove.promotion; } - - if (window.toaster) { + + if (getValueConfig(enumOptions.Notifications) && window.toaster) { window.toaster.add({ id: "premove-mate", duration: 2000, @@ -1040,16 +1224,18 @@ class StockfishEngine { }, }); } - + this.BetterMintmaster.game.controller.move(moveData); } } } } - + if (getValueConfig(enumOptions.TextToSpeech)) { const topMove = this.topMoves[0]; // Select the top move from the PV list - const msg = new SpeechSynthesisUtterance(topMove.move); // Use topMove.move for the spoken text + const currentFEN = this.BetterMintmaster.game.controller.getFEN(); + const verboseMove = getVerboseMove(topMove, currentFEN); + const msg = new SpeechSynthesisUtterance(verboseMove); // Use topMove.move for the spoken text const voices = window.speechSynthesis.getVoices(); const femaleVoices = voices.filter((voice) => voice.voiceURI.includes("Google UK English Female") @@ -1208,13 +1394,43 @@ class StockfishEngine { top_pv_moves = [fastestMateMove]; } } - let auto_move_time = - getValueConfig(enumOptions.AutoMoveTime) + - (Math.floor( - Math.random() * getValueConfig(enumOptions.AutoMoveTimeRandom) - ) % - getValueConfig(enumOptions.AutoMoveTimeRandomDiv)) * - getValueConfig(enumOptions.AutoMoveTimeRandomMulti); + let panicActive = false; + if (getValueConfig(enumOptions.PanicMode) && getValueConfig(enumOptions.LegitAutoMove)) { + const remaining = getRemainingPlayerTimeSeconds(); + const threshold = parseInt(getValueConfig(enumOptions.PanicTime)); + if (remaining !== null && remaining <= threshold) panicActive = true; + } + + let auto_move_time; + if (panicActive) { + auto_move_time = Math.floor(Math.random() * 600); // 0-599 ms + } else { + auto_move_time = + getValueConfig(enumOptions.AutoMoveTime) + + (Math.floor( + Math.random() * getValueConfig(enumOptions.AutoMoveTimeRandom) + ) % + getValueConfig(enumOptions.AutoMoveTimeRandomDiv)) * + getValueConfig(enumOptions.AutoMoveTimeRandomMulti); + } + + if (this.BetterMintmaster && this.BetterMintmaster.instantPremoveActive) { + auto_move_time = 0; + this.BetterMintmaster.instantPremoveActive = false; + } + + if (getValueConfig(enumOptions.FastOpeningMoves)) { + const fenParts = this.BetterMintmaster.game.controller.getFEN().split(" "); + const fullMoveNumber = parseInt(fenParts[5], 10) || 1; + if (fullMoveNumber <= 7) { + const speedPercentage = parseInt(getValueConfig(enumOptions.FastOpeningSpeed), 10); + const speedMultiplier = 1 + (speedPercentage * 2) / 100; + if (speedMultiplier > 0) { + auto_move_time /= speedMultiplier; + } + } + } + if ( isNaN(auto_move_time) || auto_move_time === null || @@ -1223,7 +1439,7 @@ class StockfishEngine { auto_move_time = 100; } const secondsTillAutoMove = (auto_move_time / 1000).toFixed(1); - if (window.toaster) { + if (getValueConfig(enumOptions.Notifications) && window.toaster) { window.toaster.add({ id: "chess.com", duration: (parseFloat(secondsTillAutoMove) + 1) * 1000, @@ -1293,6 +1509,7 @@ class BetterMint { // show a notification when the settings is updated, but only if the previous // notification has gone if ( + getValueConfig(enumOptions.Notifications) && window.toaster && window.toaster.notifications.findIndex( (noti) => noti.id == "BetterMint-settings-updated" @@ -1308,14 +1525,31 @@ class BetterMint { }, false ); + this.instantPremoveActive = false; + document.addEventListener('keydown', (e) => { + if (!getValueConfig(enumOptions.InstantPremove)) return; + const cfgKey = (getValueConfig(enumOptions.InstantPremoveKey) || 'P').toLowerCase(); + if (e.key.toLowerCase() === cfgKey) { + this.instantPremoveActive = true; + if (getValueConfig(enumOptions.Notifications) && window.toaster) { + window.toaster.add({ + id: 'instant-premove-armed', + duration: 1500, + icon: 'circle-lightning', + content: `Instant Premove armed (key '${cfgKey.toUpperCase()}') for next move!`, + style: {position:'fixed',bottom:'120px',right:'30px',backgroundColor:'#5d3fd3',color:'white'} + }); + } + } + }); } onEngineLoaded() { - if (window.toaster) { + if (getValueConfig(enumOptions.Notifications) && window.toaster) { window.toaster.add({ id: "chess.com", duration: 3000, icon: "circle-info", - content: `BetterMint V2 is enabled!`, + content: `Mint V2.0 is enabled!`, }); } } diff --git a/BetterMint/js/loader.js b/BetterMint/js/loader.js index 43476db..f17038d 100644 --- a/BetterMint/js/loader.js +++ b/BetterMint/js/loader.js @@ -28,6 +28,14 @@ let inputObjects = { "option-move-analysis": { default_value: true }, "option-depth-bar": { default_value: true }, "option-evaluation-bar": { default_value: true }, + "option-auto-queue": { default_value: false }, + "option-panic-mode": { default_value: false }, + "option-panic-time": { default_value: 10 }, + "option-notifications": { default_value: true }, + "option-fast-opening-moves": { default_value: false }, + "option-fast-opening-speed": { default_value: 50 }, + "option-instant-premove": { default_value: false }, + "option-instant-premove-key": { default_value: "P" }, }; let DefaultExtensionOptions = {}; diff --git a/BetterMint/js/options.js b/BetterMint/js/options.js index 47a577b..a045e4f 100644 --- a/BetterMint/js/options.js +++ b/BetterMint/js/options.js @@ -85,6 +85,30 @@ let inputObjects = { "option-evaluation-bar": { default_value: true, }, + "option-auto-queue": { + default_value: false, + }, + "option-panic-mode": { + default_value: false, + }, + "option-panic-time": { + default_value: 10, + }, + "option-notifications": { + default_value: true, + }, + "option-fast-opening-moves": { + default_value: false, + }, + "option-fast-opening-speed": { + default_value: 50, + }, + "option-instant-premove": { + default_value: false, + }, + "option-instant-premove-key": { + default_value: "P", + }, }; let DefaultExtensionOptions = {}; diff --git a/BetterMint/manifest.json b/BetterMint/manifest.json index 4153019..7210a09 100644 --- a/BetterMint/manifest.json +++ b/BetterMint/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, - "name": "Mint V2 - Chess", - "short_name": "Mint V2 - Chess", + "name": "Mint V2.0 - Chess", + "short_name": "Mint V2.0 - Chess", "icons": { "16": "img/logo-16.png", "48": "img/logo-48.png", diff --git a/EngineWS/__pycache__/main.cpython-313.pyc b/EngineWS/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..8262259 Binary files /dev/null and b/EngineWS/__pycache__/main.cpython-313.pyc differ diff --git a/EngineWS/main.py b/EngineWS/main.py index 788645a..4054960 100644 --- a/EngineWS/main.py +++ b/EngineWS/main.py @@ -24,13 +24,33 @@ def enqueue_output(out, queue): class EngineChess: def __init__(self, path_engine): - self._engine = subprocess.Popen( - path_engine, - universal_newlines=True, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) + print(f"Attempting to start engine: {path_engine}") + if not os.path.exists(path_engine): + show_message(f"ERROR: Engine path does not exist:\n{path_engine}") + raise FileNotFoundError(f"Engine not found at {path_engine}") + + try: + self._engine = subprocess.Popen( + [path_engine], + universal_newlines=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + except OSError as e: + print(f"FATAL ERROR starting engine: {path_engine}") + print(f"DETAILS: {e}") + if hasattr(e, 'winerror') and e.winerror == 193: + show_message( + "The selected file is not a valid Windows application.\\n" + "This can happen if:\\n" + " - The file is for a different OS (e.g., Linux).\\n" + " - The file requires a CPU feature you don't have (like AVX2).\\n" + " - The file is corrupt or not a real engine.\\n\\n" + "Please restart and try selecting a different engine, like 'opental_x64plain.exe'." + ) + raise e + self.queueOutput = Queue() self.thread = Thread(target=enqueue_output, args=(self._engine.stdout, self.queueOutput)) self.thread.daemon = True @@ -78,7 +98,11 @@ def read_line(self) -> str: root.withdraw() show_message("Please select engine executable files.") -engine_exe_paths = filedialog.askopenfilenames(title="Select engine executable files") +initial_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'engines & books') +engine_exe_paths = filedialog.askopenfilenames( + title="Select engine executable files", + initialdir=initial_dir +) if not engine_exe_paths: print("No engine selected. Exiting.")