// ==UserScript==
// @name linuxDo 2048 AI玩家 Plus
// @namespace http://tampermonkey.net/
// @version 2.0
// @description 专为2048.linux.do设计的高性能AI
// @author littleleo
// @match https://2048.linux.do/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// Prevent password manager interference
document.addEventListener('keydown', function(e) { e.stopImmediatePropagation(); }, true);
const AI_VERSION = "v2.0";
const TARGET_SCORE = 250000;
// ===================================================================================
// WEB WORKER CODE: The AI's brain with a new, stable simulation engine.
// ===================================================================================
const workerCode = `
self.onmessage = function(e) {
const { board } = e.data;
const bestMove = getNextMove(board);
self.postMessage({ bestMove: bestMove });
};
/**
* NEW v8.1: A rewritten, robust, and easy-to-understand simulation function.
* This is the stable core of the AI.
*/
function simulateMove(board, direction) {
const tempBoard = JSON.parse(JSON.stringify(board));
let moved = false;
function slide(row) {
const arr = row.filter(val => val);
const missing = 4 - arr.length;
const zeros = Array(missing).fill(0);
return arr.concat(zeros);
}
function combine(row) {
for (let i = 0; i < 3; i++) {
if (row[i] !== 0 && row[i] === row[i + 1]) {
row[i] *= 2;
row[i + 1] = 0;
}
}
return row;
}
function operate(row) {
const originalJson = JSON.stringify(row);
let newRow = slide(row);
newRow = combine(newRow);
newRow = slide(newRow);
if (JSON.stringify(newRow) !== originalJson) {
moved = true;
}
return newRow;
}
if (direction === 'left' || direction === 'right') {
for (let r = 0; r < 4; r++) {
let row = tempBoard[r];
if (direction === 'right') row.reverse();
row = operate(row);
if (direction === 'right') row.reverse();
tempBoard[r] = row;
}
} else if (direction === 'up' || direction === 'down') {
for (let c = 0; c < 4; c++) {
let col = [tempBoard[0][c], tempBoard[1][c], tempBoard[2][c], tempBoard[3][c]];
if (direction === 'down') col.reverse();
col = operate(col);
if (direction === 'down') col.reverse();
for (let r = 0; r < 4; r++) {
tempBoard[r][c] = col[r];
}
}
}
if (moved) {
Object.assign(board, tempBoard);
}
return moved;
}
function getGameStage(board) {
let maxTile = 0;
let emptyCells = 0;
for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { if (board[r][c] > maxTile) maxTile = board[r][c]; if (board[r][c] === 0) emptyCells++; } }
if (emptyCells <= 3) return 'survival';
if (maxTile >= 8192) return 'endgame';
if (maxTile >= 2048) return 'late';
if (maxTile >= 512) return 'middle';
return 'early';
}
function evaluateByStage(b) {
const stage = getGameStage(b);
let score = 0;
let emptyCount = 0, smoothness = 0, monotonicity = 0, maxTile = 0;
let maxTilePos = {r: 0, c: 0};
let potentialMerges = 0;
let islandPenalty = 0;
const weights = [[15, 14, 13, 12], [8, 9, 10, 11], [7, 6, 5, 4], [0, 1, 2, 3]];
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
const tileValue = b[r][c];
if (tileValue === 0) {
emptyCount++;
} else {
if (tileValue > maxTile) { maxTile = tileValue; maxTilePos = {r, c}; }
score += Math.log2(tileValue) * weights[r][c];
if (c < 3) { const rightVal = b[r][c+1]; if (rightVal !== 0) { if (rightVal === tileValue) potentialMerges++; smoothness -= Math.abs(Math.log2(tileValue) - Math.log2(rightVal)); } }
if (r < 3) { const downVal = b[r+1][c]; if (downVal !== 0) { if (downVal === tileValue) potentialMerges++; smoothness -= Math.abs(Math.log2(tileValue) - Math.log2(downVal)); } }
if (tileValue <= 4) { if (c > 0 && c < 3 && b[r][c-1] > tileValue * 4 && b[r][c+1] > tileValue * 4) islandPenalty++; if (r > 0 && r < 3 && b[r-1][c] > tileValue * 4 && b[r+1][c] > tileValue * 4) islandPenalty++; }
}
}
}
let monoTotals = { up: 0, down: 0, left: 0, right: 0 };
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 3; j++) { if (b[i][j] >= b[i][j+1]) monoTotals.right++; else monoTotals.left++; }
for (let j = 0; j < 3; j++) { if (b[j][i] >= b[j+1][i]) monoTotals.down++; else monoTotals.up++; }
}
monotonicity = Math.max(monoTotals.right, monoTotals.left) + Math.max(monoTotals.up, monoTotals.down);
let cornerBonus = 0;
if (maxTilePos.r === 3 && maxTilePos.c === 0) { cornerBonus = Math.log2(maxTile) * 10; }
else if (maxTilePos.r === 3) { cornerBonus = Math.log2(maxTile) * 5; }
switch (stage) {
case 'survival': score += emptyCount * 20 + potentialMerges * 10 - islandPenalty * 5; break;
case 'endgame': case 'late': score += emptyCount * 5 + smoothness * 2.0 + monotonicity * 2.5 + cornerBonus * 2.0 - islandPenalty * 10; break;
case 'middle': score += emptyCount * 3.0 + smoothness * 1.5 + monotonicity * 1.5 + cornerBonus - islandPenalty * 5; break;
case 'early': score += emptyCount * 2.0 + smoothness * 1.0 + monotonicity * 1.0; break;
}
return score;
}
function getNextMove(board) {
const directions = { up: 'ArrowUp', right: 'ArrowRight', down: 'ArrowDown', left: 'ArrowLeft' };
let moveScores = [];
for (const dirKey in directions) {
const simBoard = JSON.parse(JSON.stringify(board));
if (simulateMove(simBoard, dirKey)) {
moveScores.push({ direction: directions[dirKey], score: evaluateByStage(simBoard), board: simBoard });
}
}
if (moveScores.length === 0) return null; // No valid moves
moveScores.sort((a, b) => b.score - a.score);
const initial8192Count = board.flat().filter(v => v === 8192).length;
if (initial8192Count >= 2) {
for (const move of moveScores) {
const final8192Count = move.board.flat().filter(v => v === 8192).length;
if (final8192Count >= initial8192Count) {
return move.direction; // This move is safe
}
}
}
return moveScores[0].direction;
}
`;
// =====================================================================
// MAIN SCRIPT: Manages the worker, UI, and game interaction.
// =====================================================================
let moveSpeed = 100;
let isAiRunning = false;
let gameLoopTimeout = null;
let gamesPlayed = 0;
let highestScore = 0;
let highestTile = 0;
const workerBlob = new Blob([workerCode], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(workerBlob);
const aiWorker = new Worker(workerUrl);
aiWorker.onmessage = async function(e) {
if (!isAiRunning) return;
const { bestMove } = e.data;
updateStatsUI();
if (bestMove === null) { // No valid moves left
endGame();
return;
}
if (!await attemptMove(bestMove)) {
const fallbackMoves = ['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp'].filter(m => m !== bestMove);
for (const move of fallbackMoves) { if (await attemptMove(move)) break; }
}
gameLoopTimeout = setTimeout(executeThinkCycle, moveSpeed);
};
async function attemptMove(direction) {
const stateBefore = JSON.stringify(window.canvasGame.board);
document.body.dispatchEvent(new KeyboardEvent('keydown', { key: direction, bubbles: true }));
return new Promise(resolve => {
const TIMEOUT_FOR_INVALID_MOVE = 500;
const startTime = Date.now();
const checkInterval = setInterval(() => {
if (JSON.stringify(window.canvasGame.board) !== stateBefore) {
clearInterval(checkInterval);
resolve(true);
} else if (Date.now() - startTime > TIMEOUT_FOR_INVALID_MOVE) {
clearInterval(checkInterval);
resolve(false);
}
}, 50);
});
}
function executeThinkCycle() {
if (!isAiRunning) return;
if (window.canvasGame.gameOver) {
endGame();
return;
}
aiWorker.postMessage({ board: window.canvasGame.board });
}
function endGame() {
if (!isAiRunning) return; // Prevent multiple calls
isAiRunning = false;
clearTimeout(gameLoopTimeout);
gamesPlayed++;
updateStatsUI();
const btn = document.getElementById('auto-play-btn');
if(btn) {
btn.disabled = false;
btn.style.backgroundColor = '#27ae60';
btn.innerHTML = '▶ 启动AI';
}
console.log("🎉 Game Over! Final Score: " + window.canvasGame.score);
}
function createControlPanel() {
if (document.getElementById('ai-control-panel')) return;
const panel = document.createElement('div');
panel.id = 'ai-control-panel';
panel.style.cssText = `position: fixed; top: 20px; right: 20px; background: rgba(255, 255, 255, 0.95); padding: 15px; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.25); z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-width: 250px; border: 1px solid #ddd;`;
panel.innerHTML = `
<h3 style="margin-top:0; color: #776e65; border-bottom: 1px solid #eee; padding-bottom: 10px;">2048 AI ${AI_VERSION}</h3>
<div style="margin-bottom:15px;">
<button id="auto-play-btn" style="padding:10px 15px; width:100%; background:#27ae60; color:white; border:none; border-radius:4px; cursor:pointer; font-weight:bold; font-size:16px;">
▶ 启动AI
</button>
</div>
<div id="ai-stats" style="font-size:14px; line-height:1.8; margin-bottom:15px; background:#f9f9f9; padding:10px; border-radius:5px;">
<div>游戏次数: <span id="games-count" style="float:right;">0</span></div>
<div>最高分数: <span id="high-score" style="float:right;">0</span></div>
<div>最大方块: <span id="max-tile" style="float:right;">0</span></div>
<div style="font-weight:bold;">目标进度: <span id="target-progress" style="float:right; color:#e74c3c;">0%</span></div>
</div>
<div>
<label style="display:block; margin-bottom:12px;">
<div style="margin-bottom:5px; font-weight:bold;">速度控制 (ms/步):</div>
<input type="range" id="speed-slider" min="50" max="500" value="${moveSpeed}" style="width:100%;">
<div style="text-align:center; font-size:12px; color:#776e65"><span id="speed-value">${moveSpeed}</span></div>
</label>
</div>`;
document.body.appendChild(panel);
const autoPlayBtn = document.getElementById('auto-play-btn');
autoPlayBtn.onclick = function() {
if (!isAiRunning) {
if (window.canvasGame.gameOver) { alert("游戏已结束! 请先开始新游戏。"); return; }
isAiRunning = true;
this.disabled = true;
this.style.backgroundColor = '#7f8c8d';
this.innerHTML = '■ 停止AI'; // Changed to a stop button
executeThinkCycle();
} else {
isAiRunning = false;
clearTimeout(gameLoopTimeout);
this.disabled = false;
this.style.backgroundColor = '#27ae60';
this.innerHTML = '▶ 启动AI';
}
};
document.getElementById('speed-slider').oninput = function() {
moveSpeed = parseInt(this.value);
document.getElementById('speed-value').textContent = moveSpeed;
};
}
function updateStatsUI() {
const score = window.canvasGame.score || 0;
const maxTileVal = Math.max(0, ...window.canvasGame.board.flat());
if (score > highestScore) highestScore = score;
if (maxTileVal > highestTile) highestTile = maxTileVal;
const progress = Math.min(100, (score / TARGET_SCORE * 100)).toFixed(1);
document.getElementById('games-count').textContent = gamesPlayed;
document.getElementById('high-score').textContent = highestScore.toLocaleString();
document.getElementById('max-tile').textContent = highestTile;
document.getElementById('target-progress').textContent = `${progress}%`;
}
window.addEventListener('load', () => { setTimeout(createControlPanel, 1000); });
})();