2048 AI Solver

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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求解器脚本已加载');}

})();