import { createState, act, solve } from './model.js'; import { position, hexPoints, trackPoints, PORTS } from '../../src/path-math.js'; const $ = id => document.getElementById(id), canvas = $('board'), ctx = canvas.getContext('2d'); const colors = ['#d66c4d', '#477fc0']; const copy = { color: ['分色归站', '橙色棋子进入橙色出口,蓝色棋子进入蓝色出口。外围短线就是终点;终点固定,旋转底盘改变去向。'], switch: ['机关换轨', '带 A 标记的棋子离场,会切换 A 格的轨道连接。先观察它影响的路线,再安排出发顺序。'], relay: ['接力停靠', '棋子经过中央 P 点时会停下,箭头沿后续轨道转向。再次点击继续出发,也可以先转盘或让其他棋子通过。'] }; let levels, mode = 'color', index = 0, state, history = [], animation = null, frame, size, scale, hint; function poly(points, stroke, width = 1, fill) { ctx.beginPath(); points.forEach((p,i) => i ? ctx.lineTo(p.x,p.y) : ctx.moveTo(p.x,p.y)); if(fill){ctx.closePath();ctx.fillStyle=fill;ctx.fill();} if(stroke){ctx.strokeStyle=stroke;ctx.lineWidth=width;ctx.stroke();} } function piece(tile, point, alpha = 1) { const p = point || position(tile.q,tile.r);ctx.save();ctx.globalAlpha=alpha; poly(hexPoints(p.x,p.y,.88),hint===tile.id?'#fff':'#ffffffcc',hint===tile.id?.09:.035,colors[tile.color]); ctx.save();ctx.translate(p.x,p.y);ctx.rotate(tile.dir*Math.PI/3); poly([{x:0,y:.3},{x:0,y:-.3}], '#fff', .09); poly([{x:-.17,y:-.08},{x:0,y:-.3},{x:.17,y:-.08}], '#fff', .09);ctx.restore(); ctx.fillStyle='#fff';ctx.font='bold .24px system-ui';ctx.textAlign='center'; ctx.fillText(tile.trigger || tile.id.slice(1)*1+1,p.x+.38,p.y+.37);ctx.restore(); } function draw(now = performance.now()) { if(!state)return; ctx.setTransform(canvas.width/size,0,0,canvas.width/size,0,0);ctx.clearRect(0,0,size,size); ctx.translate(size/2,size/2);ctx.scale(scale,scale);ctx.lineCap='round';ctx.lineJoin='round'; for(const c of state.cells){const p=position(c.q,c.r);poly(hexPoints(p.x,p.y,.97),'#d3dfe3',.025,'#fafbf7');} for(const c of state.cells){const p=position(c.q,c.r);for(let a=0;a<6;a++)if(ac.switchId)){const p=position(c.q,c.r);ctx.fillStyle='#7d5824';ctx.font='bold .28px system-ui';ctx.textAlign='center';ctx.fillText('A',p.x,p.y+.1);} if(mode==='relay')for(const c of state.stops){const p=position(c.q,c.r);ctx.beginPath();ctx.arc(p.x,p.y,.4,0,Math.PI*2);ctx.fillStyle='#e4edda';ctx.fill();ctx.strokeStyle='#65874d';ctx.lineWidth=.055;ctx.stroke();ctx.fillStyle='#507238';ctx.font='bold .35px system-ui';ctx.textAlign='center';ctx.fillText('P',p.x,p.y+.12);} for(const t of state.tiles)if(animation?.tile.id!==t.id)piece(t); if(animation){ let t=Math.min(1,(now-animation.start)/animation.duration); const success=['exit','stop'].includes(animation.status); const fraction=success?t:Math.sin(t*Math.PI); const lengths=animation.lengths,total=lengths.at(-1),distance=fraction*total; let i=1;while(i=1){animation=null;sync();}else frame=requestAnimationFrame(draw); } } function resize(){size=canvas.getBoundingClientRect().width;scale=size/9.3;canvas.width=canvas.height=Math.round(size*Math.min(devicePixelRatio||1,2));draw();} function sync(){ $('count').textContent=`剩余 ${state.tiles.length} 枚`; const busy=!!animation; for(const id of ['left','right','hint'])$(id).disabled=busy||!state.tiles.length; $('undo').disabled=busy||!history.length; $('next').hidden=busy||!!state.tiles.length||index===4; $('pieces').replaceChildren(...state.tiles.map(t=>{const b=document.createElement('button');b.textContent=`${t.id.slice(1)*1+1} 号${t.trigger?' · 机关 A':''}`;b.disabled=busy;b.onclick=()=>perform({type:'tap',id:t.id});return b;})); if(!state.tiles.length&&!busy)$('message').textContent=index===4?'这个模式的 5 关已完成,可以切换另一种玩法。':'全部到达!可以进入下一关。'; } function load(){cancelAnimationFrame(frame);animation=null;history=[];hint=null;state=createState(levels.filter(l=>l.mode===mode)[index]);$('title').textContent=copy[mode][0];$('rule').textContent=copy[mode][1];$('message').textContent='点击箭头出发,遇阻可以免费试路或旋转。';$('level').value=index;sync();resize();} function selectMode(next){mode=next;index=0;for(const b of document.querySelectorAll('[data-mode]'))b.setAttribute('aria-pressed',b.dataset.mode===mode);$('level').replaceChildren(...levels.filter(l=>l.mode===mode).map((l,i)=>new Option(`${i+1} · ${l.name}`,i)));load();} function perform(action){ if(animation||!state.tiles.length)return;hint=null; const tile=state.tiles.find(t=>t.id===action.id),result=act(state,action); if(result.state!==state){history.push(state);state=result.state;} const messages={rotate:'底盘已旋转,棋子与外围标记保持原位。',exit:tile?.trigger?'A 机关触发,金色轨道已经换路。':'已成功离场。',stop:'已停靠 P 点。可以转盘换路,再点它继续出发。',wrong:'出口颜色不匹配,试试旋转底盘。',blocked:'前方有棋子挡路,先让路或转盘再试。',loop:'这条路线绕回了原处,试试旋转底盘。'}; $('message').textContent=messages[result.status]; if(result.points?.length>1){const lengths=[0];for(let i=1;i{const r=canvas.getBoundingClientRect(),x=(e.clientX-r.left-size/2)/scale,y=(e.clientY-r.top-size/2)/scale;const tile=state?.tiles.find(t=>{const p=position(t.q,t.r);return Math.hypot(x-p.x,y-p.y)<.85;});if(tile)perform({type:'tap',id:tile.id});}); $('left').onclick=()=>perform({type:'rotate',direction:-1});$('right').onclick=()=>perform({type:'rotate',direction:1}); $('reset').onclick=load;$('undo').onclick=()=>{if(!animation&&history.length){state=history.pop();hint=null;$('message').textContent='已恢复上一步,包括轨道机关与停靠位置。';sync();draw();}}; $('hint').onclick=()=>{const solution=solve(state);const action=solution.actions[0];if(!action){$('message').textContent=solution.exhausted?'搜索尚未完成,可以撤销再试。':'当前状态没有通关路径,请撤销或重来。';return;}hint=action.id;$('message').textContent=action.type==='rotate'?`向${action.direction===1?'右':'左'}旋转一次。`:`点击 ${action.id.slice(1)*1+1} 号棋子${state.tiles.find(t=>t.id===action.id)?.trigger?'(A 机关)':''}。`;draw();}; $('level').onchange=()=>{index=Number($('level').value);load();};$('next').onclick=()=>{index++;load();}; for(const b of document.querySelectorAll('[data-mode]'))b.onclick=()=>selectMode(b.dataset.mode); window.addEventListener('resize',resize); try{const response=await fetch('./levels.json');if(!response.ok)throw new Error(response.status);levels=await response.json();selectMode(new URLSearchParams(location.search).get('mode') in copy?new URLSearchParams(location.search).get('mode'):'color');}catch(error){$('message').textContent=`关卡加载失败,请通过 npm run dev 启动后刷新。${error.message}`;}