将分层反馈强化版提升为正式 main,统一协作入口并移除外部构建路径
This commit is contained in:
@@ -9,8 +9,8 @@
|
||||
@font-face{font-family:NumberSwipePlayful;src:url(../fonts/NumberSwipePlayful.ttf)}
|
||||
*{box-sizing:border-box}body{margin:0;background:#fff6df;color:#43382f;font:15px NumberSwipeSans500,sans-serif}header{padding:28px 32px 20px;border-bottom:1px solid #e5d7ba}h1{margin:0 0 8px;font:25px NumberSwipeSans700}p{margin:8px 0;line-height:1.7;color:#71604f}main{display:flex;gap:32px;padding:28px 32px;align-items:flex-start}.tools{width:270px;position:sticky;top:24px}.tools h2{font-size:17px;margin:24px 0 10px}.tools button,select{font:14px NumberSwipeSans600;cursor:pointer;border:1px solid #d8c4a0;background:#fffbf2;color:#43382f;border-radius:12px;padding:12px;margin:0 6px 8px 0}.tools button:hover{border-color:#6552d9;color:#5140b7}.tools select{width:100%}.phone{border:1px solid #d8c4a0;border-radius:24px;overflow:hidden;box-shadow:0 12px 34px #43382f14;background:#ffe69a}canvas{display:block;touch-action:none}.note{font-size:13px}#status{background:#ece7ff;color:#5140b7;border-radius:12px;padding:12px;margin-top:16px}a{color:#5140b7}@media(max-width:750px){main{padding:16px;flex-direction:column}.tools{width:100%;position:static}.phone{align-self:center}header{padding:20px}}
|
||||
</style>
|
||||
<header><h1>数字一笔 · 明亮教具室</h1><p>200关实际游戏包的 Canvas 预览。全部画面由程序绘制,字体随包提供。</p></header>
|
||||
<main><aside class="tools"><label for="size">屏幕尺寸</label><select id="size"><option>375×812</option><option>320×568</option><option>375×667</option><option>390×844</option><option>414×896</option></select><h2>真实关卡样张</h2><div id="samples"></div><h2>指定关卡</h2><select id="level"></select><button id="partial">画前几步</button><button id="hint">显示指路</button><button id="clear">完成本关</button><button id="select">返回选关</button><p class="note">在右侧直接点击或滑动。减少动态与音效开关在游戏选关页的“设置”中。辅助演示按钮仅属于此验收页。</p><div id="status">加载字体与游戏包…</div><p class="note" id="audio-status" role="status">音效:点击游戏后播放</p><p class="note">这是浏览器预览,微信手机上的字体、触摸与性能仍需真机验收。</p><p><a href="hints.html" target="_blank">运行200关提示像素检查</a></p></aside><div class="phone" id="phone"></div></main>
|
||||
<header><h1>数字一笔 · 分层反馈独立实验版</h1><p>200关实际游戏包的 Canvas 预览。全部画面由程序绘制,字体随包提供。</p></header>
|
||||
<main><aside class="tools"><label for="size">屏幕尺寸</label><select id="size"><option>375×812</option><option>320×568</option><option>375×667</option><option>390×844</option><option>414×896</option></select><label for="feedback-mode">反馈对照</label><select id="feedback-mode"><option value="enhanced">强化版</option><option value="original">原版</option></select><h2>真实关卡样张</h2><div id="samples"></div><h2>指定关卡</h2><select id="level"></select><button id="partial">画前几步</button><button id="hint">显示指路</button><button id="clear">完成本关</button><button id="select">返回选关</button><p class="note">在右侧直接点击或滑动。音乐、音效与振动开关在游戏选关页的“设置”中。辅助演示按钮仅属于此验收页。</p><div id="status">加载字体与游戏包…</div><p class="note" id="audio-status" role="status">音效:点击游戏后播放</p><p class="note">本页不触发触觉。原版/强化版切换用于音色和表现对照;iPhone、Android触觉与性能仍需真机验收。</p><p><a href="hints.html" target="_blank">运行200关提示像素检查</a></p></aside><div class="phone" id="phone"></div></main>
|
||||
<script type="module">
|
||||
import {solve} from '../src/upstream/rules.mjs';
|
||||
import {browserAudio} from './browser-audio.mjs';
|
||||
@@ -18,7 +18,7 @@ await Promise.all(['NumberSwipeSans500','NumberSwipeSans600','NumberSwipeSans700
|
||||
const source=await(await fetch('../game.js')).text(),pack=await(await fetch('../src/upstream/levels.json')).json();
|
||||
const levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
||||
const size=document.querySelector('#size'),levelSelect=document.querySelector('#level'),status=document.querySelector('#status'),phone=document.querySelector('#phone');
|
||||
let runtime;
|
||||
let runtime,feedbackMode='enhanced';
|
||||
for(const [i,l] of levels.entries()){const o=document.createElement('option');o.value=i;o.textContent=`第 ${i+1} 关 · ${l.title}`;levelSelect.append(o);}
|
||||
const samples=[['基础大块',3],['异形拼块',levels.findIndex(l=>l.cells.some(c=>c.shape)&&l.cells.length>=12)],['通道与数字',levels.findIndex(l=>l.features.includes('direction')&&l.cells.some(c=>c.clue&&c.direction))],['双色与奇偶',levels.findIndex(l=>l.features.includes('alternation')&&l.features.includes('parity'))],['节奏门',levels.findIndex(l=>l.doors?.length)],['六边墙',levels.findIndex(l=>l.geometry==='hex'&&l.walls?.length)],['密集36块',levels.findIndex(l=>l.id==='r2-163')]];
|
||||
for(const [name,index] of samples){const b=document.createElement('button');b.textContent=name;b.onclick=()=>show(index);document.querySelector('#samples').append(b);}
|
||||
@@ -28,7 +28,7 @@ function boot(){
|
||||
const later=(callback,delay)=>{const timer=setTimeout(()=>{timers.delete(timer);callback();},delay);timers.add(timer);return timer;};
|
||||
const cancel=timer=>{clearTimeout(timer);timers.delete(timer);};
|
||||
class Clock extends Date{static now(){return now;}}
|
||||
const wx={createCanvas(){const c=document.createElement('canvas');if(!canvases.length)canvases.push(c);return c;},loadFont:p=>p.split('/').at(-1).replace('.ttf',''),getWindowInfo:()=>({windowWidth:width,windowHeight:height,pixelRatio:2,safeArea:{top:24,bottom:height-24}}),getMenuButtonBoundingClientRect:()=>({bottom:42}),getPerformance:()=>({now:()=>now}),getStorageSync:k=>store.get(k)||'',setStorageSync:(k,v)=>store.set(k,v),removeStorageSync:k=>store.delete(k),setInnerAudioOption:sound.setInnerAudioOption,createInnerAudioContext:sound.createInnerAudioContext,showModal(){},env:{USER_DATA_PATH:'/preview'}};
|
||||
const wx={__feedbackPreview:true,__feedbackMode:typeof feedbackMode==='undefined'?'enhanced':feedbackMode,createCanvas(){const c=document.createElement('canvas');if(!canvases.length)canvases.push(c);return c;},loadFont:p=>p.split('/').at(-1).replace('.ttf',''),getWindowInfo:()=>({windowWidth:width,windowHeight:height,pixelRatio:2,safeArea:{top:24,bottom:height-24}}),getMenuButtonBoundingClientRect:()=>({bottom:42}),getPerformance:()=>({now:()=>now}),getStorageSync:k=>store.get(k)||'',setStorageSync:(k,v)=>store.set(k,v),removeStorageSync:k=>store.delete(k),setInnerAudioOption:sound.setInnerAudioOption,createInnerAudioContext:sound.createInnerAudioContext,showModal(){},env:{USER_DATA_PATH:'/preview'}};
|
||||
for(const n of ['TouchStart','TouchMove','TouchEnd','TouchCancel','Hide','Show','WindowResize'])wx['on'+n]=cb=>handlers[n]=cb;
|
||||
const global={};new Function('wx','GameGlobal','requestAnimationFrame','cancelAnimationFrame','setTimeout','clearTimeout','Date',source)(wx,global,fn=>{frames.set(++frame,fn);return frame;},id=>frames.delete(id),later,cancel,Clock);
|
||||
const api=global.numberSwipeR2,canvas=canvases[0];canvas.style.width=width+'px';canvas.style.height=height+'px';phone.style.width=width+'px';phone.replaceChildren(canvas);
|
||||
@@ -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.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);}
|
||||
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.page>Math.floor(index/15)+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();};
|
||||
@@ -47,6 +47,7 @@ function boot(){
|
||||
}
|
||||
function show(index){runtime?.dispose();runtime=boot();runtime.select(index);levelSelect.value=index;update();}
|
||||
function update(){const s=runtime.api.snapshot();status.textContent=`第 ${s.displayOrder} 关 · ${s.path.length} / ${levels.find(l=>l.id===s.level).cells.length} 步 · ${s.theme}`;}
|
||||
document.querySelector('#feedback-mode').onchange=e=>{feedbackMode=e.target.value;show(Number(levelSelect.value));};
|
||||
levelSelect.onchange=()=>show(Number(levelSelect.value));size.onchange=()=>show(Number(levelSelect.value));
|
||||
document.querySelector('#partial').onclick=()=>{const l=levels.find(l=>l.id===runtime.api.snapshot().level);runtime.button('重开');for(const id of solve(l)[0].slice(0,Math.min(5,l.cells.length-1)))runtime.cell(id);runtime.tick(300);update();};
|
||||
document.querySelector('#hint').onclick=()=>{runtime.button('指路');update();};
|
||||
|
||||
@@ -14,6 +14,7 @@ export function browserAudio(report){
|
||||
player.addEventListener('error',()=>error(player.error));
|
||||
return {
|
||||
set src(path){player.src=new URL('../'+path,location.href).href;},
|
||||
set loop(value){player.loop=value;},
|
||||
set volume(value){player.volume=value;},
|
||||
set obeyMuteSwitch(value){},
|
||||
onError(callback){errorHandler=callback;},
|
||||
|
||||
@@ -7,7 +7,7 @@ const source=await(await fetch('../game.js')).text(),baseline=await(await fetch(
|
||||
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'}};
|
||||
const wx={__feedbackPreview:true,__feedbackMode:typeof feedbackMode==='undefined'?'enhanced':feedbackMode,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;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Browser tab: 8, Title: "指路与玩家填色像素回归", URL: "http://127.0.0.1:17861/qa/hints.html".
|
||||
0 AXWebArea 指路与玩家填色像素回归, URL: 127.0.0.1:17861/qa/hints.html
|
||||
1 heading 指路与玩家填色像素回归, Value: 1
|
||||
2 text 指路与玩家填色像素回归
|
||||
3 text PASS:200/200 关,点击指路前后和撤销重画后,玩家已走格子的实际 Canvas 填色像素一致。左图无提示,右图开启高对比提示。
|
||||
|
||||
The focused UI element is 0 AXWebArea 指路与玩家填色像素回归, URL: 127.0.0.1:17861/qa/hints.html
|
||||
@@ -16,7 +16,7 @@ const source=await(await fetch('../game.js')).text();
|
||||
const levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
||||
const canvases=[],handlers={},frames=new Map(),storage=new Map();let frameId=0,now=100000;
|
||||
class Clock extends Date{static now(){return now;}}
|
||||
const wx={
|
||||
const wx={__feedbackPreview:true,__feedbackMode:typeof feedbackMode==='undefined'?'enhanced':feedbackMode,
|
||||
loadFont:p=>p.split('/').at(-1).replace('.ttf',''),
|
||||
createCanvas(){const c=document.createElement('canvas');if(!canvases.length)canvases.push(c);return c;},
|
||||
getWindowInfo:()=>({windowWidth:375,windowHeight:812,pixelRatio:1,safeArea:{top:44,bottom:778}}),
|
||||
@@ -32,7 +32,7 @@ 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.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 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.page>Math.floor(i/15)+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');
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"date": "2026-10-22",
|
||||
"checkedFiles": 133,
|
||||
"currentMainUntouched": true,
|
||||
"mainGameSHA256": "bb35d5edf8d96eb3253c78052d219b32f9a4241efd9971537f6c9c8896188125",
|
||||
"originalPackageSHA256": "62fab8cc84316deba50294c4fc2a3dfdf02b91486eae567d4919bd9a8f98c63c",
|
||||
"branch": "experimental-layered-feedback",
|
||||
"iphone": "not-tested",
|
||||
"android": "not-tested"
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
✔ 手机没有浏览器音频接口时,原生播放器仍能播放全部 36 步和六类反馈音 (4.1103ms)
|
||||
✔ 关闭音效停止播放;再次开启会试听并保存偏好;后台中断停止所有播放器 (0.7541ms)
|
||||
✔ 延迟通关音在静音或进入后台时取消,不在返回后突然播放 (45.9298ms)
|
||||
✔ 音频错误可诊断,不再静默吞掉 (0.4347ms)
|
||||
✔ 所有音频均为非静音 PCM;原版第13步比第1步高一个八度 (19.472ms)
|
||||
✔ 教学和组合规则用具体动作及奇偶例子解释,没有未解释的术语 (7.6131ms)
|
||||
✔ 过关提示同步使用实际播放起点,非请求时间 (0.598ms)
|
||||
✔ 过关和弦收尾持续衰减,不在后半段重新起音 (2.2637ms)
|
||||
✔ merged irregular outlines retain holes and never draw internal unit edges (47.6935ms)
|
||||
✔ all 200 boards fit four screen budgets with stable safe anchors and real shared entrances (522.3449ms)
|
||||
✔ guides through every irregular block stay inside its occupied shape (25.4141ms)
|
||||
✔ visited, conflicting and hinted category cells retain their material identity (10.5537ms)
|
||||
✔ five font subsets match their manifest and fit the package budget (3.8279ms)
|
||||
✔ upstream adapter fails closed if the inspected render seams change (0.6365ms)
|
||||
✔ procedural cushion pixels preserve holes and light the upper bevel without a border (19.9765ms)
|
||||
✔ visible glyph bounds are centered independently of font ascent and side bearings (1.1753ms)
|
||||
✔ stage with zero depth casts alpha shadows without painting a solid backing (5.6615ms)
|
||||
✔ native width-only TextMetrics still centers the rasterized glyph ink (1.7123ms)
|
||||
✔ fixed channels remain anchored at their block centers after number enlargement (737.8066ms)
|
||||
✔ checkpoint ribbons trigger on correct arrival only and never duplicate per frame (2.4057ms)
|
||||
✔ checkpoint pulse is small, centered in time and settles without looping (0.2773ms)
|
||||
✔ event batches preserve every landing and sequence but commit only highest audio and haptic (2.5258ms)
|
||||
✔ completion waits at most 120ms and ignores late audio callbacks (0.502ms)
|
||||
✔ light haptics are limited, checkpoint replaces light, and completion adds only one delayed short pulse (0.6548ms)
|
||||
✔ unsupported graded haptic falls back to one short pulse, never long vibration (0.5295ms)
|
||||
✔ haptic disable, preview and lifecycle cancellation suppress delayed or expired work (0.4659ms)
|
||||
✔ late failed haptic requests cannot revive old pulses and ungraded completion falls back once (0.2774ms)
|
||||
✔ rectangles, concave shapes and hexagons require geometric inward distance without treating internal seams as walls (1.2234ms)
|
||||
✔ one high-speed native move records all intermediate cells and completion without duplicate final node (101.4652ms)
|
||||
✔ legal but off-solution moves use public rules, while invalid clues only produce error (60.733ms)
|
||||
✔ drag near a previous-block boundary cannot undo; deeper entry can, and undo button remains immediate (50.3433ms)
|
||||
✔ tutorial, redraw, hint and restored saved routes do not generate player feedback (73.5198ms)
|
||||
✔ pressure and rebound have bounded amplitude and duration (0.3127ms)
|
||||
✔ interpolation cannot duplicate the same failed-cell event within one native movement (0.2822ms)
|
||||
✔ nodes are driven by explicit events even when multiple arrive before rendering; at most two ribbons survive (15.717ms)
|
||||
✔ audio fast moves merge to latest pitch and completion stops the whole four-channel pool (1.2373ms)
|
||||
✔ settings persist haptic preference independently and chapter cards always use three columns (65.661ms)
|
||||
✔ original audio is preserved and high pitches lose energy rather than growing sharper (6.1515ms)
|
||||
✔ empty, partial and wrong routes reveal the first incomplete full segment (2.0087ms)
|
||||
✔ completion hides the hint; repeat clicks cannot skip; undo and restart return to earlier segments (0.3693ms)
|
||||
✔ all 200 levels reveal consecutive segments without changing the player path (1237.8614ms)
|
||||
✔ hint draws skip player edges and mask visited cells on their own layer; compositing is restored (0.9316ms)
|
||||
✔ parity is a segment anchor; colour is not; flow moves continuously across cells (0.3255ms)
|
||||
✔ coral teaching ribbon preserves visited masks and offers direction in reduced motion (1.484ms)
|
||||
✔ hint rounds shared guide junctions and keeps separate runs disconnected (0.3731ms)
|
||||
✔ 实际教学关动画:basic (2.5789ms)
|
||||
✔ 实际教学关动画:walls (0.4723ms)
|
||||
✔ 实际教学关动画:straight (1.2477ms)
|
||||
✔ 实际教学关动画:vertical (2.4091ms)
|
||||
✔ 实际教学关动画:corner (0.6768ms)
|
||||
✔ 实际教学关动画:colour (0.3905ms)
|
||||
✔ 实际教学关动画:parity (0.3252ms)
|
||||
✔ 实际教学关动画:rhythm-door (0.4597ms)
|
||||
✔ 实际教学关动画:hex (0.3918ms)
|
||||
✔ 实际教学关动画:unknown-start (1.2078ms)
|
||||
✔ 200关的组合规则均能找到对应真实教学关 (80.3ms)
|
||||
✔ 教学减速,结束停留1秒后自动循环 (3.2682ms)
|
||||
✔ music loops independently, persists mute and cancels in background (2.2845ms)
|
||||
✔ old arrows retain distance, rush to endpoint and fresh arrows move at base speed (0.8258ms)
|
||||
✔ settings have exactly three independent preferences, and hint has its own cue (104.2538ms)
|
||||
✔ actual fixed numeric node uses checkpoint pitch, final cell uses complete chord (95.8327ms)
|
||||
✔ two original music loops are quiet, nonempty and have a smooth loop seam (1.6874ms)
|
||||
✔ first launch goes directly to gameplay (73.4922ms)
|
||||
✔ resume unfinished route after exit from gameplay (99.9457ms)
|
||||
✔ resume unfinished route after exit from selection (89.3875ms)
|
||||
✔ completed level resumes next unfinished, including leaving from cleared screen (66.089ms)
|
||||
✔ replaying a cleared level does not replace the unfinished save (78.5557ms)
|
||||
✔ legacy saves, malformed data and full completion have valid startup targets (45.9116ms)
|
||||
✔ selection pages expose fifteen cards and fit phone safe areas (224.5336ms)
|
||||
✔ hint-only frames reuse static UI, and player input invalidates it (36.5671ms)
|
||||
✔ hint curves are cached and distance lookup is stable at boundaries (0.7168ms)
|
||||
✔ new-mechanic lesson reuses the board while the continuous cursor advances (26.3074ms)
|
||||
ℹ tests 72
|
||||
ℹ suites 0
|
||||
ℹ pass 72
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1510.9271
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"checkpoint.wav": "62c1f3c4ce18e0aa687cfd7e4d8cfd43730d41e02addb40cfc330cde8ea428ec",
|
||||
"complete.wav": "e0aa1110fa005c8065627f1ede6a90b0760c686eada27a287785e343e9233389",
|
||||
"correct.wav": "1b040858bc3f8fd501a0452d2663a462820242dbad34d854e4aca810c786b1b1",
|
||||
"error.wav": "b328d061bda8291405b3cb8f63c6eab1b83c48cf1c03be6067fd84d1de996603",
|
||||
"move-1.wav": "2cb7a17071c830f91d7c8e1b72b794793024a513e740a64732331625daf95278",
|
||||
"move-10.wav": "d8a1df1c1b1c74c94f29dd14d06947ad1d9543f4aec7d4d2a1c5e8e2ac7557bd",
|
||||
"move-11.wav": "ea277ad4987eec7ffff3d14c9ea5c051e064f29f3634d3048c9c191cf11671bd",
|
||||
"move-12.wav": "d57e253de04ed10a2736c4a69098fe94fe873e5d28c09c412f5d2adc008a146c",
|
||||
"move-13.wav": "15aa71193b42a46720f21929669b5b866128ff5e43a25e0ce88eac87c00ff939",
|
||||
"move-14.wav": "ccfdec5c8c12b94bb05bc4deb6fd2a6ffa608865d347b097a5759b8c48154f97",
|
||||
"move-15.wav": "84a28726f45a9cabc52d30fdbabb247419e39ef60e20687c89718e3ce96f69d4",
|
||||
"move-16.wav": "83ab78d62e915f93b55d075cc71fd6bb11d103dfce0256b7e44201ba23e096ef",
|
||||
"move-17.wav": "26480d2fecda33861576b6eeb242a126c66ef14f65e48797155c149cf638b074",
|
||||
"move-18.wav": "be2d747372b769c9b1e28682b423091c24bda68d01074cbf09535cf6e6f80d69",
|
||||
"move-19.wav": "8f9e80d304b3f90a1558bbe48939a9762eff38cc51940b66071fd12eb85c3039",
|
||||
"move-2.wav": "ccc2e1998f7568e530b87ab695428ae3f798aff38ebaef9ee990a80b1897d8d1",
|
||||
"move-20.wav": "df78ae2d6d79e8184c227507ed1bed184b1f897b9f8101b31de9d1c0fc87050b",
|
||||
"move-21.wav": "681df4799b05c9bf3d89dffd2245a7d2d8186bd3ab25495d25b914e419ec4666",
|
||||
"move-22.wav": "dacc7c5c35c0ab214418774959979cee223684ebcdac5c8d1e8d5c5cf6c22e72",
|
||||
"move-23.wav": "abfe0331fef331efe246249629483afd63eab00595c95edbd67e21a063e06c05",
|
||||
"move-24.wav": "15e1efa8dd8d70b72854d2edc6d0f88d14e1b82c63498308144106276be88d5b",
|
||||
"move-25.wav": "7f0fbefa1cd27ad7129c9c43a48c2f2282c3cab3992e3eaf1632f23d01bbda29",
|
||||
"move-26.wav": "7270bd76f01bf2832c2c79150224113d7edea208d7027209dd927d4ffdf15fb8",
|
||||
"move-27.wav": "c724cd052110da11293c07910d4925dbbcdb78b9ee2c77f21d069dbf6f274eb2",
|
||||
"move-28.wav": "cee5d869a4ec628462a56bbd49c8ce55846e53e6c89f57d5a186880cc1f5da61",
|
||||
"move-29.wav": "3c98b4be23094287245c34c71777941c295e00c7305849df4cb6e20c3e540271",
|
||||
"move-3.wav": "0385e90c0bc9c71c74add0952a5c2ece55314d7ed244d1fa73cbe08f3bf3124b",
|
||||
"move-30.wav": "64176f92fa48bd6c26e68cab22bc976ba42cfb522d27f6ce2a071d12a50fe3fb",
|
||||
"move-31.wav": "369e698faba84fbb1b45cfcc42f49f880bce72fe4b46e5f898393b59cb5fb7fb",
|
||||
"move-32.wav": "c003b6c7e88b7fd36a33cbf377d0a3b3e43cc0da7664dc2e41f89222654b1dc2",
|
||||
"move-33.wav": "79a6d84994e26f5fa1f6416a52c0970e29918d06d580af040f4318ab0136c274",
|
||||
"move-34.wav": "01e0b61313116d2d186a6343ca2d06cd6c15c85b50559875c52f5faeb4836f63",
|
||||
"move-35.wav": "c2b69306cf1eac8ca9934e1f5ab358908fc6f5d6194e43bd8c406b9b0936b1f5",
|
||||
"move-36.wav": "0907a295fe0e1c4b25e24b55738463047dea7b4139ac4705f482ed536fc1f059",
|
||||
"move-4.wav": "bf9d73bd04df5e59ff79257155b5ec4ab0d080a334809e049d17978ef34d76b7",
|
||||
"move-5.wav": "23b988b5532580b16296cb040d92feb921a266e3310fc81612b308f8661c942d",
|
||||
"move-6.wav": "ee91fb56c5f0aa56944fb25c8dda6c2d94dbcf5d998c56f09656cf406a0edfac",
|
||||
"move-7.wav": "d6c186f4f1e6a03f928626ea46dbce1f78cafa779529a5c337f70bd8a6650df0",
|
||||
"move-8.wav": "b1d2f3f2d7bcee8690cd783086b474dc8697b7f5183ccc4c5bfcea6dbdb12120",
|
||||
"move-9.wav": "3d8e2e6f494bd7ff063fbedf25fca6071e9e8408a6a27ae5f49c6315ef0da35e",
|
||||
"restart.wav": "35ac269596672213066e1fd2fc5c2890d144746f87dab8f79c6b32bebd495419",
|
||||
"undo.wav": "d88b38079b37b0c7a25d690fa949574d37fb343d5332113bdbbcba546cbfd868"
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function boot(storage = new Map(), width = 375, height = 812) {
|
||||
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?'上一页':'下一页');
|
||||
button(api.snapshot().selection.page>Math.floor(i/15)+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); };
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -6,5 +6,5 @@ const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
||||
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();}
|
||||
try{res.setHeader('Content-Type',file.endsWith('.html')?'text/html; charset=utf-8':file.endsWith('.wav')?'audio/wav':file.endsWith('.json')?'application/json':'text/javascript; charset=utf-8');res.end(fs.readFileSync(file));}catch{res.writeHead(404).end();}
|
||||
}).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