<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>CloudLake</title><link>https://cloudlake.eu.org/</link><description>Recent content on CloudLake</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Sat, 24 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://cloudlake.eu.org/index.xml" rel="self" type="application/rss+xml"/><item><title>在浏览器里养一台计算器：TI-Nspire 在线模拟器从 0 到「长期使用」</title><link>https://cloudlake.eu.org/p/nspire-web-emulator/</link><pubDate>Sat, 24 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/nspire-web-emulator/</guid><description>&lt;img src="https://cloudlake.eu.org/p/nspire-web-emulator/cover.jpg" alt="Featured image of post 在浏览器里养一台计算器：TI-Nspire 在线模拟器从 0 到「长期使用」" /&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;一个 GPL 的 ARM 计算器模拟器、一个从 2016 年捞出来的 Palm 游戏、一台 1995 年的 Windows 95 机器，最后都被塞进了同一个网页里。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;现在打开 &lt;a class="link" href="https://cloudlake.eu.org/nspire/" target="_blank" rel="noopener"&#10; &gt;cloudlake.eu.org/nspire&lt;/a&gt;，你会看到四张卡片：经典灰度 Touchpad、彩色 CX II CAS、一个彩蛋 Windows 95，以及长期使用的登录入口。下面这篇文章把这个平台从架构到踩坑完整讲一遍。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="入口页：四张卡片" class="gallery-image" data-flex-basis="305px" data-flex-grow="127" height="1100" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-entry.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-entry_hu_623494c3318391c8.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-entry.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="一最后做出来的东西"&gt;&lt;a href="#%e4%b8%80%e6%9c%80%e5%90%8e%e5%81%9a%e5%87%ba%e6%9d%a5%e7%9a%84%e4%b8%9c%e8%a5%bf" class="header-anchor"&gt;&lt;/a&gt;一、最后做出来的东西&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;三种「机器」共用一个页面&lt;/strong&gt;：经典 Nspire Touchpad（灰度 320×240）、CX II CAS（彩色 16 位色）、以及通过 WiNspire 跑的 Windows 95&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;完整键盘 + 触摸板 + 文件传输&lt;/strong&gt;：70 个键的屏幕键盘、触摸板滑动/点击、拖拽/整文件夹上传、目录浏览与下载&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;游戏一键启动&lt;/strong&gt;：点一下就从服务器把 nPal（仙剑奇侠传）传进计算器并自动运行&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;长期使用模式&lt;/strong&gt;：用户名登录，状态存服务器，每 2 分钟自动保存，下次登录直接接着用&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img alt="CX II 在线模拟器整页：左边屏幕、右边键盘、下面是文件传输和快速启动游戏" class="gallery-image" data-flex-basis="258px" data-flex-grow="107" height="1301" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-cx2.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-cx2_hu_6547d1e51d011008.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-cx2.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;p&gt;经典灰度机的页面是同一个文件，只是换了镜像：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="经典 Touchpad 页面" class="gallery-image" data-flex-basis="252px" data-flex-grow="105" height="1331" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-classic.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-classic_hu_872dbeeb73d70df4.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-classic.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二地基把-firebird-编译成-webassembly"&gt;&lt;a href="#%e4%ba%8c%e5%9c%b0%e5%9f%ba%e6%8a%8a-firebird-%e7%bc%96%e8%af%91%e6%88%90-webassembly" class="header-anchor"&gt;&lt;/a&gt;二、地基：把 Firebird 编译成 WebAssembly&#10;&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/nspire-emus/firebird" target="_blank" rel="noopener"&#10; &gt;Firebird&lt;/a&gt; 是一个开源（GPL-3.0）的 TI-Nspire 模拟器，桌面版基于 Qt。它模拟的是 ARM926 级别的 SoC、LCD 控制器、键盘矩阵、NAND flash 和 USB 控制器。&lt;/p&gt;&#10;&lt;p&gt;网页版做的事情是：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;用 emscripten 把 Firebird core（约 33 个源文件）编译成 &lt;code&gt;firebird.js&lt;/code&gt;（约 88 KB）+ &lt;code&gt;firebird.wasm&lt;/code&gt;（约 230 KB）&lt;/li&gt;&#10;&lt;li&gt;页面把 &lt;strong&gt;flash 镜像&lt;/strong&gt;（整块 NAND 转储，经典机 33 MB，CX II 132 MB）写进 Emscripten 的内存文件系统&lt;/li&gt;&#10;&lt;li&gt;可选加载 &lt;strong&gt;snapshot&lt;/strong&gt;（gzip 压缩的 RAM + CPU 状态），实现「秒开」&lt;/li&gt;&#10;&lt;li&gt;用 &lt;code&gt;paintLCD()&lt;/code&gt; 把 320×240 的帧缓冲画到 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; 上&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;几个设计决定后来被证明是对的：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;一个 &lt;code&gt;emu.html&lt;/code&gt; 支持所有机型&lt;/strong&gt;：机型差异全部收敛到 URL 参数 &lt;code&gt;?flash=...&amp;amp;boot=...&amp;amp;snap=...&lt;/code&gt;，页面代码是同一份。经典机、CX II、Windows 95、长期使用模式都复用这套加载逻辑。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;镜像放服务器、状态放浏览器/账号&lt;/strong&gt;：静态镜像走 Caddy 的 precompressed（brotli/gzip），经典 flash 压缩后约 12 MB，CX II 约 17 MB，浏览器缓存 30 天。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;快照优先&lt;/strong&gt;：冷启动 60 秒 vs 快照恢复几秒，体验差距巨大，所以后来能存快照的场景都尽量存。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="三让模拟器能用键盘触摸板文件传输"&gt;&lt;a href="#%e4%b8%89%e8%ae%a9%e6%a8%a1%e6%8b%9f%e5%99%a8%e8%83%bd%e7%94%a8%e9%94%ae%e7%9b%98%e8%a7%a6%e6%91%b8%e6%9d%bf%e6%96%87%e4%bb%b6%e4%bc%a0%e8%be%93" class="header-anchor"&gt;&lt;/a&gt;三、让模拟器「能用」：键盘、触摸板、文件传输&#10;&lt;/h2&gt;&lt;p&gt;只有画面没有输入，模拟器就是个屏保。这部分是平台真正的骨架。&lt;/p&gt;&#10;&lt;h3 id="31-键盘70-键矩阵"&gt;&lt;a href="#31-%e9%94%ae%e7%9b%9870-%e9%94%ae%e7%9f%a9%e9%98%b5" class="header-anchor"&gt;&lt;/a&gt;3.1 键盘：70 键矩阵&#10;&lt;/h3&gt;&lt;p&gt;Firebird 的键盘是 8×11 矩阵，网页端只暴露一个 &lt;code&gt;setKey(row, col, state)&lt;/code&gt;。页面上用 DOM 画了 70 个键，按下时转成矩阵坐标注入。&lt;/p&gt;&#10;&lt;p&gt;唯一的小坑：OS 的键盘扫描有间隔，太快松开会漏键，所以前端给每次按键加了 &lt;strong&gt;最少 80 ms 的保持时间&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;h3 id="32-触摸板从只认方向键到真正能推鼠标"&gt;&lt;a href="#32-%e8%a7%a6%e6%91%b8%e6%9d%bf%e4%bb%8e%e5%8f%aa%e8%ae%a4%e6%96%b9%e5%90%91%e9%94%ae%e5%88%b0%e7%9c%9f%e6%ad%a3%e8%83%bd%e6%8e%a8%e9%bc%a0%e6%a0%87" class="header-anchor"&gt;&lt;/a&gt;3.2 触摸板：从「只认方向键」到真正能推鼠标&#10;&lt;/h3&gt;&lt;p&gt;这是整个项目最值得写的一段。TI-Nspire 的触摸板在 OS 里既当 &lt;strong&gt;5 向键&lt;/strong&gt;用（菜单导航），又当 &lt;strong&gt;鼠标&lt;/strong&gt;用（图形、几何、Windows 95）。&lt;/p&gt;&#10;&lt;p&gt;原来的实现只会模拟「按触摸板边缘 → 方向键」，菜单能走，但 &lt;strong&gt;GUI 指针纹丝不动&lt;/strong&gt;。原因是：OS 的指针用的是触摸板驱动算出来的&lt;strong&gt;相对位移&lt;/strong&gt;（&lt;code&gt;touchpad_rel_x/y&lt;/code&gt;），而页面从来没把连续滑动坐标送进去。&lt;/p&gt;&#10;&lt;p&gt;修复思路：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;拖动时按 ~38 Hz 连续调用 &lt;code&gt;setTouchpadNorm(x, y, contact=1, down=0)&lt;/code&gt;，让 Firebird 的 &lt;code&gt;touchpad_set_state()&lt;/code&gt; 计算相对位移&lt;/li&gt;&#10;&lt;li&gt;轻点 = 按下脉冲（中心 = 左键，边缘 = 方向键；&lt;code&gt;Ctrl&lt;/code&gt; + 轻点 = 右键）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;混合回退&lt;/strong&gt;：如果滑动了 220 ms、画面完全没变化（TI-OS 菜单只认方向事件），自动退化成方向键手势，这样拖动导航不丢&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;修复后的效果（拖动前 / 后，Win95 的鼠标从画面中心移动到右下）：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="触摸板指针修复前后对比" class="gallery-image" data-flex-basis="593px" data-flex-grow="247" height="530" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-touchpad.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-touchpad_hu_68942d18c4994de5.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-touchpad.png 1310w" width="1310"&gt;&lt;/p&gt;&#10;&lt;h3 id="33-文件传输把虚拟-usb-线变成拖拽上传"&gt;&lt;a href="#33-%e6%96%87%e4%bb%b6%e4%bc%a0%e8%be%93%e6%8a%8a%e8%99%9a%e6%8b%9f-usb-%e7%ba%bf%e5%8f%98%e6%88%90%e6%8b%96%e6%8b%bd%e4%b8%8a%e4%bc%a0" class="header-anchor"&gt;&lt;/a&gt;3.3 文件传输：把虚拟 USB 线变成拖拽上传&#10;&lt;/h3&gt;&lt;p&gt;Firebird 的 USB Link 层允许宿主机和模拟器里的 OS 互传文件。我们在 WASM 里加了这些导出：&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;emuPutFile(local, remote) MEMFS → 计算器&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuGetFile(remote, dest) 计算器 → MEMFS&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuNewDir / emuDirList / emuDelete / emuMove&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;emuUsblinkProgress / Busy / Done / Abort&#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;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;上传队列&lt;/strong&gt;：串行传输、逐文件进度条、失败自动重试（OS 侧偶发返回负数错误码时，&lt;code&gt;emuUsblinkAbort()&lt;/code&gt; + 重连再试）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;整文件夹上传&lt;/strong&gt;：&lt;code&gt;&amp;lt;input webkitdirectory&amp;gt;&lt;/code&gt; + 拖拽目录时用 &lt;code&gt;webkitGetAsEntry()&lt;/code&gt; 递归，保留相对路径，按层级 &lt;code&gt;emuNewDir&lt;/code&gt; 自动建目录&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;只收 &lt;code&gt;.tns&lt;/code&gt; 的 OS 限制&lt;/strong&gt;：实测 OS 文档服务只接受 &lt;code&gt;.tns&lt;/code&gt;，其它扩展名（&lt;code&gt;.txt/.dat/.bin/.zip/.py/...&lt;/code&gt;）一律被拒；文件夹里的非 &lt;code&gt;.tns&lt;/code&gt; 会在界面上标「跳过」，并提示先改名&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img alt="文件传输面板：文件夹上传、队列、只收 .tns 的说明" class="gallery-image" data-flex-basis="583px" data-flex-grow="243" height="497" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-folders.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-folders_hu_aa2fe1ce3e9363eb.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-folders.png 1208w" width="1208"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="四踩过的最大的坑页面弹输入窗口然后冻结"&gt;&lt;a href="#%e5%9b%9b%e8%b8%a9%e8%bf%87%e7%9a%84%e6%9c%80%e5%a4%a7%e7%9a%84%e5%9d%91%e9%a1%b5%e9%9d%a2%e5%bc%b9%e8%be%93%e5%85%a5%e7%aa%97%e5%8f%a3%e7%84%b6%e5%90%8e%e5%86%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;四、踩过的最大的坑：页面「弹输入窗口」然后冻结&#10;&lt;/h2&gt;&lt;p&gt;有一段时间用户反馈：&lt;strong&gt;装 Ndless 的时候浏览器弹出一个输入窗口，然后整个页面卡死&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;追下去发现链路非常长：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Ndless 安装器在模拟器里触发了非法指令（&lt;code&gt;Prefetch abort&lt;/code&gt; / &lt;code&gt;Cannot set T bit&lt;/code&gt;）&lt;/li&gt;&#10;&lt;li&gt;Firebird 的异常处理会进入内置调试器&lt;/li&gt;&#10;&lt;li&gt;WASM 版的调试器输入函数还在读 &lt;code&gt;stdin&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Emscripten 的 stdin 兜底实现是 &lt;strong&gt;&lt;code&gt;window.prompt(&amp;quot;Input: &amp;quot;)&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;window.prompt&lt;/code&gt; 会&lt;strong&gt;阻塞页面主线程&lt;/strong&gt; → 整个模拟器看起来死了&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;修复是在 &lt;code&gt;firebird/emscripten/main.cpp&lt;/code&gt; 里给 &lt;code&gt;gui_debugger_request_input()&lt;/code&gt; 换成直接返回一条 &lt;code&gt;c&lt;/code&gt;（continue），让异常路径走 &lt;code&gt;EVENT_RESET&lt;/code&gt; 重启 OS，而不是等用户输入。&lt;/p&gt;&#10;&lt;p&gt;顺带修掉的还有一个自己引入的回归：&lt;strong&gt;点击画面曾经等于点触摸板中心&lt;/strong&gt;，会误开文档/对话框。现在画面单击只负责取键盘焦点，点击请用触摸板区域。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="五npal-移植把-2016-年的-palm-游戏跑进浏览器"&gt;&lt;a href="#%e4%ba%94npal-%e7%a7%bb%e6%a4%8d%e6%8a%8a-2016-%e5%b9%b4%e7%9a%84-palm-%e6%b8%b8%e6%88%8f%e8%b7%91%e8%bf%9b%e6%b5%8f%e8%a7%88%e5%99%a8" class="header-anchor"&gt;&lt;/a&gt;五、nPal 移植：把 2016 年的 Palm 游戏跑进浏览器&#10;&lt;/h2&gt;&lt;p&gt;nPal 是网友 ntzyz 把 &lt;strong&gt;PalmOS 时代的《仙剑奇侠传》&lt;/strong&gt; 移植到 TI-Nspire 的项目。我们拿到的是 2016 年的 nPal 1.2 发布包，里面是一个 1.25 MB 的 &lt;code&gt;sdlpal.tns&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h3 id="51-旧包为什么会在-cx-ii-上弹兼容性模式"&gt;&lt;a href="#51-%e6%97%a7%e5%8c%85%e4%b8%ba%e4%bb%80%e4%b9%88%e4%bc%9a%e5%9c%a8-cx-ii-%e4%b8%8a%e5%bc%b9%e5%85%bc%e5%ae%b9%e6%80%a7%e6%a8%a1%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;5.1 旧包为什么会在 CX II 上弹「兼容性模式」&#10;&lt;/h3&gt;&lt;p&gt;旧的 &lt;code&gt;sdlpal.tns&lt;/code&gt; 是 &lt;strong&gt;PRG 容器&lt;/strong&gt;（文件头 &lt;code&gt;50 52 47 00&lt;/code&gt;），里面根本没有 &lt;code&gt;Zehn&lt;/code&gt; 签名。Ndless r2022 会用旧加载路径跑它，于是：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;读不到 Zehn 标志位表&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;supports_hww&lt;/code&gt; 保持 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Ndless 弹出「Activating compatibility mode. This application hasn&amp;rsquo;t been updated to work with your hardware.」&lt;/li&gt;&#10;&lt;li&gt;走 320×320 的 LCD 兼容层，CX II 的原生 240×320 旋转屏适配失效&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="52-重新编译"&gt;&lt;a href="#52-%e9%87%8d%e6%96%b0%e7%bc%96%e8%af%91" class="header-anchor"&gt;&lt;/a&gt;5.2 重新编译&#10;&lt;/h3&gt;&lt;p&gt;用 &lt;strong&gt;Ndless r2022 SDK&lt;/strong&gt; 重新编译 nPal 的 SDL 部分：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;SDK 自带的 nSDL 1.1.1 已经改用新的 &lt;code&gt;lcd_blit&lt;/code&gt; 屏幕 API&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;genzehn&lt;/code&gt; 打包时扫描 ELF 符号，发现 &lt;code&gt;_genzehn_new_lcd_api&lt;/code&gt; 标记，自动写入 &lt;code&gt;USES_LCD_BLIT = true&lt;/code&gt; 和 &lt;code&gt;RUNS_ON_HWW = true&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;产物 &lt;code&gt;sdlpal.tns&lt;/code&gt; 419,248 B（纯 Zehn，无 PRG 外壳），&lt;code&gt;genzehn --info&lt;/code&gt; 能直接验证：&lt;/li&gt;&#10;&lt;/ul&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;Application: sdlpal&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This executable uses the new lcd_blit API.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This executable supports the HW-W 240x320 LCD.&#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;另外两条工具链（官方 GCC 14.2 和 apt GCC 13）构建出的版本，在模拟器里&lt;strong&gt;逐帧逐像素完全一致&lt;/strong&gt;，最后分发用官方工具链版。&lt;/p&gt;&#10;&lt;h3 id="53-资源文件与启动"&gt;&lt;a href="#53-%e8%b5%84%e6%ba%90%e6%96%87%e4%bb%b6%e4%b8%8e%e5%90%af%e5%8a%a8" class="header-anchor"&gt;&lt;/a&gt;5.3 资源文件与启动&#10;&lt;/h3&gt;&lt;p&gt;nPal 的资源是 &lt;code&gt;resources.bin.tns&lt;/code&gt;（23.9 MB）和 &lt;code&gt;desc.dat.tns&lt;/code&gt;（16 KB）——它们本质上就是数据文件，只是 &lt;strong&gt;故意用 &lt;code&gt;.tns&lt;/code&gt; 扩展名&lt;/strong&gt;，这样才能通过 OS 的文档服务传进计算器。全部放进计算器的 &lt;code&gt;/npal&lt;/code&gt;，运行 &lt;code&gt;sdlpal.tns&lt;/code&gt; 即可。&lt;/p&gt;&#10;&lt;p&gt;在模拟器里看到的结果：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="nPal 启动后的仙剑标题画面" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-npal.png" width="640"&gt;&lt;/p&gt;&#10;&lt;h3 id="54-一键游戏启动"&gt;&lt;a href="#54-%e4%b8%80%e9%94%ae%e6%b8%b8%e6%88%8f%e5%90%af%e5%8a%a8" class="header-anchor"&gt;&lt;/a&gt;5.4 一键游戏启动&#10;&lt;/h3&gt;&lt;p&gt;手动上传三个文件太麻烦，于是做了「快速启动游戏」：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;服务器放 &lt;code&gt;games.json&lt;/code&gt; 清单 + 每个游戏的文件（同时生成 &lt;code&gt;.br/.gz&lt;/code&gt; 供 Caddy 预压缩）&lt;/li&gt;&#10;&lt;li&gt;页面选择 nPal → 依次下载 3 个文件 → 自动创建 &lt;code&gt;/npal&lt;/code&gt; → 走 USB 上传 → 主程序最后上传，保证文件浏览器选中它 → 自动发回车运行&lt;/li&gt;&#10;&lt;li&gt;旁边还有「安装 Ndless」按钮：自动上传安装器+资源，再按固定节奏发 Enter 走完安装流程&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;实测（线上）：&lt;strong&gt;安装 Ndless 66 秒 + 启动游戏 59 秒&lt;/strong&gt;，然后就是标题画面。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="文件面板里的快速启动游戏行" class="gallery-image" data-flex-basis="458px" data-flex-grow="190" height="633" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-gamepanel.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-gamepanel_hu_a319d50a3e91baf0.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-gamepanel.png 1208w" width="1208"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Ndless 安装器在模拟器里的界面" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-ndless.png" width="640"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="六彩蛋在计算器里跑-windows-95"&gt;&lt;a href="#%e5%85%ad%e5%bd%a9%e8%9b%8b%e5%9c%a8%e8%ae%a1%e7%ae%97%e5%99%a8%e9%87%8c%e8%b7%91-windows-95" class="header-anchor"&gt;&lt;/a&gt;六、彩蛋：在计算器里跑 Windows 95&#10;&lt;/h2&gt;&lt;p&gt;WiNspire 是 &lt;strong&gt;tiny386（x86 PC 模拟器）的 CX II 移植&lt;/strong&gt;，以 Ndless 程序运行，Native 模式给 guest 16 MB RAM / 256 KB 显存。于是流程变成：&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;浏览器 → Firebird(WASM，模拟 CX II) → TI-OS + Ndless → winspire.tns(tiny386) → Windows 95&#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;磁盘镜像是从 PCjs 的 Windows 95 RTM 演示机用它的 “Save HD” 导出的 68 MB raw image&lt;/li&gt;&#10;&lt;li&gt;把镜像改名成 &lt;code&gt;disk.img.tns&lt;/code&gt;，和 &lt;code&gt;winspire.tns&lt;/code&gt;、bios、vgabios、ini 一起上传到 &lt;code&gt;/winspire&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;冷启动大约 4–5 分钟：SeaBIOS → Windows 95 开机画面 → 桌面&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;img alt="WiNspire 里的 SeaBIOS 在浏览器中启动" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-seabios.png" width="640"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Windows 95 启动画面" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-win95-boot.png" width="640"&gt;&lt;/p&gt;&#10;&lt;p&gt;为了把「开机 4 分钟」变成「秒进桌面」，又做了预烘焙：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;在模拟器里跑到 Win95 桌面&lt;/li&gt;&#10;&lt;li&gt;调用 &lt;code&gt;saveSnapshot()&lt;/code&gt; 生成快照&lt;/li&gt;&#10;&lt;li&gt;关键：Firebird 的 NAND 修改默认只在内存里，必须调用新加的 &lt;code&gt;saveFlash()&lt;/code&gt;（内部就是 &lt;code&gt;flash_save_changes()&lt;/code&gt;）把修改块写回 &lt;code&gt;flash.img&lt;/code&gt;，否则导出的镜像还是原样&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;最终页面加载 &lt;code&gt;flash&lt;/code&gt;（brotli 后 17.2 MB）+ &lt;code&gt;snapshot&lt;/code&gt;（33 MB，基本不可压缩），点开始几秒后直接是 Windows 95 桌面：&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Windows 95 桌面（浏览器内）" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="480" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-win95.png" width="640"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="七长期使用用户名登录--状态持久化"&gt;&lt;a href="#%e4%b8%83%e9%95%bf%e6%9c%9f%e4%bd%bf%e7%94%a8%e7%94%a8%e6%88%b7%e5%90%8d%e7%99%bb%e5%bd%95--%e7%8a%b6%e6%80%81%e6%8c%81%e4%b9%85%e5%8c%96" class="header-anchor"&gt;&lt;/a&gt;七、长期使用：用户名登录 + 状态持久化&#10;&lt;/h2&gt;&lt;p&gt;短期玩一玩，刷新页面就重来；但总有人想「养」一台自己的计算器。于是加了长期使用模式：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;登录页输入用户名（无密码，HMAC 签名 cookie 记身份）&lt;/li&gt;&#10;&lt;li&gt;一个用户名同一时间只能开一个窗口（心跳续锁，180 秒无心跳自动过期）&lt;/li&gt;&#10;&lt;li&gt;新用户从干净的 CX II 系统开始（默认镜像）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;保存 = flash 镜像 + 内存快照&lt;/strong&gt;：&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;saveFlash()&lt;/code&gt; 把 NAND 修改写回 MEMFS 的 &lt;code&gt;flash.img&lt;/code&gt;（138 MB）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;saveSnapshot()&lt;/code&gt; 生成 &lt;code&gt;snapshot.bin&lt;/code&gt;（gzip 流）&lt;/li&gt;&#10;&lt;li&gt;浏览器里用 &lt;code&gt;CompressionStream('gzip')&lt;/code&gt; 把 flash 压到约 19 MB，和快照（约 10–33 MB）一起 POST 到服务器&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;恢复 = 直接加载 flash + snapshot&lt;/strong&gt;，从快照恢复现场&lt;/li&gt;&#10;&lt;li&gt;每 2 分钟自动保存一次；「保存」按钮手动存；「退出」先保存再注销&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;后端是一个 &lt;strong&gt;Python 标准库写的单文件服务&lt;/strong&gt;（&lt;code&gt;nspire-lt&lt;/code&gt;，systemd 托管），Caddy 把 &lt;code&gt;/nspire/api/&lt;/code&gt; 反代到它，数据落在 &lt;code&gt;/var/lib/nspire-lt/users/&amp;lt;user&amp;gt;/&lt;/code&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;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-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&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;saved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="err"&gt;已保存&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;19&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;second&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&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;restore&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="mi"&gt;9236&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="n"&gt;vs&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="err"&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;p&gt;&lt;img alt="长期使用登录页" class="gallery-image" data-flex-basis="305px" data-flex-grow="127" height="1100" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-lt.png" srcset="https://cloudlake.eu.org/p/nspire-web-emulator/nspire-lt_hu_27e2ee6570edcb8b.png 800w, https://cloudlake.eu.org/p/nspire-web-emulator/nspire-lt.png 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="八数据与性能"&gt;&lt;a href="#%e5%85%ab%e6%95%b0%e6%8d%ae%e4%b8%8e%e6%80%a7%e8%83%bd" 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;flash 原始&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;首屏（br 压缩）&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;经典 Touchpad&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;33 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;~12 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;快照恢复极快&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;灰度 320×240&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;CX II CAS&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;132 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;17.2 MB&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;冷启动 ~60 s&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;彩色 16 位色&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;Windows 95&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;132 MB + 68 MB 磁盘&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;~50 MB（含快照）&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;冷启动 4–5 分钟，快照秒开&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;每用户 flash + 快照&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;登录时下载&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;快照恢复 ~9 s&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;一些数字：模拟 USB 传输约 &lt;strong&gt;550 KB/s&lt;/strong&gt;（23 MB 的 nPal 资源约 40–60 秒）；一次长期存档上行约 &lt;strong&gt;20–50 MB&lt;/strong&gt;；长期模式每人磁盘占用约 &lt;strong&gt;30–50 MB&lt;/strong&gt;（gzip 后）。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="九还欠着的"&gt;&lt;a href="#%e4%b9%9d%e8%bf%98%e6%ac%a0%e7%9d%80%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;九、还欠着的&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Windows 95 冷启动版&lt;/strong&gt;：预烘焙时遇到 NAND 写回/flash 保存的边界问题，目前只保留快照直启入口&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Ndless 冷启动激活状态&lt;/strong&gt;：干净镜像冷启动后 Ndless 不是激活状态，需要在游戏面板点一次「安装 Ndless」（约 1 分钟）；长期使用模式里装好保存后，快照会保留激活状态&lt;/li&gt;&#10;&lt;li&gt;移动端触控（目前触摸板/鼠标事件主要按桌面调优）&lt;/li&gt;&#10;&lt;li&gt;音频：Firebird 的 PC 喇叭/声音还没接&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="结语"&gt;&lt;a href="#%e7%bb%93%e8%af%ad" class="header-anchor"&gt;&lt;/a&gt;结语&#10;&lt;/h2&gt;&lt;p&gt;这个项目最花时间的部分，其实不是「把模拟器编成 WASM」——那是几小时的活。真正难的是&lt;strong&gt;层与层之间的接缝&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;p&gt;浏览器输入 → ARM 模拟器 → TI-OS 驱动 → Ndless → 游戏/Windows，每一层都有自己的假设。触摸板的「方向 vs 指针」、文件系统的「只收 .tns」、调试器的「读 stdin」、NAND 的「写缓存」，每一个都足以让整个平台看起来「坏了」。&lt;/p&gt;&#10;&lt;p&gt;把这些接缝一个个对上之后，才有现在这个能在浏览器里养计算器、打仙剑、开 Windows 95、还能下次登录接着用的东西。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;入口：&lt;a class="link" href="https://cloudlake.eu.org/nspire/" target="_blank" rel="noopener"&#10; &gt;https://cloudlake.eu.org/nspire/&lt;/a&gt;&lt;br&gt;&#10;长期使用：&lt;a class="link" href="https://cloudlake.eu.org/nspire/lt/index.html" target="_blank" rel="noopener"&#10; &gt;https://cloudlake.eu.org/nspire/lt/index.html&lt;/a&gt;&lt;br&gt;&#10;技术栈：Firebird（GPL-3.0）+ emscripten/WASM + Hugo + Caddy + 一个 Python 小服务&lt;/p&gt;&#10;</description></item><item><title>给 AI 装上一双能测距的眼睛</title><link>https://cloudlake.eu.org/p/visionmate/</link><pubDate>Tue, 20 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/visionmate/</guid><description>&lt;img src="https://cloudlake.eu.org/p/visionmate/cover.jpg" alt="Featured image of post 给 AI 装上一双能测距的眼睛" /&gt;&lt;p&gt;你戴着它，问一句「前面有什么」。&lt;/p&gt;&#10;&lt;p&gt;它需要回答的其实&lt;strong&gt;不是&lt;/strong&gt;「前面有一个人」——而是「&lt;strong&gt;前面 0.8 米有一个人&lt;/strong&gt;」。&lt;/p&gt;&#10;&lt;p&gt;这两句话对一个只会做目标检测的系统来说，差着一整个维度。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="只有-2d-检测是看不见多远的"&gt;&lt;a href="#%e5%8f%aa%e6%9c%89-2d-%e6%a3%80%e6%b5%8b%e6%98%af%e7%9c%8b%e4%b8%8d%e8%a7%81%e5%a4%9a%e8%bf%9c%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;只有 2D 检测，是看不见「多远」的&#10;&lt;/h2&gt;&lt;p&gt;YOLO 能给你一个框、一个类别、一个置信度。&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;person 0.89 [x1,y1,x2,y2]&#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;/p&gt;&#10;&lt;p&gt;而这件事在真实场景里是致命的：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;「前面有人」→ 你继续走&lt;/li&gt;&#10;&lt;li&gt;「前面 0.4 米有人」→ 你必须停&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;同一个检测结果，两种完全相反的行动。&lt;/p&gt;&#10;&lt;p&gt;所以我给这个系统加了两个东西：&lt;strong&gt;深度估计&lt;/strong&gt;和&lt;strong&gt;激光雷达&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="三层感知"&gt;&lt;a href="#%e4%b8%89%e5%b1%82%e6%84%9f%e7%9f%a5" class="header-anchor"&gt;&lt;/a&gt;三层感知&#10;&lt;/h2&gt;&lt;p&gt;&lt;img alt="VisionMate 感知管线" class="gallery-image" data-flex-basis="473px" data-flex-grow="197" height="760" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/visionmate/vision-pipeline.jpg" srcset="https://cloudlake.eu.org/p/visionmate/vision-pipeline_hu_af1eff1de34b85b2.jpg 800w, https://cloudlake.eu.org/p/visionmate/vision-pipeline.jpg 1500w" width="1500"&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;&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;strong&gt;① 输入&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;文本指令，或 Whisper 实时语音转写&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;strong&gt;② 调度&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Orchestrator 把自然语言映射到一个视觉能力&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;strong&gt;③ 视觉&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;YOLOv8n 检测 / MiDaS 深度 / EasyOCR 文字&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;strong&gt;④ 标定&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;用 LiDAR 快照重标定深度尺度&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;strong&gt;⑤ 输出&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;hazard_level&lt;/code&gt; + 人话 message + 结构化 meta&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;FastAPI 的 MCP 接口&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;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-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;POST&lt;/span&gt; &lt;span class="err"&gt;/api/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/mcp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;描述周围&amp;#34;&lt;/span&gt;&lt;span class="p"&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;p&gt;返回里既有给人看的句子，也有给程序用的字段。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="五个能力带优先级"&gt;&lt;a href="#%e4%ba%94%e4%b8%aa%e8%83%bd%e5%8a%9b%e5%b8%a6%e4%bc%98%e5%85%88%e7%ba%a7" class="header-anchor"&gt;&lt;/a&gt;五个能力，带优先级&#10;&lt;/h2&gt;&lt;p&gt;调度器不猜——它把指令映射到一个明确的 function：&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;span class="lnt"&gt;5&#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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;vision.ocr_read&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;high&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 读文字&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;vision.detect_objects&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;medium&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 检测物体&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;vision.distance_query&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;medium&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 问距离&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;vision.describe_scene&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;medium&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 描述场景&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;vision.check_hazards&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;low&amp;#34;&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;p&gt;&lt;code&gt;check_hazards&lt;/code&gt; 优先级最低，因为它&lt;strong&gt;要调用前面全部能力&lt;/strong&gt;才能回答——它是管线末端，不是入口。&lt;/p&gt;&#10;&lt;p&gt;意图分类有两路：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;规则匹配&lt;/strong&gt;（关键词、正则）—— 快、可预测&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;sklearn 模型&lt;/strong&gt;（&lt;code&gt;intent_sklearn.joblib&lt;/code&gt;，965 KB）—— 处理规则覆盖不到的说法&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;规则优先，模型兜底。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="核心设计lidar-只做标定不做判定"&gt;&lt;a href="#%e6%a0%b8%e5%bf%83%e8%ae%be%e8%ae%a1lidar-%e5%8f%aa%e5%81%9a%e6%a0%87%e5%ae%9a%e4%b8%8d%e5%81%9a%e5%88%a4%e5%ae%9a" class="header-anchor"&gt;&lt;/a&gt;核心设计：LiDAR 只做标定，不做判定&#10;&lt;/h2&gt;&lt;p&gt;这是整个项目里我最想讲的一个决定。&lt;/p&gt;&#10;&lt;p&gt;先说结论（这也是代码注释里的原话）：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;&lt;strong&gt;基于视觉深度和 YOLO 距离统一评估危险，LiDAR 仅用于深度标定。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;h3 id="为什么不直接用雷达测距"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%8d%e7%9b%b4%e6%8e%a5%e7%94%a8%e9%9b%b7%e8%be%be%e6%b5%8b%e8%b7%9d" 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;p&gt;&lt;code&gt;forward_min_distance&lt;/code&gt; 是 ±30° 扇区内的最小距离——它告诉你「正前方最近的东西有多远」，但&lt;strong&gt;不告诉你是谁、有几个、在哪&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;如果我拿这个数字直接判定危险，会有两个问题：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;误报&lt;/strong&gt;：地上一个 5 厘米的纸盒，距离 0.3 米 → 判定「危险」。但你可以直接跨过去。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;漏报&lt;/strong&gt;：右边 0.5 米有个台阶，不在 ±30° 扇区内 → 雷达看不到，可你会撞上。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;strong&gt;雷达给的是标量，安全需要的是语义。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;h3 id="那怎么用雷达"&gt;&lt;a href="#%e9%82%a3%e6%80%8e%e4%b9%88%e7%94%a8%e9%9b%b7%e8%be%be" class="header-anchor"&gt;&lt;/a&gt;那怎么用雷达？&#10;&lt;/h3&gt;&lt;p&gt;把它当&lt;strong&gt;标尺&lt;/strong&gt;，不当&lt;strong&gt;裁判&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;MiDaS 输出的是&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;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;1. 先做线性投影，把相对深度映射到一个先验区间 near=0.4m, far=8.0m&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 再用 LiDAR 的 forward_min_distance 对这个投影做一次重标定&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 如果雷达认为近，就动态放宽 YOLO 的置信度阈值（0.5 → 0.3）&#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;第 3 步是个很实用的联动：&lt;strong&gt;雷达说近，就宁可多报一点&lt;/strong&gt;。因为近距离下漏检的代价，远大于误检。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="深度估计可视化" class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="720" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/visionmate/vision-depth.jpg" srcset="https://cloudlake.eu.org/p/visionmate/vision-depth_hu_5c09b38f815b486a.jpg 800w, https://cloudlake.eu.org/p/visionmate/vision-depth.jpg 1280w" width="1280"&gt;&lt;/p&gt;&#10;&lt;p&gt;上图是 MiDaS 的输出叠加——颜色代表距离（暖色近、冷色远）。可以看出它能把空间关系画得很清楚，但&lt;strong&gt;具体数值需要标尺&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="危险分级"&gt;&lt;a href="#%e5%8d%b1%e9%99%a9%e5%88%86%e7%ba%a7" class="header-anchor"&gt;&lt;/a&gt;危险分级&#10;&lt;/h2&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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nearest_box_dist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hazard_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;danger&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 有物体且极近&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;nearest_box_dist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hazard_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;warning&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 有物体但还远&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hazard_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;safe&amp;#34;&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;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;有语义 → 用「谁 + 多远」&lt;/li&gt;&#10;&lt;li&gt;无语义 → 用「最近的表面多远」&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;两条路兜底，不会出现沉默失败。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="没有硬件也能跑stub-后端"&gt;&lt;a href="#%e6%b2%a1%e6%9c%89%e7%a1%ac%e4%bb%b6%e4%b9%9f%e8%83%bd%e8%b7%91stub-%e5%90%8e%e7%ab%af" class="header-anchor"&gt;&lt;/a&gt;没有硬件也能跑：Stub 后端&#10;&lt;/h2&gt;&lt;p&gt;这套系统依赖摄像头、激光雷达、YOLO 权重、MiDaS 权重。&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;Stub 后端&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;YOLOv8n&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;MiDaS&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;EasyOCR&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;LiDAR&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;/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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;SIMULATION_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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;p&gt;&lt;strong&gt;这不是为了偷懒，是为了让测试能跑。&lt;/strong&gt; 有了 Stub，CI 里不需要 GPU、不需要串口、不需要摄像头，全套流程照样能验证。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="测试让软融合这个说法可被验证"&gt;&lt;a href="#%e6%b5%8b%e8%af%95%e8%ae%a9%e8%bd%af%e8%9e%8d%e5%90%88%e8%bf%99%e4%b8%aa%e8%af%b4%e6%b3%95%e5%8f%af%e8%a2%ab%e9%aa%8c%e8%af%81" class="header-anchor"&gt;&lt;/a&gt;测试：让「软融合」这个说法可被验证&#10;&lt;/h2&gt;&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;&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;test_p0_smoke&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;API 契约、分阶段耗时、雷达快照新鲜度&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;test_p1_lidar_soft_fusion&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;test_p2_depth_metadata&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;P1&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;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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;hazard_level&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;high&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;forward_min_distance&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&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;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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;thresholds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;yolo_conf_threshold&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&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;p&gt;这两条断言把「雷达软融合」从一个&lt;strong&gt;形容词&lt;/strong&gt;变成了一个&lt;strong&gt;可验证的行为&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;雷达报 0.4 米 → 危险等级必须是 high&lt;/li&gt;&#10;&lt;li&gt;雷达报近 → 置信度阈值必须降到 0.3&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;如果哪天我重构代码把这个逻辑弄丢了，测试会立刻失败。&lt;/p&gt;&#10;&lt;p&gt;P2 守的是另一件事——&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;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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;depth_error&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;midas_missing&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&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;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%ba%e4%bb%80%e4%b9%88%e8%bf%99%e4%b8%aa%e9%a1%b9%e7%9b%ae%e5%80%bc%e5%be%97%e5%81%9a" class="header-anchor"&gt;&lt;/a&gt;为什么这个项目值得做&#10;&lt;/h2&gt;&lt;p&gt;因为「AI 能看见」这件事，大多数人理解成「能识别」。&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;/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;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;要让它能驱动机器人、能辅助视障者、能放进 XR 头显——它必须先知道距离。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="技术栈"&gt;&lt;a href="#%e6%8a%80%e6%9c%af%e6%a0%88" class="header-anchor"&gt;&lt;/a&gt;技术栈&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Python&lt;/code&gt; · &lt;code&gt;FastAPI&lt;/code&gt; · &lt;code&gt;YOLOv8n&lt;/code&gt; · &lt;code&gt;MiDaS&lt;/code&gt; · &lt;code&gt;EasyOCR&lt;/code&gt; · &lt;code&gt;Whisper&lt;/code&gt; · &lt;code&gt;RPLIDAR A1&lt;/code&gt; · &lt;code&gt;scikit-learn&lt;/code&gt; · &lt;code&gt;pytest&lt;/code&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个项目最后教会我的，不是怎么调深度模型。&lt;/p&gt;&#10;&lt;p&gt;是&lt;strong&gt;「多一个传感器」不等于「多一份信任」&lt;/strong&gt;——多一个传感器，意味着你要更清楚地决定&lt;strong&gt;该在什么时候听它的&lt;/strong&gt;。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item><item><title>让 AI 给自己写决策函数</title><link>https://cloudlake.eu.org/p/ai-evolution/</link><pubDate>Tue, 20 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/ai-evolution/</guid><description>&lt;img src="https://cloudlake.eu.org/p/ai-evolution/cover.jpg" alt="Featured image of post 让 AI 给自己写决策函数" /&gt;&lt;p&gt;表面上看，这是个网页小游戏。&lt;/p&gt;&#10;&lt;p&gt;一个机器人在地图上走，收集正分地块，躲开负分地块，在限定步数内拿最高分。&lt;/p&gt;&#10;&lt;p&gt;但真正被测试的不是机器人。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;是 AI 会不会给自己写一个能赢的函数。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="第一轮ai-亲自玩"&gt;&lt;a href="#%e7%ac%ac%e4%b8%80%e8%bd%aeai-%e4%ba%b2%e8%87%aa%e7%8e%a9" class="header-anchor"&gt;&lt;/a&gt;第一轮：AI 亲自玩&#10;&lt;/h2&gt;&lt;p&gt;Round 1，机器人每一步都由 API 驱动：&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;观察 → 调 DeepSeek → 返回「思考 + 行动」→ 移动 → 下一步&#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;/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;思考: 右下角有正分格，但中间隔着墙，先向右绕&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;行动: RIGHT&#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;每一步都是一次 API 调用&lt;/strong&gt;。一局 60 步，就是 60 次请求。&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="#%e5%8f%8d%e6%80%9d%e6%8a%8a%e6%97%a5%e5%bf%97%e5%8f%98%e6%88%90%e4%bb%a3%e7%a0%81" class="header-anchor"&gt;&lt;/a&gt;反思：把日志变成代码&#10;&lt;/h2&gt;&lt;p&gt;一局结束后，我把整局记录发回给 AI：&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;然后要求它：&lt;strong&gt;不要再一步一步决定。写一个函数，让函数来替你做决定。&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;span class="lnt"&gt; 3&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 4&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 5&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 6&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 7&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 8&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 9&#10;&lt;/span&gt;&lt;span class="lnt"&gt;10&#10;&lt;/span&gt;&lt;span class="lnt"&gt;11&#10;&lt;/span&gt;&lt;span class="lnt"&gt;12&#10;&lt;/span&gt;&lt;span class="lnt"&gt;13&#10;&lt;/span&gt;&lt;span class="lnt"&gt;14&#10;&lt;/span&gt;&lt;span class="lnt"&gt;15&#10;&lt;/span&gt;&lt;span class="lnt"&gt;16&#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-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// observation: 周围 8 格（NW/N/NE/W/E/SW/S/SE）&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// state: 已走步数、剩余步数、已收集分数&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// config: 网格配置&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// memory: 跨轮记忆&#10;&lt;/span&gt;&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; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;moves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;UP&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;DOWN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;LEFT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;RIGHT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#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; &lt;span class="c1"&gt;// 优先走向正分且未收集的格子&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;moves&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 没有正分就随机&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;moves&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;p&gt;这就是 &lt;strong&gt;v1&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="关键转折成本从-o步数-变成-o1"&gt;&lt;a href="#%e5%85%b3%e9%94%ae%e8%bd%ac%e6%8a%98%e6%88%90%e6%9c%ac%e4%bb%8e-o%e6%ad%a5%e6%95%b0-%e5%8f%98%e6%88%90-o1" class="header-anchor"&gt;&lt;/a&gt;关键转折：成本从 O(步数) 变成 O(1)&#10;&lt;/h2&gt;&lt;p&gt;&lt;img alt="AI 决策函数进化循环" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="evolution-loop.jpg"&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;API 模式&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;每步一次 API 调用&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;O(步数)&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;O(1)&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;每秒 1~2 步（受网络限制）&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;&#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;同一个 AI，在第二轮里变成了一个 100 行的函数。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;然后这个函数可以跑几千局——不需要再花任何 API 费用。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="进化让函数一代代变强"&gt;&lt;a href="#%e8%bf%9b%e5%8c%96%e8%ae%a9%e5%87%bd%e6%95%b0%e4%b8%80%e4%bb%a3%e4%bb%a3%e5%8f%98%e5%bc%ba" class="header-anchor"&gt;&lt;/a&gt;进化：让函数一代代变强&#10;&lt;/h2&gt;&lt;p&gt;第二轮结束后，把「函数表现」和「上一轮表现」一起发回给 AI，让它改进。&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;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;span class="lnt"&gt;6&#10;&lt;/span&gt;&lt;span class="lnt"&gt;7&#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;Round 1 API 亲自玩 → 收集日志&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;反思 日志 → v1 函数&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Round 2 v1 函数跑 → 记录得分&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;反思 对比 → v2 函数&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Round 3 v2 函数跑 → 记录得分&#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;最终 最优函数 + 完整进化轨迹&#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;默认 5 轮。每一轮的函数版本都存进 localStorage，可以：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;查看任意版本的代码&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;手动编辑&lt;/strong&gt;再测&lt;/li&gt;&#10;&lt;li&gt;导出/导入&lt;/li&gt;&#10;&lt;li&gt;对比各代得分&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="一个反直觉的地方"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e5%8f%8d%e7%9b%b4%e8%a7%89%e7%9a%84%e5%9c%b0%e6%96%b9" 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;p&gt;我观察到的情况是：前两代函数通常一路膨胀（加启发式、加规则、加特判），但从第三代开始往往会&lt;strong&gt;缩短&lt;/strong&gt;——AI 会主动删掉那些在实测里没帮助的分支。&lt;/p&gt;&#10;&lt;p&gt;这其实很像人类写代码的过程：先什么都想加，再靠数据砍掉。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="沙箱ai-写的代码不能直接信任"&gt;&lt;a href="#%e6%b2%99%e7%ae%b1ai-%e5%86%99%e7%9a%84%e4%bb%a3%e7%a0%81%e4%b8%8d%e8%83%bd%e7%9b%b4%e6%8e%a5%e4%bf%a1%e4%bb%bb" class="header-anchor"&gt;&lt;/a&gt;沙箱：AI 写的代码不能直接信任&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;decide&lt;/code&gt; 是 AI 生成的任意 JavaScript。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;直接 &lt;code&gt;eval&lt;/code&gt; 等于把浏览器交出去。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;所以：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;代码在 &lt;strong&gt;Web Worker&lt;/strong&gt; 里执行，拿不到 DOM 和网络&lt;/li&gt;&#10;&lt;li&gt;执行前做语法校验（&lt;code&gt;new Function&lt;/code&gt; 能构造出来才算合法）&lt;/li&gt;&#10;&lt;li&gt;必须有名为 &lt;code&gt;decide&lt;/code&gt; 的函数，否则拒绝装载&lt;/li&gt;&#10;&lt;li&gt;抛异常就捕获、记录、&lt;strong&gt;不回退到默认策略&lt;/strong&gt;（否则你永远不知道它坏了）&lt;/li&gt;&#10;&lt;/ol&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;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;span class="lnt"&gt;6&#10;&lt;/span&gt;&lt;span class="lnt"&gt;7&#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-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;runDecide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;observation&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;state&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;config&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;memory&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sb"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sb"&gt; return decide(observation, state, config, memory);&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sb"&gt; `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;observation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;一个 AI 写的函数，要么在受限环境里跑，要么别跑。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="顺带测的第二件事长上下文记忆"&gt;&lt;a href="#%e9%a1%ba%e5%b8%a6%e6%b5%8b%e7%9a%84%e7%ac%ac%e4%ba%8c%e4%bb%b6%e4%ba%8b%e9%95%bf%e4%b8%8a%e4%b8%8b%e6%96%87%e8%ae%b0%e5%bf%86" class="header-anchor"&gt;&lt;/a&gt;顺带测的第二件事：长上下文记忆&#10;&lt;/h2&gt;&lt;p&gt;这个游戏还测了另一条线——&lt;strong&gt;AI 在不同信息密度下的决策质量&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;每步传给 API 的内容&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;strong&gt;完整历史&lt;/strong&gt;&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;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;strong&gt;压缩摘要&lt;/strong&gt;&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;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;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="#%e7%bd%91%e6%a0%bc%e6%9c%ac%e8%ba%ab%e4%b9%9f%e6%98%af%e5%8f%98%e9%87%8f" 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;ul&gt;&#10;&lt;li&gt;均匀分布 / 正态分布（可调均值、标准差）&lt;/li&gt;&#10;&lt;li&gt;正分格占比（默认 60%）&lt;/li&gt;&#10;&lt;li&gt;数值取整，正态钳制到 &lt;code&gt;[min, max]&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;ul&gt;&#10;&lt;li&gt;&lt;code&gt;corridor&lt;/code&gt; — 曲折走廊，终点高奖励&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;room&lt;/code&gt; — 封闭房间，1~2 个入口，内部高奖励&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;maze&lt;/code&gt; — 递归分割小迷宫，死胡同里藏高奖励&lt;/li&gt;&#10;&lt;li&gt;随机递归分割（可设递归深度）&lt;/li&gt;&#10;&lt;li&gt;结构深处的格子奖励 &lt;strong&gt;2x~3x&lt;/strong&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;hr&gt;&#10;&lt;h2 id="一个必须处理的边界情况"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e5%bf%85%e9%a1%bb%e5%a4%84%e7%90%86%e7%9a%84%e8%be%b9%e7%95%8c%e6%83%85%e5%86%b5" class="header-anchor"&gt;&lt;/a&gt;一个必须处理的边界情况&#10;&lt;/h2&gt;&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;&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;strong&gt;函数代码执行异常&lt;/strong&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;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;hr&gt;&#10;&lt;h2 id="这个项目真正在问什么"&gt;&lt;a href="#%e8%bf%99%e4%b8%aa%e9%a1%b9%e7%9b%ae%e7%9c%9f%e6%ad%a3%e5%9c%a8%e9%97%ae%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;这个项目真正在问什么&#10;&lt;/h2&gt;&lt;p&gt;表面上它在问：AI 能不能在网格游戏里拿高分？&lt;/p&gt;&#10;&lt;p&gt;实际上它在问三个问题：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;AI 能不能从「做决策」上升到「写决策程序」？&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;/ol&gt;&#10;&lt;p&gt;第一个是能力边界，第二个是进化机制，第三个是工程现实。&lt;/p&gt;&#10;&lt;p&gt;而这三件事，恰好也是今天做 AI 应用的人每天在纠结的。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="技术栈"&gt;&lt;a href="#%e6%8a%80%e6%9c%af%e6%a0%88" class="header-anchor"&gt;&lt;/a&gt;技术栈&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;原生 JavaScript&lt;/code&gt; · &lt;code&gt;Canvas&lt;/code&gt; · &lt;code&gt;DeepSeek API&lt;/code&gt; · &lt;code&gt;Web Worker 沙箱&lt;/code&gt; · &lt;code&gt;localStorage&lt;/code&gt; · 零构建步骤&lt;/p&gt;&#10;&lt;hr&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个项目让我改了一个看法。&lt;/p&gt;&#10;&lt;p&gt;以前我觉得「AI 会不会写代码」是个演示性问题。&lt;/p&gt;&#10;&lt;p&gt;现在我觉得它是&lt;strong&gt;成本问题&lt;/strong&gt;——&lt;strong&gt;当 AI 学会写一个函数来代替自己反复推理时，它就从「一次服务」变成了「一份资产」。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item><item><title>对着杯子按一下扳机：把生成式 3D 接进 XR 运行时</title><link>https://cloudlake.eu.org/p/xr-generative-3d/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/xr-generative-3d/</guid><description>&lt;img src="https://cloudlake.eu.org/p/xr-generative-3d/cover.jpg" alt="Featured image of post 对着杯子按一下扳机：把生成式 3D 接进 XR 运行时" /&gt;&lt;p&gt;举着 Quest，对着桌上的杯子按下扳机。&lt;/p&gt;&#10;&lt;p&gt;几十秒后，一个可以拿在手里转的杯子出现在你面前。&lt;/p&gt;&#10;&lt;p&gt;这就是这个项目要做的事：&lt;strong&gt;把生成式 3D 接进 XR 运行时&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;也正因为&amp;quot;接进&amp;quot;两个字，它比听起来难得多。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="为什么难"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e9%9a%be" class="header-anchor"&gt;&lt;/a&gt;为什么难&#10;&lt;/h2&gt;&lt;p&gt;把 2D 生成接进网页，你只要拿回一张图，塞进一个 image 标签就行。&lt;/p&gt;&#10;&lt;p&gt;但把 3D 生成接进 XR，你要面对三个运行时约束：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;① 材质不能在运行时可靠重建&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;生成式管线经常把材质烘进 shader graph。而在 XR 渲染管线里，&lt;strong&gt;动态加载的 shader graph 无法可靠还原&lt;/strong&gt;——它依赖编辑器期的编译结果。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;② 生成模型的坐标是不可信的&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;自动生成的模型，pivot 位置随机、坐标原点不规则、内部结构千奇百怪。直接拿它的 pivot 去转，你会看到模型一边转一边漂。&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;p&gt;&lt;strong&gt;这三个问题都不是&amp;quot;模型生成得不好&amp;quot;——是生成结果和 XR 运行时之间的接口没有对齐。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="系统的三个部分"&gt;&lt;a href="#%e7%b3%bb%e7%bb%9f%e7%9a%84%e4%b8%89%e4%b8%aa%e9%83%a8%e5%88%86" class="header-anchor"&gt;&lt;/a&gt;系统的三个部分&#10;&lt;/h2&gt;&lt;p&gt;&lt;img alt="系统工作流" class="gallery-image" data-flex-basis="601px" data-flex-grow="250" height="638" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/xr-generative-3d/xr-workflow.jpg" srcset="https://cloudlake.eu.org/p/xr-generative-3d/xr-workflow_hu_bbf3a20695dd6a8f.jpg 800w, https://cloudlake.eu.org/p/xr-generative-3d/xr-workflow.jpg 1600w" width="1600"&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;&lt;strong&gt;XR 运行时&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Meta Quest 一体机（Unity）&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;strong&gt;服务端编排&lt;/strong&gt;&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;/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;strong&gt;生成管线&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;ComfyUI + Hunyuan3D&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;通信走 REST：异步上传、状态轮询、资产拉取。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;整个系统完全在运行时工作&lt;/strong&gt;——不需要任何离线预处理或编辑器期资产准备。这是刻意的：真实部署的 XR 应用就是这么跑的。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="决策一在生成端就把几何和纹理拆开"&gt;&lt;a href="#%e5%86%b3%e7%ad%96%e4%b8%80%e5%9c%a8%e7%94%9f%e6%88%90%e7%ab%af%e5%b0%b1%e6%8a%8a%e5%87%a0%e4%bd%95%e5%92%8c%e7%ba%b9%e7%90%86%e6%8b%86%e5%bc%80" class="header-anchor"&gt;&lt;/a&gt;决策一：在生成端就把几何和纹理拆开&#10;&lt;/h2&gt;&lt;p&gt;这是整个系统最核心的一条。&lt;/p&gt;&#10;&lt;p&gt;常规做法是生成一个&lt;strong&gt;带材质的整体 GLB&lt;/strong&gt;，丢进引擎直接用。&lt;/p&gt;&#10;&lt;p&gt;我们没这么做。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;ComfyUI 工作流最后输出两个独立的东西&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;一个只含几何的 &lt;strong&gt;GLB 网格&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;p&gt;然后在 XR 运行时里，&lt;strong&gt;显式创建一个原生渲染材质，把纹理绑到对应的输入通道上&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;h3 id="为什么"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;为什么&#10;&lt;/h3&gt;&lt;p&gt;因为这样&lt;strong&gt;绕过了隐式的 shader 编译机制&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;一旦材质是烘在资产里的，你就把控制权交给了资产生成方。而生成方给的材质，在 XR 运行时里未必能加载、未必能编译、也未必要得。&lt;/p&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;牺牲了&amp;quot;资产自包含&amp;quot;的便利，换来了运行时行为的确定性。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="决策二运行时显式装配"&gt;&lt;a href="#%e5%86%b3%e7%ad%96%e4%ba%8c%e8%bf%90%e8%a1%8c%e6%97%b6%e6%98%be%e5%bc%8f%e8%a3%85%e9%85%8d" 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;ol&gt;&#10;&lt;li&gt;收到两个独立资产（GLB + 纹理）&lt;/li&gt;&#10;&lt;li&gt;实例化原生材质、绑定纹理&lt;/li&gt;&#10;&lt;li&gt;挂到转盘锚上（下一节）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;自动归一化&lt;/strong&gt;：用包围盒估算空间尺寸，算出一个统一缩放系数，让模型落进一个预设的&amp;quot;感知尺寸区间&amp;quot;&lt;/li&gt;&#10;&lt;li&gt;挂到更高层的空间根节点，成为可交互的 XR 对象&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;第 4 步解决的就是&amp;quot;有的像指甲盖有的像房子&amp;quot;。&lt;/p&gt;&#10;&lt;p&gt;而且这个归一化是&lt;strong&gt;在运行时算的&lt;/strong&gt;——不需要人工调、不需要离线预处理。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="决策三转盘锚turntable-anchor"&gt;&lt;a href="#%e5%86%b3%e7%ad%96%e4%b8%89%e8%bd%ac%e7%9b%98%e9%94%9aturntable-anchor" class="header-anchor"&gt;&lt;/a&gt;决策三：转盘锚（Turntable Anchor）&#10;&lt;/h2&gt;&lt;p&gt;这是解决&amp;quot;pivot 不可信&amp;quot;的答案。&lt;/p&gt;&#10;&lt;p&gt;思路很干脆：&lt;strong&gt;不去修正模型的 pivot，而是把它挂在一个锚下面。&lt;/strong&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;所有关于&amp;quot;呈现&amp;quot;的变换——旋转、漂浮、缩放——&lt;strong&gt;全部作用在锚上&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&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;最终世界变换 T = T_a ∘ T_m&#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;T_m&lt;/code&gt; 是模型的局部变换（不动），&lt;code&gt;T_a&lt;/code&gt; 是锚的变换（动态更新）。&lt;/p&gt;&#10;&lt;h3 id="为什么这比修-pivot好"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e8%bf%99%e6%af%94%e4%bf%ae-pivot%e5%a5%bd" class="header-anchor"&gt;&lt;/a&gt;为什么这比&amp;quot;修 pivot&amp;quot;好&#10;&lt;/h3&gt;&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;模型只会&lt;strong&gt;原地震转&lt;/strong&gt;，不会一边转一边漂&lt;/li&gt;&#10;&lt;li&gt;不需要依赖任何几何启发式（比如&amp;quot;用包围盒中心当 pivot&amp;quot;——对不对称模型经常算错）&lt;/li&gt;&#10;&lt;li&gt;缩放、漂浮、透视/虚拟场景切换，全部在锚这一层统一处理&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;与其去猜一个结构不可信的对象&amp;quot;中心在哪&amp;quot;，不如&lt;strong&gt;给它一个外部坐标系&lt;/strong&gt;。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="一个很具体的设计问题什么时候该阻塞用户输入"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e5%be%88%e5%85%b7%e4%bd%93%e7%9a%84%e8%ae%be%e8%ae%a1%e9%97%ae%e9%a2%98%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e8%af%a5%e9%98%bb%e5%a1%9e%e7%94%a8%e6%88%b7%e8%be%93%e5%85%a5" 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;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：什么时候禁止再次触发？&lt;/p&gt;&#10;&lt;p&gt;如果&lt;strong&gt;等到服务端响应才阻塞&lt;/strong&gt;——那么在这段时间窗口里，用户可以连续触发多次取景。每一次都会向 ComfyUI 发一个新请求，&lt;strong&gt;任务队列直接被压垮&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;更麻烦的是输入抖动&lt;/strong&gt;：一次物理按键，可能产生多个输入事件。这会让系统平白提交好几个重复的生成任务，烧掉大量算力。&lt;/p&gt;&#10;&lt;h3 id="解法"&gt;&lt;a href="#%e8%a7%a3%e6%b3%95" 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;p&gt;然后在&lt;strong&gt;上传完成并拿到服务端响应&lt;/strong&gt;时释放——包括失败和超时的情况。&lt;/p&gt;&#10;&lt;p&gt;正常流程下这个阻塞窗口很短，用户基本感知不到。而如果生成成功，阻塞会一直保持到整个工作流结束。&lt;/p&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;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="生成管线"&gt;&lt;a href="#%e7%94%9f%e6%88%90%e7%ae%a1%e7%ba%bf" class="header-anchor"&gt;&lt;/a&gt;生成管线&#10;&lt;/h2&gt;&lt;p&gt;&lt;img alt="ComfyUI 生成管线" class="gallery-image" data-flex-basis="387px" data-flex-grow="161" height="866" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/xr-generative-3d/xr-pipeline.jpg" srcset="https://cloudlake.eu.org/p/xr-generative-3d/xr-pipeline_hu_5d5f4bdfbc747982.jpg 800w, https://cloudlake.eu.org/p/xr-generative-3d/xr-pipeline.jpg 1400w" width="1400"&gt;&lt;/p&gt;&#10;&lt;p&gt;工作流搭在 ComfyUI 里，核心生成模型是 &lt;strong&gt;Hunyuan3D-dit-v2.0&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;&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;strong&gt;输入预处理&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;把透视 ROI 图缩放到固定方形分辨率&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;strong&gt;去背景&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;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;几何生成（GenerateMesh）&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;&#9;&#9;&lt;tr&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;/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;strong&gt;去光照（Delighting）&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;把原图的光照信息剥掉，避免&amp;quot;烤&amp;quot;进纹理&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;strong&gt;多视角渲染&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;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;多视角纹理采样 + UV 烘焙&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;把各视角的颜色烘到统一 UV 上&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;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="实测结果"&gt;&lt;a href="#%e5%ae%9e%e6%b5%8b%e7%bb%93%e6%9e%9c" class="header-anchor"&gt;&lt;/a&gt;实测结果&#10;&lt;/h2&gt;&lt;p&gt;&lt;img alt="生成结果：正面与背面" class="gallery-image" data-flex-basis="306px" data-flex-grow="127" height="1000" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://cloudlake.eu.org/p/xr-generative-3d/cover.jpg" srcset="https://cloudlake.eu.org/p/xr-generative-3d/cover_hu_9e2df9ef77370fc1.jpg 800w, https://cloudlake.eu.org/p/xr-generative-3d/cover.jpg 1276w" width="1276"&gt;&lt;/p&gt;&#10;&lt;p&gt;系统在 Meta Quest 一体机上完整跑通。&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;strong&gt;端到端延迟&lt;/strong&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;正确释放输入阻塞、回到 idle，&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;XR 交互&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;h3 id="一个诚实的说明"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e8%af%9a%e5%ae%9e%e7%9a%84%e8%af%b4%e6%98%8e" 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;p&gt;&amp;ldquo;近实时&amp;quot;在这里指的是：&lt;strong&gt;通过异步执行和视觉反馈，让用户在等待期间保持参与感&lt;/strong&gt;，而不是&amp;quot;几十秒内出结果&amp;rdquo;。&lt;/p&gt;&#10;&lt;p&gt;这个边界我写进了论文，也写在这里。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="这个项目真正学到的东西"&gt;&lt;a href="#%e8%bf%99%e4%b8%aa%e9%a1%b9%e7%9b%ae%e7%9c%9f%e6%ad%a3%e5%ad%a6%e5%88%b0%e7%9a%84%e4%b8%9c%e8%a5%bf" class="header-anchor"&gt;&lt;/a&gt;这个项目真正学到的东西&#10;&lt;/h2&gt;&lt;p&gt;回头看，最值钱的不是&amp;quot;跑通了 Hunyuan3D&amp;quot;。&lt;/p&gt;&#10;&lt;p&gt;而是这三个判断：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;接口比模型重要。&lt;/strong&gt; 生成质量再好，如果和运行时的接口对不齐，也用不了。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;不可信的东西，给它一个外部参照。&lt;/strong&gt; pivot 不可信就给锚，尺度不可控就归一化。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;先想时序，再写代码。&lt;/strong&gt; 输入阻塞那个问题，想清楚了只有十行代码；没想清楚会让服务端崩溃。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="技术栈"&gt;&lt;a href="#%e6%8a%80%e6%9c%af%e6%a0%88" class="header-anchor"&gt;&lt;/a&gt;技术栈&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Unity&lt;/code&gt; · &lt;code&gt;Meta Quest&lt;/code&gt; · &lt;code&gt;OpenXR&lt;/code&gt; · &lt;code&gt;C#&lt;/code&gt; · &lt;code&gt;ComfyUI&lt;/code&gt; · &lt;code&gt;Hunyuan3D-dit-v2.0&lt;/code&gt; · &lt;code&gt;Python / Flask&lt;/code&gt; · &lt;code&gt;REST 异步编排&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;论文：A System for Near Real-Time Generation and Runtime Visualization of Generative 3D Models in XR Environments（IEEE 会议格式）&lt;/p&gt;&#10;&lt;hr&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;最后一句是我写在论文里的判断：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;这套系统的价值不在于生成得多好，而在于它把一条不可控的生成管线，接上了一个必须可控的运行时。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item><item><title>广而告知：像投放广告一样投放知识</title><link>https://cloudlake.eu.org/p/computhon-guangergaozhi/</link><pubDate>Sun, 18 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/computhon-guangergaozhi/</guid><description>&lt;img src="https://cloudlake.eu.org/p/computhon-guangergaozhi/cover.jpg" alt="Featured image of post 广而告知：像投放广告一样投放知识" /&gt;&lt;p&gt;期末前一周，我打开自己的浏览器收藏夹。&lt;/p&gt;&#10;&lt;p&gt;里面有 &lt;strong&gt;137 节&lt;/strong&gt; 网课。一节都没点开过。&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="#%e6%8d%a2%e4%b8%aa%e9%97%ae%e6%b3%95" 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;&lt;strong&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;/p&gt;&#10;&lt;p&gt;没有「我该开始学习了」。没有意志力消耗。&lt;strong&gt;连一个决定都不需要。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;这就是「广而告知」——COMPUTHON 2026 的参赛作品，也是我们拿下的冠军方案。&lt;/p&gt;&#10;&lt;figure class="video-embed"&gt;&#10; &lt;video controls preload="metadata" playsinline poster="/media/computhon-promo-poster.jpg"&gt;&#10; &lt;source src="https://cloudlake.eu.org/media/computhon-promo.mp4" type="video/mp4"&gt;&#10; 你的浏览器不支持视频播放，&lt;a href="https://cloudlake.eu.org/media/computhon-promo.mp4"&gt;点此下载&lt;/a&gt;。&#10; &lt;/video&gt;&#10; &lt;figcaption&gt;COMPUTHON 2026 冠军方案 · 3 分钟宣传片（服务器本地播放）&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;hr&gt;&#10;&lt;h2 id="我们到底在解决什么"&gt;&lt;a href="#%e6%88%91%e4%bb%ac%e5%88%b0%e5%ba%95%e5%9c%a8%e8%a7%a3%e5%86%b3%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;我们到底在解决什么&#10;&lt;/h2&gt;&lt;h3 id="不是资料不够是启动不了"&gt;&lt;a href="#%e4%b8%8d%e6%98%af%e8%b5%84%e6%96%99%e4%b8%8d%e5%a4%9f%e6%98%af%e5%90%af%e5%8a%a8%e4%b8%8d%e4%ba%86" class="header-anchor"&gt;&lt;/a&gt;不是「资料不够」，是「启动不了」&#10;&lt;/h3&gt;&lt;p&gt;用户嘴上说要的是「学习资料」。但资料从来不缺——B 站、MOOC、教材、论文到处都是。&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;&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;strong&gt;元认知误判&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;&#9;&#9;&lt;tr&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;「点开一节 50 分钟的网课，意味着接下来 50 分钟都得交出去。而 15 秒的短视频随时可以划走」&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;strong&gt;长内容回避&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;还有一个更隐蔽的：&lt;strong&gt;「现在学会不会考试时忘了？」&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;这个念头，让人心安理得地把所有学习推到 DDL 前三天——然后后悔莫及。&lt;/p&gt;&#10;&lt;h3 id="所以产品不是更多内容是更少意志力"&gt;&lt;a href="#%e6%89%80%e4%bb%a5%e4%ba%a7%e5%93%81%e4%b8%8d%e6%98%af%e6%9b%b4%e5%a4%9a%e5%86%85%e5%ae%b9%e6%98%af%e6%9b%b4%e5%b0%91%e6%84%8f%e5%bf%97%e5%8a%9b" class="header-anchor"&gt;&lt;/a&gt;所以产品不是「更多内容」，是「更少意志力」&#10;&lt;/h3&gt;&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;hr&gt;&#10;&lt;h2 id="怎么做的"&gt;&lt;a href="#%e6%80%8e%e4%b9%88%e5%81%9a%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;怎么做的&#10;&lt;/h2&gt;&lt;h3 id="三个端点一条主线"&gt;&lt;a href="#%e4%b8%89%e4%b8%aa%e7%ab%af%e7%82%b9%e4%b8%80%e6%9d%a1%e4%b8%bb%e7%ba%bf" class="header-anchor"&gt;&lt;/a&gt;三个端点，一条主线&#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;第一梯队&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;新标签页卡片、视频站侧边栏注入、Quiz 弹窗&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;&#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;手机 APP&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;为什么从浏览器扩展起步？&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;h3 id="核心是那个投放引擎"&gt;&lt;a href="#%e6%a0%b8%e5%bf%83%e6%98%af%e9%82%a3%e4%b8%aa%e6%8a%95%e6%94%be%e5%bc%95%e6%93%8e" class="header-anchor"&gt;&lt;/a&gt;核心是那个「投放引擎」&#10;&lt;/h3&gt;&lt;p&gt;真正让它成为产品而不是插件的，是背后那个 &lt;strong&gt;Agentic AI 引擎&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;从课程大纲、课件、教材里自动提取全部知识点，生成知识地图&lt;/li&gt;&#10;&lt;li&gt;把每个知识点拆成 &lt;strong&gt;15 秒可读完&lt;/strong&gt;的微卡片（核心概念 + 关键要点 + 一个自查问题）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;自主决策&lt;/strong&gt;：什么时机推、推什么、推多少&lt;/li&gt;&#10;&lt;li&gt;按遗忘曲线安排间隔重复——用机制解决「提前学了也会忘」&lt;/li&gt;&#10;&lt;li&gt;根据行为信号动态调整：已读/跳过、停留时长、Quiz 正确率、活跃时段&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;决赛阶段升级成了一个&lt;strong&gt;五 Agent 投票&lt;/strong&gt;的决策引擎：每个 Agent 从不同维度打分，投票决定这一刻该不该推、推哪张。&lt;/p&gt;&#10;&lt;h3 id="两种模式把选择权交还给用户"&gt;&lt;a href="#%e4%b8%a4%e7%a7%8d%e6%a8%a1%e5%bc%8f%e6%8a%8a%e9%80%89%e6%8b%a9%e6%9d%83%e4%ba%a4%e8%bf%98%e7%bb%99%e7%94%a8%e6%88%b7" class="header-anchor"&gt;&lt;/a&gt;两种模式，把选择权交还给用户&#10;&lt;/h3&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;：Quiz 弹窗，答对才关&lt;/li&gt;&#10;&lt;/ul&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="#%e6%80%8e%e4%b9%88%e9%aa%8c%e8%af%81%e5%ae%83%e7%9c%9f%e7%9a%84%e6%9c%89%e7%94%a8" 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;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;strong&gt;被动曝光率&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;&#9;&#9;&lt;tr&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;/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;strong&gt;知识点覆盖率&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;&#9;&#9;&lt;tr&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;7 天 / 30 天仍在使用&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;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;侵入感评分&lt;/strong&gt;（1–10）——如果平均 ≤ 3 但曝光达标，说明模式成立&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;学习感知调查&lt;/strong&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;/ol&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%e4%b8%aa%e5%85%b7%e4%bd%93%e7%9a%84%e7%9f%9b%e7%9b%be" class="header-anchor"&gt;&lt;/a&gt;一个具体的矛盾&#10;&lt;/h2&gt;&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;/ul&gt;&#10;&lt;p&gt;我们的解法是把它变成一个&lt;strong&gt;可调的&lt;/strong&gt;问题，而不是一个要&amp;quot;选对&amp;quot;的问题：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;默认只在新标签页出现（用户主动打开，天然不侵入）&lt;/li&gt;&#10;&lt;li&gt;侧边栏 30 秒无交互自动收起&lt;/li&gt;&#10;&lt;li&gt;Quiz 默认关闭，用户自己开&lt;/li&gt;&#10;&lt;/ul&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%bf%99%e4%bb%b6%e4%ba%8b%e7%9c%9f%e6%ad%a3%e7%9a%84%e6%84%8f%e4%b9%89" class="header-anchor"&gt;&lt;/a&gt;这件事真正的意义&#10;&lt;/h2&gt;&lt;p&gt;颁奖典礼上我说了一句话：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个奖不是终点，&lt;strong&gt;它是一次确认&lt;/strong&gt;——确认「拖延」不是一个该被责备的道德问题，而是一个&lt;strong&gt;可以被设计解决的技术问题。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;如果把它当成道德问题，答案只能是&amp;quot;你要自律一点&amp;quot;。&lt;/p&gt;&#10;&lt;p&gt;但如果它是技术问题，那就可以被拆解：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;启动阻力 → 降低到零（不用做决定）&lt;/li&gt;&#10;&lt;li&gt;内容长度 → 拆成 15 秒&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;&lt;strong&gt;每一个都能被工程化。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="现在的状态"&gt;&lt;a href="#%e7%8e%b0%e5%9c%a8%e7%9a%84%e7%8a%b6%e6%80%81" class="header-anchor"&gt;&lt;/a&gt;现在的状态&#10;&lt;/h2&gt;&lt;p&gt;筛选阶段的 MVP 已经跑起来了：Chrome 扩展（新标签页卡片 + 侧边栏注入 + Quiz 弹窗 + 系统通知），配一个能用的 LLM 知识拆解管线。&lt;/p&gt;&#10;&lt;p&gt;决赛阶段交付了：Agentic 决策引擎 + PC 桌面端 + 三端数据互通。&lt;/p&gt;&#10;&lt;p&gt;下一步是手机端，以及申请 PolyU Micro Fund 做孵化。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;最后把我们的信念留在这里：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;让知识找到你，而不是你去找知识。&lt;/strong&gt;&lt;/p&gt;&#10;</description></item><item><title>一个人的 COMPUTHON 2026</title><link>https://cloudlake.eu.org/p/computhon-solo/</link><pubDate>Sat, 17 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/computhon-solo/</guid><description>&lt;p&gt;COMPUTHON 2026 是两个阶段的赛制：先交一页设计思维作业过筛，再进决赛做真东西。&lt;/p&gt;&#10;&lt;p&gt;我没有队友。&lt;/p&gt;&#10;&lt;p&gt;报名表上「Team」一栏，我填了「&lt;strong&gt;反DDL队&lt;/strong&gt;」——一个只有一个人的队。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="为什么一个人"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%80%e4%b8%aa%e4%ba%ba" 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;hr&gt;&#10;&lt;h2 id="从一页作业开始"&gt;&lt;a href="#%e4%bb%8e%e4%b8%80%e9%a1%b5%e4%bd%9c%e4%b8%9a%e5%bc%80%e5%a7%8b" class="header-anchor"&gt;&lt;/a&gt;从一页作业开始&#10;&lt;/h2&gt;&lt;p&gt;筛选阶段只要求交一份设计思维四步（Demand / Feasibility / Prototype / Test）的作业。&lt;/p&gt;&#10;&lt;p&gt;我把它当成了产品说明书来写。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Demand&lt;/strong&gt;：把「我学不进去」拆成三个可操作的卡点（元认知误判、仪式感陷阱、长内容回避）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Feasibility&lt;/strong&gt;：每一项技术选型都直接对标一个用户需求，不用任何还没成熟的东西&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Prototype&lt;/strong&gt;：Chrome 扩展 MVP，三个核心功能&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Test&lt;/strong&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;hr&gt;&#10;&lt;h2 id="一个人包办的所有事"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e4%ba%ba%e5%8c%85%e5%8a%9e%e7%9a%84%e6%89%80%e6%9c%89%e4%ba%8b" class="header-anchor"&gt;&lt;/a&gt;一个人包办的所有事&#10;&lt;/h2&gt;&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;&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;strong&gt;代码&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Chrome 扩展、Agentic 决策引擎、桌面端&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;strong&gt;文字&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;10 分钟双语答辩稿、5 分钟演示稿、2 分钟颁奖致辞&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;strong&gt;视频&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3 分钟 Pitch、1 分钟演示、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;strong&gt;动画&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;&#9;&#9;&lt;tr&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;自己写脚本调 TTS 生成旁白&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;strong&gt;PPT&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;15 页 Pitch Deck&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;strong&gt;材料&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;申请表单、CARES 模型、后续工作计划&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;/p&gt;&#10;&lt;p&gt;是那次为了赶 1 分钟演示视频，我连续调了六版动画时间轴——因为&lt;strong&gt;旁白的每一个断句都必须和画面转场对齐&lt;/strong&gt;，差 0.3 秒就出戏。&lt;/p&gt;&#10;&lt;p&gt;而它最后只播放 60 秒。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="最难的那一关"&gt;&lt;a href="#%e6%9c%80%e9%9a%be%e7%9a%84%e9%82%a3%e4%b8%80%e5%85%b3" 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;&#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;/p&gt;&#10;&lt;p&gt;因为「加功能」是看得见的产出：&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;li&gt;加个勋章&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;每个都能写进 PPT，每个都显得&amp;quot;功能丰富&amp;quot;。&lt;/p&gt;&#10;&lt;p&gt;而「&lt;strong&gt;减意志力&lt;/strong&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;评委看不到「努力」&lt;/li&gt;&#10;&lt;li&gt;演示视频里没有炫目的交互&lt;/li&gt;&#10;&lt;li&gt;甚至用户自己都说不出&amp;quot;我用了它学到了什么&amp;quot;&lt;/li&gt;&#10;&lt;/ul&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="#%e8%bf%99%e4%b8%aa%e9%a1%b9%e7%9b%ae%e6%98%af%e8%a2%ab%e6%89%b9%e8%af%84%e5%87%ba%e6%9d%a5%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;这个项目是被批评出来的&#10;&lt;/h2&gt;&lt;p&gt;答辩稿里我写了这么一句，是真的。&lt;/p&gt;&#10;&lt;p&gt;内测阶段收集到的反馈里，最难听也最有用的几条：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;「侧边栏太烦了，我刚要专注就被打断」&#10;→ 改成 30 秒无交互自动收起&lt;/li&gt;&#10;&lt;li&gt;「这些卡片我扫一眼就划走了，根本没读」&#10;→ 卡片从&amp;quot;一段话&amp;quot;压缩到&amp;quot;一句话 + 三个要点 + 一个问题&amp;quot;&lt;/li&gt;&#10;&lt;li&gt;「凭什么它觉得我该学这个？」&#10;→ 加了行为追踪和薄弱点优先&lt;/li&gt;&#10;&lt;li&gt;「我不想被它管着」&#10;→ Quiz 默认关闭，验证模式完全可选&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;hr&gt;&#10;&lt;h2 id="那天下午"&gt;&lt;a href="#%e9%82%a3%e5%a4%a9%e4%b8%8b%e5%8d%88" class="header-anchor"&gt;&lt;/a&gt;那天下午&#10;&lt;/h2&gt;&lt;p&gt;颁奖典礼在 PolyU 的一个报告厅。&lt;/p&gt;&#10;&lt;p&gt;我在台上说了那段 2 分钟的致辞，中间有 4 个 [停顿]——写稿时就标好的位置，用来换气，也用来让话落下去。&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;strong&gt;可以被设计解决的技术问题。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&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;hr&gt;&#10;&lt;h2 id="拿到冠军之后"&gt;&lt;a href="#%e6%8b%bf%e5%88%b0%e5%86%a0%e5%86%9b%e4%b9%8b%e5%90%8e" 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;p&gt;因为在这个项目之前，我做过很多东西：一个 MR 解谜游戏、几个游戏复刻、一个汇编器和模拟器、一堆逆向工程。它们都有技术含量，但都只证明「我能做」。&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="#%e5%a6%82%e6%9e%9c%e9%87%8d%e6%9d%a5%e4%b8%80%e6%ac%a1" class="header-anchor"&gt;&lt;/a&gt;如果重来一次&#10;&lt;/h2&gt;&lt;p&gt;三件会改的事：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;早点找内测用户。&lt;/strong&gt; 我做完 MVP 才开始找人试，如果一开始就找，&amp;ldquo;侧边栏太烦&amp;quot;这个坑能省两周。&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;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="现在"&gt;&lt;a href="#%e7%8e%b0%e5%9c%a8" class="header-anchor"&gt;&lt;/a&gt;现在&#10;&lt;/h2&gt;&lt;p&gt;「广而告知」还在继续：桌面端已经能跑，手机端在排期，也在准备申请 PolyU Micro Fund。&lt;/p&gt;&#10;&lt;p&gt;如果你也在被 DDL 追着跑——这个产品想说的其实是：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;不是你的错。是知识出现的方式不对。&lt;/strong&gt;&lt;/p&gt;&#10;</description></item><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>没有 nasm 也没有 qemu，我从零写了汇编器和模拟器</title><link>https://cloudlake.eu.org/p/kiln/</link><pubDate>Thu, 15 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/kiln/</guid><description>&lt;img src="https://cloudlake.eu.org/p/kiln/cover.jpg" alt="Featured image of post 没有 nasm 也没有 qemu，我从零写了汇编器和模拟器" /&gt;&lt;p&gt;我想在这台机器上跑一段能在裸机上启动的代码。&lt;/p&gt;&#10;&lt;p&gt;需要两样东西：汇编器（把汇编变成字节）和模拟器（把字节执行起来）。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;nasm&lt;/code&gt; 装不上，&lt;code&gt;qemu&lt;/code&gt; 装不上——&lt;code&gt;sudo&lt;/code&gt; 要密码、Docker 没启动、直接下载超时。于是这个项目把两样都自己实现了。&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;&lt;code&gt;kiln/cpu.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8086 寄存器组、段寄存器、ModRM 全部寻址方式、8/16 位运算与标志位、中断、VGA 文本显存、键盘控制器&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;code&gt;kiln/asm.py&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;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;kiln/decode.py&lt;/code&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;hr&gt;&#10;&lt;h2 id="为什么反汇编器要独立写"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e5%8f%8d%e6%b1%87%e7%bc%96%e5%99%a8%e8%a6%81%e7%8b%ac%e7%ab%8b%e5%86%99" 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;如果反汇编器照着汇编器的逻辑写，那它只能证明&amp;quot;我按我的想法解释我自己的输出&amp;quot;——这是自证。&lt;/p&gt;&#10;&lt;p&gt;所以两者&lt;strong&gt;互不参照&lt;/strong&gt;。然后测试断言它们在 &lt;code&gt;boot.asm&lt;/code&gt; 的&lt;strong&gt;每一条语句&lt;/strong&gt;上都一致。&lt;/p&gt;&#10;&lt;p&gt;这样，&amp;ldquo;两边以完全相同的方式出错&amp;quot;才会漏过去——比&amp;quot;汇编器没崩溃&amp;quot;强得多。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;但仍然不够。&lt;/strong&gt; 因为两边&lt;strong&gt;可以同时错得一致&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;所以还有第二重证据：把程序&lt;strong&gt;真的执行&lt;/strong&gt;一遍，用&lt;strong&gt;手算的期望值&lt;/strong&gt;对最终寄存器和内存下断言。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;tests/test_core.py&lt;/code&gt; 里的每个期望值都是手写推导的，不是从模拟器输出里抄的。否则又成了自证。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="测试抓到的-12-个真实缺陷"&gt;&lt;a href="#%e6%b5%8b%e8%af%95%e6%8a%93%e5%88%b0%e7%9a%84-12-%e4%b8%aa%e7%9c%9f%e5%ae%9e%e7%bc%ba%e9%99%b7" class="header-anchor"&gt;&lt;/a&gt;测试抓到的 12 个真实缺陷&#10;&lt;/h2&gt;&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;解码器对 &lt;code&gt;0x83 /r&lt;/code&gt; 的立即数宽度判错&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;add ax,1&lt;/code&gt; 编码 3 字节、解码成 4 字节，之后全部错位&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;code&gt;_cond&lt;/code&gt; 里局部变量名与标志位常量同名&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;CF&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;code&gt;_apply_alu&lt;/code&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;4&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;ALU 的 form 分支反了&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;add ax,[mem]&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;ModRM 构造把 src/dst 写反&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;add ax,bx&lt;/code&gt; 编成 &lt;code&gt;03 D8&lt;/code&gt;（实为 &lt;code&gt;add bx,ax&lt;/code&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;6&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;strong&gt;误导出 3 次错误&amp;quot;修复&amp;rdquo;&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;7&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;_shift&lt;/code&gt; 对 mod=3 的寄存器操作数走了内存写入&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;shl ax,8&lt;/code&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;8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;0F&lt;/code&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;9&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;C1 /4 ib&lt;/code&gt; 立即数取在 ModRM &lt;strong&gt;之前&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;把 ModRM 字节当移位位数&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;10&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;段寄存器 push/pop 未实现，位判断用错&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;1E&lt;/code&gt;（&lt;code&gt;push ds&lt;/code&gt;）被当成 4 字节 NOP，指令流错位&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;11&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;call blit&lt;/code&gt; 跳到标签&lt;strong&gt;后 3 字节&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;12&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;test reg,reg&lt;/code&gt; 用位测试而非 &lt;code&gt;84/85&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;test al,al&lt;/code&gt; 把寄存器当立即数&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;第 5 和第 6 条最值得说：&lt;strong&gt;它们是对称的&lt;/strong&gt;。汇编器把方向搞反，解码器也把方向搞反，于是&amp;quot;汇编→解码&amp;quot;的往返测试&lt;strong&gt;完全通过&lt;/strong&gt;——两个错误互相抵消了。只有拿手算字节去对，才看得见。&lt;/p&gt;&#10;&lt;p&gt;第 11 条促成了根本性重新设计：改成&lt;strong&gt;迭代到收敛&lt;/strong&gt;，并且&lt;strong&gt;加宽单调&lt;/strong&gt;（一旦加宽就不回缩）。否则跳转长度和标签地址会互相影响，永远不收敛。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="引导扇区在裸机上画-mandelbrot"&gt;&lt;a href="#%e5%bc%95%e5%af%bc%e6%89%87%e5%8c%ba%e5%9c%a8%e8%a3%b8%e6%9c%ba%e4%b8%8a%e7%94%bb-mandelbrot" class="header-anchor"&gt;&lt;/a&gt;引导扇区：在裸机上画 Mandelbrot&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;examples/boot.asm&lt;/code&gt; 是一个 512 字节的引导扇区，末尾带 &lt;code&gt;55 AA&lt;/code&gt; 签名。BIOS 把它载入 &lt;code&gt;0000:7C00&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;流程：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;建立 &lt;code&gt;DS/SS&lt;/code&gt; 与栈&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;int 10h&lt;/code&gt; 切到 80×25 彩色文本&lt;/li&gt;&#10;&lt;li&gt;打印横幅&lt;/li&gt;&#10;&lt;li&gt;用 &lt;strong&gt;8.8 定点整数&lt;/strong&gt;渲染 Mandelbrot 到 &lt;code&gt;0800:0000&lt;/code&gt; 的缓冲区（40×24 字符）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;rep movsw&lt;/code&gt; 整块搬到 &lt;code&gt;B800:0000&lt;/code&gt; 显存&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;hlt&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;缓冲区放在扇区&lt;strong&gt;之外&lt;/strong&gt;有原因：800 个字符塞不进 512 字节。而且放在已知地址后，验证器可以直接从 RAM 里读渲染结果，和&lt;strong&gt;独立的浮点实现&lt;/strong&gt;逐格比对。&lt;/p&gt;&#10;&lt;h3 id="定点乘法两个看着都对的错版本"&gt;&lt;a href="#%e5%ae%9a%e7%82%b9%e4%b9%98%e6%b3%95%e4%b8%a4%e4%b8%aa%e7%9c%8b%e7%9d%80%e9%83%bd%e5%af%b9%e7%9a%84%e9%94%99%e7%89%88%e6%9c%ac" class="header-anchor"&gt;&lt;/a&gt;定点乘法，两个看着都对的错版本&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;imul&lt;/code&gt; 把有符号 32 位积放在 &lt;code&gt;DX:AX&lt;/code&gt;。8.8 定点结果就是该积的 bit 8..23，由两半拼成：&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;span class="lnt"&gt;5&#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-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;imul&lt;/span&gt; &lt;span class="no"&gt;bx&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;mov&lt;/span&gt; &lt;span class="no"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;dx&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;shl&lt;/span&gt; &lt;span class="no"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="c1"&gt;; bit 16..23 -&amp;gt; bit 8..15&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;shr&lt;/span&gt; &lt;span class="no"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="c1"&gt;; bit 8..15 -&amp;gt; bit 0..7&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;or&lt;/span&gt; &lt;span class="no"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;cx&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;p&gt;两个诱人的简写都是错的，而且&lt;strong&gt;都表现为&amp;quot;逻辑 bug&amp;quot;而不是&amp;quot;算术 bug&amp;quot;&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;只做 &lt;code&gt;shl ax,8&lt;/code&gt;：保留 bit 16..31，&lt;strong&gt;丢掉低字的小数部分&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;对低字做 &lt;code&gt;shr cx,8 / shl ax,8 / or&lt;/code&gt;：&lt;strong&gt;丢掉整数部分&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;这类错误的可怕之处在于：结果总是&amp;quot;差不多但不是&amp;quot;，而&amp;quot;差不多&amp;quot;在图形上表现为轻微的错位，很容易被误认为&amp;quot;渲染问题&amp;quot;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="现在的状态如实说"&gt;&lt;a href="#%e7%8e%b0%e5%9c%a8%e7%9a%84%e7%8a%b6%e6%80%81%e5%a6%82%e5%ae%9e%e8%af%b4" 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;ul&gt;&#10;&lt;li&gt;&lt;code&gt;python tests/test_core.py&lt;/code&gt; → &lt;strong&gt;62/62&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;汇编器对 &lt;code&gt;boot.asm&lt;/code&gt; 收敛输出&lt;strong&gt;正好 512 字节&lt;/strong&gt;，签名 &lt;code&gt;55 AA&lt;/code&gt; 正确&lt;/li&gt;&#10;&lt;li&gt;反汇编器与汇编器在冒烟测试的每一条指令上长度一致，逐条核对助记符&lt;/li&gt;&#10;&lt;li&gt;模拟器通过全部条件码、栈/call/ret、段 wraparound、除零故障、BIOS 显存、键盘阻塞/轮询断言&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;strong&gt;Mandelbrot 渲染循环在末尾仍有一处控制流缺陷&lt;/strong&gt;，会跑到扇区外执行。&lt;/p&gt;&#10;&lt;p&gt;定点乘法本身已经被单独验证正确（&lt;code&gt;imul 0xFE00×0xFE00 → 0x0400&lt;/code&gt;），渲染循环也能正确遍历 960 个像素、正确推进缓冲区指针。&lt;strong&gt;出问题的是渲染之后的返回路径&lt;/strong&gt;，&lt;code&gt;tests/test_boot.py&lt;/code&gt; 的 14 项断言目前 10 项通过。&lt;/p&gt;&#10;&lt;p&gt;这一条我没有修完就停手了。&lt;/p&gt;&#10;&lt;p&gt;它属于&amp;quot;能自己走完的收尾&amp;quot;，但它前面连续暴露了 12 个更深的机制性缺陷。我判断继续在同一处反复试错的收益，已经低于把已有成果讲清楚。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="已知的模拟器边界"&gt;&lt;a href="#%e5%b7%b2%e7%9f%a5%e7%9a%84%e6%a8%a1%e6%8b%9f%e5%99%a8%e8%be%b9%e7%95%8c" 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;32 位操作数/地址、保护模式、FPU、多数字符串指令、&lt;code&gt;0F&lt;/code&gt; 扩展表、本项目未用到的 BIOS 服务。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="如果继续"&gt;&lt;a href="#%e5%a6%82%e6%9e%9c%e7%bb%a7%e7%bb%ad" class="header-anchor"&gt;&lt;/a&gt;如果继续&#10;&lt;/h2&gt;&lt;ol&gt;&#10;&lt;li&gt;先用一个 &lt;code&gt;trace&lt;/code&gt; 工具把渲染后的返回路径逐条打出来——这是定位最后那个缺陷最短的路。&lt;/li&gt;&#10;&lt;li&gt;给模拟器加&lt;strong&gt;&amp;ldquo;指令边界断言&amp;quot;开关&lt;/strong&gt;：任何一次 &lt;code&gt;step()&lt;/code&gt; 落在清单记录之外就立即报错。本次 12 个缺陷里有 5 个都是靠等价的观察才找到的，做成常驻检查会省很多时间。&lt;/li&gt;&#10;&lt;li&gt;加&lt;strong&gt;收敛性检查&lt;/strong&gt;：断言任意源码都在 N 遍内收敛，并把实际遍数记进清单。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个项目最后教会我的不是 x86。&lt;/p&gt;&#10;&lt;p&gt;是&lt;strong&gt;&amp;ldquo;能跑&amp;quot;和&amp;quot;对&amp;quot;之间隔着多远&lt;/strong&gt;——中间那 12 个缺陷，每一个都穿着&amp;quot;能跑&amp;quot;的外衣。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item><item><title>大灾变纪念馆：从存档里读出五个人</title><link>https://cloudlake.eu.org/p/cdda-memorial/</link><pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/cdda-memorial/</guid><description>&lt;img src="https://cloudlake.eu.org/p/cdda-memorial/cover.jpg" alt="Featured image of post 大灾变纪念馆：从存档里读出五个人" /&gt;&lt;p&gt;&lt;code&gt;F:\CDDA\graveyard\&lt;/code&gt; 里躺着 10 次角色死亡记录。&lt;/p&gt;&#10;&lt;p&gt;这是《大灾变：黑暗之日》（Cataclysm: Dark Days Ahead）删档后自动留下的完整档案：每次一份 &lt;code&gt;.sav&lt;/code&gt;（角色与世界在死亡那一刻的状态）和一份 &lt;code&gt;.log&lt;/code&gt;（大事记）。&lt;/p&gt;&#10;&lt;p&gt;存档格式是&lt;strong&gt;开着的&lt;/strong&gt;——首行 &lt;code&gt;# version N&lt;/code&gt;，之后是一整个 JSON 对象。所以那个角色的姓名、属性、技能、特性、变异、击杀数、走过的每一处地方，甚至&lt;strong&gt;一生中打破的每一条戒律&lt;/strong&gt;，都能直接读出来。&lt;/p&gt;&#10;&lt;p&gt;我把它读出来，然后做了两件事：&lt;strong&gt;把这些人还原成可看的记录&lt;/strong&gt;，以及&lt;strong&gt;给他们写墓志铭&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="读出来的五个人"&gt;&lt;a href="#%e8%af%bb%e5%87%ba%e6%9d%a5%e7%9a%84%e4%ba%94%e4%b8%aa%e4%ba%ba" class="header-anchor"&gt;&lt;/a&gt;读出来的五个人&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;graveyard/&lt;/code&gt; 里 5 份存档、3 个角色：&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;&#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;/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&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;29&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;&#9;&#9;&lt;td&gt;23.6 小时&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;50&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;40&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;&#9;&#9;&lt;td&gt;1.53 天&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;114&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;97&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;&#9;&#9;&lt;td&gt;&lt;strong&gt;3.47 天&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;313&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;125&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;&#9;&#9;&lt;td&gt;0.2 小时&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;86&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;hr&gt;&#10;&lt;h2 id="一个我最初写错的判断"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e6%88%91%e6%9c%80%e5%88%9d%e5%86%99%e9%94%99%e7%9a%84%e5%88%a4%e6%96%ad" 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;p&gt;测试直接把它打回来了。&lt;/p&gt;&#10;&lt;p&gt;三次存档里 STR/DEX/INT 确实都是 24/24/23——但&lt;strong&gt;最后一次 PER 从 24 掉到了 23&lt;/strong&gt;。而特性数从 69 → 57 → &lt;strong&gt;73&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;p&gt;这个小错误是这个项目第一个教训：&lt;strong&gt;你以为稳定的标识，恰恰是最容易变的那个。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="墓志铭以及它凭什么可信"&gt;&lt;a href="#%e5%a2%93%e5%bf%97%e9%93%ad%e4%bb%a5%e5%8f%8a%e5%ae%83%e5%87%ad%e4%bb%80%e4%b9%88%e5%8f%af%e4%bf%a1" 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;一个 AI 写出来的墓志铭，读起来可以很动人，但里面的&amp;quot;击杀 313 个敌人&amp;quot;——是真的吗？还是它为了让句子好看随口说的？&lt;/p&gt;&#10;&lt;p&gt;所以这里的做法是：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;每一句话涉及的每个数字和名字，都作为一条「可核查声明」（Claim）被登记下来，&lt;code&gt;verify()&lt;/code&gt; 再从解析出的存档里重新推导一遍。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;比如这一段：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;阿里安娜·娜吉雅，女。存活 3.47 天后死去。生前击杀 313 个敌人。身上留下 125 处变异。带着 57 项特性离世。最擅长咒术（15 级）。足迹遍及 9 处，最常出现在紧急避难所 B-19（36 次）、森林（25 次）、公路（17 次）。一生打破 10 条戒律……她把这个世界推开了一段距离，然后被它合上。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;里面每一个数字——&lt;code&gt;3.47 / 313 / 125 / 57 / 15 / 9 / 36 / 25 / 17 / 10&lt;/code&gt;——&lt;strong&gt;全部是声明，全部被重新推导核对过&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;h3 id="校验器自己也要被校验"&gt;&lt;a href="#%e6%a0%a1%e9%aa%8c%e5%99%a8%e8%87%aa%e5%b7%b1%e4%b9%9f%e8%a6%81%e8%a2%ab%e6%a0%a1%e9%aa%8c" class="header-anchor"&gt;&lt;/a&gt;校验器自己也要被校验&#10;&lt;/h3&gt;&lt;p&gt;只有正向检查还不够。&lt;/p&gt;&#10;&lt;p&gt;因为&amp;quot;校验器&amp;quot;本身可能是个摆设——它可能永远返回 true。&lt;/p&gt;&#10;&lt;p&gt;所以测试里加了两条&lt;strong&gt;反向验证&lt;/strong&gt;：故意塞一个伪造的击杀数（9999）和伪造的姓名进去，&lt;code&gt;verify()&lt;/code&gt; &lt;strong&gt;必须报错&lt;/strong&gt;。&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;hr&gt;&#10;&lt;h2 id="测试抓到的两个真实缺陷"&gt;&lt;a href="#%e6%b5%8b%e8%af%95%e6%8a%93%e5%88%b0%e7%9a%84%e4%b8%a4%e4%b8%aa%e7%9c%9f%e5%ae%9e%e7%bc%ba%e9%99%b7" class="header-anchor"&gt;&lt;/a&gt;测试抓到的两个真实缺陷&#10;&lt;/h2&gt;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;小时数校验逻辑写错了。&lt;/strong&gt; &lt;code&gt;存活小时&lt;/code&gt; 这条声明拿 &lt;code&gt;round(days24)&lt;/code&gt; 去和 &lt;code&gt;days&lt;/code&gt; 比，23.6 小时被判成不匹配。修法是让声明带上单位（&lt;code&gt;hours&lt;/code&gt; 与 &lt;code&gt;days&lt;/code&gt; 分开）。&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;地点到访次数没有登记为声明。&lt;/strong&gt; 墓志铭里印着「幸存者营地（12 次）」，但 &lt;code&gt;12&lt;/code&gt; 不在任何 Claim 里。是测试里那条「&lt;strong&gt;文本中出现的每个数字都必须有出处&lt;/strong&gt;」把它揪出来的。&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&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="#%e8%b6%b3%e8%bf%b9" class="header-anchor"&gt;&lt;/a&gt;足迹&#10;&lt;/h2&gt;&lt;p&gt;每条 &lt;code&gt;.log&lt;/code&gt; 事件都带 &lt;code&gt;oter_id&lt;/code&gt; / &lt;code&gt;oter_name&lt;/code&gt;（发生地点），所以&amp;quot;足迹&amp;quot;不是编的，是从数据长出来的：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;克里斯托弗&lt;/strong&gt;：幸存者营地 ×12、无线发射塔 ×4、民宅 ×4、森林 ×4&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;阿里安娜（最远那次）&lt;/strong&gt;：紧急避难所 B-19 ×&lt;strong&gt;36&lt;/strong&gt;、森林 ×25、公路 ×17&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;阿里安娜（最后一次）&lt;/strong&gt;：紧急避难所 C-41 ×2、&lt;strong&gt;坚岩 ×2、实心土 ×1&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;最后一行不需要任何解释。&lt;/p&gt;&#10;&lt;p&gt;它只是数据——她死在岩石和泥土之间。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="一个说明"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e8%af%b4%e6%98%8e" 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;hr&gt;&#10;&lt;h2 id="为什么做这个"&gt;&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e5%81%9a%e8%bf%99%e4%b8%aa" 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;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/memorial/" 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;&lt;hr&gt;&#10;&lt;h2 id="技术细节"&gt;&lt;a href="#%e6%8a%80%e6%9c%af%e7%bb%86%e8%8a%82" 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;&lt;code&gt;cddamem/save.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;存档解析（&lt;code&gt;.sav&lt;/code&gt; + &lt;code&gt;.log&lt;/code&gt;），产出 &lt;code&gt;Life&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;cddamem/epitaph.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;墓志铭生成 + Claim 校验&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;summarise.py&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;make_memorial.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;纪念馆页面（自包含 HTML）&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;tests/test_save.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;19 项断言，主要是&lt;strong&gt;与原始 JSON 交叉核对&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;tests/test_epitaph.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5 项断言，含伪造数据必须被抓&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;：解析出的技能集必须与原始 JSON 的 &lt;code&gt;level&amp;gt;0&lt;/code&gt; 集合&lt;strong&gt;完全相等&lt;/strong&gt;；击杀数必须等于 &lt;code&gt;kill_tracker.kills&lt;/code&gt; 之和；&lt;code&gt;.log&lt;/code&gt; 里&amp;quot;开始大灾变&amp;quot;的回合必须等于 &lt;code&gt;.sav&lt;/code&gt; 的 &lt;code&gt;game_start&lt;/code&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;/ul&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个项目最后让我想的一句话是：&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;写一个动人的故事不难。难的是故事里的每个数字都经得起回头查。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item><item><title>拆开 TI-Nspire 的 .tns 看里面</title><link>https://cloudlake.eu.org/p/zehnscope/</link><pubDate>Tue, 13 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/zehnscope/</guid><description>&lt;img src="https://cloudlake.eu.org/p/zehnscope/cover.jpg" alt="Featured image of post 拆开 TI-Nspire 的 .tns 看里面" /&gt;&lt;p&gt;这台机器上有一个真实的 TI-Nspire 开发环境：OS 3.9.0.463 classic Touchpad、Ndless、从源码构建的 ARM 工具链。&lt;/p&gt;&#10;&lt;p&gt;目录里躺着 &lt;strong&gt;1055 个 &lt;code&gt;.tns&lt;/code&gt; 文件&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;.tns&lt;/code&gt; 不是一个格式。它是一层 &lt;code&gt;PRG\0&lt;/code&gt; 外壳，里面是 &lt;strong&gt;Zehn 容器&lt;/strong&gt;——Ndless 自己的可执行容器格式。而这个格式&lt;strong&gt;没有公开文档&lt;/strong&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="#%e8%87%aa%e5%b7%b1%e8%a7%a3%e5%87%ba%e6%9d%a5%e7%9a%84%e6%a0%bc%e5%bc%8f" 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;span class="lnt"&gt; 4&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 5&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 6&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 7&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 8&#10;&lt;/span&gt;&lt;span class="lnt"&gt; 9&#10;&lt;/span&gt;&lt;span class="lnt"&gt;10&#10;&lt;/span&gt;&lt;span class="lnt"&gt;11&#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;.tns 文件&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 装载器桩 0 ~ 0x1EC 字节（常见；也有 0/480/604/616/632 字节的变体）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── Zehn 容器&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 头 32 字节 8 个小端 u32：&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ &amp;#34;Zehn&amp;#34; / version=1 / file_size / reloc_count /&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ flag_count / extra_size / alloc_size / entry_offset&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 重定位表 reloc_count × 4 字节&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 低 8 位 = 类型，高 24 位 = 偏移&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 标志表 flag_count × 4 字节（同样的 8/24 位切分）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 额外数据 extra_size 字节（名字/作者/声明等字符串住在这里）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 代码 剩下的；若首个重定位是 FILE_COMPRESSED 则为 zlib 流&#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;hr&gt;&#10;&lt;h2 id="两个坑我都先踩了"&gt;&lt;a href="#%e4%b8%a4%e4%b8%aa%e5%9d%91%e6%88%91%e9%83%bd%e5%85%88%e8%b8%a9%e4%ba%86" class="header-anchor"&gt;&lt;/a&gt;两个坑，我都先踩了&#10;&lt;/h2&gt;&lt;h3 id="坑一zehn这个字符串会骗你"&gt;&lt;a href="#%e5%9d%91%e4%b8%80zehn%e8%bf%99%e4%b8%aa%e5%ad%97%e7%ac%a6%e4%b8%b2%e4%bc%9a%e9%aa%97%e4%bd%a0" class="header-anchor"&gt;&lt;/a&gt;坑一：「Zehn」这个字符串会骗你&#10;&lt;/h3&gt;&lt;p&gt;最直接的思路是——搜文件里 &lt;code&gt;Zehn&lt;/code&gt; 这四个字节，从那里开始解。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;但装载器桩里也出现了这个字符串。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;只搜这 4 个字节会找到诱饵。真正的头必须同时满足三个条件：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;version == 1&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;各表长度放得下（不越界）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;file_size&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="坑二能力不等于要求"&gt;&lt;a href="#%e5%9d%91%e4%ba%8c%e8%83%bd%e5%8a%9b%e4%b8%8d%e7%ad%89%e4%ba%8e%e8%a6%81%e6%b1%82" class="header-anchor"&gt;&lt;/a&gt;坑二：「能力」不等于「要求」&#10;&lt;/h3&gt;&lt;p&gt;genzehn 的 &lt;code&gt;--color-support&lt;/code&gt; &lt;strong&gt;默认是 true&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;它的含义是「&lt;strong&gt;支持&lt;/strong&gt; CX/CM 彩屏」，不是「&lt;strong&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;0&lt;/strong&gt;：0 才表示&amp;quot;不支持这类机器&amp;quot;。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个坑值得单独说：&lt;strong&gt;当一个字段名叫 &lt;code&gt;supports&lt;/code&gt;，人们会下意识把它读成 &lt;code&gt;requires&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="验证逐字节重建"&gt;&lt;a href="#%e9%aa%8c%e8%af%81%e9%80%90%e5%ad%97%e8%8a%82%e9%87%8d%e5%bb%ba" class="header-anchor"&gt;&lt;/a&gt;验证：逐字节重建&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;rebuild()&lt;/code&gt; 从解析出来的字段把容器&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;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;span class="lnt"&gt;5&#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;$ python -m zehnscope --rebuild &amp;#34;E:\QA\nspire-dev&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;11/11 containers rebuilt byte for byte&#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;$ python tests/test_zehn.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ok all 100 containers rebuild identically&#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;这比&amp;quot;解析没崩溃&amp;quot;强得多。&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;h3 id="语料是-1055-个真实文件不是精选样本"&gt;&lt;a href="#%e8%af%ad%e6%96%99%e6%98%af-1055-%e4%b8%aa%e7%9c%9f%e5%ae%9e%e6%96%87%e4%bb%b6%e4%b8%8d%e6%98%af%e7%b2%be%e9%80%89%e6%a0%b7%e6%9c%ac" class="header-anchor"&gt;&lt;/a&gt;语料是 1055 个真实文件，不是精选样本&#10;&lt;/h3&gt;&lt;p&gt;其中 100 个是真正的 Ndless 程序，955 个只是沿用了 &lt;code&gt;.tns&lt;/code&gt; 扩展名的文档/资源（&lt;code&gt;boot1.img.tns&lt;/code&gt;、Hitori 关卡、Liero 素材……）。&lt;/p&gt;&#10;&lt;p&gt;一个只见过&amp;quot;干净样本&amp;quot;的解析器，证明力弱得多。&lt;strong&gt;跨十年、多个工具链、8 种不同桩的 100 个真实容器&lt;/strong&gt;，才是像样的考验。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="手写一个-deflate-解码器"&gt;&lt;a href="#%e6%89%8b%e5%86%99%e4%b8%80%e4%b8%aa-deflate-%e8%a7%a3%e7%a0%81%e5%99%a8" class="header-anchor"&gt;&lt;/a&gt;手写一个 DEFLATE 解码器&#10;&lt;/h2&gt;&lt;p&gt;Zehn 的压缩用 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;再读回来。&lt;/p&gt;&#10;&lt;p&gt;解码器是手写的 RFC 1951 + 1950：stored / fixed / dynamic 三种块、LZ77 回引用、Adler-32 校验。&lt;/p&gt;&#10;&lt;p&gt;然后对照系统 zlib，在 &lt;strong&gt;40 组&lt;/strong&gt;样本上比对输出（10 种载荷 × 4 个压缩级别）。&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;hr&gt;&#10;&lt;h2 id="一个被窄样本骗过的断言"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e8%a2%ab%e7%aa%84%e6%a0%b7%e6%9c%ac%e9%aa%97%e8%bf%87%e7%9a%84%e6%96%ad%e8%a8%80" class="header-anchor"&gt;&lt;/a&gt;一个被窄样本骗过的断言&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;tests/test_zehn.py&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;0x1EC (492)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;53&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;strong&gt;0&lt;/strong&gt;&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;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;632&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;11&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;480&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8&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;616&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;6&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;604&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5&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;17 个容器完全没有桩&lt;/strong&gt;——容器就在文件偏移 0。&lt;/p&gt;&#10;&lt;p&gt;我最初写了一条「所有程序共用一个桩」的断言，在 14 个 &lt;code&gt;samples-tns&lt;/code&gt; 上&lt;strong&gt;全部通过&lt;/strong&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;hr&gt;&#10;&lt;h2 id="兼容性判定一个被推翻的直觉"&gt;&lt;a href="#%e5%85%bc%e5%ae%b9%e6%80%a7%e5%88%a4%e5%ae%9a%e4%b8%80%e4%b8%aa%e8%a2%ab%e6%8e%a8%e7%bf%bb%e7%9a%84%e7%9b%b4%e8%a7%89" class="header-anchor"&gt;&lt;/a&gt;兼容性判定：一个被推翻的直觉&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;compat.py&lt;/code&gt; 拿程序&lt;strong&gt;自己声明的能力&lt;/strong&gt;和&lt;strong&gt;具体机器&lt;/strong&gt;对判。&lt;/p&gt;&#10;&lt;p&gt;这台机器的约束很紧：classic 非 CX、320×240 黑白、Touchpad、Ndless r2016、64MB。&lt;/p&gt;&#10;&lt;p&gt;原本的直觉解释是：「SDK 是 r2022、设备是 r2016，所以自写的程序被拒载重启」。&lt;/p&gt;&#10;&lt;p&gt;但实测结论是：&lt;strong&gt;这 100 个程序里，没有一个声明 Ndless 版本窗口&lt;/strong&gt;（&lt;code&gt;NDLESS_VERSION_MIN&lt;/code&gt; / &lt;code&gt;REVISION_MIN&lt;/code&gt; 全缺）。&lt;/p&gt;&#10;&lt;p&gt;也就是说 &lt;strong&gt;版本不匹配不会导致装载被拒&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;那条&amp;quot;合理&amp;quot;的解释，直接被数据否掉了。真正的原因得往别处找——&lt;code&gt;ut_read_os_version_index()&lt;/code&gt; 的地址哈希、&lt;code&gt;sc_addrs_ptr&lt;/code&gt; 查表、或者 classic 上看门狗没关。&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;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;strong&gt;已有&lt;/strong&gt;容器。接上 ELF 解析就能把任意 ARM ELF 打包成合法 &lt;code&gt;.tns&lt;/code&gt;——包括 genzehn 那部分（扫 &lt;code&gt;.rel.text&lt;/code&gt;、发 &lt;code&gt;ADD_BASE&lt;/code&gt;/&lt;code&gt;ADD_BASE_GOT&lt;/code&gt;、处理 GOT 回滚）&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;OS 镜像&lt;/strong&gt;：&lt;code&gt;boot1.img.tns&lt;/code&gt; / &lt;code&gt;boot2.img.tns&lt;/code&gt; / &lt;code&gt;diags.img.tns&lt;/code&gt; 不是 Zehn 容器，是 TI 的 OS/引导镜像，格式完全不同&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;syscall 面&lt;/strong&gt;：把容器里的重定位和 &lt;code&gt;syscall-list.h&lt;/code&gt; 的 ID 表关联，就能静态列出每个程序用到的 syscall，再和 r2016 的表做差集——&lt;strong&gt;那才是&amp;quot;为什么重启&amp;quot;的正确问法&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&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/zehn/" 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;看 100 个程序的容器报告 →&lt;/a&gt;&#13;&#10;&lt;/p&gt;&#13;&#10;&lt;hr&gt;&#10;&lt;h2 id="技术细节"&gt;&lt;a href="#%e6%8a%80%e6%9c%af%e7%bb%86%e8%8a%82" 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;&lt;code&gt;zehnscope/core.py&lt;/code&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;code&gt;zehnscope/inflate.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;手写 DEFLATE/zlib 解码器&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;zehnscope/__main__.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CLI：inspect / &lt;code&gt;--rebuild&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;compat.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;能力 vs 机型判定（含&amp;quot;能力≠要求&amp;quot;的说明）&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;tests/test_zehn.py&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;18 项断言&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;逆向工程真正的方法论，不是&amp;quot;猜对格式&amp;quot;。&lt;/p&gt;&#10;&lt;p&gt;是&lt;strong&gt;找到一种只有完全理解它才能通过的检验&lt;/strong&gt;，然后不断撞上去。&lt;/p&gt;&#10;&lt;p&gt;逐字节重建就是这样一种检验。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#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><item><title>摩擦越大，原始汤越活</title><link>https://cloudlake.eu.org/p/primordium/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://cloudlake.eu.org/p/primordium/</guid><description>&lt;img src="https://cloudlake.eu.org/p/primordium/cover.jpg" alt="Featured image of post 摩擦越大，原始汤越活" /&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;1) 径向力 f = w_ij · (1 − d/R) 正吸引 / 负排斥&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2) 对齐力矩 速度朝邻居速度方向旋转 （群游、漩涡的来源）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3) 摩擦 每步速度 × fric&#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;code&gt;w_ij&lt;/code&gt; 可以编辑&lt;/strong&gt;——所以它不是&amp;quot;一个模拟&amp;quot;，而是一个&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="#%e6%88%91%e5%9c%a8%e8%bf%99%e5%8f%b0%e5%ae%9e%e9%aa%8c%e5%8f%b0%e4%b8%8a%e5%8f%91%e7%8e%b0%e7%9a%84%e4%b8%a4%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;p&gt;而扫描本身给了两个&lt;strong&gt;反直觉&lt;/strong&gt;的结果。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="发现一摩擦越大体系反而越活"&gt;&lt;a href="#%e5%8f%91%e7%8e%b0%e4%b8%80%e6%91%a9%e6%93%a6%e8%b6%8a%e5%a4%a7%e4%bd%93%e7%b3%bb%e5%8f%8d%e8%80%8c%e8%b6%8a%e6%b4%bb" class="header-anchor"&gt;&lt;/a&gt;发现一：摩擦越大，体系反而越&amp;quot;活&amp;quot;&#10;&lt;/h3&gt;&lt;p&gt;第一轮扫描 &lt;strong&gt;486 组对称规则&lt;/strong&gt;（同类相吸 + 异类相斥），跑 900 步后统计&amp;quot;是否冻结&amp;quot;：&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;0.900&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;44%&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;0.950&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;31%&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;strong&gt;0.975&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;11%&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;直觉会说&amp;quot;摩擦越大越容易静止&amp;quot;。实测正相反。&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;时，这套&amp;quot;摆&amp;quot;很快把能量耗散干净，然后卡死在力平衡的位置，变成一动不动的静态点阵。&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;ul&gt;&#10;&lt;li&gt;高摩擦下所有活动规则都&lt;strong&gt;碎成 100+ 个孤立小团&lt;/strong&gt;（最大团占比 5–11%），聚不成大结构&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;对齐&lt;/strong&gt;这一项在对称规则里几乎不影响存活率，只改变形状&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="发现二持续活动来自力的不对称不是来自加能量"&gt;&lt;a href="#%e5%8f%91%e7%8e%b0%e4%ba%8c%e6%8c%81%e7%bb%ad%e6%b4%bb%e5%8a%a8%e6%9d%a5%e8%87%aa%e5%8a%9b%e7%9a%84%e4%b8%8d%e5%af%b9%e7%a7%b0%e4%b8%8d%e6%98%af%e6%9d%a5%e8%87%aa%e5%8a%a0%e8%83%bd%e9%87%8f" class="header-anchor"&gt;&lt;/a&gt;发现二：持续活动来自&amp;quot;力的不对称&amp;quot;，不是来自加能量&#10;&lt;/h3&gt;&lt;p&gt;对称规则要么冻结、要么碎成小团。改一处结构就能解决：&lt;strong&gt;让力矩阵不对称&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;A 追 B、B 躲 A——即&lt;strong&gt;非互易作用&lt;/strong&gt;。这样&amp;quot;作用—反作用&amp;quot;不再成对抵消，体系可以自己驱动自己。&lt;/p&gt;&#10;&lt;p&gt;三物种循环（A 追 B，B 追 C，C 追 A）扫描 108 组：&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;100%&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;59–69%&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;/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;chase flee fric align core | 非互易 moved/phi/团数 | 对称化 moved/phi/团数&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 0.80 -0.70 0.985 0.00 2.0 | 100% 0.694 186 | 59% 0.440 134&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 1.20 -1.20 0.985 0.00 2.0 | 100% 0.622 161 | 63% 0.182 126&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 0.40 -0.20 0.960 0.30 4.0 | 98% 0.724 75 | 69% 0.437 73&#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-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="c1"&gt;// 摩擦 0.96，对齐 0.3，斥核 4.0&#10;&lt;/span&gt;&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;100% 粒子持续移动，混合度 Φ=0.48，最大团占比 16%，而&amp;quot;结构还在重组&amp;quot;的指标（末段熵的波动）是全扫描最高——&lt;strong&gt;0.0245&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="把看起来有意思变成可测量的数"&gt;&lt;a href="#%e6%8a%8a%e7%9c%8b%e8%b5%b7%e6%9d%a5%e6%9c%89%e6%84%8f%e6%80%9d%e5%8f%98%e6%88%90%e5%8f%af%e6%b5%8b%e9%87%8f%e7%9a%84%e6%95%b0" class="header-anchor"&gt;&lt;/a&gt;把&amp;quot;看起来有意思&amp;quot;变成可测量的数&#10;&lt;/h2&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;&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;strong&gt;聚类系数 C&lt;/strong&gt;&lt;/td&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;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&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;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;晶体≈100%，气体≈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;strong&gt;混合度 Φ&lt;/strong&gt;&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;相分离→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;strong&gt;空间熵 H&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;网格计数的归一化熵&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;均匀→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;strong&gt;不可预测性&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;与&amp;quot;同初值 + 1.5px 微扰&amp;quot;副本的平均位置发散&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;从 0.02 起步，1200 步后可读&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;实测对照（1500 步，800–1100 粒子）：&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 style="text-align: right"&gt;C&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: right"&gt;Φ&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: right"&gt;H&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: right"&gt;最大团&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: right"&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 style="text-align: right"&gt;0.85&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;0.48&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;0.74&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;16%&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;&lt;strong&gt;5.61&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 style="text-align: right"&gt;1.00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;0.13&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;0.79&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;98%&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&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 style="text-align: right"&gt;0.77&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;0.95&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;0.79&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;33%&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;&lt;strong&gt;0.004&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 style="text-align: right"&gt;1.00&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;&lt;strong&gt;0.02&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;0.75&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;61%&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;1.64&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 style="text-align: right"&gt;0.57&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;0.49&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;0.77&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&gt;98%&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: right"&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;「相分离」的 Φ=0.02 和「翻涌」的 Φ=0.48——&lt;strong&gt;同一个指标把两种完全不同的结构分开了&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;不可预测性上，「翻涌」的最大发散是冻结体系的数十倍。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;同一个 1.5px 的扰动，在自我驱动的体系里会长成完全不同的世界，在冻结体系里什么都不会发生。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="验证不靠肉眼"&gt;&lt;a href="#%e9%aa%8c%e8%af%81%e4%b8%8d%e9%9d%a0%e8%82%89%e7%9c%bc" 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;/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;node verify.mjs # 71 项断言&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node verify-browser.mjs # 34 项断言（真机 Chromium）&#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;：7 个预设各跑 1500 步，无 NaN / Infinity，坐标不出界，速度有界&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;指标区分力&lt;/strong&gt;：三个结构指标在预设之间跨度都在 0.05 以上&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;：单线程 900 粒子 0.19 ms/步、3000 粒子 1.4 ms/步；浏览器里 3000 粒子仍 45–60 fps&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;关键的一条：&lt;code&gt;verify.mjs&lt;/code&gt; &lt;strong&gt;直接从 HTML 里抽出 UMD 核心在 Node 里跑&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;所以&amp;quot;这个模拟是什么行为&amp;quot;可以&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/primordium/" 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;&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;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;自适应规则&lt;/strong&gt;：让 &lt;code&gt;w_ij&lt;/code&gt; 本身随接触历史变化，看是否自发出现分工或领地&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;三维&lt;/strong&gt;：核心已经是纯数组合集，换成三个坐标即可，代价只是渲染要改写&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个项目最让我意外的地方，不是某个预设好看。&lt;/p&gt;&#10;&lt;p&gt;是&lt;strong&gt;&amp;ldquo;加能量&amp;quot;并不是让系统活起来的唯一办法&lt;/strong&gt;——把对称性打破，它自己就会动。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item></channel></rss>