2048 AI Solver

使用 WebAssembly 加速的 2048 AI求解器,支持合成丘丘王

// ==UserScript==
// @name         2048 AI Solver
// @namespace    https://github.com/MakotoArai-CN/2048-ai-solver
// @version      1.0.0
// @author       MakotoArai
// @description  使用 WebAssembly 加速的 2048 AI求解器,支持合成丘丘王
// @license       MIT
// @icon         https://play2048.co/faviconSimple.svg
// @include      *://*2048*/*
// @match        *://*.mihoyo.com/*/event/*/index.html*
// @match        *://act.hoyoverse.com/*/event/*/index.html*
// @match        *://play2048.co/*
// @match        *://2048game.com/*
// @connect      raw.githubusercontent.com
// @connect      raw.kkgithub.com
// @connect      wget.la
// @connect      hk.gh-proxy.com
// @connect      hub.glowp.xyz
// @connect      ghfast.top
// @connect      ghproxy.net
// @connect      gh.catmak.name
// @connect      fastly.jsdelivr.net
// @connect      g.blfrp.cn
// @connect      github.3x25.com
// @grant        GM_deleteValue
// @grant        GM_getValue
// @grant        GM_notification
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// ==/UserScript==

(function () {
	'use strict';

	const t=['https://raw.githubusercontent.com/ziap/2048-ai/master/ai.wasm','https://raw.kkgithub.com/ziap/2048-ai/master/ai.wasm','https://wget.la/https://raw.githubusercontent.com/ziap/2048-ai/master/ai.wasm','https://hk.gh-proxy.com/https://raw.githubusercontent.com/ziap/2048-ai/master/ai.wasm','https://hub.glowp.xyz/https://raw.githubusercontent.com/ziap/2048-ai/master/ai.wasm','https://ghfast.top/https://raw.githubusercontent.com/ziap/2048-ai/master/ai.wasm','https://ghproxy.net/https://raw.githubusercontent.com/ziap/2048-ai/master/ai.wasm','https://gh.catmak.name/https://raw.githubusercontent.com/ziap/2048-ai/master/ai.wasm','https://fastly.jsdelivr.net/gh/ziap/2048-ai@master/ai.wasm','https://g.blfrp.cn/https://raw.githubusercontent.com/ziap/2048-ai/master/ai.wasm','https://github.3x25.com/https://raw.githubusercontent.com/ziap/2048-ai/master/ai.wasm'],n='2048_wasm_cache',e='1.0.0';class o{constructor(){this.dbName='2048SolverDB',this.storeName='wasmStore',this.db=null;}async init(){return new Promise((t,n)=>{const e=indexedDB.open(this.dbName,1);e.onerror=()=>n(e.error),e.onsuccess=()=>{this.db=e.result,t();},e.onupgradeneeded=t=>{const n=t.target.result;n.objectStoreNames.contains(this.storeName)||n.createObjectStore(this.storeName);};})}async get(){return this.db||await this.init(),new Promise((t,o)=>{const s=this.db.transaction([this.storeName],'readonly').objectStore(this.storeName).get(n);s.onerror=()=>o(s.error),s.onsuccess=()=>{const n=s.result;if(n)return Date.now()-n.timestamp>6048e5?(console.log('🗑️ WASM 缓存已过期,需要重新下载'),this.delete(),void t(null)):n.version!==e?(console.log('🔄 WASM 版本更新,需要重新下载'),this.delete(),void t(null)):void t(n);t(null);};})}async set(t){this.db||await this.init();const o={data:t,timestamp:Date.now(),version:e};return new Promise((t,e)=>{const s=this.db.transaction([this.storeName],'readwrite').objectStore(this.storeName).put(o,n);s.onerror=()=>e(s.error),s.onsuccess=()=>t();})}async delete(){return this.db||await this.init(),new Promise((t,e)=>{const o=this.db.transaction([this.storeName],'readwrite').objectStore(this.storeName).delete(n);o.onerror=()=>e(o.error),o.onsuccess=()=>t();})}}function s(t){return new Promise((n,e)=>{GM_xmlhttpRequest({method:'GET',url:t,responseType:'arraybuffer',timeout:1e4,onload:t=>{200===t.status?n(t.response):e(new Error(`HTTP ${t.status}`));},onerror:t=>e(t),ontimeout:()=>e(new Error('Timeout'))});})}function r(t){const n=new Blob(['\n    // WASM 配置常量\n    const WASM_PAGE_SIZE = 65536;\n    const INITIAL_MEMORY = 134217728; // 128MB\n    \n    let wasmModule = null;\n    let wasmMemory = null;\n    let HEAP32 = null;\n    \n    // Emscripten 运行时函数\n    function _abort() {\n      throw new Error(\'abort() called\');\n    }\n    \n    function _clock_gettime(clk_id, tp) {\n      let now;\n      if (clk_id === 0) {\n        now = Date.now();\n      } else if (clk_id === 1 || clk_id === 4) {\n        now = performance.now();\n      } else {\n        return -1;\n      }\n      \n      if (HEAP32 && tp) {\n        HEAP32[tp >> 2] = (now / 1000) | 0;\n        HEAP32[(tp + 4) >> 2] = ((now % 1000) * 1000000) | 0;\n      }\n      return 0;\n    }\n    \n    function _emscripten_run_script(ptr) {\n      // 在 Worker 中不执行脚本\n      console.log(\'[Worker] emscripten_run_script called\');\n    }\n    \n    // 更新堆视图\n    function updateGlobalBufferViews(buf) {\n      HEAP32 = new Int32Array(buf);\n    }\n    \n    // 消息处理\n    onmessage = async (e) => {\n      if (e.data.type === \'init\') {\n        try {\n          // 1. 创建 WebAssembly.Memory\n          wasmMemory = new WebAssembly.Memory({\n            initial: INITIAL_MEMORY / WASM_PAGE_SIZE,\n            maximum: INITIAL_MEMORY / WASM_PAGE_SIZE\n          });\n          \n          updateGlobalBufferViews(wasmMemory.buffer);\n          \n          // 2. 构建导入对象 - 关键:必须匹配 Emscripten 的命名空间结构\n          //    Import namespace "a" 包含:\n          //    - Import #0 "a" "b" -> _abort\n          //    - Import #1 "a" "c" -> _clock_gettime  \n          //    - Import #2 "a" "d" -> _emscripten_run_script\n          //    - Import #3 "a" "a" -> wasmMemory (Memory 对象)\n          const importObject = {\n            a: {\n              b: _abort,\n              c: _clock_gettime,\n              d: _emscripten_run_script,\n              a: wasmMemory  // 直接传入 Memory 对象,而不是包装对象\n            }\n          };\n          \n          // 3. 实例化 WASM\n          const wasmBytes = new Uint8Array(e.data.wasmBuffer);\n          const { instance } = await WebAssembly.instantiate(wasmBytes, importObject);\n          \n          wasmModule = instance.exports;\n          \n          // 4. 调用初始化函数\n          // f -> ___wasm_call_ctors (ATINIT 构造函数)\n          if (wasmModule.f) {\n            wasmModule.f();\n          }\n          \n          // h -> main (设置内部状态)\n          if (wasmModule.h) {\n            wasmModule.h();\n          }\n          \n          console.log(\'[Worker] WASM initialized successfully\');\n          postMessage({ type: \'ready\' });\n          \n        } catch (error) {\n          console.error(\'[Worker] Init error:\', error);\n          postMessage({ \n            type: \'error\', \n            error: error.message || String(error),\n            stack: error.stack \n          });\n        }\n        \n      } else if (e.data.type === \'work\') {\n        try {\n          // g -> _jsWork(row1, row2, row3, row4, dir)\n          const { board, dir } = e.data;\n          \n          if (!wasmModule || !wasmModule.g) {\n            throw new Error(\'WASM module not initialized\');\n          }\n          \n          const result = wasmModule.g(\n            board[0], \n            board[1], \n            board[2], \n            board[3], \n            dir\n          );\n          \n          postMessage({ type: \'result\', result });\n          \n        } catch (error) {\n          console.error(\'[Worker] Work error:\', error);\n          postMessage({ \n            type: \'error\', \n            error: error.message || String(error) \n          });\n        }\n      }\n    };\n    \n    // 错误处理\n    onerror = (error) => {\n      console.error(\'[Worker] Uncaught error:\', error);\n      postMessage({ \n        type: \'error\', \n        error: error.message || String(error) \n      });\n    };\n  '],{type:'application/javascript'}),e=new Worker(URL.createObjectURL(n));return e.postMessage({type:'init',wasmBuffer:t}),e}class Solver{constructor(){this.workers=[],this.wasmReady=false,this.pendingInit=null;}async init(){if(!this.wasmReady)return this.pendingInit||(this.pendingInit=(async()=>{console.log('🔧 初始化求解器...');try{const n=await async function(){return console.log(`🎯 加载模式: ${'online'.toUpperCase()}`),async function(){console.log('🔍 检查 WASM 缓存...');const n=new o,e=await n.get();if(e)return console.log('✅ 使用缓存的 WASM'),e.data;console.log('📥 下载 WASM 文件...');for(let o=0;o<t.length;o++){const e=t[o];try{console.log(`🌐 尝试源 [${o+1}/${t.length}]: ${e.split('/').slice(0,3).join('/')}`);const r=await s(e);return console.log(`✅ 下载成功 (${(r.byteLength/1024).toFixed(1)} KB)`),await n.set(r),console.log('💾 已缓存 WASM'),r}catch(r){if(console.warn(`❌ 源 ${o+1} 失败:`,r),o===t.length-1)throw new Error('所有 WASM 源均下载失败')}}throw new Error('无法下载 WASM')}()}();console.log(`📦 WASM 已加载: ${(n.byteLength/1024).toFixed(1)} KB`);const e=[];for(let t=0;t<4;t++){const o=r(n);this.workers.push(o),e.push(new Promise((n,e)=>{const s=setTimeout(()=>{e(new Error(`Worker ${t+1} 初始化超时`));},1e4);o.onmessage=o=>{'ready'===o.data.type?(clearTimeout(s),console.log(`✅ Worker ${t+1}/4 已就绪`),n()):'error'===o.data.type&&(clearTimeout(s),console.error(`❌ Worker ${t+1} 初始化失败:`,o.data.error),o.data.stack&&console.error('Stack:',o.data.stack),e(new Error(o.data.error)));},o.onerror=n=>{clearTimeout(s),console.error(`❌ Worker ${t+1} 错误:`,n),e(n);};}));}await Promise.all(e),this.wasmReady=!0,console.log('✅ 求解器已就绪 (4 Workers)');}catch(n){throw console.error('❌ 求解器初始化失败:',n),this.cleanup(),n}})()),this.pendingInit}async getBestMove(t){this.wasmReady||await this.init();const n=function(t){const n=[];for(let e=0;e<4;e++){let o=0;for(let n=0;n<4;n++){const s=t[e][n];o|=(15&(0===s?0:Math.log2(s)))<<12-4*n;}n.push(o);}return n}(t),e=['up','right','down','left'],o=[0,1,2,3].map((t,e)=>new Promise(o=>{const s=this.workers[e];let r=false;const i=t=>{r||('result'===t.data.type?(r=true,s.removeEventListener('message',i),o(t.data.result)):'error'===t.data.type&&(r=true,s.removeEventListener('message',i),console.error(`Worker ${e} 计算错误:`,t.data.error),o(0)));};s.addEventListener('message',i),s.postMessage({type:'work',board:n,dir:t}),setTimeout(()=>{r||(r=true,s.removeEventListener('message',i),console.warn(`Worker ${e} 超时`),o(0));},5e3);})),s=await Promise.all(o);let r=0,i=s[0];for(let a=1;a<4;a++)s[a]>i&&(i=s[a],r=a);return console.log(`🎯 方向得分: ↑${s[0].toFixed(1)} →${s[1].toFixed(1)} ↓${s[2].toFixed(1)} ←${s[3].toFixed(1)} | 选择: ${e[r]}`),e[r]}cleanup(){this.workers.forEach(t=>{try{t.terminate();}catch(n){}}),this.workers=[],this.wasmReady=false,this.pendingInit=null;}destroy(){console.log('🗑️ 销毁求解器'),this.cleanup();}}function i(){var t,n;try{const e=document.querySelectorAll('*');for(let r=0;r<e.length;r++){const o=e[r];if(null==(n=null==(t=o.__vue__)?void 0:t.$data)?void 0:n.gridData){const t=o.__vue__.$data;if(Array.isArray(t.gridData)&&4===t.gridData.length)return {gridData:JSON.parse(JSON.stringify(t.gridData)),score:t.score??0,maxTile:Math.max(...t.gridData.flat()),isPlaying:t.isGamePlaying??!0}}}const o=document.querySelectorAll('.tile');if(o.length>0){const t=Array(4).fill(0).map(()=>Array(4).fill(0));for(let n=0;n<o.length;n++){const e=o[n].className.split(' '),s=e.find(t=>t.startsWith('tile-position-')),r=e.find(t=>t.startsWith('tile-')&&!t.includes('position'));if(s&&r){const n=s.match(/tile-position-(\d)-(\d)/);if(n){const e=n[1],o=n[2],s=parseInt(r.replace('tile-',''));e&&o&&s&&(t[parseInt(o)-1][parseInt(e)-1]=s);}}}return {gridData:t,score:0,maxTile:Math.max(...t.flat()),isPlaying:!0}}const s=[document.querySelector('.game-container'),document.querySelector('#game-container'),document.querySelector('[class*="game"]'),document.querySelector('.board')];for(let t=0;t<s.length;t++){const n=s[t];if(n){const t=a(n);if(t)return t}}return null}catch(e){return console.error('游戏检测失败:',e),null}}function a(t){var n;try{const e=t.querySelectorAll('[class*="cell"], [class*="tile"]');if(0===e.length)return null;const o=Array(4).fill(0).map(()=>Array(4).fill(0));let s=!1;for(let t=0;t<e.length;t++){const r=null==(n=e[t].textContent)?void 0:n.trim(),i=r?parseInt(r):0;if(i>0){s=!0;const n=Math.floor(t/4),e=t%4;n<4&&e<4&&(o[n][e]=i);}}return s?{gridData:o,score:0,maxTile:Math.max(...o.flat()),isPlaying:!0}:null}catch(e){return null}}class IsolatedUI{constructor(t){this.isDragging=false,this.dragOffset={x:0,y:0},this.callbacks=t,this.container=document.createElement('div'),this.container.setAttribute('data-solver-ui','true'),this.container.style.cssText='all: initial; position: fixed; z-index: 2147483647;',this.shadowRoot=this.container.attachShadow({mode:'closed'}),this.render(),this.attachEventListeners(),document.documentElement.appendChild(this.container);}render(){this.shadowRoot.innerHTML='\n      <style>\n        /* 重置所有样式,确保不受外部影响 */\n        * {\n          margin: 0;\n          padding: 0;\n          box-sizing: border-box;\n          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\n        }\n        \n        :host {\n          all: initial;\n          display: block;\n          position: fixed;\n          top: 20px;\n          right: 20px;\n          z-index: 2147483647;\n        }\n        \n        .panel {\n          background: linear-gradient(135deg, #0094f7ff 0%, #ff019eff 100%);\n          color: #ffffff;\n          padding: 16px;\n          border-radius: 12px;\n          min-width: 220px;\n          box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n          cursor: move;\n          user-select: none;\n          backdrop-filter: blur(10px);\n          border: 1px solid rgba(255, 255, 255, 0.18);\n          z-index: 9999999999;\n        }\n        \n        .panel-header {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          margin-bottom: 12px;\n          padding-bottom: 12px;\n          border-bottom: 1px solid rgba(255, 255, 255, 0.2);\n        }\n        \n        .panel-title {\n          font-size: 16px;\n          font-weight: 600;\n          display: flex;\n          align-items: center;\n          gap: 6px;\n        }\n        \n        .close-btn {\n          background: rgba(255, 255, 255, 0.2);\n          border: none;\n          color: white;\n          width: 24px;\n          height: 24px;\n          border-radius: 50%;\n          cursor: pointer;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          font-size: 18px;\n          line-height: 1;\n          transition: all 0.2s;\n        }\n        \n        .close-btn:hover {\n          background: rgba(255, 255, 255, 0.3);\n          box-shadow: 0 0 10px rgba(240, 61, 61, 1);\n        }\n        \n        .controls {\n          display: flex;\n          flex-direction: column;\n          gap: 8px;\n        }\n        \n        .btn {\n          padding: 10px 16px;\n          border: none;\n          border-radius: 8px;\n          font-size: 14px;\n          font-weight: 500;\n          cursor: pointer;\n          transition: all 0.3s;\n          text-align: center;\n          outline: none;\n        }\n        \n        .btn:active {\n          transform: scale(0.98);\n        }\n        \n        .btn-start {\n          background: rgba(255, 255, 255, 0.9);\n          color: #667eea;\n        }\n        \n        .btn-start:hover {\n          background: rgba(255, 255, 255, 1);\n          box-shadow: 0 4px 12px rgba(255, 255, 255, 0.3);\n        }\n        \n        .btn-stop {\n          background: rgba(239, 68, 68, 0.9);\n          color: white;\n        }\n        \n        .btn-stop:hover {\n          background: rgba(239, 68, 68, 1);\n          box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);\n        }\n        \n        .status {\n          margin-top: 12px;\n          padding: 10px;\n          background: rgba(0, 0, 0, 0.2);\n          border-radius: 6px;\n          font-size: 13px;\n          text-align: center;\n          border: 1px solid rgba(255, 255, 255, 0.1);\n        }\n        \n        .status-dot {\n          display: inline-block;\n          width: 8px;\n          height: 8px;\n          border-radius: 50%;\n          margin-right: 6px;\n          animation: pulse 2s infinite;\n        }\n        \n        .status-ready .status-dot {\n          background: #10b981;\n        }\n        \n        .status-running .status-dot {\n          background: #f59e0b;\n        }\n        \n        .status-stopped .status-dot {\n          background: #6b7280;\n        }\n        \n        .status-error .status-dot {\n          background: #ef4444;\n        }\n        \n        @keyframes pulse {\n          0%, 100% {\n            opacity: 1;\n          }\n          50% {\n            opacity: 0.5;\n          }\n        }\n        \n        .minimize-btn {\n          background: rgba(255, 255, 255, 0.2);\n          border: none;\n          color: white;\n          width: 24px;\n          height: 24px;\n          border-radius: 50%;\n          cursor: pointer;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          font-size: 14px;\n          margin-right: 4px;\n          transition: all 0.2s;\n        }\n        \n        .minimize-btn:hover {\n          background: rgba(255, 255, 255, 0.3);\n        }\n        \n        .panel.minimized .controls,\n        .panel.minimized .status {\n          display: none;\n        }\n        \n        .panel.minimized {\n          min-width: auto;\n        }\n      </style>\n      \n      <div class="panel" id="panel">\n        <div class="panel-header">\n          <div class="panel-title">\n            <span>2048 AI</span>\n          </div>\n          <div style="display: flex; gap: 4px;">\n            <button class="minimize-btn" id="minimizeBtn" title="最小化">➖</button>\n            <button class="close-btn" id="closeBtn" title="关闭">❌</button>\n          </div>\n        </div>\n        \n        <div class="controls">\n          <button class="btn btn-start" id="startBtn">开始求解</button>\n          <button class="btn btn-stop" id="stopBtn" style="display: none;">停止</button>\n        </div>\n        \n        <div class="status status-ready" id="status">\n          <span class="status-dot"></span>\n          <span id="statusText">就绪</span>\n        </div>\n      </div>\n    ';}attachEventListeners(){const t=this.shadowRoot.getElementById('panel'),n=this.shadowRoot.getElementById('startBtn'),e=this.shadowRoot.getElementById('stopBtn'),o=this.shadowRoot.getElementById('closeBtn'),s=this.shadowRoot.getElementById('minimizeBtn'),r=this.shadowRoot.getElementById('status');n.addEventListener('click',t=>{t.stopPropagation(),n.style.display='none',e.style.display='block',r.className='status status-running',this.updateStatusText('运行中...'),this.callbacks.onStart();}),e.addEventListener('click',t=>{t.stopPropagation(),this.resetButtons(),r.className='status status-stopped',this.updateStatusText('已停止'),this.callbacks.onStop();}),o.addEventListener('click',t=>{t.stopPropagation(),this.destroy();}),s.addEventListener('click',n=>{n.stopPropagation(),t.classList.toggle('minimized'),s.textContent=t.classList.contains('minimized')?'+':'−';}),t.addEventListener('mousedown',n=>{if('BUTTON'===n.target.tagName)return;this.isDragging=true;const e=this.container.getBoundingClientRect();this.dragOffset.x=n.clientX-e.left,this.dragOffset.y=n.clientY-e.top,t.style.cursor='grabbing';}),document.addEventListener('mousemove',t=>{if(!this.isDragging)return;const n=t.clientX-this.dragOffset.x,e=t.clientY-this.dragOffset.y,o=window.innerWidth-this.container.offsetWidth,s=window.innerHeight-this.container.offsetHeight;this.container.style.left=Math.max(0,Math.min(n,o))+'px',this.container.style.top=Math.max(0,Math.min(e,s))+'px',this.container.style.right='auto';}),document.addEventListener('mouseup',()=>{this.isDragging&&(this.isDragging=false,t.style.cursor='move');}),t.addEventListener('selectstart',t=>t.preventDefault());}updateStatusText(t){const n=this.shadowRoot.getElementById('statusText');n&&(n.textContent=t);}setStatus(t,n){const e=this.shadowRoot.getElementById('status');e&&(e.className=`status status-${t}`),n&&this.updateStatusText(n);}resetButtons(){const t=this.shadowRoot.getElementById('startBtn'),n=this.shadowRoot.getElementById('stopBtn');t&&n&&(t.style.display='block',n.style.display='none');}destroy(){this.callbacks.onDestroy&&this.callbacks.onDestroy(),this.container.remove();}}let l=null;function c(t,n){if(l)return void console.warn('⚠️ UI已存在,跳过创建');const e=document.querySelector('[data-solver-ui="true"]');e&&(console.warn('⚠️ 检测到已存在的UI元素,移除后重新创建'),e.remove()),l=new IsolatedUI({onStart:t,onStop:n,onDestroy:()=>{l=null;}}),console.log('✅ 隔离UI已创建');}function d(t){l&&l.updateStatusText(t);}function h(t,n){l&&(l.setStatus(t,n),'stopped'!==t&&'ready'!==t&&'error'!==t||l.resetButtons());}function u(){l&&(l.destroy(),l=null);const t=document.querySelector('[data-solver-ui="true"]');t&&t.remove();}class AutoSolver{constructor(){this.isRunning=false,this.intervalId=null,this.lastGrid=null,this.lastChangeTime=null,this.totalMoves=0,this.initialized=false,this.startTime=null,this.NO_CHANGE_TIMEOUT=5e3,this.MIHOYO_DELAY=80,this.DEFAULT_DELAY=30,this.solver=new Solver,this.isMihoyoSite=this.detectMihoyoSite(),console.log('🌐 网站类型: '+(this.isMihoyoSite?'米哈游 (80ms)':'其他 (30ms)'));}detectMihoyoSite(){const t=window.location.hostname.toLowerCase();return ['mihoyo.com','hoyoverse.com','miyoushe.com','yuanshen.com','starrail.com','honkaiimpact3.com'].some(n=>t.includes(n))}getMoveDelay(){return this.isMihoyoSite?this.MIHOYO_DELAY:this.DEFAULT_DELAY}async init(){if(this.initialized)return void console.warn('⚠️ 求解器已初始化,跳过重复初始化');this.initialized=true,console.log('🎮 2048 AI求解器启动');const t=i();t?console.log('✅ 检测到游戏:',t):console.warn('⚠️ 未检测到游戏,将在后台等待...'),this.solver.init().then(()=>{h('ready','求解器已就绪');}).catch(t=>{console.error('❌ 求解器初始化失败:',t),h('error','初始化失败');}),c(()=>this.start(),()=>this.stop()),GM_registerMenuCommand('🚀 开始求解',()=>this.start()),GM_registerMenuCommand('⏹ 停止求解',()=>this.stop()),GM_registerMenuCommand(`⚡ 当前速度: ${this.getMoveDelay()}ms`,()=>{alert(`当前网站: ${this.isMihoyoSite?'米哈游':'其他'}\n移动延迟: ${this.getMoveDelay()}ms`);}),GM_registerMenuCommand('🔄 重置UI',()=>{u(),setTimeout(()=>{c(()=>this.start(),()=>this.stop());},100);}),GM_registerMenuCommand('🗑️ 清除缓存',async()=>{await async function(){const t=new o;await t.delete(),console.log('🗑️ WASM 缓存已清除');}(),alert('缓存已清除,请刷新页面');}),GM_registerMenuCommand('❌ 销毁UI',()=>{this.stop(),u();});}async start(){if(this.isRunning)console.warn('⚠️ 已在运行中');else {this.isRunning=true,this.lastGrid=null,this.lastChangeTime=Date.now(),this.totalMoves=0,this.startTime=Date.now(),console.log('🚀 开始自动求解'),console.log(`⚡ 移动速度: ${this.getMoveDelay()}ms`),h('running','初始化中...');try{await this.solver.init(),this.runLoop();}catch(t){console.error('❌ 启动失败:',t),h('error','启动失败'),this.isRunning=false;}}}stop(t){this.isRunning=false,null!==this.intervalId&&(clearTimeout(this.intervalId),this.intervalId=null);const n=t||'已停止';if(console.log('⏹ 停止求解:',n),this.totalMoves>0&&this.startTime){const t=(Date.now()-this.startTime)/1e3,n=this.totalMoves/t;console.log('📊 本次求解统计:'),console.log(`   - 总移动: ${this.totalMoves} 次`),console.log(`   - 耗时: ${t.toFixed(1)} 秒`),console.log(`   - 平均速度: ${n.toFixed(2)} 步/秒`);}h('stopped',n),this.lastGrid=null,this.lastChangeTime=null,this.totalMoves=0,this.startTime=null;}gridToString(t){return t.map(t=>t.join(',')).join('|')}gridsEqual(t,n){return t===n}async runLoop(){if(this.isRunning)try{const t=function(){const t=i();return (null==t?void 0:t.gridData)??null}();if(!t)return d('等待游戏...'),void(this.intervalId=window.setTimeout(()=>this.runLoop(),300));const n=this.gridToString(t),e=Date.now();if(null===this.lastGrid)this.lastGrid=n,this.lastChangeTime=e;else if(this.gridsEqual(n,this.lastGrid)){const t=e-this.lastChangeTime,n=Math.ceil((this.NO_CHANGE_TIMEOUT-t)/1e3);if(t>=this.NO_CHANGE_TIMEOUT)return console.error(`❌ ${this.NO_CHANGE_TIMEOUT/1e3}秒内棋盘无变化,停止求解`),this.stop(this.NO_CHANGE_TIMEOUT/1e3+'秒内无变化'),void this.showNotification('求解已停止',`检测到 ${this.NO_CHANGE_TIMEOUT/1e3} 秒内棋盘无变化,可能游戏已结束或出现异常。`,'warning');console.warn(`⚠️ 棋盘未变化 (${n}秒后停止)`);}else console.log('✅ 棋盘已更新'),this.lastGrid=n,this.lastChangeTime=e,this.totalMoves++;if(this.isGameOver(t)){console.log('🏁 游戏结束'),this.stop('游戏已结束');const n=Math.max(...t.flat());return void this.showNotification('游戏结束',`最大方块: ${n}, 总移动: ${this.totalMoves} 次`,'info')}const o=Math.max(...t.flat()),s=e-this.lastChangeTime,r=Math.ceil((this.NO_CHANGE_TIMEOUT-s)/1e3);d(s>2e3?`计算中... (${this.totalMoves}步) [${r}s]`:`计算中... (${this.totalMoves}步)`);const a=await this.solver.getBestMove(t);this.makeMove(a),d(`${a} (步数:${this.totalMoves} 最大:${o})`);const l=this.getMoveDelay();this.intervalId=window.setTimeout(()=>this.runLoop(),l);}catch(t){console.error('❌ 求解出错:',t),h('error','出错: '+t);const n=Date.now();if(this.lastChangeTime&&n-this.lastChangeTime>=this.NO_CHANGE_TIMEOUT)return void this.stop('错误且超时,已停止');this.intervalId=window.setTimeout(()=>this.runLoop(),500);}}isGameOver(t){for(let n=0;n<4;n++)for(let e=0;e<4;e++)if(0===t[n][e])return  false;for(let n=0;n<4;n++)for(let e=0;e<4;e++){const o=t[n][e];if(e<3&&t[n][e+1]===o)return  false;if(n<3&&t[n+1][e]===o)return  false}return  true}showNotification(t,n,e){console.log(`[${e.toUpperCase()}] ${t}: ${n}`),'undefined'!=typeof GM_notification&&GM_notification({title:`${t}`,text:n,timeout:5e3});}makeMove(t){const n={up:'ArrowUp',right:'ArrowRight',down:'ArrowDown',left:'ArrowLeft'}[t],e={ArrowUp:38,ArrowRight:39,ArrowDown:40,ArrowLeft:37}[n],o=new KeyboardEvent('keydown',{key:n,code:n,keyCode:e,which:e,bubbles:true}),s=new KeyboardEvent('keyup',{key:n,code:n,keyCode:e,which:e,bubbles:true});document.dispatchEvent(o),setTimeout(()=>document.dispatchEvent(s),10);}}if(window.__2048_SOLVER_INITIALIZED__)console.warn('⚠️ 2048求解器已存在,跳过重复初始化');else {window.__2048_SOLVER_INITIALIZED__=true;const t=new AutoSolver;window.__2048_SOLVER_INSTANCE__=t,'loading'===document.readyState?document.addEventListener('DOMContentLoaded',()=>{setTimeout(()=>t.init(),100);}):setTimeout(()=>t.init(),100),console.log('✅ 2048求解器脚本已加载');}

})();