保存强化版提升前的主版本工作区
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -37,7 +37,7 @@ function boot(){
|
||||
function tap(x,y){touch('TouchStart',x,y);touch('TouchEnd',x,y);tick();}
|
||||
function button(name){const b=api.buttons().find(b=>b.label===name&&b.enabled);if(!b)throw Error('缺少按钮 '+name);tap(b.x+b.w/2,b.y+b.h/2);}
|
||||
function cell(id){const s=api.snapshot(),p=s.centers[id];tap(p[0],p[1]+s.boardRect.y);}
|
||||
function select(index){if(api.snapshot().screen!=='select')button('选关');let n=0;while(!api.buttons().some(b=>b.label===String(index+1)||b.label===`${index+1} ✓`)){button('下一页');if(++n>30)throw Error('分页失败');}button(api.buttons().some(b=>b.label===String(index+1))?String(index+1):`${index+1} ✓`);if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');tick(300);}
|
||||
function select(index){if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');if(api.snapshot().screen!=='select')button('选关');let n=0;while(!api.buttons().some(b=>b.label===String(index+1)||b.label===`${index+1} ✓`)){button(api.snapshot().selection.chapter>Math.floor(index/10)+1?'上一页':'下一页');if(++n>30)throw Error('分页失败');}button(api.buttons().some(b=>b.label===String(index+1))?String(index+1):`${index+1} ✓`);if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');tick(300);}
|
||||
let down=false;
|
||||
const coords=e=>{const r=canvas.getBoundingClientRect();return [(e.clientX-r.left)*width/r.width,(e.clientY-r.top)*height/r.height];};
|
||||
canvas.onpointerdown=e=>{down=true;canvas.setPointerCapture(e.pointerId);touch('TouchStart',...coords(e));tick();};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<!doctype html><meta charset="utf-8"><title>提示动画对比检查</title>
|
||||
<style>body{font:16px sans-serif;line-height:1.6;padding:24px;background:#fff6df;color:#43382f}pre{white-space:pre-wrap}</style>
|
||||
<h1>提示动画对比检查</h1><p id="result">检查中…</p><pre id="details"></pre>
|
||||
<script type="module">
|
||||
const lessonMode=new URL(location.href).searchParams.get('mode')==='lesson';document.querySelector('h1').textContent=lessonMode?'新机制教学动画对比检查':'提示动画对比检查';
|
||||
const source=await(await fetch('../game.js')).text(),baseline=await(await fetch('baseline-game.js')).text();
|
||||
function boot(bundle){
|
||||
const handlers={},frames=new Map();let id=0,now=100000;
|
||||
class Clock extends Date{static now(){return now;}}
|
||||
const wx={createCanvas:()=>document.createElement('canvas'),loadFont:()=>'',getWindowInfo:()=>({windowWidth:375,windowHeight:812,pixelRatio:2,safeArea:{top:44,bottom:778}}),getMenuButtonBoundingClientRect:()=>({bottom:80}),getPerformance:()=>({now:()=>now}),getStorageSync:()=>'',setStorageSync(){},removeStorageSync(){},setInnerAudioOption(){},createInnerAudioContext:()=>({stop(){},onError(){},play(){}}),showModal(){},env:{USER_DATA_PATH:'/qa'}};
|
||||
for(const n of ['TouchStart','TouchMove','TouchEnd','TouchCancel','Hide','Show','WindowResize'])wx['on'+n]=f=>handlers[n]=f;
|
||||
const global={};new Function('wx','GameGlobal','requestAnimationFrame','cancelAnimationFrame','setTimeout','clearTimeout','Date',bundle)(wx,global,fn=>{frames.set(++id,fn);return id;},i=>frames.delete(i),()=>1,()=>{},Clock);
|
||||
const api=global.numberSwipeR2;
|
||||
const tick=(ms=16)=>{now+=ms;const entry=frames.entries().next().value;if(entry){frames.delete(entry[0]);entry[1]();}};
|
||||
const button=name=>{const b=api.buttons().find(b=>b.label===name&&b.enabled);if(!b)throw Error(name);const p={identifier:1,clientX:b.x+b.w/2,clientY:b.y+b.h/2},e={changedTouches:[p],touches:[p]};handlers.TouchStart(e);handlers.TouchEnd(e);tick();};
|
||||
tick();if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');if(api.snapshot().screen!=='select')button('选关');if(lessonMode){button('5');tick(200);}else{button('4');button('指路');tick(2000);}
|
||||
for(let i=0;i<12;i++)tick();
|
||||
return {tick,api};
|
||||
}
|
||||
const result={device:'browser Canvas, 375×812, DPR 2',framesPerTrial:120,trials:3,baselineMs:[],optimizedMs:[],staticReuseFrames:0};
|
||||
try{
|
||||
for(let trial=0;trial<3;trial++){
|
||||
for(const [kind,bundle] of [['baselineMs',baseline],['optimizedMs',source]]){
|
||||
const ui=boot(bundle),before=lessonMode?(ui.api.snapshot().lessonBoardRenders||0):(ui.api.snapshot().hintBaseHits||0),start=performance.now();for(let i=0;i<120;i++)ui.tick();result[kind].push((performance.now()-start)/120);if(kind==='optimizedMs')result.staticReuseFrames+=lessonMode?120-(ui.api.snapshot().lessonBoardRenders-before):ui.api.snapshot().hintBaseHits-before;
|
||||
document.querySelector('#details').textContent=JSON.stringify(result,null,2);await new Promise(requestAnimationFrame);
|
||||
}
|
||||
}
|
||||
const mean=a=>a.reduce((n,x)=>n+x,0)/a.length;result.baselineMeanMs=mean(result.baselineMs);result.optimizedMeanMs=mean(result.optimizedMs);result.reduction=1-result.optimizedMeanMs/result.baselineMeanMs;
|
||||
document.querySelector('#result').textContent=`完成:${lessonMode?'教学':'指路'}帧平均绘制耗时 ${result.baselineMeanMs.toFixed(2)} → ${result.optimizedMeanMs.toFixed(2)} ms/frame;${lessonMode?'示范棋盘':'静态界面'}复用 ${result.staticReuseFrames}/360 帧。浏览器对比不代表微信真机帧率。`;
|
||||
document.querySelector('#details').textContent=JSON.stringify(result,null,2);
|
||||
}catch(e){document.querySelector('#result').textContent='FAIL:'+e.message;}
|
||||
</script>
|
||||
@@ -32,19 +32,19 @@ const tick=(ms=16)=>{now+=ms;const entry=frames.entries().next().value;if(!entry
|
||||
const tap=(x,y)=>{const e={changedTouches:[{identifier:1,clientX:x,clientY:y}],touches:[{identifier:1,clientX:x,clientY:y}]};handlers.TouchStart(e);handlers.TouchEnd(e);tick();};
|
||||
const button=label=>{const b=api.buttons().find(b=>b.label===label);if(!b||!b.enabled)throw Error('缺少按钮 '+label);tap(b.x+b.w/2,b.y+b.h/2);};
|
||||
const cell=id=>{const s=api.snapshot(),[x,y]=s.centers[id];tap(x+s.boardRect.x,y+s.boardRect.y);};
|
||||
const select=i=>{if(api.snapshot().screen!=='select')button('选关');let n=0;while(!api.buttons().some(b=>b.label===String(i+1)||b.label===`${i+1} ✓`)){button('下一页');if(++n>25)throw Error('分页失败');}button(api.buttons().some(b=>b.label===String(i+1))?String(i+1):`${i+1} ✓`);if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');};
|
||||
const select=i=>{if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');if(api.snapshot().screen!=='select')button('选关');let n=0;while(!api.buttons().some(b=>b.label===String(i+1)||b.label===`${i+1} ✓`)){button(api.snapshot().selection.chapter>Math.floor(i/10)+1?'上一页':'下一页');if(++n>25)throw Error('分页失败');}button(api.buttons().some(b=>b.label===String(i+1))?String(i+1):`${i+1} ✓`);if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏');};
|
||||
const capture=()=>{const c=document.createElement('canvas');c.width=375;c.height=812;c.getContext('2d').drawImage(canvases[0],0,0);return c;};
|
||||
const samples=(route)=>{const s=api.snapshot(),ctx=canvases[0].getContext('2d');return route.map(id=>{const p=s.centers[id];return Array.from(ctx.getImageData(Math.round(p[0]-s.unit*.08),Math.round(p[1]+s.boardRect.y+s.unit*.18),5,5).data);});};
|
||||
let checked=0;const result=document.querySelector('#result');
|
||||
try{
|
||||
for(let i=0;i<levels.length;i++){
|
||||
select(i);const route=solve(levels[i])[0],n=Math.min(2,route.length-1);
|
||||
for(const id of route.slice(0,n))cell(id);
|
||||
for(const id of route.slice(0,n))cell(id);tick(1800);
|
||||
const before=samples(route.slice(0,n));const base=capture();
|
||||
button('重开');button('指路');for(const id of route.slice(0,n))cell(id);tick(300);
|
||||
button('重开');button('指路');for(const id of route.slice(0,n))cell(id);tick(1800);
|
||||
const after=samples(route.slice(0,n));
|
||||
if(JSON.stringify(before)!==JSON.stringify(after))throw Error('玩家填色像素变化:第 '+(i+1)+' 关');
|
||||
button('撤销');cell(route[n-1]);tick(300);
|
||||
if(JSON.stringify(before)!==JSON.stringify(after)){document.querySelector('#views').replaceChildren(base,capture());throw Error('玩家填色像素变化:第 '+(i+1)+' 关');}
|
||||
button('撤销');cell(route[n-1]);tick(1800);
|
||||
if(JSON.stringify(before)!==JSON.stringify(samples(route.slice(0,n))))throw Error('撤销后填色变化:'+(i+1));
|
||||
checked++;
|
||||
if(i%4===0){result.textContent=`正在检查 ${checked}/200 关…`;await new Promise(resolve=>requestAnimationFrame(resolve));}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"device": "browser Canvas, 375×812, DPR 2",
|
||||
"framesPerTrial": 120,
|
||||
"trials": 3,
|
||||
"baselineMs": [
|
||||
1.9058333332339923,
|
||||
1.4066666667660077,
|
||||
1.5700000000496706
|
||||
],
|
||||
"optimizedMs": [
|
||||
0.7458333333333333,
|
||||
0.8325000000496705,
|
||||
0.5725000001490116
|
||||
],
|
||||
"staticReuseFrames": 357,
|
||||
"baselineMeanMs": 1.627500000016557,
|
||||
"optimizedMeanMs": 0.7169444445106717,
|
||||
"reduction": 0.5594811400900903
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1,29 @@
|
||||
import fs from 'node:fs';import vm from 'node:vm';import assert from 'node:assert/strict';import path from 'node:path';import {fileURLToPath} from 'node:url';
|
||||
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');const pack=JSON.parse(fs.readFileSync(path.join(root,'src/upstream/levels.json'))),ordered=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
||||
export function boot(storage = new Map(), width = 375, height = 812) {
|
||||
const handlers = {}, frames = new Map(), played=[];
|
||||
let frameId = 0,now=100000;
|
||||
class Clock extends Date {static now(){return now;}}
|
||||
const text = [],images=[];
|
||||
const ctx = new Proxy({ drawImage:(image,x,y,w,h)=>images.push({x,y,w,h}),measureText: s => ({ width: [...String(s)].reduce((n,c)=>n+(/[^\x00-\x7f]/.test(c)?1:.55),0)*(Number(/([\d.]+)px/.exec(ctx.font)?.[1])||16) }), fillText: (s, x, y) => text.push({ s, x, y }) }, { get: (o, k) => o[k] ?? (() => { }), set: (o, k, v) => (o[k] = v, true) });
|
||||
const wx = {setInnerAudioOption:()=>{},createInnerAudioContext:()=>({stop(){},onError(){},play(){assert.ok(fs.existsSync(path.join(root,this.src)),this.src);played.push(this.src);}}), createCanvas: () => ({ getContext: () => ctx }), getWindowInfo: () => ({ windowWidth: width, windowHeight: height, pixelRatio: 2, safeArea: { top: 44, bottom: height - 34 } }), getMenuButtonBoundingClientRect: () => ({ bottom: 80 }), getStorageSync: k => storage.get(k) || '', setStorageSync: (k, v) => storage.set(k, v), removeStorageSync: k => storage.delete(k), getPerformance: () => ({ now: () => Date.now() }), showModal: () => { }, env: { USER_DATA_PATH: '/user' } };
|
||||
wx.loadFont=p=>{assert.ok(fs.existsSync(path.join(root,p)),'字体必须随包提供 '+p);return path.basename(p,'.ttf');};
|
||||
for (const name of ['TouchStart', 'TouchMove', 'TouchEnd', 'TouchCancel', 'Hide', 'Show', 'WindowResize'])
|
||||
wx['on' + name] = cb => handlers[name] = cb;
|
||||
const global = {};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(root, 'game.js'), 'utf8'), {Date:Clock, wx, GameGlobal: global, requestAnimationFrame: cb => { frames.set(++frameId, cb); return frameId; }, cancelAnimationFrame: id => frames.delete(id), setTimeout: () => 1, clearTimeout: () => { } }, { timeout: 10000 });
|
||||
const tick = () => { const entry=frames.entries().next().value;if(!entry)return;const [id,fn]=entry; frames.delete(id);text.length=0;images.length=0; fn(); };
|
||||
tick();
|
||||
const touch = (kind, x, y) => handlers[kind]({ changedTouches: [{ identifier: 1, clientX: x, clientY: y }], touches: [{ identifier: 1, clientX: x, clientY: y }] });
|
||||
const tap = (x, y) => { touch('TouchStart', x, y); touch('TouchEnd', x, y); tick(); };
|
||||
const api = global.numberSwipeR2;
|
||||
const button = label => { const b = api.buttons().find(b => b.label === label); assert.ok(b, '缺少按钮 ' + label); assert.ok(b.enabled, '按钮不可用 ' + label); tap(b.x + b.w / 2, b.y + b.h / 2); };
|
||||
const cell = id => { const s=api.snapshot(),[x,y]=s.centers[id];tap(x+s.boardRect.x,y+s.boardRect.y); };
|
||||
const select = i => { if(api.buttons().some(b=>b.label==='返回游戏'))button('返回游戏'); if (api.snapshot().screen !== 'select')
|
||||
button('选关'); const label = String(i + 1); let steps = 0; while (!api.buttons().some(b => b.label === label || b.label === label + ' ✓')) {
|
||||
button(api.snapshot().selection.chapter>Math.floor(i/10)+1?'上一页':'下一页');
|
||||
assert.ok(++steps < 25);
|
||||
} button(api.buttons().some(b => b.label === label) ? label : label + ' ✓'); if (api.buttons().some(b => b.label === '返回游戏'))
|
||||
button('返回游戏'); assert.equal(api.snapshot().level, ordered[i].id); };
|
||||
return { advance(ms){now+=ms;tick();}, api, button, cell, select, tick, handlers, frames, storage, text, played,images };
|
||||
}
|
||||
@@ -7,4 +7,4 @@ http.createServer((req,res)=>{
|
||||
const file=path.resolve(root,'.'+decodeURIComponent(new URL(req.url,'http://localhost').pathname));
|
||||
if(!file.startsWith(root+path.sep)){res.writeHead(403).end();return;}
|
||||
try{res.setHeader('Content-Type',file.endsWith('.html')?'text/html; charset=utf-8':file.endsWith('.json')?'application/json':'text/javascript; charset=utf-8');res.end(fs.readFileSync(file));}catch{res.writeHead(404).end();}
|
||||
}).listen(17859,'127.0.0.1',()=>console.log('Canvas QA: http://127.0.0.1:17859/qa/hints.html'));
|
||||
}).listen(Number(process.env.NUMBERSWIPE_QA_PORT||17859),'127.0.0.1',()=>console.log('Canvas QA: http://127.0.0.1:17859/qa/hints.html'));
|
||||
|
||||
Reference in New Issue
Block a user