// ==UserScript== // @name 2048 AI Solver // @namespace https://github.com/MakotoArai-CN/2048-ai-solver // @version 1.0.0 // @author MakotoArai // @description 使用 WebAssembly 加速的 2048 AI求解器,支持合成丘丘王 // @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{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;r0){const t=Array(4).fill(0).map(()=>Array(4).fill(0));for(let n=0;nt.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;tArray(4).fill(0));let s=!1;for(let t=0;t0){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 \n \n
\n
\n
\n 2048 AI\n
\n
\n \n \n
\n
\n \n
\n \n \n
\n \n
\n \n 就绪\n
\n
\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求解器脚本已加载');} })();