linuxDo 2048 AI玩家 Plus

专为2048.linux.do设计的高性能AI

当前为 2025-07-16 提交的版本,查看 最新版本

// ==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 new 8192-merge risk detection.
    // ===================================================================================
    const workerCode = `
    self.onmessage = function(e) {
        const { board } = e.data;
        const bestMove = getNextMove(board);
        self.postMessage({ bestMove: bestMove });
    };

    // The entire AI logic (brain) is placed inside the worker.
    function simulateMove(board, direction) {
        let moved = false;
        const tempBoard = JSON.parse(JSON.stringify(board));

        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) {
            let newRow = slide(row);
            newRow = combine(newRow);
            newRow = slide(newRow);
            return newRow;
        }
        
        const originalBoardState = JSON.stringify(tempBoard);

        switch (direction) {
            case 'left':
                for (let r = 0; r < 4; r++) tempBoard[r] = operate(tempBoard[r]);
                break;
            case 'right':
                for (let r = 0; r < 4; r++) tempBoard[r] = operate(tempBoard[r].reverse()).reverse();
                break;
            case 'up':
                for (let c = 0; c < 4; c++) {
                    let column = [tempBoard[0][c], tempBoard[1][c], tempBoard[2][c], tempBoard[3][c]];
                    const newColumn = operate(column);
                    for (let r = 0; r < 4; r++) tempBoard[r][c] = newColumn[r];
                }
                break;
            case 'down':
                for (let c = 0; c < 4; c++) {
                    let column = [tempBoard[3][c], tempBoard[2][c], tempBoard[1][c], tempBoard[0][c]];
                    const newColumn = operate(column).reverse();
                    for (let r = 0; r < 4; r++) tempBoard[r][c] = newColumn[r];
                }
                break;
        }
        
        if(JSON.stringify(originalBoardState) !== JSON.stringify(tempBoard)) {
            moved = true;
            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'; // New endgame stage
        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++; if (b[i][j] <= b[i][j+1]) monoTotals.left++; }
            for (let j = 0; j < 3; j++) { if (b[j][i] >= b[j+1][i]) monoTotals.down++; if (b[j][i] <= b[j+1][i]) 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': // In endgame, prioritize separation and structure above all
            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;
    }

    /**
     * NEW v8.0: The main decision function with 8192-merge risk assessment.
     */
    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)) {
                const moveScore = evaluateByStage(simBoard);
                moveScores.push({ direction: directions[dirKey], score: moveScore, board: simBoard });
            }
        }

        if (moveScores.length === 0) return 'ArrowRight'; // Should not happen if game is not over

        // Sort moves by score, descending
        moveScores.sort((a, b) => b.score - a.score);

        // --- 8192 MERGE RISK ASSESSMENT ---
        const initial8192Count = board.flat().filter(v => v === 8192).length;
        if (initial8192Count >= 2) {
            // Find the first move that does NOT merge the 8192s
            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, choose it.
                }
            }
            // If all moves merge 8192s (desperate situation), take the best one anyway.
        }
        
        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) {
        const { bestMove } = e.data;
        updateStatsUI(false); // Update stats continuously
        
        if (!await attemptMove(bestMove)) {
            const fallbackMoves = ['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp'].filter(m => m !== bestMove);
            for (const move of fallbackMoves) { if (await attemptMove(move)) break; }
        }

        if (isAiRunning) {
            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) {
            isAiRunning = false;
            updateStatsUI(true); // Final update
            const btn = document.getElementById('auto-play-btn');
            if(btn) {
                btn.disabled = false;
                btn.style.backgroundColor = '#27ae60';
                btn.innerHTML = '▶ 启动AI';
            }
            return;
        }
        aiWorker.postMessage({ board: window.canvasGame.board });
    }
    
    // NEW v8.0: Professional control panel
    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: Arial, 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 style="margin-bottom:15px;">
                <label style="display:block; margin-bottom:12px;">
                    <div style="margin-bottom:5px; font-weight:bold;">速度控制:</div>
                    <input type="range" id="speed-slider" min="50" max="500" value="${moveSpeed}" style="width:100%;">
                    <div style="text-align:center; font-size:14px;"><span id="speed-value">${moveSpeed}ms/步</span></div>
                </label>
            </div>`;
        document.body.appendChild(panel);

        // Bind events
        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运行中...';
                if(gamesPlayed === 0 && window.canvasGame.score === 0) gamesPlayed++;
                executeThinkCycle();
            }
        };

        const speedSlider = document.getElementById('speed-slider');
        speedSlider.oninput = function() {
            moveSpeed = parseInt(this.value);
            document.getElementById('speed-value').textContent = `${moveSpeed}ms/步`;
        };
    }

    function updateStatsUI(isFinal) {
        const score = window.canvasGame.score || 0;
        const maxTileVal = Math.max(...window.canvasGame.board.flat());

        if (score > highestScore) highestScore = score;
        if (maxTileVal > highestTile) highestTile = maxTileVal;
        if (isFinal) gamesPlayed++;

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

})();