Files

16 lines
6.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'node:fs';import path from 'node:path';import {fileURLToPath} from 'node:url';import {outlineUnits,units,constraints,directionNames} from '../rules.mjs';
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');const definitions=[{id:'campaign-200',title:'200关DEMO',rule:'每格一次;数字为访问步号,各关显示当前启用规则。'}];
const esc=s=>String(s).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;');
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]);shapes.push({id:c.id,p:outlineUnits(c)});}}
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'?'#64acf3':c.category==='triangle'?'#ffae55':'#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(const item of [...(l.walls||[]).map(edge=>({edge,kind:'wall'})),...(l.doors||[]).map(d=>({edge:d.edge,kind:'door'}))]){const [a,b]=item.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*.38}" x2="${x}" y2="${y+s*.38}" stroke="${item.kind==='wall'?'#272638':'#aa4d25'}" stroke-width="8" transform="rotate(${angle} ${x} ${y})"/>`;if(item.kind==='wall')svg+=`<line x1="${x}" y1="${y-s*.38}" x2="${x}" y2="${y+s*.38}" stroke="#ffc542" stroke-width="4" stroke-dasharray="5 4" transform="rotate(${angle} ${x} ${y})"/>`;else svg+=`<text x="${x+8}" y="${y-8}" font-size="12">步进门</text>`;}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.parity?(c.parity==='odd'?'奇':'偶'):c.direction?({lr:'─',du:'│',ru:'└',dr:'┌',dl:'┐',lu:'┘'})[c.direction]:'',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=root,pack=JSON.parse(fs.readFileSync(path.join(folder,'levels.json'),'utf8')),notes=JSON.parse(fs.readFileSync(path.join(folder,'design.json'),'utf8'));let articles='';const ordered=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));for(let i=0;i<ordered.length;i++){const l=ordered[i],n=notes.find(n=>n.level.id===l.id);fs.writeFileSync(path.join(folder,String(l.order).padStart(2,'0')+'-board.svg'),diagram(l));fs.writeFileSync(path.join(folder,String(l.order).padStart(2,'0')+'-solution.svg'),diagram(l,n.author));
articles+=`<article><h2>${i+1} · ${l.chapterTitle} · ${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="index.html">进入试玩</a><h1>${def.title} · 200关设计评审</h1><p>${esc(def.rule)}</p><p>原始盘面和作者解分开展示。标题是设计任务,真人推理路径与难度仍需试玩验证。数字、窗口、方向、类别和开关显示在格内;步差与先后关系见逐关推理依据。</p>${articles}</html>`);}
console.log('Generated 200 boards, 200 solution SVGs and design review page.');