16 lines
5.1 KiB
JavaScript
16 lines
5.1 KiB
JavaScript
import fs from 'node:fs';import path from 'node:path';import {fileURLToPath} from 'node:url';import {definitions} from './definitions.mjs';import {units,constraints} from './engine-template.mjs';
|
||
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
||
const esc=s=>String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>');
|
||
function diagram(l,author=null){let shapes=[],centers=new Map();for(const c of l.cells){if(l.geometry==='hex'){const x=1.5*c.r,y=Math.sqrt(3)*(c.q+c.r/2);centers.set(c.id,[x,y]);let p=[];for(let a=0;a<6;a++)p.push([x+Math.cos(a*Math.PI/3),y+Math.sin(a*Math.PI/3)]);shapes.push({id:c.id,p});}else{const u=units(c);centers.set(c.id,[u.reduce((s,p)=>s+p[0]+.5,0)/u.length,u.reduce((s,p)=>s+p[1]+.5,0)/u.length]);for(const [x,y]of u)shapes.push({id:c.id,p:[[x,y],[x+1,y],[x+1,y+1],[x,y+1]]});}}
|
||
const all=shapes.flatMap(s=>s.p),xmin=Math.min(...all.map(p=>p[0])),xmax=Math.max(...all.map(p=>p[0])),ymin=Math.min(...all.map(p=>p[1])),ymax=Math.max(...all.map(p=>p[1])),s=Math.min(340/(xmax-xmin),410/(ymax-ymin)),tr=([x,y])=>[(x-xmin)*s+20,(y-ymin)*s+20];const width=(xmax-xmin)*s+40,height=(ymax-ymin)*s+40;
|
||
let svg=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" role="img" aria-label="${esc(l.title)}">`;
|
||
for(const item of shapes){const c=l.cells.find(c=>c.id===item.id);svg+=`<polygon points="${item.p.map(p=>tr(p).join(',')).join(' ')}" fill="${c.category==='circle'?'#e4f3ff':c.category==='triangle'?'#fff0d4':'#f1eef9'}" stroke="#aaa1c7" stroke-width="2"/>`;}
|
||
if(author)svg+=`<polyline points="${author.map(id=>tr(centers.get(id)).join(',')).join(' ')}" fill="none" stroke="#7963c9" stroke-width="5" opacity=".4"/>`;
|
||
for(let i=0;i<(l.gates||[]).length;i++){const r=l.gates[i],[a,b]=r.edge.map(id=>tr(centers.get(id))),x=(a[0]+b[0])/2,y=(a[1]+b[1])/2,angle=Math.atan2(b[1]-a[1],b[0]-a[0])*180/Math.PI;svg+=`<line x1="${x}" y1="${y-s*.33}" x2="${x}" y2="${y+s*.33}" stroke="#cf7824" stroke-width="7" transform="rotate(${angle} ${x} ${y})"/><text x="${x+8}" y="${y-8}" font-size="12">门${i+1}</text>`;}
|
||
for(const c of l.cells){const [x,y]=tr(centers.get(c.id)),w=(l.windows||[]).find(r=>r.cell===c.id),sw=(l.gates||[]).findIndex(r=>r.switchCell===c.id);let label=author?String(author.indexOf(c.id)+1):c.clue===undefined?'':String(c.clue);let tags=[w?`${w.min}–${w.max}`:'',c.direction==='straight'?'↔':c.direction==='turn'?'↱':'',c.category==='circle'?'○':c.category==='triangle'?'△':'',sw>=0?`开${sw+1}`:''].filter(Boolean).join(' ');svg+=`<text x="${x}" y="${y-8}" text-anchor="middle" font-family="system-ui,Microsoft YaHei" font-size="${Math.max(11,Math.min(21,s*.29))}" font-weight="700">${esc(label||tags)}</text><text x="${x}" y="${y+10}" text-anchor="middle" font-family="system-ui" font-size="10" fill="#665c78">${c.id}${label&&tags?' · '+esc(tags):''}</text>`;}
|
||
return svg+'</svg>';}
|
||
for(const def of definitions){const folder=path.join(root,'modes',def.id),pack=JSON.parse(fs.readFileSync(path.join(folder,'levels.json'),'utf8')),notes=JSON.parse(fs.readFileSync(path.join(folder,'design.json'),'utf8'));let articles='';for(let i=0;i<8;i++){const l=pack.levels[i],n=notes[i];fs.writeFileSync(path.join(folder,l.id+'-board.svg'),diagram(l));fs.writeFileSync(path.join(folder,l.id+'-solution.svg'),diagram(l,n.author));
|
||
articles+=`<article><h2>${l.id} · ${l.difficulty} · ${esc(l.title)}</h2><div class="boards"><figure>${diagram(l)}<figcaption>原始盘面 · 格id用于对照JSON</figcaption></figure><details><summary>展开作者解与路线</summary><figure>${diagram(l,n.author)}</figure><p>${n.author.join(' → ')}</p></details></div><h3>推理依据</h3><ul>${n.breakdown.map(t=>`<li>${esc(t)}</li>`).join('')}</ul><p>关键分叉:第 ${n.keyFork.step} 步应走 ${n.keyFork.correct},诱惑路线走 ${n.keyFork.tempting}。排除依据:${esc(n.reason)}。</p><details><summary>逐项机制必要性反例</summary>${n.witnesses.map(w=>`<p>解除 ${w.constraint} 后:${w.path.join(' → ')}</p>`).join('')}</details></article>`;}
|
||
fs.writeFileSync(path.join(folder,'DESIGN.html'),`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${def.title} · 设计评审</title><style>body{max-width:920px;margin:24px auto;padding:0 18px;background:#f7f5fb;color:#303047;font:15px/1.7 system-ui,'Microsoft YaHei'}article{background:white;border:1px solid #ddd5ed;border-radius:16px;padding:20px;margin:20px 0;break-inside:avoid}.boards{display:flex;gap:24px;flex-wrap:wrap}figure{margin:0;width:380px;max-width:100%}svg{max-height:450px;width:100%}details{cursor:pointer}a{color:#6552aa}@media print{body{background:white}article{border:none;page-break-after:always}} </style><a href="play.html">进入试玩</a><h1>${def.title} · 8关设计评审</h1><p>${esc(def.rule)}</p><p>原始盘面和作者解分开展示。标题是设计任务,真人推理路径与难度仍需试玩验证。数字、窗口、方向、类别和开关显示在格内;步差与先后关系见逐关推理依据。</p>${articles}</html>`);}
|
||
console.log('Generated 56 original boards, 56 solutions and 7 design review pages.');
|