<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Canvas on CloudLake</title><link>https://cloudlake.eu.org/tags/canvas/</link><description>Recent content in Canvas on CloudLake</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Fri, 16 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://cloudlake.eu.org/tags/canvas/index.xml" rel="self" type="application/rss+xml"/><item><title>615 张蓝图，6.6 秒：我把《异星工厂》的蓝图拆开了</title><link>https://cloudlake.eu.org/p/factorio-lab/</link><pubDate>Fri, 16 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/factorio-lab/</guid><description>&lt;img src="https://cloudlake.eu.org/p/factorio-lab/cover.jpg" alt="Featured image of post 615 张蓝图，6.6 秒：我把《异星工厂》的蓝图拆开了" /&gt;&lt;p&gt;我有一个文件夹，里面躺着 615 个 &lt;code&gt;.txt&lt;/code&gt; 文件。每个文件里是一串以 &lt;code&gt;0eJz...&lt;/code&gt; 开头的字符——这是《异星工厂》（Factorio）的蓝图字符串。&lt;/p&gt;&#10;&lt;p&gt;蓝图是玩家花了几个月搭出来的工厂。它现在只是一串文本，你看不出里面有什么，算不出它能产多少东西，也没法验证它是不是真的能跑。&lt;/p&gt;&#10;&lt;p&gt;我想让它变成&lt;strong&gt;能看、能算、能验证&lt;/strong&gt;的东西。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="一字符串里到底装了什么"&gt;&lt;a href="#%e4%b8%80%e5%ad%97%e7%ac%a6%e4%b8%b2%e9%87%8c%e5%88%b0%e5%ba%95%e8%a3%85%e4%ba%86%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;一、字符串里到底装了什么&#10;&lt;/h2&gt;&lt;p&gt;官方没有公开蓝图格式。资料里的说法是：Base64 编码的 zlib 流，解出来是 JSON。&lt;/p&gt;&#10;&lt;p&gt;照着做，第一步就失败了：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zlib.error: Error -3 while decompressing data: incorrect header check&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;我换了好几种字母表、试了 URL-safe 变体、补了不同数量的填充，全都不对。最后不猜了，&lt;strong&gt;直接读原始字节&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0e 4a 7a 5f ...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;&lt;code&gt;0e&lt;/code&gt; 是 Base64 解码的第一个字节？不是。真相是——&lt;strong&gt;第一个字符 &lt;code&gt;0&lt;/code&gt; 根本不属于负载&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;0&lt;/code&gt; 是版本号，本身就是一个 Base64 字符（数值 52）。真正的负载从&lt;strong&gt;下标 1&lt;/strong&gt; 开始。从第二个字符往后解，第一个字节正好是 &lt;code&gt;78&lt;/code&gt;，那是 zlib 的合法头。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;坑一：开头那个字符不是负载。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;坑二：填充没被去掉。&lt;/strong&gt; 好几处资料说 Factorio 会去掉 &lt;code&gt;=&lt;/code&gt;，但三个文件&lt;strong&gt;全都以 &lt;code&gt;=&lt;/code&gt; 或 &lt;code&gt;==&lt;/code&gt; 结尾&lt;/strong&gt;。去掉就刚好差两个字符，解出来是残的。&lt;/p&gt;&#10;&lt;p&gt;结论写在项目里的一句话：&lt;strong&gt;以文件为准，不以文档为准。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;验证方式是往返：&lt;code&gt;decode&lt;/code&gt; 之后再 &lt;code&gt;encode&lt;/code&gt;，必须&lt;strong&gt;逐字符完全相同&lt;/strong&gt;。三个原始文件、以及内部嵌的&lt;strong&gt;全部 615 个蓝图&lt;/strong&gt;，逐字符都通过了。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二数值不能抄得读"&gt;&lt;a href="#%e4%ba%8c%e6%95%b0%e5%80%bc%e4%b8%8d%e8%83%bd%e6%8a%84%e5%be%97%e8%af%bb" class="header-anchor"&gt;&lt;/a&gt;二、数值不能抄，得读&#10;&lt;/h2&gt;&lt;p&gt;蓝图里只有&amp;quot;这台机器在这里、朝这个方向&amp;quot;，没有&amp;quot;它有多快&amp;quot;。速度表在游戏安装目录里。&lt;/p&gt;&#10;&lt;p&gt;所以我写了一个&lt;strong&gt;受限的 Lua 求值器&lt;/strong&gt;（&lt;code&gt;fablue/lua.py&lt;/code&gt;）：它能处理 &lt;code&gt;data:extend&lt;/code&gt; 语句、表达式、内置库和辅助函数，但&lt;strong&gt;绝不执行宿主代码&lt;/strong&gt;——没有 &lt;code&gt;require&lt;/code&gt;、没有文件系统、没有外部依赖。&lt;/p&gt;&#10;&lt;p&gt;第一版解出 1,367 个原型，还有 149 个 &lt;code&gt;data:extend&lt;/code&gt; 解不出来。加了对跨文件、局部变量、函数返回值的支持之后：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;修复前&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;现在&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;解出的原型&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1,367&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;4,633&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;解不出的 &lt;code&gt;data:extend&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;149&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;25&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;传送带速度&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;12&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;机器速度&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;17&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;剩下 25 个全是跨文件 &lt;code&gt;data.raw&lt;/code&gt; 派生的瓦片/装饰类，&lt;strong&gt;不含速度表&lt;/strong&gt;——这个上限是我报出来的，不是藏起来的。&lt;/p&gt;&#10;&lt;p&gt;读出来的真值（全部来自 &lt;code&gt;data.raw&lt;/code&gt;）：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;项目&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;值&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;传送带速度&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.03125 / 0.0625 / 0.09375 / 0.125 tile/tick&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;双 lane 吞吐&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;15 / 30 / 45 / 60 items/s&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;单 lane 吞吐&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;7.5 / 15 / 22.5 / 30 items/s&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;组装机 1/2/3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;crafting_speed &lt;strong&gt;0.5 / 0.75 / 1.25&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;电炉 / 石炉 / 钢炉&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;2 / 1 / 2&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="一个犯了两次的-2-倍错误"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e7%8a%af%e4%ba%86%e4%b8%a4%e6%ac%a1%e7%9a%84-2-%e5%80%8d%e9%94%99%e8%af%af" class="header-anchor"&gt;&lt;/a&gt;一个犯了两次的 2 倍错误&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;×480&lt;/code&gt; 是&lt;strong&gt;双 lane 合计&lt;/strong&gt;。推导是 &lt;code&gt;0.03125 × 60 / 0.25 = 7.5&lt;/code&gt;（单 lane），乘 2 才得 15。&lt;/p&gt;&#10;&lt;p&gt;早期版本把这个常数标成 &amp;ldquo;per lane&amp;rdquo;，同时 &lt;code&gt;GameDB.belt_items_per_second&lt;/code&gt; 又乘了一次 lanes——&lt;strong&gt;同一个 2 倍错误，在两个地方各犯了一次&lt;/strong&gt;。因为是&amp;quot;自洽的错&amp;quot;，测试抓不到。&lt;/p&gt;&#10;&lt;p&gt;现在常量改名为&amp;quot;双 lane 因子&amp;quot;，新增 &lt;code&gt;belt_items_per_second_lane&lt;/code&gt; 属性，并把推导写进注释。&lt;strong&gt;单位写清楚，就不会再被隐含。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="三615-个蓝图一个-canvas"&gt;&lt;a href="#%e4%b8%89615-%e4%b8%aa%e8%93%9d%e5%9b%be%e4%b8%80%e4%b8%aa-canvas" class="header-anchor"&gt;&lt;/a&gt;三、615 个蓝图，一个 canvas&#10;&lt;/h2&gt;&lt;p&gt;查看器的约束很硬：&lt;strong&gt;单文件、离线、无外链&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;615 个蓝图、334,856 个实体，如果每个实体画一个 SVG 矩形，是数十 MB 的 DOM。所以：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;一个 canvas&lt;/strong&gt;，不是每蓝图一个&lt;/li&gt;&#10;&lt;li&gt;每实体只存 4 个整数：&lt;code&gt;x2, y2, 类别, 朝向&lt;/code&gt;。坐标翻倍是因为 Factorio 有大量实体放在&lt;strong&gt;半格&lt;/strong&gt;上（整数存不下 0.5）&lt;/li&gt;&#10;&lt;li&gt;全部数据一次性嵌进页面 → &lt;strong&gt;6.04 MB&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;性能靠&lt;strong&gt;视口裁剪&lt;/strong&gt;：懒建空间哈希 + &lt;code&gt;Int32Array&lt;/code&gt;。一个 34,650 实体的蓝图，在深度缩放下&lt;strong&gt;一帧只画 13 个实体&lt;/strong&gt;；全可见时 p95 是 &lt;strong&gt;8.8 ms&lt;/strong&gt;，中位 0.6 ms。&lt;/p&gt;&#10;&lt;p&gt;实体还&lt;strong&gt;按真实尺寸绘制&lt;/strong&gt;——尺寸取自每个原型的 &lt;code&gt;collision_box&lt;/code&gt;（3×3 的机器是 2.4，补齐到 3 格；火箭发射井 8.4 → 9 格；传送带 0.8 → 1 格），690 个尺寸表项随页面发出。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="四让它算"&gt;&lt;a href="#%e5%9b%9b%e8%ae%a9%e5%ae%83%e7%ae%97" class="header-anchor"&gt;&lt;/a&gt;四、让它算&#10;&lt;/h2&gt;&lt;p&gt;看图不够，我关心的是：&lt;strong&gt;这个工厂到底产多少、卡在哪&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;计算内核输出四类东西：净产出速率、外部输入、配比失衡、传送带饱和。&lt;/p&gt;&#10;&lt;p&gt;一个真实例子（34,650 个实体）：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;传送带饱和 copper-cable 151.1/s，是 express-transport-belt 容量的 3.36 倍&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;净产出 copper-cable 151.13/s、sulfuric-acid 97.00/s、petroleum-gas 34.00/s&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;外部输入 water 1587.50/s、crude-oil 480.00/s、iron-plate 282.15/s&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;瓶颈 未知配方：assembling-machine-3 ×18 台无法确定配方，未计入产出速率&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;核心公式全部写在代码注释里，不从别处抄：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;每秒次数 = crafting_speed ÷ recipe.energy&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;每秒产出 = 次数 × 单次产出量&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;每 lane = belt_speed × 240，双 lane = belt_speed × 480&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;模块效应也计入了&lt;/strong&gt;：同一个机器内的模块加成&lt;strong&gt;相加&lt;/strong&gt;（三个 +50% 速度模块 = ×2.5，不是 1.5³）。有一条专门的断言守着：组装机 3 做铜缆，无模块 &lt;strong&gt;5.0/s&lt;/strong&gt;，装三个速度模块 &lt;strong&gt;12.5/s&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="五几处刻意做对的判断"&gt;&lt;a href="#%e4%ba%94%e5%87%a0%e5%a4%84%e5%88%bb%e6%84%8f%e5%81%9a%e5%af%b9%e7%9a%84%e5%88%a4%e6%96%ad" class="header-anchor"&gt;&lt;/a&gt;五、几处&amp;quot;刻意做对&amp;quot;的判断&#10;&lt;/h2&gt;&lt;p&gt;这些地方很容易写成&amp;quot;看起来对、其实错&amp;quot;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;stone-furnace&lt;/code&gt; 这类实体在蓝图里&lt;strong&gt;没有 &lt;code&gt;recipe&lt;/code&gt; 字段&lt;/strong&gt;，但 Factorio 也不把默认配方写进蓝图。所以必须报成&amp;quot;&lt;strong&gt;未知配方（可用 overrides 指定）&lt;/strong&gt;&amp;quot;，而不是当成&amp;quot;这台机器闲置&amp;quot;。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;信标&lt;/strong&gt;（59 个）持有模块但不执行配方，如果当机器算，每个信标都会变成一条假&amp;quot;未知配方&amp;quot;。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;实验室和采矿机&lt;/strong&gt;的产出取决于科技/矿脉，单独报，不归入&amp;quot;未知配方&amp;quot;。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&amp;ldquo;蓝图没给配方&amp;quot;和&amp;quot;蓝图给了但游戏数据里查不到&amp;quot;分开报&lt;/strong&gt;。后者我核实过 &lt;code&gt;mods&lt;/code&gt; 目录确实没装 mod，所以那批 mod 蓝图算不出来是&lt;strong&gt;如实缺口&lt;/strong&gt;，不是静默失败。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="六验证334-项断言--三个浏览器终检"&gt;&lt;a href="#%e5%85%ad%e9%aa%8c%e8%af%81334-%e9%a1%b9%e6%96%ad%e8%a8%80--%e4%b8%89%e4%b8%aa%e6%b5%8f%e8%a7%88%e5%99%a8%e7%bb%88%e6%a3%80" class="header-anchor"&gt;&lt;/a&gt;六、验证：334 项断言 + 三个浏览器终检&#10;&lt;/h2&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;测试文件&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;断言&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;在验证什么&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;test_blueprint&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;14&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;编码往返、格式事实、拒绝畸形输入&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;test_all_blueprints&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;全部 615 个蓝图&lt;/strong&gt;逐字符往返&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;test_gamedata&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;79&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;合成数据证明数值真来自文件&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;test_gamedata_independent&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;45&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;正则直接从 Lua 抠数值&lt;/strong&gt; + 物理独立推导&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;test_calc&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;90&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;精确数值、未知配方不静默丢弃、循环保护&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;test_integration&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;22&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;跨模块接线&lt;/strong&gt;：模块效应→计算、尺寸→渲染&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;test_view&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;76&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;页面无外链、嵌入数据条数等于输入&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;&lt;code&gt;test_gamedata_independent.py&lt;/code&gt; 是关键一条：它&lt;strong&gt;不经过提取器的 API&lt;/strong&gt;，而是用正则直接从原始 Lua 文件里抠数值比对——提取器的 bug 无法靠&amp;quot;自洽&amp;quot;藏住。&lt;/p&gt;&#10;&lt;p&gt;三个&lt;strong&gt;真浏览器终检&lt;/strong&gt;不是检查 HTML 文本，而是真的操作页面：缩放/平移真的改变渲染、点到饱和蓝图真的显示警告、放大后每实体像素数真的从 3.1 涨到 50,181.6。&lt;/p&gt;&#10;&lt;p&gt;这一层抓到了 &lt;strong&gt;5 个纯 Python 测试看不见的 bug&lt;/strong&gt;：相机 NaN、缺边界、图例按数字索引查却按名字存、&lt;code&gt;renderLegend()&lt;/code&gt; 重建时销毁了正在处理点击的节点、&lt;code&gt;data-i=&amp;quot;0&amp;quot;&lt;/code&gt; 的 falsy 边界。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这就是为什么要用真浏览器驱动页面。DOM 里的字符串说&amp;quot;我很正常&amp;rdquo;，不算数。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;最终：&lt;strong&gt;334/334 通过，7/7 浏览器终检全过&lt;/strong&gt;，端到端 6.6 秒。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="七还没做到的"&gt;&lt;a href="#%e4%b8%83%e8%bf%98%e6%b2%a1%e5%81%9a%e5%88%b0%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;七、还没做到的&#10;&lt;/h2&gt;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;信标效应没建模&lt;/strong&gt;——需要空间分析（每台机器周围有几个信标），目前只在 notes 里计数。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;传送带饱和是按物品总量、不是按段判定&lt;/strong&gt;——蓝图 JSON 不记录机器和传送带的连接关系，要按段判就得从坐标和朝向重建拓扑，这是独立的一大块。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;K2SE 的 mod 配方读不到&lt;/strong&gt;，因为这台机器没装 mod。装上、把 &lt;code&gt;data_root&lt;/code&gt; 指过去就能算。&lt;/li&gt;&#10;&lt;li&gt;品质加成、插入机吞吐、电力网络都还没建模。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;这些限制我都写在页面的结论里，&lt;strong&gt;不含糊过去&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p style="text-align:center;margin:2.5em 0"&gt;&#13;&#10;&lt;a href="https://cloudlake.eu.org/lab/factorio/" style="display:inline-block;padding:12px 26px;border-radius:8px;background:var(--accent-color);color:var(--accent-color-text);text-decoration:none;font-weight:600"&gt;打开蓝图平台（6 MB，含 615 张蓝图）→&lt;/a&gt;&#13;&#10;&lt;/p&gt;&#13;&#10;</description></item><item><title>中国象棋：perft 证明我的规则写对了</title><link>https://cloudlake.eu.org/p/chinese-cheese/</link><pubDate>Mon, 12 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/chinese-cheese/</guid><description>&lt;img src="https://cloudlake.eu.org/p/chinese-cheese/cover.jpg" alt="Featured image of post 中国象棋：perft 证明我的规则写对了" /&gt;&lt;p&gt;一个完整的中国象棋网页版。&lt;/p&gt;&#10;&lt;p&gt;不需要构建步骤，克隆下来 &lt;code&gt;npm start&lt;/code&gt; 就能跑。手机、平板、电脑都能连。&lt;/p&gt;&#10;&lt;p&gt;但真正让我想做这个项目的，不是&amp;quot;做个象棋&amp;quot;，而是——&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;一个规则引擎怎么写才算&amp;quot;写对了&amp;quot;？&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="规则这东西看起来简单"&gt;&lt;a href="#%e8%a7%84%e5%88%99%e8%bf%99%e4%b8%9c%e8%a5%bf%e7%9c%8b%e8%b5%b7%e6%9d%a5%e7%ae%80%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;规则这东西，看起来简单&#10;&lt;/h2&gt;&lt;p&gt;七种棋子，每种走法都耳熟能详。写起来好像没什么难的。&lt;/p&gt;&#10;&lt;p&gt;真正难的是那些&lt;strong&gt;边界条款&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;规则&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;容易漏在哪&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;帅/将&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;九宫内直走一步；&lt;strong&gt;任何导致两将同线且中间无子的着法都非法&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;仕/士&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;九宫内斜走一步&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;相/象&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;走「田」字，&lt;strong&gt;象眼被堵不能走，不能过河&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;马&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;走「日」字，&lt;strong&gt;马腿被堵则该方向不能走&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;车&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;直线滑行，遇子即止&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;炮&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;不吃子时同车；&lt;strong&gt;吃子必须隔且只隔一个炮架&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;兵/卒&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;未过河只能向前；&lt;strong&gt;过河后可向前或左右，永不后退&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;还有胜负判定层：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;将死判负&lt;/strong&gt; —— 被将军且无合法着法&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;困毙判负&lt;/strong&gt; —— 没被将军但也没有任何合法着法（中国象棋特有，国际象棋是判和）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;长将判负&lt;/strong&gt; —— 循环内某方每一步都在将军&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;长捉判负&lt;/strong&gt; —— 循环内某方每步都在追捉同一个非兵非将棋子&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;重复局面判和&lt;/strong&gt; —— 三次重复且不构成长将/长捉&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;自然限着判和&lt;/strong&gt; —— 120 个半回合无吃子&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;光靠&amp;quot;我觉得应该对了&amp;quot;是写不完的。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="perft一个不给你留面子的检验"&gt;&lt;a href="#perft%e4%b8%80%e4%b8%aa%e4%b8%8d%e7%bb%99%e4%bd%a0%e7%95%99%e9%9d%a2%e5%ad%90%e7%9a%84%e6%a3%80%e9%aa%8c" class="header-anchor"&gt;&lt;/a&gt;perft：一个不给你留面子的检验&#10;&lt;/h2&gt;&lt;p&gt;国际象棋引擎圈子里有个标准测试叫 &lt;strong&gt;perft&lt;/strong&gt;：从开局开始，逐层统计所有合法着法能生成的节点数。&lt;/p&gt;&#10;&lt;p&gt;它有一个残忍的性质：&lt;strong&gt;任何一处规则写错，数字立刻对不上，而且不告诉你错在哪。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;中国象棋的公认值是这样的。这是我的实测：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;深度&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;节点数（公认）&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;结果&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;44&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;✅&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1,920&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;✅&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;79,666&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;✅&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;3,290,240&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;✅&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;到深度 4，要生成并验证 &lt;strong&gt;329 万个局面&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;这比&amp;quot;手动走几步看起来没问题&amp;quot;强得多——它逼你把每一个边界条款都写对，因为只要有一处错，329 万会变成 328 万或者别的数。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;&lt;strong&gt;一个能被数字否证的测试，比一百次&amp;quot;看起来没问题&amp;quot;更有价值。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;另外还有 69 项单元测试，逐条覆盖蹩马腿、塞象眼、炮翻山、将帅照面、被牵制棋子、困毙、长将判负、自然限着，以及中文记谱的前后缀消歧。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="ai六档难度但不许装傻"&gt;&lt;a href="#ai%e5%85%ad%e6%a1%a3%e9%9a%be%e5%ba%a6%e4%bd%86%e4%b8%8d%e8%ae%b8%e8%a3%85%e5%82%bb" class="header-anchor"&gt;&lt;/a&gt;AI：六档难度，但不许装傻&#10;&lt;/h2&gt;&lt;p&gt;引擎是纯 JavaScript，跑在 Web Worker 里，所以深度搜索不会卡住界面。&lt;/p&gt;&#10;&lt;h3 id="搜索"&gt;&lt;a href="#%e6%90%9c%e7%b4%a2" class="header-anchor"&gt;&lt;/a&gt;搜索&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Negamax + Alpha-Beta&lt;/strong&gt; 负极大值搜索&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;迭代加深 + 时间控制&lt;/strong&gt;（随时可以返回当前最优着法）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;置换表&lt;/strong&gt;：Zobrist 校验、深度优先替换、100 万槽位&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;着法排序&lt;/strong&gt;：置换表着法 &amp;gt; MVV-LVA 吃子 &amp;gt; 杀手着法 &amp;gt; 历史启发&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;静态搜索（Quiescence）&lt;/strong&gt;：消除水平线效应&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;空着裁剪&lt;/strong&gt;、&lt;strong&gt;将军延伸&lt;/strong&gt;、&lt;strong&gt;后期着法削减（LMR）&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;搜索路径内重复局面按和棋处理，&lt;strong&gt;避免引擎自己走进循环&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="评估"&gt;&lt;a href="#%e8%af%84%e4%bc%b0" class="header-anchor"&gt;&lt;/a&gt;评估&#10;&lt;/h3&gt;&lt;p&gt;子力价值 + 位置价值表（每种棋子一张 10×9 表）+ 结构项：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;机动性（马/车/炮/兵各自可走到的格数）&lt;/li&gt;&#10;&lt;li&gt;车的纵线开放性（半开放 / 全开放）&lt;/li&gt;&#10;&lt;li&gt;缺士怕双车、残局炮贬值、残局兵升值&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;开局评估为 0 分（完全对称），速度约 &lt;strong&gt;30 万节点/秒&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;h3 id="六档难度不是简单调深度"&gt;&lt;a href="#%e5%85%ad%e6%a1%a3%e9%9a%be%e5%ba%a6%e4%b8%8d%e6%98%af%e7%ae%80%e5%8d%95%e8%b0%83%e6%b7%b1%e5%ba%a6" class="header-anchor"&gt;&lt;/a&gt;六档难度：不是简单调深度&#10;&lt;/h3&gt;&lt;p&gt;难度是&lt;strong&gt;四层&lt;/strong&gt;可调的：搜索深度 + 思考时间 + 选点随机窗口 + 失误概率。&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;档位&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;深度&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;时间&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;随机窗口&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;失误率&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;入门&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.15s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;150&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30%&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;初级&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.3s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;55&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12%&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;中级&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.7s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;18&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3%&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;高级&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1.4s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;大师&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2.8s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;宗师&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;20&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;6s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;「随机窗口」＝在「最优分 − 窗口」范围内的着法里随机挑一个——这是低难度&amp;quot;&lt;strong&gt;像人但会犯错&lt;/strong&gt;&amp;ldquo;的关键。&lt;/p&gt;&#10;&lt;p&gt;但这里有一条铁律：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;&lt;strong&gt;无论哪一档，能一步杀就一定会杀。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;低难度可以犯错，但&lt;strong&gt;不能在该赢的时候装傻&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;因为一旦那种事发生，玩家就不认为自己在跟一个对手下棋，而是在跟一个随机数生成器。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="联机服务端说了算"&gt;&lt;a href="#%e8%81%94%e6%9c%ba%e6%9c%8d%e5%8a%a1%e7%ab%af%e8%af%b4%e4%ba%86%e7%ae%97" class="header-anchor"&gt;&lt;/a&gt;联机：服务端说了算&#10;&lt;/h2&gt;&lt;p&gt;同一份服务端同时提供 HTTP 静态服务与 WebSocket 信令，端口共用一个。&lt;/p&gt;&#10;&lt;p&gt;关键是&lt;strong&gt;规则引擎浏览器与 Node 共用同一份代码&lt;/strong&gt;，所以：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;服务端为每个房间持有一份 &lt;code&gt;Position&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;每一步都用规则引擎&lt;strong&gt;重新算一遍&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;非法着法直接回 &lt;code&gt;error&lt;/code&gt;，局面不变&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;strong&gt;改前端无法作弊。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;其他细节：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;房间码 4 位，字符集去掉易混淆的 &lt;code&gt;I/O/0/1&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;座位满了自动成为观战者，可观战、可聊天，不能走棋&lt;/li&gt;&#10;&lt;li&gt;悔棋需对方同意；对方在你请求后走了子，请求自动作废&lt;/li&gt;&#10;&lt;li&gt;断线重连：token 存在 &lt;code&gt;localStorage&lt;/code&gt;，120 秒内自动回到座位，超时判负&lt;/li&gt;&#10;&lt;li&gt;心跳 25 秒一次，死连接自动清理&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="一个手机端的真实坑"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e6%89%8b%e6%9c%ba%e7%ab%af%e7%9a%84%e7%9c%9f%e5%ae%9e%e5%9d%91" class="header-anchor"&gt;&lt;/a&gt;一个手机端的真实坑&#10;&lt;/h2&gt;&lt;p&gt;移动浏览器的音频策略比桌面严格得多。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;页面加载时创建 &lt;code&gt;AudioContext&lt;/code&gt; 会被自动播放策略永久挂起。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;所以这个项目特意&lt;strong&gt;不在加载时建上下文&lt;/strong&gt;，而是在第一次用户触摸时才创建并解锁。&lt;/p&gt;&#10;&lt;p&gt;配套做了个&lt;strong&gt;音效自检&lt;/strong&gt;功能：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;状态显示&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;含义&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;运行中 · 音效已开启，却仍听不到&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;设备侧静音（iPhone 检查侧边静音拨片；Android 调&lt;strong&gt;媒体音量&lt;/strong&gt;不是通话音量）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;一直显示「已挂起」&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;浏览器还在拦截，再多点几下棋盘&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;显示「尚未开始」&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;还没产生过触摸&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;试听按钮&lt;strong&gt;始终会发声&lt;/strong&gt;——这样就能区分&amp;quot;开关关了&amp;quot;和&amp;quot;设备真没声音&amp;rdquo;。&lt;/p&gt;&#10;&lt;p&gt;自动化验证会在&lt;strong&gt;严格自动播放策略 + 手机触屏模拟&lt;/strong&gt;下跑一条完整链路：加载时不建上下文 → 首次触摸即解锁 → 首声不被吞掉 → 落子/AI 应招都有声。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="素材全部可自由使用"&gt;&lt;a href="#%e7%b4%a0%e6%9d%90%e5%85%a8%e9%83%a8%e5%8f%af%e8%87%aa%e7%94%b1%e4%bd%bf%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;素材：全部可自由使用&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;棋子汉字字形取自 &lt;strong&gt;Wikimedia Commons&lt;/strong&gt; 的&lt;strong&gt;公有领域&lt;/strong&gt;作品（作者 Inductiveload）&lt;/li&gt;&#10;&lt;li&gt;原素材是四格精灵图，只取汉字字形，去掉原图圆片，重新合成为木纹实体棋子（径向渐变 + 车削年轮 + 倒角 + 内圈阳文 + 投影）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;棋盘木纹是 SVG &lt;code&gt;feTurbulence&lt;/code&gt; 程序化生成的&lt;/strong&gt;，无第三方版权&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;音效是 WebAudio 现场合成的&lt;/strong&gt;，不打包任何音频文件&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;所以这个项目可以随便分发，没有版权风险。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p style="text-align:center;margin:2.5em 0"&gt;&#13;&#10;&lt;a href="https://cloudlake.eu.org/lab/xiangqi/" style="display:inline-block;padding:12px 26px;border-radius:8px;background:var(--accent-color);color:var(--accent-color-text);text-decoration:none;font-weight:600"&gt;下一盘（单机版）→&lt;/a&gt;&#13;&#10;&lt;/p&gt;&#13;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;上面是在线单机版。局域网联机需要跑 Node 服务端，本地 &lt;code&gt;npm start&lt;/code&gt; 即可。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="技术选型"&gt;&lt;a href="#%e6%8a%80%e6%9c%af%e9%80%89%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;技术选型&#10;&lt;/h2&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;选择&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;原因&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;原生 JS，&lt;strong&gt;无构建步骤&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;克隆下来 &lt;code&gt;npm install &amp;amp;&amp;amp; npm start&lt;/code&gt; 就能跑&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;规则引擎浏览器与 Node 共用&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;联机时服务端复用同一份规则做权威校验，&lt;strong&gt;避免两套实现不一致&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;AI 用 Web Worker&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;深度搜索不阻塞界面；拿不到 Worker 时自动退回主线程&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;棋子用内联 SVG 精灵表&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;32 枚棋子共用一份渐变与滤镜定义，零额外请求，任意缩放清晰&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;音效用 WebAudio 合成&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;不需要打包音频，落子/吃子/将军音色各不相同&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;ws&lt;/code&gt; 是唯一运行时依赖&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;WebSocket 手写容易在分片、掩码、关闭握手上出错&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;hr&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个项目让我对&amp;quot;做对&amp;quot;这件事的看法变了一点。&lt;/p&gt;&#10;&lt;p&gt;规则能凭记忆写出来，但&lt;strong&gt;只有 perft 能证明你写对了&lt;/strong&gt;。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item><item><title>17 个词画出一朵玫瑰</title><link>https://cloudlake.eu.org/p/loom/</link><pubDate>Sun, 11 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/loom/</guid><description>&lt;img src="https://cloudlake.eu.org/p/loom/cover.jpg" alt="Featured image of post 17 个词画出一朵玫瑰" /&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# 每轮固定转 1/47 圈，半径按 1+0.45·sin(3θ) 起伏 —— 走满一圈正好闭合&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0.021277 turn&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0.021277 i * 3 * sin 0.45 * 1 + 55 * fwd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0.0008 i * color&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;七行，画出一朵三瓣玫瑰。&lt;/p&gt;&#10;&lt;p&gt;这是一门语言的完整程序。语料只有 &lt;strong&gt;17 个词&lt;/strong&gt;，但它&lt;strong&gt;图灵完备&lt;/strong&gt;——&lt;code&gt;jz&lt;/code&gt; 加回跳足以表达任意控制流。&lt;/p&gt;&#10;&lt;p&gt;打开一个 html 文件就能用：左边写程序，按 &lt;code&gt;Ctrl+Enter&lt;/code&gt; 重跑，画布上出现图案。&lt;strong&gt;单文件、离线、零依赖。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="17-个词"&gt;&lt;a href="#17-%e4%b8%aa%e8%af%8d" class="header-anchor"&gt;&lt;/a&gt;17 个词&#10;&lt;/h2&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;词&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;作用&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;数字&lt;/code&gt; / &lt;code&gt;p0&lt;/code&gt;…&lt;code&gt;p5&lt;/code&gt; / &lt;code&gt;i&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;压入常量、参数滑杆、当前轮数&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;+ - / mod neg&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;二元算术；&lt;code&gt;neg&lt;/code&gt; 取负&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;&amp;gt; &amp;lt; sign&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;比较（返回 0/1）；&lt;code&gt;sign&lt;/code&gt; 取符号&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;sin cos&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;输入是&lt;strong&gt;圈&lt;/strong&gt;不是弧度：&lt;code&gt;0.25&lt;/code&gt; → 90°&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;rand&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;伪随机 0…1，种子固定 ⇒ 可复现&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;fwd back&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;沿朝向前进/后退（画线）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;turn right&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;转向：&lt;code&gt;turn 0.25&lt;/code&gt; = 左转 90°&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;jump&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;画笔瞬移到 (x, y)，不画线&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;N st&lt;/code&gt; / &lt;code&gt;N ld&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;存入 / 取出 N 号寄存器（N = 0…7）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;pen&lt;/code&gt; &lt;code&gt;color&lt;/code&gt; &lt;code&gt;width&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;抬落笔、色相 0…1、笔宽&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;jmp N&lt;/code&gt; / &lt;code&gt;jz N&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;跳到第 N 行；&lt;code&gt;jz&lt;/code&gt; 在栈顶为 0 时跳&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;pause&lt;/code&gt; / &lt;code&gt;halt&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;交还控制权（动画靠它）/ 停机&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;栈是唯一的数据通道，所以约定是&lt;strong&gt;后缀式&lt;/strong&gt;：写 &lt;code&gt;0.25 turn&lt;/code&gt;，不写 &lt;code&gt;turn 0.25&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;跳转用&lt;strong&gt;代码行号&lt;/strong&gt;。跳到注释行或空行时会自动顺延到后面第一条真指令。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;程序会被反复执行&lt;/strong&gt;——这就是动画的来源，&lt;code&gt;i&lt;/code&gt; 是已经跑过的轮数。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="设计上真正需要决定的三件事"&gt;&lt;a href="#%e8%ae%be%e8%ae%a1%e4%b8%8a%e7%9c%9f%e6%ad%a3%e9%9c%80%e8%a6%81%e5%86%b3%e5%ae%9a%e7%9a%84%e4%b8%89%e4%bb%b6%e4%ba%8b" class="header-anchor"&gt;&lt;/a&gt;设计上真正需要决定的三件事&#10;&lt;/h2&gt;&lt;p&gt;写这门语言的过程中，有三处不做决定就写不下去。&lt;/p&gt;&#10;&lt;h3 id="1-循环的进度谁提供"&gt;&lt;a href="#1-%e5%be%aa%e7%8e%af%e7%9a%84%e8%bf%9b%e5%ba%a6%e8%b0%81%e6%8f%90%e4%be%9b" class="header-anchor"&gt;&lt;/a&gt;1. 循环的进度谁提供？&#10;&lt;/h3&gt;&lt;p&gt;不是 &lt;code&gt;while&lt;/code&gt;，而是「程序反复执行 + &lt;code&gt;i&lt;/code&gt; 计数」。&lt;/p&gt;&#10;&lt;p&gt;好处是写参数方程时不用自己维护计数器。代价很实在：&lt;strong&gt;每轮的步长必须一样&lt;/strong&gt;。用 &lt;code&gt;i&lt;/code&gt; 递增角度画不出闭合曲线——每轮走的角不同，永远回不到起点。&lt;/p&gt;&#10;&lt;p&gt;这个坑是我自己写示例时踩到的，注释留在了「三瓣」示例里。&lt;/p&gt;&#10;&lt;h3 id="2-循环里的状态放哪"&gt;&lt;a href="#2-%e5%be%aa%e7%8e%af%e9%87%8c%e7%9a%84%e7%8a%b6%e6%80%81%e6%94%be%e5%93%aa" class="header-anchor"&gt;&lt;/a&gt;2. 循环里的状态放哪？&#10;&lt;/h3&gt;&lt;p&gt;最初只有栈。而栈每轮必须清空，否则运行时会一圈圈涨。&lt;/p&gt;&#10;&lt;p&gt;结果：&lt;strong&gt;累加器根本写不出来&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;所以加了&lt;strong&gt;寄存器&lt;/strong&gt;：&lt;code&gt;5 0 st&lt;/code&gt; 存、&lt;code&gt;0 ld&lt;/code&gt; 取。&lt;/p&gt;&#10;&lt;p&gt;栈保持&amp;quot;每轮归零&amp;quot;的干净语义，跨轮状态一律走寄存器。分析器因此能给出很强的保证：&lt;strong&gt;程序停止时栈必须是空的&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;h3 id="3-怎么保证这些约定不被写坏"&gt;&lt;a href="#3-%e6%80%8e%e4%b9%88%e4%bf%9d%e8%af%81%e8%bf%99%e4%ba%9b%e7%ba%a6%e5%ae%9a%e4%b8%8d%e8%a2%ab%e5%86%99%e5%9d%8f" class="header-anchor"&gt;&lt;/a&gt;3. 怎么保证这些约定不被写坏？&#10;&lt;/h3&gt;&lt;p&gt;靠 &lt;code&gt;analyze()&lt;/code&gt;：对编译出的指令做前向数据流分析。&lt;/p&gt;&#10;&lt;p&gt;一条指令被不同路径以不同栈深到达，就重新入队（上限 6 个栈深，再多就是无限增长）。&lt;/p&gt;&#10;&lt;p&gt;判据是&lt;strong&gt;程序停止时栈深为 0&lt;/strong&gt;。这一条覆盖三种坑：栈被弹穿、分支不均衡、循环里偷偷堆值。&lt;/p&gt;&#10;&lt;p&gt;被拒绝时给的是可读的诊断：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✗ 栈不平衡：第 1 行（栈在循环里无限增长（同一条指令被越来越深的栈反复到达））。&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 每个词都要自己把栈收干净，跳转的两个分支不能一个多一个少。&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;编译期还有一道 peephole：&lt;code&gt;N fwd&lt;/code&gt; 折成一条带参指令（&lt;code&gt;fwd#N&lt;/code&gt;），既让指令数少一半，也消掉了&amp;quot;两个字面量在分支里叠在栈上&amp;quot;的歧义。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="验证"&gt;&lt;a href="#%e9%aa%8c%e8%af%81" class="header-anchor"&gt;&lt;/a&gt;验证&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mjs&lt;/span&gt; &lt;span class="c1"&gt;# 63 项断言&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mjs&lt;/span&gt; &lt;span class="c1"&gt;# 27 项断言（真机 Chromium）&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;make&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mjs&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="c1"&gt;# 逐个示例：编译 → 栈分析 → 真跑一遍&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;确定性&lt;/strong&gt;：同一种子逐位一致，不同种子不同图 ⇒ 任何观察都可复现&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;敌意输入&lt;/strong&gt;：空程序、只有注释、除零、取模零、寄存器槽越界、&lt;code&gt;1e9&lt;/code&gt; 大数 —— 都不崩&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;：约 &lt;strong&gt;2400 万条指令/秒&lt;/strong&gt;（单线程）；界面默认每秒执行 600 条&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;示例是机械生成的&lt;/strong&gt;：生成器先编译、做栈分析、真跑 8000 步，全部通过才写回 &lt;code&gt;loom.html&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;最后这条是被逼出来的做法——&lt;strong&gt;在此之前我手工改示例错了五次&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;h3 id="验证抓到的-8-个真实缺陷"&gt;&lt;a href="#%e9%aa%8c%e8%af%81%e6%8a%93%e5%88%b0%e7%9a%84-8-%e4%b8%aa%e7%9c%9f%e5%ae%9e%e7%bc%ba%e9%99%b7" class="header-anchor"&gt;&lt;/a&gt;验证抓到的 8 个真实缺陷&#10;&lt;/h3&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;#&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;缺陷&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;症状&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;跳转目标被折成 &lt;code&gt;[&amp;quot;lit&amp;quot;, idx]&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;jz&lt;/code&gt; 拿到 0 而不是目标行，条件跳转全部失效&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;跳转占位指令多占一个槽位&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;目标整体错位，&lt;code&gt;i&lt;/code&gt; 永不递增&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;跳转参数是&lt;strong&gt;字符串&lt;/strong&gt; &lt;code&gt;&amp;quot;0&amp;quot;&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;Number.isFinite(&amp;quot;0&amp;quot;)&lt;/code&gt; 为 false ⇒ 回边从未被分析&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;末端判定读的是执行&lt;strong&gt;前&lt;/strong&gt;的栈深&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;1 fwd&lt;/code&gt; 这类正常程序被误判不平衡&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;分析器只记&amp;quot;首达&amp;quot;栈深&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;每轮偷偷堆值的程序被放行&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;6&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;缺&amp;quot;每轮入口栈深一致&amp;quot;不变量&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;同上——运行 1500 步后栈涨到 1500&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;7&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;示例里 &lt;code&gt;i 0.1 color&lt;/code&gt; 的栈残留&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;每轮留一个数，栈无限增长&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;自动取景用&amp;quot;全部历史外接框&amp;quot;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;随机游走把视野撑到无穷，画面全黑&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;第 8 条不是语言问题，是&lt;strong&gt;可视化策略&lt;/strong&gt;问题。用历史包络去取景，对无界图形必然是错的；改成跟随画笔当前位置之后才正常。&lt;/p&gt;&#10;&lt;p&gt;第 3 条尤其典型：&lt;code&gt;&amp;quot;0&amp;quot;&lt;/code&gt; 和 &lt;code&gt;0&lt;/code&gt; 在 JavaScript 里看起来都像 0，但 &lt;code&gt;Number.isFinite(&amp;quot;0&amp;quot;)&lt;/code&gt; 是 &lt;code&gt;false&lt;/code&gt;。于是回边从来没被分析过，&lt;strong&gt;静态检查等于不存在&lt;/strong&gt;——而界面一切正常。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="它现在能画什么"&gt;&lt;a href="#%e5%ae%83%e7%8e%b0%e5%9c%a8%e8%83%bd%e7%94%bb%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;它现在能画什么&#10;&lt;/h2&gt;&lt;p&gt;它只画线。但 17 个词加上反复执行，能出来的东西比想象的多：蕨类、格栅、漂移的涡、以及那朵玫瑰。&lt;/p&gt;&#10;&lt;p&gt;参数滑杆 &lt;code&gt;p0&lt;/code&gt;…&lt;code&gt;p5&lt;/code&gt; 让你不改代码就能调形状——这也是它作为一个&amp;quot;玩的东西&amp;quot;成立的原因。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p style="text-align:center;margin:2.5em 0"&gt;&#13;&#10;&lt;a href="https://cloudlake.eu.org/lab/loom/" style="display:inline-block;padding:12px 26px;border-radius:8px;background:var(--accent-color);color:var(--accent-color-text);text-decoration:none;font-weight:600"&gt;打开 Loom 玩一下 →&lt;/a&gt;&#13;&#10;&lt;/p&gt;&#13;&#10;&lt;hr&gt;&#10;&lt;h2 id="还能往哪走"&gt;&lt;a href="#%e8%bf%98%e8%83%bd%e5%be%80%e5%93%aa%e8%b5%b0" class="header-anchor"&gt;&lt;/a&gt;还能往哪走&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;图形输出&lt;/strong&gt;：现在只画线。加 &lt;code&gt;fill&lt;/code&gt; / &lt;code&gt;stroke&lt;/code&gt; 路径模式就能做面&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;多画笔&lt;/strong&gt;：&lt;code&gt;pen&lt;/code&gt; 已经是状态机，加若干&amp;quot;画笔槽&amp;quot;就能一次画多层&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;编辑器&lt;/strong&gt;：行号已经在了，差行内报错高亮；&lt;code&gt;analyze()&lt;/code&gt; 已经能给出准确行号，接口是现成的&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;17 个词听起来很少。&lt;/p&gt;&#10;&lt;p&gt;但语言的表达能力从来不是词表决定的，是&lt;strong&gt;组合规则&lt;/strong&gt;决定的。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item></channel></rss>