12 lines
5.3 KiB
JavaScript
12 lines
5.3 KiB
JavaScript
import {solve,doorOpen,doorLatched,graph,violations} from './upstream/rules.mjs';
|
|
import {tutorials} from './copy.mjs';
|
|
export function lessonKeys(level){if(level.tutorial)return [level.tutorial];const keys=['basic'];if(level.features.includes('walls'))keys.push('walls');for(const [key,kind]of [['straight','lr'],['vertical','du']])if(level.cells.some(c=>c.direction===kind))keys.push(key);if(level.cells.some(c=>c.direction&&!['lr','du'].includes(c.direction)))keys.push('corner');if(level.features.includes('alternation'))keys.push('colour');if(level.features.includes('parity'))keys.push('parity');if(level.doors?.length)keys.push('rhythm-door');if(level.geometry==='hex')keys.push('hex');if(!level.cells.some(c=>c.clue===1))keys.push('unknown-start');return keys;}
|
|
export function makeLesson(pack,key){const level=pack.levels.find(l=>l.tutorial===key);if(!level)throw Error('缺少教学关 '+key);const solution=solve(level)[0];const routes=Array.from({length:solution.length+1},(_,n)=>solution.slice(0,n));if(key==='rhythm-door'){for(let n=solution.length-1;n>=0;n--)routes.push(solution.slice(0,n));}const examples=[];const g=graph(level);
|
|
if(key==='walls')examples.push({after:1,route:solution.slice(0,1),blocked:level.walls[0],message:'试着穿墙:这条边被挡住,需要绕行'});
|
|
if(key==='colour') {const first=level.cells.find(c=>c.id===solution[0]);const other=g.adj[g.ids.get(first.id)].map(i=>g.cells[i]).find(c=>c.category===first.category);if(other)examples.push({after:1,route:solution.slice(0,1),blocked:[first.id,other.id],message:'同色不能连续走;改走旁边的橙色格'});}
|
|
if(['straight','vertical','corner','parity'].includes(key)){for(let n=1;n<solution.length;n++){const prefix=solution.slice(0,n);const bad=g.adj[g.ids.get(prefix.at(-1))].map(i=>g.cells[i].id).find(id=>!prefix.includes(id)&&violations(level,[...prefix,id]).some(v=>v.key.startsWith((key==='parity'?'parity':'direction')+':')));if(bad){examples.push({after:n,route:[...prefix,bad],message:key==='parity'?'错误示例:奇偶与当前步数不符,格子变红':'错误示例:没有沿图中的两端进出,图标变红'});break;}}}
|
|
if(key==='parity'){const start=solution[0];for(const j of g.adj[g.ids.get(start)])for(const k of g.adj[j]){const route=[start,g.cells[j].id,g.cells[k].id];if(new Set(route).size===3&&violations(level,route).some(v=>v.key.startsWith('parity:'))&&!examples.length)examples.push({after:1,route,message:'错误示例:第 3 步走到“偶”格,格子变红'});}}
|
|
const beats=[];routes.forEach((route,i)=>{beats.push({route});if(i<solution.length)for(const e of examples.filter(e=>e.after===i)){beats.push(e);for(let back=e.route.length-1;back>route.length;back--)beats.push({route:e.route.slice(0,back),message:'沿来时路逐格退回,再按规则走'});beats.push({route,message:e.blocked?'改走符合规则的相邻格子':'沿来时路退回,重新按规则走'});}});
|
|
const continuous=[];for(const beat of beats){const previous=continuous.at(-1);if(previous&&JSON.stringify(previous.route)===JSON.stringify(beat.route)&&!beat.blocked&&!previous.blocked)continue;if(previous&&beat.route.length>previous.route.length+1){for(let n=previous.route.length+1;n<beat.route.length;n++)continuous.push({route:beat.route.slice(0,n)});}continuous.push(beat);}return {key,level,solution,routes,beats:continuous,copy:tutorials[key],duration:1000};}
|
|
export function lessonFrame(lesson,elapsed){const duration=lesson.duration,segments=lesson.beats.length-1,position=Math.max(0,Math.min(1,elapsed/duration))*segments,index=Math.min(segments,Math.floor(position)+1),beat=lesson.beats[Math.min(index,lesson.beats.length-1)],route=beat.route,previous=lesson.beats[Math.max(0,Math.min(index-1,lesson.beats.length-1))].route;const n=route.length,key=lesson.key,c=lesson.level.cells.find(c=>c.id===route.at(-1));let message=n?'第 '+n+' 步':'先观察格子,再开始连线';if(key==='basic')message=n?'第 '+n+' 步 · 每个格子只经过一次':'从数字 1 开始';if(key==='colour'&&n)message+=' · '+(c.category==='circle'?'蓝色;下一步换橙色':'橙色;下一步换蓝色');if(key==='parity'&&n)message+=' · '+(n%2?'奇数步:1、3、5…':'偶数步:2、4、6…');if(['straight','vertical','corner'].includes(key)){const at=route.findIndex(id=>lesson.level.cells.find(c=>c.id===id)?.direction);message=at<0?'观察线条连接的两端':at===n-1?'从线的一端进入,下一步沿另一端离开':'沿指定的线穿过,图标亮起';}if(key==='rhythm-door'){const d=lesson.level.doors[0];message+=' · '+(doorLatched(d,route)?'已经穿过:门保持打开':doorOpen(d,route)?'门开:现在可以穿过':'门关:此时不能穿过');if(previous.length>n)message='回退到第 '+n+' 步 · '+(doorLatched(d,route)?'尚未退过门,保持打开':'已撤销穿门,恢复开关切换');}if(key==='walls')message=n?'沿其他格子绕过去,黑黄墙始终不能穿越':'找到黑黄墙:这条边不能走';if(key==='hex')message=n?'第 '+n+' 步 · 沿相邻的整条边移动':'观察六边形相邻的边';if(key==='unknown-start')message=n?'第 '+n+' 步 · 核对是否按时到达数字格':'没有数字 1:观察其他数字,选择起点';message=beat.message||message;return {route,previous,message,blocked:beat.blocked,progress:elapsed>=duration?1:position-Math.floor(position),finished:elapsed>=duration,index};}
|