普通视图

发现新文章,点击刷新页面。
昨天以前云风的 BLOG

除法的意义

作者 云风
2026年4月12日 20:52

可可已经在三年级下学期了,数学似乎还是有点问题。这个阶段考试成绩其实都不会太差,但一旦作业或考卷上的错题并非粗心大意就值得警惕。乘除法是二年级学的,三年级已经在学两位数除一位数的除法。但会计算并不难,计算只是一项机械性技能,难的是理解乘除法的意义。理解乘除比理解加减法困难的多。

我翻出几个月前的一篇 blog,发现过了 4 个月,她的问题依旧:乘除法作为一项计算技能和其背后的意义是割裂的。这导致了很多问题到底如何解决一筹莫展。固然多作练习就能开悟,毕竟几乎没有成人回头看小学数学会觉得难以理解的。但我还是想尽力搞清楚她的小脑袋里到底是哪打结了。

今晚讲一道相当简单的数学题:

有 96 个鸡蛋,8 个一盒装,可以装多少盒?

可可不知道如何解决这个问题,我一开始是很诧异的。我先反复确认她理解了题目的文本,并非语言理解的问题。真的是无法联想到应该使用除法这个工具,而 96 这个数字过大,即使不使用除法,也不知道该如何处理。我默不作声,让她仔细想想,她愣在那里不知所措,都急得掉眼泪了。

我决定一步步推演这个问题。

先问一个简单的版本:有 12 个鸡蛋,10 个一盒装,最多可以装满几盒?

我本以为她能一口答出,但可能是前面的问题受挫,她还是不知道如何下手。我想想,从桌游盒中找了一堆 token 和若干小碗,说你自己装碗试试吧。装完 12 个后,又把问题改成了 30 个,她重新摆弄了一次,这下明白了。

我说,现在要把道具收起来了,换成草稿纸,你该如何解决这个问题呢?

我教她用减法:用 30 - 10 = 20 , 20 - 10 = 10 ,10 - 10 = 0 ;数一下一共减了 3 次。可可说,我知道了,其实不用数,只要看数字是几十,那么就是几盒了。

那么,回到一开始的问题,不是 10 个一盒而是 8 个一盒就不能直接看出来了,该怎么办呢?可可说那我也会:她从 96 - 8 = 88 开始一步步的做减法计算,很耐心的减到了 0 ,数了一下是 12 ,中间居然没有算错。

我说,96 / 8 = 12 ,并不真的要花这么多时间做减法。你其实会算除法,只是不知道除法有什么用。除法就是连续计算减法的次数,就好比乘法就是连续做多次加法一样。你需要把 token 一个个放进碗里的过程抽象化成数字写到草稿纸上,打草稿就是把脑子里想的东西具象化出来。这个过程借助数学符号可以更简单。数字是符号,加减乘除也是符号,符号能帮助你思考,但先要明白这些符号代表的道理。

我再换个问题:

有 80 个鸡蛋,8 个一盒装,可以装多少盒?

可可没犹豫,马上告诉我是 8 盒。我说你别着急,拿草稿纸仔细算一下。她算完不好意思的告诉我是 10 盒。我画了张矩形图,给她讲解了一下 8 x 10 = 10 x 8 的道理:10 行 8 个与 8 行 10 个其实只是图形旋转了一下,总数是一样的。

那么,从 96 个鸡蛋里先拿出 80 个装满 10 盒后,剩下的还可以装多少盒呢?她计算了一下 96 - 80 = 16 ,16 / 8 = 2 ;然后 10 盒与 2 盒合在一起也是 12 。

再看除法的竖式草稿,其实是一样的。

今天花了一个小时讲这道数学题(她的考卷上的错题),这次似乎真的懂了。

soluna 外挂 C 模块

作者 云风
2026年3月11日 14:49

soluna 集成了 lua 虚拟机,但默认构建方式是将 lua 库静态链接到唯一的执行文件中。这将导致无法以动态库的形式外挂 Lua 的 C 扩展。

这是因为,如果独立编译 Lua 的 C 扩展库,通常需要链接 Lua 的 C API 。标准的方法是动态链接 lua 实现,如果静态链接 liblua.a ,会导致进程中有多份 lua 的实现。在 Lua 的历史版本中,这将导致运行期错误。

这是因为,Lua 的实现中有一个静态的“空”对象,所有的 nil 都指向这个对象。如果进程空间中有多份 Lua 实现,就会出现多个空对象。运行时的数据结构中会引用这个空对象,而不同副本的实现将“空”和自身保留的“空”对象引用做比较时,就会出现错误的判断。

在更早期的版本,出现这种链接出现的项目,bug 会隐藏得很深。所以后来 Lua 增加了 luaL_checkversion() ,倡议在外部库初始化时调用,除了检查版本号,还会检查当前执行的 lua 实现是否和虚拟机创建时用的实现是同一个副本。

但不知道怎样正确链接 lua 的项目(保证进程中只有一份 Lua 实现)还是太多,从 Lua 5.4 以后,这个“空”对象就被移入了 lua_State 这个运行期结构。以牺牲一点运行时的代价,挽救那些似乎永远也搞不懂“加载和链接”的程序员。终于,错误的链接 Lua 也能不出错了。

但我还是认为,在同一进程中置入多份 Lua 实现是不好的。

注:这也是 Windows 动态库的一个独有问题。因为 Windows 的 DLL 不允许有未完成的符号,必须在编译链接时指定所有符号(Lua C API)的来源;如果是 Linux ,可以不链接 Lua C API 的库,在运行时加载动态库,加载器就能把进程内的对应符号装载起来。


回到题头的问题:soluna 静态链接了 Lua ,并未导出 C API ,要用 C 写额外的库怎么办?

曾经在 Ant Engine 中,我采用了一个方法:提供一个假的代理动态库,提供所有 Lua C API 的符号。外部库可以动态链接它,而它将所有 Lua C API 调用转发到 engine 内部链接的 Lua 实现中。

这样做的好处是,即使是预编译好的 Lua C 库,只要它正确的以动态链接形式链接了 Lua ,就能直接被 Ant Engine 加载。如果不需要外部库,这个代理库也可以不发布。

今天,我想给 soluna 加上类似的特性,但尝试了新的方案:外部库在构建时额外实现一个简单的入口函数,它不依赖真的 Lua 实现,而是链接 soluna 项目中的 extlua/extlua.c 这个 Lua API 代理实现。再由 soluna 的定制加载器来加载这个外部库。

比如,我有一个叫做 foobar 的外部库,原本的实现是这样的:

static int
lhello(lua_State *L) {
    lua_pushstring(L, "Hello World");
    return 1;
}

extern int
luaopen_foobar(lua_State *L) {
    luaL_Reg l[] = {
        { "hello", lhello },
        { NULL, NULL },
    };
    luaL_newlib(L, l);
    return 1;
}

当我们编译成动态库时,导出的 luaopen_foobar() 是库的入口。lua 的 require 可以正确的导入它。但这个实现依赖若干 lua C APIs ,例如 lua_pushstring() 等。

如何在 soluna 里正确加载它呢?我们需要在调用 luaopen_foobar() 这个入口函数前,将进程中的 Lua C APIs 注入这个动态库。

在这个方案中,只需要链接 soluna 项目中的 extlua/extlua.c 单个文件,然后导出一个额外的库入口函数:

extern int
extlua_init(lua_State *L) {
    luaapi_init(L);
    luaL_Reg l[] = {
        { "ext.foobar", luaopen_foobar },
        { NULL, NULL },
    };
    luaL_newlib(L, l);
    return 1;
}

这个函数的第一行需要调用 luaapi_init(L) ,它的实现在 extlua.c 中。然后用 luaL_newlib() 注入原有的模块入口函数即可。

luaapi_init(L) 并不依赖任何 Lua 的内部实现,只依赖 Lua 的一个官方宏 lua_getextraspace() 完成了注入 Lua C APIs 的魔法。

这是个有趣的技巧:

lua_getextraspace(L) 的官方定义是这样的:

#define lua_getextraspace(L)    ((void *)((char *)(L) - LUA_EXTRASPACE))

每个 Lua_State 结构前都保留有一个指针的空间,可以用来传递数据。soluna.external.load 会构建一个空的 Lua 虚拟机,并把所有 Lua C APIs 的引用放在它的 extraspace 。因为上面的 extlua_init() 是一个标准的 lua_CFunction ,所以可以用标准函数 package.loadlib 读出。传入这个带 C APIs 的空 Lua 虚拟机,luaapi_init() 就能正确的导入所有 API 了。随后的 luaL_newlib() 会把所有真正的入口函数放在这个空虚拟机中。当然,只是一些字符串(入口名)和 C 函数指针。

接下来,soluna.external.load 再从这个虚拟机中把整个入口函数表复制到当前虚拟机,并销毁掉这个临时虚拟机,就完成了整个外部模块的动态加载。


soluna.extlib(name) 的实现是这样的:

function soluna.extlib(name)
    local extlua = require "soluna.extlua"
    local filename = assert(package.searchpath(name, package.cpath))
    settings = settings and soluna.settings()
    local entry = assert(package.loadlib(filename, settings.extlua_entry))
    return extlua.load(entry)
end

要使用上面例子中的放在 sample.dll 中的库 ext.foobar 只需要这样:

local libs = soluna.extlib "sample"
local foobar = require "ext.foobar"
assert(libs["ext.foobar"] == foobar)

即使要静态链接 sample 模块(iOS 不支持动态库,可能必须静态链接),只需要采用以下编译方案即可正确工作:

  1. 静态链接 sample 模块
  2. 不要链接 extlua/extlua.c
  3. luaapi_init() 定义为一个空函数
  4. extlua_init() 这个入口函数导入
  5. 用 soluna.extlua.load(入口函数) 加载

Star Trek : Captain's Chair 初体验

作者 云风
2026年3月1日 19:24

今年过年,我沉迷于 Star Trek : Captain's Chair 这款 2025 年的桌游。暂时还没有中文版,如果直译的话,名为《星际迷航:船长之椅》。这是一款以卡牌构筑为核心玩法的桌游,在游戏过程中,不断完善自己的牌堆,构筑一个高效的得分引擎。如果能比对手获得更多的 VP 就可以获得游戏胜利,但也要避免突然死亡。这是一款新游戏,但作者 Nigel Buckle 和 David Turczi 之前已经用类似的系统出过 Imperium (帝国)三部曲。其中《帝国:经典版》和《帝国:传奇版》有中文版,在淘宝上就可以买到。btw, 前段时间我玩过的 VoidFall 也是他们的作品。

这个游戏的规则还是挺复杂的,在 BGG 上的 weight 评级达到了 4.06 。注:游戏的重度(weight)是由玩家评分综合而来,最高为 5 。它指的是规则的繁杂程度,而并非游戏的策略深度(通常有相关性)。例如围棋虽然策略深度几乎达到了桌游的天花板,但它的 weight 就不到 4 。而 bgg 上 weight 超过 4 的游戏并不多见,大部分超过 3 的桌游,一般就被归为重度游戏了。我大概花了 10 多个小时试玩,看了几个小时的教学视频,才感觉学会了游戏的基本规则。不过一旦理解了游戏的设计逻辑,玩起来还颇为流畅,规则书以及规则助记版都非常符合直觉,简单好认。重度游戏大多不太讨人喜欢,但设计良好的重度游戏也能带来更多乐趣。

我认为 ST:CC 是我这些年玩过的所有卡牌构筑类桌游中机制、策略和局势变化最丰富的。它提供了及其丰富的机制让玩家控制牌组的构成,这也是“构筑”这个机制的核心玩点。和最早的《Dominion 领土》作比较:这类游戏的基本玩法就是从市场购买新卡,构建一个得分引擎。分往往也体现在牌组中,但会稀释行动牌的价值(通常分卡在游戏过程中没有收益),让玩家在构筑过程中做出权衡。Dominion 每局游戏的后期通常会面对厚厚的牌堆,行动会变得越来越不可预测。后来的同类游戏逐步加入了更丰富的机制来帮助卡组瘦身,提供给玩家更多确定性,更好的控制自己的行动。

ST:CC 以及它的前身 Imperium 提供了非常丰富的卡组瘦身机制:

  1. 可以把卡堆里的牌 LOG 起来:和早期卡牌构筑游戏不同,得分卡并不是专门的卡,而是每张卡本身就带有 VP 。这更像银河竞逐这样的引擎构建游戏。收集的卡越多得分越高。LOG 可以把当局游戏不再用的卡从当前卡堆里移除,但得分依旧保留。

  2. 可以把卡片 deploy 到桌面:放在桌面的卡可以提供持久的被动能力,也可以有限的提供主动能力或响应能力。同时,活动卡组也得到的瘦身。根据卡片属性不同,提供有差异的回收规则。船员卡可以常驻一张,新的船员卡晋升后 dismiss 旧的;飞船卡则在占领星球后自动 dismiss ;事件卡则每张有不同的回收前置行动(不回收会在游戏结束结算时计为负分)。

  3. 卡片可以 beam 到飞船或星球上:这可以对卡组作更灵活的临时瘦身。几乎所有的飞船都有主动能力可以 beam 手牌,但反向回到手牌的 recall 操作却比较稀少。不过,beam 在飞船上的卡也可以随飞船 dismiss 而一同回到弃牌堆。

永久(不可逆)和临时(可收回)的卡牌瘦身操作,可以让玩家在游戏过程中动态的调整卡组,让游戏的确定性更高,而不会在抽牌堆太大时,过于依赖抽卡的手气。就我这几天玩的数盘游戏体验,通常我的活动牌组(抽牌堆加上弃牌堆和手牌)在整局游戏里也很少超过 20 张。

ST:CC 在游戏过程中的卡组升级也有新意。

首先,和大多数卡牌构筑游戏一样,初始卡组是 10 张左右,每轮抽 5 张。这样可以保证前两轮可以作一个轮回,让随机性限制在 10 张卡的不同组合上。但和之前的很多游戏不同,它的 10 张卡是完全不同的,每张都特别设计过。甚至游戏带了 6 套风格迥异的初始牌组。而传统上的设计更偏好在初始卡组中放上雷同的初始能力卡,加上很少量的特殊卡。如果没玩过桌游的话,可以对比杀戮尖塔这样受桌游启发的电子游戏:一开始的初始卡组中只有一张特殊能力卡加上普通的打击和防御。

而和一般的卡牌构筑游戏的升级流程不同,它会为每个初始牌组设计 5 张左右的固定补充卡堆和 8 张左右的高级补充卡,以固定节奏补充进来:每次抽牌堆抽空都会自动触发这个补充操作,加入一张额外的补充卡。基本的补充卡的随机性在于每局游戏的进入次序是打乱的,而高级卡则需要用不同资源购买,但可以让玩家指定(没有抽卡的随机环节)。这样熟练牌组的玩家可以预先学习好每个角色牌组的策略,再实际玩的时候又不至于形成太固定的套路。

因为补充卡是通过卡组循环进入抽卡堆的,添加新的补充卡可以带来更多的组合,相当于卡组升级。所以调节抽卡堆的消耗速度就相当于控制玩家卡组升级速度。ST:CC 在主动控制牌堆轮转这一点上设计得比大多数卡牌构筑前辈出色。

1.抽牌能力。这是一个常规设计,不仅用来补充当前回合的行动选择,同时加快了抽牌堆的轮转。

  1. 弃掉抽牌堆顶的牌。这在《Dominion》中多以攻击效果出现,说明早期这种设计更多强调的是其负面影响:让玩家暂时失去潜在的行动能力。但由于卡组瘦身很容易,它也出现了有益的一面:加快牌堆轮转。

  2. 在回合结束时,玩家可以任意保留手牌。这给玩家了选择:确保下一回合能做的行动,但减缓了牌堆轮转速度。

  3. 从市场获取新牌后,可以自由选择放入弃牌堆还是放入抽牌堆。前者加快了牌堆轮转,后者提供了确定性:可用抽牌能力立刻获取,或确保下一轮可以抽到。

由于有大量从弃牌堆抽牌的能力。这极大的丰富了获取行动卡的途径。抽牌堆抽卡是随机的,弃牌堆抽卡是确定的(挑选),牌堆轮转加快固然是好事,但弃牌堆清空也是需要考虑的问题。

除了固定补充卡升级,游戏还提供公共市场和供双方争夺的中立地点卡。但很多传统的市场机制是用资源从市场买卡,而 SC:CC 并不通过积累资源购买市场卡,而是改为用特定行动卡片直接获取。市场被分为了四类:船员、货物、飞船、盟友,分别对应不同的行动卡去获取。根据选择的初始牌组不同,获取这些市场卡的行动卡使用方式也不一样。由于行动力有限,规划行动的分配获取市场卡就变成了卡片 combo 重要的一环。玩家很难积累获取市场卡的能力,抢夺地点卡更是这样:规则限制了每个回合最多只能获得一个中立地点。整局游戏中不会获得太多的额外卡片,且每张公共卡都是单独设计的,这让引入每张卡到自己的卡组都需要仔细规划。

ST:CC 的卡片被设计成一卡多用。卡片处于不同位置:从手牌打出或桌面上激活会有不同的能力。而即使是同一种方式使用它,一般也有多种能力供选择,只能选其一使用。虽然每种使用方式大多有前置条件或副作用,但本身的多种选择让每张卡片在不同场景下都有用。

因为卡片的位置非常丰富:除了传统的抽牌堆、弃牌堆、手牌外,还有桌面区、市场区、当前市场、市场库存、中立地、废牌堆、附着在其它卡片上、LOG 区、升级区、事件区等等。就我主要玩的 PICARD 牌阵来说,大量的行动就是将卡在这些这些区域之间调度。所以在玩的时候,有一点工人分配游戏的感觉。不仅提供了丰富的卡牌策略,还非常好的契合了星际迷航那种驾驶飞船探索宇宙的主题。


为了让游戏不限于千篇一律的构建得分引擎循环,游戏给每个牌组都设计了不同主题的任务。任务不同于很多引擎构建游戏的终局任务卡,那个在 ST:CC 里也有,被设计为 Encouter 卡片,通常可以提供大笔的 VP 。任务就是固定在每个初始卡组上的,像是堆每组不同风格的牌作一个游戏引导,引导在游戏过程中侧重某种玩法。例如,PICARD 的基本任务就是获得三张同盟卡,并把他们都 beam 到同一艘飞船上,且获得至少 4 点科技点和 4 点影响力,就可以完成。

这个设计不会让玩家(熟悉后)玩游戏时不会走一步看一步,每步寻找当下行动的利益最大化。玩家必须作一个长远规划:因为任务必不可少的需要分成很多步骤,同步相当多的行动在好几个回合才可能达成。以我玩的经验来看,基本任务一般在游戏中后期才可以达成,而以开始不作计划的话,常常忙到快结束时还差上一点点。

由于只靠固定牌组很难有效的完成任务,随机出现的公共牌加入卡组都能带来意想不到的高效组合,所以每局游戏的过程都会差异很大。我用 PICARD 玩了 3,4 局游戏,都选的 KOLOTH 这个 bot ,但每局游戏体验完全不同。更别说换掉对手会有完全不同的局面。游戏为每个舰长的 bot 定制了不同的自动化策略来模拟人类玩家选择不同舰长会出现的不一样的打牌倾向。

这是一个两人对战游戏,但也可以用设计好的自动化规则来模拟一个对手。但在 BGG 上,大多数玩家认为这个单人对抗 bot 的玩法更好玩。游戏的教学作的不错,提供了一个更存粹没有对手的单人模式,通常用于熟悉牌组。这个教学模式就是无干扰的刷分,刷够足够的分就胜利了。通过玩这个模式,可以体验不同舰长牌风格迥异的 combo 策略。通常建议把 6 个舰长都刷够分,这样在对战时既能知道自己应该怎么玩,还能熟知对手的策略。

正式的单人模式是对抗固定规则的对手。采用的是不对称规则:玩家和 bot 的行动法则是不一样的。我没有玩过对战模式,但据 BGG 论坛玩家的反馈,预设规则把和真人玩家的对抗时会产生的交互:争夺市场卡片、抢占中立地点等模拟的很好。一开始玩的时候,操作 bot 很容易出错,但玩过一盘之后就非常顺畅了,bot 每个回合一两分钟就能操作完,反之自己这边的行动每个回合会花很长时间。可想而之,和人对战应该会有极大的 downtime ,怪不得大多数人都选择了单人 solo 。

但我觉得,如果有个人类对手和自己一样玩过很多盘 solo 的话,再在一起对战应该也是非常有趣的。

官方还为单人模式设计了一个长线的五年计划规则。让玩家可以连着玩 5~10 盘游戏,在每盘游戏间加入了牌组升级:每次胜利都可以加入当局游戏终局时的某张市场卡进入初始牌组,或是 boost 一些初始能力。由于游戏设计了 6 组不同的牌,这相当于需要击败 5 个不同的对手(自动化 bot ),想来不会有太多重复感。我打算熟悉玩所有卡组后就尝试一下这个长线任务,应该会很有趣。


很想买一套实体版,但在淘宝上找不到代购,甚至目前美国那边也缺货等着重印。我这几天都是在桌面模拟器上玩的(有玩家制作的 mod )。我的感觉是,由于电子版缺少触感,细节更容易玩错。即使很熟悉后,游戏效率还是比不上实体。这点和版图游戏颇为不同,这个几乎全部用卡牌作道具,假若是实体牌的话,电子版只在洗牌时会便利一点,打牌及查看牌面要麻烦很多。而很多版图游戏,电子模拟器在 setup 以及游戏过程中的摆放都会更方便。

作为 solo 游戏,实体版最方便的地方在于易于反悔。只要没有信息揭示环节(例如抽牌后查看),大多数行动你都可以方便的在牌桌上 undo ,尝试各种不同的组合。电子模拟器上的 undo 操作一不小心就把桌面状态弄乱了。毕竟桌游除了桌面,人脑里还有一整套游戏状态,缺少实体会让大脑负荷要重得多。


谈点体外话。由于这款游戏规则相对繁杂,我尝试用 AI 辅助学习游戏规则,使用的 Gemini 。可惜这个游戏还太新,网上资料太少。导致 Gemini 对游戏规则细节知之甚少。但它又表现得很懂,对话中自信满满。我问了很多规则细节结果都是错的,即使我让它指出细节出至规则书上具体哪里,也全是幻觉。甚至引用论坛网友的讨论也能理解错误。最后,我还是得自己推敲规则书,或是用传统的搜索方法找到 bgg 论坛规则讨论版面的帖子,研读作者写的 FAQ 等等。和 AI 的问答阅读起来固然舒服,针对性很强(不像规则书读起来那么累),但我实在没有能力鉴别 AI 的错误。毕竟我原本就是因为不懂规则才去问的呀。

有些错误还是能看出来。毕竟我玩的游戏很多,可以从作者的游戏设计思路角度去考虑。玩的过程中有疑问去问 AI 。对反直觉(感觉游戏不应该这样设计)的答案有所警惕,可继续追问。但有些真看不出来。

比如我在和 bot 对战时,触发了一条 bot 需要 log 一艘飞船,我不知道该如何处理。(特地用英文术语)问了下 AI 。AI 告诉我应该把最近 bot 部署的 ship 卡 log 起来,并将同一地区的所有外派部队收回。但后一条是 AI 自己编的规则,我在规则书中怎么都找不到对应的文字。反复询问,AI 都表现的信誓旦旦。让我去查规则书某个章节(其实不存在)。它还引用了 BGG 论坛的帖子。而我仔细研读了大篇的帖子后,确定是 AI 混淆了 log 和 dismiss 的处理方法。

再有一例:游戏的舰长面板分 A/B 两面,供玩家选择。A 面只有一个任务,B 面有三个任务,其中一个和 A 面任务完全相同。完成 B 面的任务还有额外的 VP 奖励。我一开始非常不解,初看起来,A 面没有任何优势,因为 B 面不仅提供了 A 面的选择,有额外 VP ,还可以有更多选项。我在规则书上也没有找到选择 B 面的惩罚。带着这个问题我询问了 gemini ,它在搜索了 bgg 论坛后,又胡扯了一堆什么 A 面让玩家更专注,完成难度和行动奖励不同(实际完全一致的)。但实际上,核心差别其实是:B 面的科技/军事/影响力等导轨设计不同(我一开始没注意到,规则书里也没提这个差异),而 BGG 论坛里针对这点讨论的帖子中,下面好几条回复都强调了这一点,gemini 恁是在查看帖子后,把这条最重要的信息忽略掉了没告诉我。

结果,我和 AI 的这些对话并没有帮我节省理解规则的时间。不仅自己重新反复研究规则书,还花了更多时间去论坛看帖(当然这不是坏事)。我想,如果我让一个人类游戏玩家教我,若是自己没怎么玩这个游戏的话,都不会表现的如此自信吧。如何辨别 LLM 提供的信息中哪些确有价值会变得更加重要。LLM 的语言表达能力越来越强,也会变得越来越有欺骗性。

用 AI 辅助读书

作者 云风
2026年2月16日 13:23

最近一年闲下来,我重新挖掘了读书的乐趣,尤其是读小说。

读小说真的需要时间和心境,因为进入心流状态更慢。如果长时间无法进入状态,很容易就读不下去;但一旦读进去了,比玩游戏(互动形式)或看影视剧(多媒体形式)更让人沉浸和回味。你可以对精彩处反复斟酌体会其中的情感,也更容易停下来脑补作者在情节上的留白。阅读节奏完全由自己控制,可快可慢。鉴于制作成本,小说的多样性远超其它媒介,提供的选择就更为宽泛。

我最近尝试使用 AI 来提升我的阅读体验。首先发现的是 AI 非常适合荐书。我使用的主要是 Gemini ,免费的版本就足够了。我可以先列举一些我很喜欢的书,让它帮我推荐更多。在初选的名单中,再通过对话了解书的特色。为了避免自己总是阅读类似的书,也会让 AI 推荐一些我之前没有尝试过的类型。当然,小说本身还是人创作的,通过推荐作者比推荐书本身更有效率。

这两个月我想读点太空歌剧类的小说,但老一点的名著基本都看过了,所以转向近十年的新作。另一方面的原因是大多数科幻小说本身就有时效性,这些年人类现实中的科技发展很快,文学家的幻想很容易随着时间和现实脱节。

但我很快就发现,想读新一点科幻小说最大的问题是中文版的翻译速度完全跟不上。AI 推荐的书 90% 都没有中译版。即使把时间放宽一点,十年前的长篇,往往也只翻译了开头。这很好理解:如果出版了第一本销量不如意,可想而知后续会更不理想。这在经济上是绝对理性的行为,可对粉丝来说颇有点难受。

题外话,桌游领域也有点类似。桌面游戏通常也是由单个人设计,受者也非大众。即使设计者想好了出一个系列,若前作卖得不够好,扩展包也就难以发行。我最喜欢的桌游设计师 Thomas Lehmann 解释过 Res Arcana 的第三个扩展 Res Arcana Duo 为什么作为一个(看似简化过的)独立游戏发行而不延续扩展包的形式:必须想点办法扩大这个系列的玩家群,否则扩展包的销量只会越来越少。作为中文用户,我对 Res Arcana Duo 至今没能出中文版还是有点伤心的。希望今年的新作 Dark Pact (2026) 黑暗契约可以出中文版。看完介绍,我对这个纯粹的卡牌构筑游戏颇感兴趣。


我是 Old Man's War 系列的忠实粉丝,很喜欢 John Scalzi 。他的书读起来一点也不累,那种书中遍处可见的程式员式的冷幽默颇对我胃口。我前段时间在京东上买了一本互惠帝国系列的第一本《崩塌的帝国》。收到书时是一个暖日的下午五点,晚上十点就合上了书页,中间除了正常吃了个晚饭,别的时间都在读书。读这本书的另一个动机是我想多看看关于太空旅行的不同设定(以给设计我那个关于太空航行的游戏提供灵感)。读完了这本书后,除了很满意书里的科幻设定外,还很期待后续的故事发展。

可惜这套三部曲的后两本一直都没有翻译成中文版。

我觉得我近些年的英文阅读水平提升了不少,要不尝试一下直接读英文吧。试了一下,离享受读书还是颇有距离。阅读小说需要一个流畅的体验过程,无法顺势进入心流,阅读就变成了一个苦差事。文学类作品和技术类文章差异很大,能顺利阅读技术类英文,不等于读小说也没问题。我想还需要更多的阅读练习,而学习必然辛苦,这不是我目前想要的。

隔了两天,我尝试了另一个方法,这是我发现 AI 能提供给我的另一项重要帮助:翻译阅读。

我觉得,技术类文章和文学类创作最大的不同是:前者追求用精炼准确表达知识,后者需要在描述作者构思的情节之外传达情感。理解一本小说,需要基于对小说中人物和故事的理解;正如翻译一本技术书,你得理解其中的技术原理。这也是为何机器的逐句翻译无法做到准确的原因。大语言模型应该能改善翻译,但我一开始尝试的还是直接的 google translate 和装在本地 ollama 中的 translategemma 本地模型,对小说直译。

用不同方法,经过几个章节的体验,我发现最适合我的是让机器完全对译,不做任何针对中文语境的加工,并以中英对照的形式一段段话展示供我阅读。我主要还是针对中文阅读,虽然语言感觉有点蹩脚,但因为我知道信息原本是英文的,而我又有相当的英文语法知识,所以大脑很快就能适应,在阅读过程中自动转换为合适的中文理解。由于是机械直译,反而不会缺失信息。当觉得句子难以理解时,迅速跳转到英文原文处,通常就明了了。读到精彩的对话,往往回味一下英文原句更有感触。

有些句子颇难理解。这时可以打开一个 Gemini 对话,提供它足够多的上下文,然后贴上原文,Gemini 可以解释得非常清楚。毕竟这是 10 年前的小说了,我估计小说的原文本身(甚至第一卷的中译本)就是大模型的训练材料。比如这次我就学到一个知识:在英文语境中,皇帝会自称 We/Our 而不是 I/My ,用来指代个人和背后皇权的双重身份,这和中文背景下,皇帝自称“朕”颇有共通之处。第一次读英文直译时,我会对翻译器输出的“我们”有所疑惑,但随即和 AI 讨论就学到了这个。第一卷的中译本中,译者恰如其分的选择了“朕”来翻译 We ,google translate 这种直译显然是做不到的,但 Gemini 有了上下文就能选择这种译法。我很怀疑它受了训练语料的影响(被中译版的文本训练过)。

我用这个方法读完了第二本《the consuming fire》,大约花了 2-3 倍第一本的时间,阅读速度的下降是很明显的,但可以接受。我觉得稍加训练就可以改善到完全不影响阅读心流的状态。然后我读了第三本《The Last Emperox》,居然和读第一本一样的时长。但我觉得倒不是我快速适应了这些新的阅读方法,而是这个系列三本书的故事结构其实是类似的,读到了后面,跟上书的节奏越来越容易了。阅读长篇小说的过程有点像是在在脑子里逐步搭建作者构建的世界,然后一点点填上细节,最艰难的部分在最前面,后面就是顺理成章的活。

即使情节上有点雷同,我还是很喜欢这套三部曲。


这两天在补《The Expanse》小说的最后三卷,不需要等美剧了 :)

日常锻炼的一些记录

作者 云风
2026年2月11日 17:28

我大概是去年 4 月左右开始跑步的。离上次的记录已过了半年

最近坚持的还不错,每周可以保证至少 3 次跑步。现在的心肺能力明显好了很多。去年刚跑步时,每跑 5-10 分钟就需要步行几分钟缓缓,不然心率很容易超过 140 (我给自己定的心率上限)。现在差不多可以保持心率在 140 以下连续跑完 4 km 了。大约花费 30 分钟。这差不多是 8km/h 的平均速度,前半程会更快一点,后半程为了保持心率需要降一点速度。

我觉得另一方面的原因是在冬季,跑起来不那么热。现在会挑选中午有太阳的时候跑,晒晒太阳更舒适。跑完后也没特别累的感觉,只是在最后 10 分钟有一点点难受,希望快点结束。但每次还是坚持跑满 30 分钟。


我家附近 500 米处开了家抱石馆。在两个月前,我带可可去了一次,她莫名其妙的喜欢上了抱石。去了两次后就让我给她办张月卡。我说,次卡每次 95 ,月卡 750 ,一个月要去 8 次以上才划算。她说没问题,几乎天天晚上让我带她去。虽然有一半的动力是去岩馆撸那只胖猫,但看得出来是真的喜欢。我之前也带云豆出去攀岩,从小到大爬过上百次,谈不上讨厌,但始终爱不起来,我也没逼他。后来可可长大了一点,去过两次明显没有兴趣,我干脆就不带她去了。这次莫名其妙的爱上抱石,我是没想到的。

一开始,她只能爬 v0/v1 的线路。但进步非常快,她有从小练起的舞蹈基本功打底,身体的柔韧性特别好,尤其在爬平衡线上特别有优势。在岩馆中超过很多大人(新手)也颇为得意。在第二张月卡时,几乎可以完成所有的 V2 线路,并勉强可以挑战 V3 了。毕竟身高臂展上有劣势,一些成人可以顺利完成的 V2 线路,她需要多做几个动作,无形中提高了难度。

我跟着她也办了月卡,但不会每天爬,有时就是看着教一下,但也比过去勤快了许多。水平也跟着上升。现在可以爬一些 V4-V5 的线路了,而上次在这个水平还是小孩没出生前,体重在 75kg 以下的时候。

现在体重保持在 83kg-84kg 之间,已经很久没有降低了。比半年前再减了大约 2 kg ,比开始跑步前最重 93kg 时几乎减了 10 kg 。考虑到力量(肌肉?)也有所增长,还算满意。身边很多人都说我前两年日益见长的肚子又消失了。希望未来一年可以把体重降到 80kg 以下。

体能的上升对爬高墙的帮助特别明显。去年时,我去岩馆爬高墙,差不多 3-4 条线后就需要躺下休息。现在可以爬满两个小时。最近开始恢复爬先锋(比顶绳更消耗体力,我已经有 10 年没爬过了),发现自己又可以比较轻松的完成 5.10c/d 左右的先锋线路了。去年野外去了多次英西,一次阳朔,一次六盘水。野外先锋还没怎么爬,明年应该可以逐步恢复。

另,痛风未再来过。但尿酸水平并未降低,也没有更高。

还有一个身体的小问题值得注意:有次在去阳朔的车上和同车的岩友聊天。我说我的指关节常年疼痛,是不是大部分攀岩者都是这样。他们的水平都比我高一大截,说并不是这样,这种现象只在部分超高水平的岩友中听过,并建议我保护好指关节,减少抱石中那些指力线路。

我回头和 gemini 讨论了一下,建议是差不多的。另外可以做一些反向的力量训练,我买了一根套在指头上外撑的橡皮筋每日练习。也正是这个原因,我现在没有跟着可可一起每天抱石,并在刻意减少了需要做 Crimps 的线路。目前恢复的还不错,至少日常不爬的时候关节不疼了。

可可还拉了一个同班的小女孩一起抱石,我意外的发现她爸爸的爱好是跑马拉松。我请教了他许多长跑的问题,他说下次带我跑一次 8km 再加到 10km 。据说他从高中开始长跑,一直停留在每次 5km 的量,直到有人带着跑才越过这个坎。虽然他真的很爱长跑,但说每次跑马拉松,跑到最后也是非常难受的,全靠意志力坚持下来。


虽然云豆对和我攀岩兴趣不大,却意外的愿意和我一起跑步。部分原因是他意识到自己体重有点超标了。目前是六年级的寒假,身高 1.74m ,体重最重时有 77kg 。我说你还是跟我跑步吧,我能减下来,你也可以。

寒假第一次跟我跑了 4 km 累得不行,后来我便随着他减到 3km 一次。毕竟是小孩,慢慢的就适应了。和他一起跑步,也帮我把速度提了起来。他嫌我跑得太慢(一开始我跑 4km 需要 35 分钟),父子俩跑了几次后便在半小时之内了。这跑步的兴趣也来得莫名其妙,最近一周就跑了 5 次。(体重还真减了一些,75kg)

今天跑完我告诫他,切忌一时热情,锻炼身体是个长期的过程,贵在坚持。每次跑到最后,总会有点难受的,需要一些意志力说服自己坚持下来。有个伴当然最好,可以相互督促。养成习惯后,日后住校,也能有自驱力。

ps. 教育子女真的是个长期的活。我琢磨着儿子愿意跟我跑步还有一部分原因是最近两个月每晚带着妹妹攀岩有点懈怠了他,或许是有点吃醋:过去我总是陪他比妹妹多一点的。而妹妹似乎不愿意跑步…… 结果,我也被动的增加了颇多的运动量,何尝不是件好事。

和 AI 聊游戏设计

作者 云风
2026年1月15日 16:05

最近一段时间和 AI 聊游戏设计比较多。我主要用的是 google 首页上的 AI 模式,也试过 twitter 上的 grok 。

去年也和朋友聊过很多,但对理清楚自己的想法帮助有限。因为和人聊容易陷入不断的细节解释当中,一些天马行空的想法更容易被质疑,一旦陷入辩论就不太容易跳出来。而且每个人的时间并不固定,很容易造成时间和精力的浪费。和 AI 聊要轻松得多,AI 毕竟见多识广,随便提到的点都能接得上话。不想聊下去尽可以中断,不用担心浪费时间。即使怀疑 AI 出现幻觉,也可以随时暂停下来通过搜索核实。

不过,我觉得和 AI 讨论也有另一方面的问题。那就是太容易顺着你的思路夸大其词。它更像是一个貌似领域知识渊博但只是想讨好你的同好,不断的放出一些华丽的辞藻却在逻辑上经不起推敲。分析起游戏来头头是道,直到谈到我真的玩过上百小时的游戏时,对游戏的细节错误百出。如果我没有这些游戏的真实体验,几乎不可能分辨真伪。一开始我还会想和真人讨论时一样指出它的错误,让它修正后重新发表观点。后来就渐渐放弃了从 AI 那里直接获得真知。把它当成一个比搜索引擎更方便的信息获取入口就够了。

但我依然偶尔被 AI 的总结惊艳到。比如说有一场主题为模拟类型游戏的话题,聊到最后 AI 总结:

成功的模拟游戏本质上是“熵增模拟器”。设计师的任务是制造一个不断趋向于“热寂”和“混乱”的世界(交通拥堵、热量堆积、资源耗尽、情感崩溃),而玩家的乐趣在于投入自己的脑力作为“负熵”,去建立一个脆弱但精密的秩序。

所以,在设计时,不要害怕设计“笨拙”的规则(如巡逻员、有体积的货车、会变质的食物)。这些“物理摩擦力”正是通往成就感的阶梯。如果没有这些混乱,游戏就只是一张算好了答案的报表。

要让玩家感到“战胜了混乱”而不是“被垃圾规则恶心”,需要遵循以下原则:确定性的混乱:混乱的原因必须是可回溯的。 提供“高级工具”来解决“基础问题”:游戏初期给玩家简单的工具去面对混乱,中后期给玩家更高级的逻辑工具。从“点”到“面”的连锁反应:混乱不应是孤立的错误,而应该是系统性的连锁反应。

我觉得在这种形而上的话题上,它讲得还是蛮有道理的。但我依然不觉得这些总结真的可以成为游戏设计的指导工具。

当然,让 AI 说什么依然极度依赖你对它说了什么。大多数时候 AI 会信誓旦旦的帮忙做一些具体设计(当我想设计卡牌驱动的游戏时),可我真的实体化这些卡片试玩后,完全玩不下去。我实在没信心再和它讨论这些卡片设计上的具体细节:真的不如我自己从零设计高效。但有时候,AI 也会对设计游戏这件事情有所畏缩,一旦强调我应该先用卡纸或在桌面模拟器中自己尝试做个原型试试。总之,让我感觉本质上它还是在跟着我的情绪走。完全没有独立思考的痕迹。

意识到这点,我现在更想把今天的 AI 当成一种更高级的知识搜索引擎。从这个角度看,这段时间 AI 的确给我推荐了不少不错的游戏。虽然我仔细玩过之后,这些游戏给我的体验并非完全符合 AI 的描述,我还是感谢 AI 让我挖掘出了它们。

尤其值得一提的是 AI 极力向我推荐的 dotAGE 这个游戏。我一口气玩了接近 160 小时。

其实它刚上 steam 的时候我就玩过 demo ,当时只是觉得还不错,但没有深入。前几天 AI 反复督促我要仔细玩一下,我才沉浸了进去。这是款回合制没有战斗的城市建设游戏。随机元素很少,几乎所有要面对的灾难,都在游戏规则下提前预示给玩家。玩家要做的就是提前规划每个回合的行动,通过精算赢得游戏。值得一提的是,游戏推荐的难度和最高难度给我的几乎是截然不同的游戏体验。在默认难度下,即使对游戏规则不甚了解,不需要精确规划,也能通过运气赢得游戏胜利。那种“Push Your Luck”机制驱动带来的胜利是一种相当刺激的游戏体验;但在最高难度下,游戏变得无法通过运气获胜,转而必须精密规划。而这种精算带来的又是另一种成就感。

在玩游戏之余,我又阅读了作者在游戏发售前夕于 reddit 上写的两篇文章,方才了解到这个游戏几乎是作者一人在攻读完游戏设计专业的博士学位后,独自花了 9 年时间制作出来的,颇为震撼。怪不得我在玩的时候感觉这个游戏要深度有深度要广度有广度,完全不像是能短期做出来的。只能说,设计出好的游戏真的很难。

Skynet 升级到 Lua 5.5.0

作者 云风
2025年12月23日 10:19

Lua 5.5.0 已经正式发布。所以,skynet 的 Lua 版本也随之升级。

skynet 维护了一份修改版的 Lua ,允许在多个虚拟机之间共享函数原型。这可以节省初始化 Lua 服务的时间,减少内存占用。

跨虚拟机共享函数原型最困难的部分是函数原型会引用常量字符串,而 Lua 在处理短字符串时,需要在虚拟机内部做 interning 。所以 skynet 的这个 patch 主要解决的是正确处理被 interning 的短字符串和从外部导入的函数原型中包含的字符串共存的问题。具体方法记录在这篇 blog 中

这个 patch 的副产品是允许在多个 Lua VM 间共享常量表。打了这个 patch 后,就可以使用 skynet.sharetable 这个库共享只读常量表了。

这次 Lua 5.5 的更新引入了 external strings 这个特性,已经大幅度提升了 Lua 加载字节码的速度。我比较倾向于在未来不再依赖额外的 patch 减少维护成本。所以建议新项目避免再使用共享常量表,减少对 patch 过的 Lua 版本的依赖。


Lua 5.5 基本上兼容 Lua 5.4 ,我认为绝大多数 skynet 项目都不需要特别改动。但在升级后,还是建议充分测试。注意:更新仓库后,需要用 make cleanall 清除 lua 的编译中间文件,强制 Lua 重新编译。直接 make clean 并不清理它们。

Lua 5.5 有几处更新我认为值得升级:

  1. 增加了 global 关键字。对减少拼写错误引起的 bug 很有帮助。skynet 自身代码暂时还没有使用,但后续会逐步添加。

  2. 分代 GC 的主流程改为步进式进行。过去版本如果采用分代模式,对于内存占用较大的服务,容易造成停顿。所以这类服务往往需要切换为步进模式。升级到 Lua 5.5 后,应该就不需要了。

  3. 新的不定长参数语法 ...args 可以用 table 形式访问不定长参数列表。以后可以简化一部分 skynet 中 Lua 代码的实现。

带可可学数学

作者 云风
2025年12月16日 09:50

可可三年级,前段老师说她数学成绩不好,需要在家加强一下。

这段时间我每天晚上给她讲一点点数学,都是课本上的内容,然后我再稍稍发挥一下。几次之后,我发现最大的问题是她觉得数学很无聊。

她似乎比较抗拒学新的知识,更喜欢用熟悉的方法。去年我发现她计算能力有问题,每天给她做加减法练习,总算不再用更早年我教她的 +1 法算加法了:即计算 7+8 的时候,算 8 次 +1 ,也就是数数。二年级学了乘法,乘法表也背了,但现在做应用题,本该用乘法的场合,她还是习惯连续算加法,一旦乘数太大就会出错。要用除法的时候就更混乱了,并不是用减法,而是靠猜测来试。大脑里完全没有建立乘除的概念。乘法表更像是独立的有背诵任务的诗词,还没古诗那么有趣。

我说:数学其实是这个世界上最有趣的东西。

她说:为什么呢?

我说:这个世界上有趣的东西很多,但数学是性价比最高的。一本数学书很便宜,但可以让你读很久。从中发现有那么多有趣的事实。原来解决不了的问题,知道方法突然就明白了。如果是自己找到的方法,就更让人兴奋了。

她说:我还是觉得数学没意思。

我说:慢慢来,不着急。首先不要排斥它。学数学其实不需要硬背那么多东西,我小时候最不喜欢背书了,所以才喜欢数学的。因为数学是最不需要背的,只要你从原理出发,一步步理解,最后什么问题都能解决,只是速度慢一点。多练习就快了。那些需要记住的知识,用的多就自动记住了,不需要专门背。


还好她不排斥我给她讲数学。前段还给我说,为什么只有你给我讲数学我才懂呢?

就这么,我每天(半个小时左右)给她讲一点点。我觉得其实也讲不了什么,聊胜于无。有一点我觉得还不错,她有数学作业不会做,会主动来问,不需要等我检查作业。一点拨就懂了,但第二天又会有新问题,依旧靠自己解决不了。不过,知道自己不懂算是个好的开始吧?

昨天晚上,她说今天学的直线、射线和线段。这个好简单,之前的都好难。

我说,我出道题目吧。在纸上画五个点,你看看一共可以连出多少条线段。

她说,我去拿张纸来。

画了半天,数错了。可可说,我脑子好乱啊。

我说,我来教你方法。我们先把你画的点标上数字编号,从 1 标到 5 。然后你每次连一条线段,就把两端的数字记在图案下方,顺着写整齐。最后,数下面的记号。

这次她算对了。但是,好麻烦啊,为什么要用这么麻烦的方法。我想偷懒就会搞错。

我说,其实,你不需要画图,只用写数字就可以了。但是不要随便连线。1 号点先和 2 号点连,再和 3 号点连…… 顺着数字从小到大,这样就不会搞错。不需要真的把连线图画在纸上,在脑子里相像就可以了,只需要在纸上写下 1 号点能和几个点连成线段就可以了。这里,你写个 4 。然后再看 2 号点。

她画了一点时间,终于发现了规律,列出了算式 4 + 3 + 2 + 1 = 10 。

我说看吧,其实这不是一道绘图题,用数学方法,它转化为一道计算题了。

可可好像有点兴趣了,说我现在可厉害了,我能算 10 个点的问题。我说你试试。可可的悟性没我相像的那么高,并没有直接写出 9 到 1 的等差数列,每个数字都想了一会,直到 4 以后才有点把握后面应该是 3 2 1 。但是,面对长达 10 个数字的加法算式,可可说,我知道方法了,这个算起来太麻烦了,我不算了。

我说,你不是学了乘法吗?其实,这个问题不一定要用加法做,我告诉你怎么用乘法解决这个问题,就不用算这么麻烦的算式了。你看,刚才你连线的时候,把 1 号和 2 号连起来之后,从 2 号做起点就不连 1 号了。因为 1 到 2 和 2 到 1 是同一条线段。两个点之间只可以连一次。但是,如果我们每两个点都连两次,那么这 10 个点,每个都可以连出去 9 条(射线)。

最后,总共画了 10 组,每组 9 条射线,一共是 10 * 9 = 90 条。因为我们每条线段都计算了 2 次,所以答案就是 90 / 2 = 45 条线段。

有了这个方法,100 个点的问题你也能算出来了吧。

可可问,家里有没有有趣一点的数学书,我要看。我翻出了前年带着云豆读过的《我的第一本数学书》。可可翻了一下说,怎么这么多字啊。我说,周末我带着你读。


早上起床的时候,可可问,今天我能不能把昨天那本书带到学校去?

希望她能发现一点点数学的乐趣。

最近玩的几款卡牌构筑类电子游戏

作者 云风
2025年12月15日 16:32

最近玩了几个卡牌构筑类的电子游戏,觉得颇为有趣,值得记录一下。

首先是 Decktamer(训牌师)。我玩了十几个小时,把初级难度通关了。

它的新设计是用卡牌构筑的形式重新做了一个宝可梦。和杀戮尖塔开创的战斗结束后抽卡,用战斗胜利的奖励钱买卡、洗卡、升级的模式不同。它的战斗卡是不需要洗的,战斗中死亡就直接消失;新卡片是在战斗中捕获对手获得。加强战斗卡的方式主要是用道具卡杂交战斗卡:从一张战斗卡上抽取需要的技能,加到另一张战斗卡上。

战斗过程更像是万智牌那种更传统的卡牌战斗模式:摆放战斗卡都场上,再由上场的卡片发动能力。这种传统战斗模式不同,发动战斗技能没有额外的资源消耗,而修改成每回合必然从卡片上所有技能中选择一个。这可以避免给同一张卡片合成太多能力造成的不平衡。更多能力往往只是增加了容错性,可以应付更多场景。

玩家卡牌被分成了两个牌堆:战斗卡堆和道具卡堆。这种双卡堆的模式最近的卡牌构筑游戏中比较常见,下面还会再提到。不过这里道具卡堆并不是抽牌堆,更像是一个道具背包,可以随时使用。

我在简单难度通关的感受是:只有最终 boss 有挑战。而这种挑战更像是一个谜题。所以第一次面对最终 boss 我没有一次通过。而是熟悉了它的技能,第二次刻意针对这些技能来升级牌组,这样才通关。整个游戏给我的感觉是,解密成分更重一些,也就是该如何养卡才能解决对手。所以游戏里(简单模式下)有无限次的 undo 。我相信选择更高难度后会有不同的感受。不过暂时没有玩下去。


第二个游戏是 Rogue Hex 。大约玩了 7 个小时,四个设计角色中,前三个获得了胜利。

这个游戏用卡牌构筑的形式重新实现了一个简化版的文明。这是我之前就特别想做,但是没想到合适方案的点子。所以,我在初玩时感觉相当有趣。能把卡牌构筑的乐趣融合到 4x 游戏中相当不错。不过,玩到游戏后期,数值还是有点崩,往往中盘就几乎碾压对手了,但为了胜利,依旧需要机械性的玩很多回合。

当然,我觉得它的核心机制设计的还是挺好的。这是个新游戏,平衡还有很大的改善空间。

在这个游戏中,基本资源分别是:劳动力用于抽牌打牌、食物用来发展发展城市、计划用来移动地图单位、信仰用来重置、金钱用来替换前面的基本资源。这些资源有仓储上限,回合结束的时候超过上限的部分会浪费掉。我认为“仓储限制”是让玩家有更多选择的核心设计点之一。

无仓储限制的成长涉及两个点数,科技用于增加正面 buf (类似杀戮尖塔中的神器)以及生产用于增强牌组。我认为设计两个的原因也是给玩家提供选择。大多数情况下,发展科技就会降低生产的增速,反之亦然。

像文明那样,玩家可以在版图上探索采集一次性资源转换为一次性消耗的卡片;扩展城市获得永久资源再用卡牌转换为永久建筑或用版图上的工人单位采集转换为卡片。这部分对文明原本的系统还原的挺不错的(至少在每局游戏的前半段很好)。

游戏循环基本上是用抽牌的手牌积累科技点、生产点和上述的基本资源。用这些点数兑换成发展。和文明一样,指挥地图单位探索和攻击对手也是发展重要的一环。把文明游戏机制中的各种操作翻译为打牌,最大的区别在于:原本玩家可以自由分配每个回合的行动,而在卡牌模式下,需要根据抽到的手牌做决定。这有两个方面的变化:其一、原始机制下提供给玩家的选择非常多,容易产生选择困难;手牌是有限的,玩家可以聚焦在有限行动选择中。其二、可选行动有了随机性,玩家需要根据牌组、抽牌堆、抽牌能力去管理随机性。这也是卡牌构筑类型游戏的核心玩法。

但嫁接卡牌构筑类型和 4x 类型系统有一个需要设计的地方: 4x 游戏中每个回合给玩家的选择都是有意义的,如果把太多东西做成卡牌供玩家选择,而每个回合选择受限,很可能极大的降低游戏的容错性,变得太看脸。所以,在这个游戏中,劳动力即用于支付打牌成本,又可以用来抽牌。这等于提供给玩家一个主动增加选择的能力,而不是像杀戮尖塔中那样,抽牌本身也是特定卡片的技能。

我发现很多类似游戏(下面会再次提到)都有这个设计:允许玩家主动花一个特定资源点,就可以抽牌。

但是,这类游戏成长点太多。不像杀戮尖塔那样只是提高和优化卡组,每局游戏后期很难不崩掉。要么前期开荒死掉,要么后期碾压变得选择没有太大意义。我觉得这个游戏还可以改进,我也很期待会使用怎样的解决方法。


最后一个游戏是 Dawnmaker ,也是最近我最喜欢的一个。我玩了 30 个小时,所有难度都通关了。

这个游戏回答了我很多之前没想到设计解法的问题。它对卡牌构筑类型做了更多的保留,没有卡牌战斗部分,更接近一个生存类型的基地建造游戏。玩这个游戏时让我想到了 retromine 和 Stellar Orphans (星际孤儿)。但很多设计处理的更好。

这个游戏就是打出卡牌在六边形棋盘上建造建筑让基地活下去。没有敌人,无需战斗,需要对抗的只是逐步增加的粮食需求。

单局游戏内只有五种资源:粮食、科技点、工程点、行动点、胜利点。赢得单局游戏可以获得金钱,金钱用来升级卡组,在后续的游戏局种获得优势,以对抗难度逐步上升的挑战。

游戏设定了两个卡堆:手牌堆和市场建筑堆。这两个卡堆分开抽牌。手牌在当前回合打出的工程点可用来在市场堆购买建筑,并在版图上建造。建筑放在版图上就有了持久能力,通常用于配合手牌更有效的获取资源。和 Rogue Hex 不同(它没有双卡堆),购买的建筑卡必须立刻摆在版图上,而不是置入抽牌/弃牌堆。从我游戏的感受看,这是个没有巧妙的改良设计。

星际孤儿也有市场设定,但受传统卡牌构筑规则的影响,还是设计成从市场购买,投入弃牌堆,抽到手牌使用的循环。控制前期卡和后期卡的方式也只是简单的通过市场掉率决定。但在 Dawnmaker 中,通过市场卡分级严格区分了前期卡和后期卡。如果城镇中心没升级到特定等级,对应的卡片也不会在市场上出现。刷市场只需要消耗科技点,这点和 Rogue Hex 用劳动力抽卡异曲同工。但双卡堆设计避免了直接抽行动牌。因为这类游戏中,行动牌和建筑牌本质上是有区别的。Rogue Hex 用一次性消耗卡表达建筑,我觉得是因为没能跳出传统卡牌构筑规则。

在版图上摆建筑是这个游戏的核心玩法。在非卡牌构筑游戏中非常常见,我也见过很多企图和卡牌构筑类型结合的游戏,只有这个我认为结合的最流畅。在星际孤儿中也有四个 slot 用于安装卡片获得永久能力,但只是点缀,而且安装位置并不改变游戏。

Dawnmaker 的建筑摆放相当重要。甚至比构筑行动卡堆更重要。其变化也非常多,留给玩家很大的选择空间。这种在六边形弃牌上填格子的玩法在桌游中很常见,技巧深度和乐趣很有保障。最近几年也有很多电子游戏基于这类玩法,但我觉得更像桌游电子化,而我玩 Dawnmaker 的感觉则更接近电子游戏的体验。

Dawnmaker 在 steam 上有 demo ,有兴趣的同学可以试试。但 demo 只能玩第一个角色,我在正式版本中尝试了另外两个角色,让我惊讶的是,其实三个角色共享同一套完整的卡池。仅仅只是初始卡组不同,但游戏风格差别非常巨大。而且,游戏中并不设常规卡和稀有卡,抽到的概率是一致的。也就是说无论你用那个初始卡组开局,都可能抽到所有卡片,概率是一致的。但玩游戏的感受却并没有那种:我抽到了特定卡以后,游戏就变容易了的感觉。在熟悉了游戏之后,几乎任何卡片搭配都可以玩出花来。

三个初始卡组(角色)标注的是 regular ,complex ,extreme 。我一开始以为是三档难度,通过调整数值减少容错性,需要逐级更熟悉游戏规则才能玩下去。

通关后的感受是:的确越后面的角色需要对游戏更熟悉。但并没有在数值上减少容错性,而是更复杂的角色代表的需要更复杂的卡片 combo 。三个角色玩的体验也很不一样。

虽然在玩第一个角色时,我能感受到不同的流派:用大块农田组合堆砌粮食产能、用主动激活的方式生产粮食、用科技转换粮食等等。胜利点的来源也可以来源于建筑、科技等。但直到我玩第二个角色才发现,靠版图主动技能的组合也可以另成一番景象。而第一个角色更多的思考如何在版图上摆放建筑的位置。第三个角色更是要求不断的拆建建筑,让版图的布局变成动态的。学会这些玩法后,回头在不同角色中也可以实现(因为共享一个大卡池)。

Dawnmaker 的数值调的非常好。每局游戏的生死总是在一线间。尤其是后面的角色更偏重于主动触发版图技能,相比调配行动手牌多了一层选择。有时候看似死局,仔细思考后居然是有解的。同时又需要一点点运气,概率管理原本就是卡牌构筑游戏的核心,在小丑牌中体现得淋漓尽致,Dawnmaker 也有类似的体验。


在游戏规则上,给我的一点启发:

如何把卡牌构筑结合到基地建设类别游戏上?加入版图元素,把建筑卡堆和行动卡堆分离是个不错的设计。

游戏资源种类不需要太多,不需要完全用卡牌表达。传统的卡牌构筑基本规则中,资源点都是靠当前回合的行动手牌生成,不留到下个回合。但建设类游戏的建造资源完全可以跨回合保留积累,但需要强调仓储上限。这样就可以提供给玩家足够的行动选择和组牌的多样性。

非战斗类游戏,可以用不断上升的需求来制造压力。而需求的上升速度可以和玩家的发展规模挂钩。这样玩家就必须在长期发展和短期生存困境上做出抉择。从自己组建的卡堆中抽卡是卡牌构筑类游戏的核心乐趣来源:提供给玩家更丰富的风险管理手段。

欧陆风云5的游玩笔记

作者 云风
2025年11月28日 18:53

最近一个月共玩了 270 小时的欧陆风云5 ,这两天打算停下来。最近在游戏后期打大战役时,交互已经卡得不行。我已经是 i9-14900K 的 CPU ,估计升级硬件已经无法解决这个问题,只能等版本更新优化了。

ps. 其实只要把游戏暂停下来立刻就不卡了。虽然我直到这个游戏需要的计算量非常大,但是卡交互操作肯定是实现的不对。因为这并不是因为渲染负荷造成的卡顿,可以让游戏时间流逝更慢一些,也不应该让鼠标点击后的界面弹出时间变长。

在暂置游戏前,我先把一些关于游戏设计上的理解先记录下来。也是对上一篇的补充

在最初几十小时的游戏时间里,我一直想确认游戏经济系统的基础逻辑。和很多类似策略游戏不同,欧陆风云5 在游戏一开始,展现给玩家的是一个发展过(或者说是设定出来)的经济系统版图。玩家更需要了解的是他选择扮演的国家在当下到底面临什么问题,该怎样解决。这不只是经济,也包括政治、文化和军事。而很多游戏则是设定好规则,让玩家从零开始建设,找到合适的发展路径。

大多数情况下,EU5 玩家一开始考虑的并不是从头发展,所以在游戏新手期也没有强烈的理解游戏底层设计细节的动机。不过游戏也有开荒玩法,在游戏中后期势必会在远方殖民、开拓新大陆;甚至游戏还设计了让玩家直接转换视角以新殖民地为核心来继续游戏。但即使的重新殖民,在四周鸟无人烟的地方开荒,和在已有部分发展的区域附近拓展也完全不同。

我十分好奇这样一个复杂的经济系统是怎样启动起来的,所以仔细做了一点归纳笔记。不一定全对,但很多信息在游戏内的说明和目前的官方 wiki 都不完整,只能自己探索。


游戏中的一切来源于“原产”,官方称为 ROG ,比较类似异星工厂里的矿石。上层的一切都是从原产直接或间接获得。版图上的任何一个最小单位的地块,只要上面有人口,就会不断生产出唯一品种的原材料进入这个世界。它和国家控制力、市场接入度都无关系。比原材料更高级的产品都是由原产直接或间接转换而来。

货币本身在世界中不以资源形式存在,货币本身也没有价值。货币的存在在于推动包括原产在内的原材料和产品等在世界中的流动。所以,世界中即使不存在经济活动、没有货币,亦或是货币急剧膨胀,这些因为国家破产而债务消失等让货币总值急剧变化的行为也不会直接影响这个世界中的物资变化。即没有很多游戏中直接用钱凭空兑换成物资的途径。

换句话说,如果整个世界缺铁,那么只能通过生产手段慢慢的产出,再多的钱也无法变出铁来。但分配更多的人力去生产、更高的科技水平可以获得铁产量的提升、使用更高效的配方、各种提升生产率的增益等等都可以加快铁的产出速度。

从一个世界的局部看(这是一般玩家的视野),获得原材料的方式有三种:

  1. 养活更多的劳工或奴隶开发对应原产。
  2. 在合适的地理位置上用劳动或奴隶生产。
  3. 从附近的市场进口。

第一种方式,玩家拥有对应原产地,然后在地皮上增加人口。但新增人口是农民,还需要从农民升级成劳工。国家 buf 中,默认只有原住民满意度对产量有轻微的增益。

第二种方式,玩家有更大的自主性。以铁为例,只要是湿地地形或者地皮邻接湖泊,就可以主动产铁。这种生产除同样需要劳工外,还有原料开销:把炭转换为铁。这种生产方式直接被市场接入率打折,即离市场越远的地方单位人口的生产效率越低,但同时有更多增加生产率的增益途径:最基本的就有当地劳工识字率和市民满意度。和虽然和第一种方式一样需要劳工,但游戏似乎会先满足原产需求的劳工,多出部分才进行建筑生产。所以在劳工不足时,若需进行建筑生产,需要主动减少原产等级。因为生产建筑可以由玩家主动关闭,但原产似乎不行。

第三种方式,通常需要在本地市场拥有一定的市场容量。在不考虑成本时,甚至可以亏本进货。对开荒来说,进口原料比进口成品的优势就在于占用更少的贸易容量。

为什么上面以铁举例,因为铁是开荒时最重要的资源。虽然木头和石头也很重要,但游戏把木材、粘土、沙、石头设定为一般物资,所有地块都有一个很低的默认产能,从市场角度看,根据市场规模,每个市场总有一定量的供给。但铁不属于这种物资。

建造建筑需要的基本材料是砖头,砖头可以通过基础建筑,从粘土或石头转换。

开发原产需要的基本材料是木头或工具。大多数基础建筑的生产配方里都需要工具。而工具在非城市生产建筑中,只有乡村市场可以把铁转换为工具。所以、如果开荒时的市场中缺铁,就只能通过进口。进口制造工具的铁比进口工具更能利用上贸易容量(铁和工具的单位贸易容量相同,但铁到工具以 4:5 转换)。

铁矿在版图上相对其它资源更少,所以一般开荒需更关注市场覆盖下有无湿地地形或有无湖泊,同时需要用充足的木材供应,可以把木材转换为炭再转换为铁。

一旦单一地块上的人口超过 5000 ,就可以升级为城镇,这种生产就有更多选择。以工具制造来说,城镇里就多出了用石头或铜转换为工具的配方。尤其是石制工具的途径,虽然效率很低,通常利润也很低,但贵在石头有保底产出。城镇的升级需要砖头和玻璃,玻璃可通过沙子转换,而沙子有保底产出或通过工具加木材转换。

开荒期间,解决了木头、砖头、玻璃、工具这四种基本货物(前三种是各种基础建筑建设需求,最后是大部分生产转换配方的必须)后,就要考虑提高产能的问题,这里的核心之一是纸。因为劳工识字率影响着生产率。纸是印书的原料、书是图书馆的维持品,而图书馆以及更多提高识字率相关的建筑都需要书。

造纸术需要纤维作物或皮革或布匹。纤维作物的基本生产方法是在对应农场通过牲畜木材工具制造;而牲畜则在耕种村落通过工具加粘土转换;皮革则可以在森林村落通过沙加焦油和野味转换,其中焦油在一般木材产区都可以通过木材转换得到。

另一个重要的资源是人口。它在游戏中和钱一样重要、甚至更重要。因为一切的生产行为都需要人。对开荒而言,升级到城市对效率影响最大,这里的硬性要求就是 5000 基础人口。除了主动殖民,就是本地土著转换、周边迁移(集中)以及自然生长率。这些都可以通过内阁行为略微加速,同样关键是修定居点(同时加移民吸引度和自然生长率)。定居点除了 250 农民外的维持成本是石头、木头、羊毛、野味。前两个一定有保底产出,后两个不是必须,缺少只会让效率打折,但供应充足可以发挥全部性能。定居点和乡村市场都占用农民,在最初阶段,我感觉乡村市场更重要一些,毕竟可以制造工具,还能提供宝贵的贸易容量。

不同阶层人口除了产生固定需求(吃掉本地市场的部分产出)外,更基本的需求是食物。EU5 中的食物系统设计,我觉得也是很巧妙的。

食物并不是货物的一种,而是和钱一样,表示为一个单一值。最主要的食物来源是生产带食物属性的货物(被称为食物原料)的副产品。属于食物原料的货物,被设定了不同的食物倍率,这就让有些食物原料产生食物的效率更高。不工作的劳工默认会以一个较低的产能生产食物,所以不必担心多出来的劳动力被浪费。另外,农民在森林村落里,虽然产品皮革并非食物原料,但生产行为本身被设定了食物产能。另外,农村相对城市有额外的食物产能的乘数增益。

食物的仓储按省份为单位计算,省份归属的若干地块共享一个食物仓库。在每个省份会优先填满仓亏,多出的部分卖给了所属市场,这里是不计市场接入度的。而一旦仓库有空间,就会从所属市场购买。战争是影响它的一个变数。因为军队也会从这个仓库中获取食物,围攻会阻止市场上的食物交易。

食物在本地市场上的交易行为会影响到本地食物价格,购买食物的开销和销售食物的收入是分开计算的,全部通过国库完成。市场间并不能单独交易食物,但通过对有食物原料属性的商品的交易,会产生附带的食物流通(但并不会产生额外的货币流动)。我觉得理论上会出现市场仓库中的食物储量为 0 ,但依然出口食物原料的情况,但实际玩的时候并没有发现,所以不清楚游戏怎样处理。但我猜测,食物流通是在单独层面计算的。既然超出市场食物容量的食物似乎就消失了,那么也可以接受万一食物储量为 0 却继续出口的情况,把储量设为 0 即可。


最后,写写我对税收的看法。

简单说,游戏里的经济活动产生了税基。税基中按王室影响力直接把钱进入国库,另外的钱按阶层影响力分到了各阶层。但玩家可以对阶层分得的钱征税,让这些钱进入国库。

看起来,在不造成阶层不满的前提下,税率越高,国库收入就越高。但实际我玩的感觉是,其实税基才是整个国家的收入,国库仅仅是玩家可以主动调配的部分。阶层保留更多的收入,也会投入到国家发展中去,只不过有时不是玩家想要的方向,甚至是负方向。例如当玩家想削弱某个阶层的影响力时,阶层把钱投入都修建扩大本阶层影响力的建筑上。但总的来说,如果国库钱够用,更低的税收更好。因为税基相同时,税收影响的是分配。低税收必定增加阶层满意度,带来的正面增益是额外的。正所谓藏富于民。

而影响税基最重要的是地区控制度。当然地区控制度不仅仅影响税基,还影响了更多建筑的效率。从这个意义上来说,地方分权比中央集权更有利于经济发展。分封属国,尤其是朝贡国,比大一统国家会获得更好的经济局面。

但权力分配在游戏中也相当重要,因为它直接影响调配价值观的能力。价值观在一盘游戏进程中必须配合时代发展而演变才能更好的发展经济。而集权以及王室影响力是权利分配能力的来源。

所以说,最终玩整个游戏的体验还是在和面,只是多出了一份历史感。有了真实历史这种后验知识,才更为有趣。

嵌入主线程消息循环的任务调度器

作者 云风
2025年11月22日 13:52

最近在网友协助下把 soluna port 到包括 wasm 在内的非 windows 平台。其间遇到很多难题,大多是多线程环境的问题。因为 soluna 的根基就是基于 ltask 的多线程调度器,如果用单线程实现它,整个项目的意义就几乎不存在,所以它是把项目维护下去必须解决的问题。

好在 lua 有优秀的 coroutine 支持,它可以把运行流程抽象成数据,而 Lua 本身并未限制数据的具体储存方式,所以完全可以存在于内存堆中,脱离于 C 栈存在,这为各种在 C 环境下的多线程难题开了后门。C 语言依赖栈运行代码逻辑,而栈绑定于线程,线程调度通常由操作系统完成,所以用常规方式无法让代码跨线程运行:即,无法通过常规手法让一段代码的流程前半段在一个线程运行,而用另一个线程运行后半段;但是,在 C 上建立一个 Lua 层,则很容易绕开这个限制,只用标准方法就可以自由控制程序运行流程。

上一次发现利用一些技巧就可以完成一些看似不可能却的确可行的调度方式是 多线程串行运行 Lua 虚拟机

简单复述一下当时的需求:

希望可以在单个 Lua 虚拟机内模拟多线程并发。当一个 Lua 的 coroutine 运行到 C 函数中时,若此刻 C 函数希望阻塞等待一个 IO 请求,常规的方法是 yield 回 Lua 虚拟机,让调度器持有一个 Lua coroutine 的状态,待完成 IO 请求后,再由调度器 resume 这个 coroutine 。这样做的难题是,运行到一半的 C 函数,上下文状态还在 C 所属线程的栈中,一旦 yield 回 Lua 虚拟机,必须放弃 C 栈上的状态,并在下次 resume 时可以重建。这通常难以实现,这也是为何 Lua 的 coroutine C api 难以理解又很难使用的原因。尤其使用第三方 C 库,几乎没可能适配。

另一个折中的方法是让 Lua 虚拟机在 C 函数中阻塞,硬等到 IO 操作完成。但在阻塞过程中,无法使用这个 Lua 虚拟机。若使用者期待 Lua 虚拟机中多个 coroutine 以多线程方式并行工作,恐怕会失望。即使其它 coroutine 的业务和 IO 完全无关,一个 IO 阻塞操作会让它完全无法并行工作。

变通的方式是(在编译时)打开 Lua 的线程锁。在调用 IO 阻塞前解开线程锁,只要 IO 操作本身不涉及对 Lua State 的操作,那么 Lua 解释器在调用 C 函数前的那一刻会解开线程锁,这样就可以允许阻塞操作过程中,Lua 虚拟机可以执行其它操作。

线程锁本身依赖系统线程库的调度器。不适合像 ltask 这样自己实现任务调度(即在有限个系统线程下调度远超系统线程数的任务)。但是,我们可以配合 ltask 实现类似的锁机制。这就是之前这个 patch 实现的东西:Lua 层调用可能阻塞的 C 函数前加锁通知 ltask 调度器,在 C 函数中,用户主动在阻塞操作前解锁。ltask 的调度器在 C 函数返回前就将虚拟机提前放回调度表。当阻塞操作完成后,重新加锁会等待调度器完成(如果有)正在运行的在同一个 Lua 虚拟机上的任务完成。这样,整个 Lua 虚拟机实质上还在串行运行其中的任务。而使用者看起来在一个 coroutine 尚未 yield 之前就开始运行另一个 coroutine ,直到其它 coroutine yield 后再继续未完的工作。同一个 Lua 虚拟机的多个 coroutine 是在多个操作系统线程上完成的,但却保持串行。

这个 patch 最终并未合并进 ltask ,因为我觉得它对使用者有更高的要求。但经此,我开了不少脑洞,明白在必要时牺牲一些复杂度就可以完成一些超乎寻常的任务。


这次我面临的是新的问题:sokol 并未设计成线程安全。api 不能并发。一开始我并不想使用复杂的解决方案,以为只要保证 sokol 不并发就够了。期间遇到的问题是 Windows API 死锁 ,也很容易绕过。

对于图形 API ,我只是简单的将图形 API 调用都塞在同一个 render 服务中。并在主线程的 sokol 回调函数中利用一个信号量和渲染过程同步。虽然 Direct3D ,Matal ,Vulkan 这些为多线程设计的底层图形 API 这么用都没有问题,但 OpenGL (在 Linux 上开启)却将状态放在当前线程上。一开始,我们通过额外调用 MakeCurrent 绕开限制,但在我们向 wasm 移植时却遇到障碍。

最终,我还是希望找到一个方法让所有图形 API 的调用都真正从主线程,也就是 sokol 提供的 callback 函数中发起。而不是用信号量同步,让它们在其它工作线程运行。

难题在于,主线程是通过事件消息循环驱动的,没有全部的控制权。不适合在其上实现任务调度器。一个任务调度器最好有所有时间片的控制权,它才好简单有效的分配时间片,没有任务时可以休眠而不是在事件循环没有新事件时强制休眠。我不想为这种特别的工作方式改造 ltask 的任务调度器,让主线程的事件回调函数伪装成一个功能不完整的特殊工作线程。我实际需要的是:把一个 Lua 虚拟机内的特定任务分配给主线程回调函数运行,在没有这种特定任务时,其它任务还是交给 ltask 做常规调度。

细想之下,解决方法和上一个需求有异曲同工之处:Lua 在启动这种特殊任务(必须在主线程回调函数内运行)前通知调度器。这时把虚拟机暂时移出调度表,而在主线程的回调函数中(通过信号量)发现有新任务到来,就接手处理特殊片段。处理完毕后,再把它归还给调度器。

通过这个方案,我们顺利把 soluna port 到 wasm 环境,同时简化了 Linux/OpenGL 实现。当我了解到 wasm 上有 pthread api 和原生 web worker api 两套多线程 api 后,我又信心满满的想用 worker api 来实现。但最终未能如愿。具体讨论在这个 issue 中 ,倒不是完全做不到,而是我觉得不应该牺牲太多复杂度。比如把 soluna 中所有的 IO 操作都转发到主线程中运行(这是 web worker 的限制所在,也是 wasm pthread 原本要解决的问题)。


昨天发现了上面解决方案实施中的一点纰漏:虽然给 ltask 打了个洞,可以在系统主线程夺过指定任务运行,但在交换控制权回调度器时,忽略了 ltask 的所有工作线程可能因为没有任务而全部休眠的可能性。仅仅把任务推回(线程安全的)任务队列是不够的。还需要重启调度器(如果处于休眠状态)。具体讨论见这个 issue

ps. 自从搬家后,我的 Linux 机器一直没有开机。昨天为了在 Linux 环境下测试,才重新装起来。bug 虽然重现,但视乎在我的机器上更为严重:一旦程序失去响应,整个系统都卡住了,甚至冷启动都没用。直接把机器弄死,而且五分钟内都开不了机(BIOS 进不去,屏幕无信号)。我怀疑是显卡驱动的 bug ,因为太久没升级系统,头一次升级还失败了,pacman 报告出现依赖问题拒绝更新。强删了几个 Electron (这个毒瘤)的几个历史版本后,系统升级才得以继续。最后更新了最新版的 Nvidia 包似乎就一切正常了。

欧陆风云 5 的经济系统

作者 云风
2025年11月5日 06:07

很早从“舅舅”那里拿到了《欧陆风云 5》的试玩版。因为开发期的缘故,更新版本后需要重玩,所以一开始只是陆陆续续玩了十几个小时。前段时间从阳朔攀岩回来,据说已经是发售前最后一版了,便投入精力好好玩了 50 小时,感觉非常好。

我没有玩过这个系列的前作,但有 800 小时《群星》的经验,还有维多利亚 2/3 以及十字军之王 2/3 的近百小时游戏时间,对 P 社的大战略游戏的套路还是比较了解的。这一作中有很多似曾相识的机制,但玩进去又颇为新鲜,未曾在其它游戏中体验过。

我特别喜欢 P 社这种在微观上使用简洁公式,宏观展现出深度的游戏设计。我试着对游戏的一小部分设计作一些分析,记录一下它的经济系统是如何构建的。

这里有一篇官方的开发日志:贸易与经济 其实说得很清楚。但没自己玩恐怕无法理解。

我在玩了几十小时后,也只模糊勾勒出经济系统的大轮廓。下面是我自己的理解,可能还存在不少错误。

EU5 的经济系统由人口/货币/商品构成,市场为其中介。

游戏世界由无数地区构成。地区在一起可以构成国家,也能构成市场。一个国家可以对应一个市场,也可以由多个市场构成,也可以和其它国家共享市场。

每个市场都会以一个地区为市场中心,这反应了这个地区的经济影响力,它不同于国家以首都为中心的政治版图。市场自身会产生影响力,而市场中心地区的所属国家则因其政治影响力而产生市场保护力,两相作用决定了市场向外辐射的范围。每个地区在每个时刻都会根据受周围不同市场的影响强弱,最终归属到一个唯一市场。

每个地区有单一的原产物资(商品)。原产在版图上不会改变,可以被人口开发,计入所属市场供给。

地区上可以修建生产建筑,生产建筑通过人口把若干种商品转换为一种商品。转换效率受地区的市场接入率影响。市场中心地区的接入率为 100% ,远离市场的地区接入率下降,在边远地区甚至为 0 (这降低了同样人口的工作效率)。注:原产不受市场接入率的影响。

市场每种商品有供给和需求。每种商品有一个额定价格。需求和供给的多寡决定了目标价格和额定价格的差距,目标价格在 10% 到 500% 间变化。实际价格每个月会向目标价格变动,变动速度受物价波动率影响。

注:商品在每个市场有库存。库存和需求是独立的,库存多少不影响价格(供需状态影响它)。当需求超过供给时会消耗库存,反之则增加库存。库存有上限,一旦达到上限就无法进口。当贸易的交易对手需求大于供给时可以对其出口而无法进口,供给大于需求时可以从其进口,而无法出口;而自己只要有库存就可以出口(即使需求大于供给)。

商品的价格减去原料成本(原产物资没有原料成本)为其利润,利润以货币形式归属生产人口,并产生税基。

负利润的生产建筑会逐渐减员,削减产量,除非政府提供补贴。缺少原料的生产建筑会减产。

人口会对商品产生需求。食物类型的商品是最基本的需求。 不能满足食物需求的人口会饿死,不能满足其它需求的人口会产生不满。

对于单一市场:

  1. 人口在市场上产生商品需求。
  2. 生产建筑通过人口生产出商品供给市场。
  3. 市场上的供给和需求影响了商品价格。
  4. 人口通过生产获取货币形式的利润,并产生税基。

税基中的一部分货币留给人口,一部分以税收形式收归国库。

货币用来投资新增生产建筑,或对其升级。建筑升级需要商品,这部分商品以需求形式出现在市场。人口会用自己的钱自动投资建筑,玩家可以动用国库升级建筑。

多个市场间以贸易形式交换商品:

每个市场有一个贸易容量,贸易容量由市场中地区中的建筑获得。贸易容量用来向其它市场进出口商品。

市场所属国家拥有贸易竞争力,贸易竞争力决定了向市场交易的优先级。高优先级贸易竞争力的市场先消耗贸易容量达成交易。

商品在不同市场中的价差构成了贸易利润,其中需要扣除贸易成本(通常由两个市场中心间的距离决定)。贸易利润的一部分(由王权力度决定)进入国库,其它部分变为税基。

在国家主动进行贸易外,人口也有单独的贸易容量,自动在市场间贸易平衡供需。


我觉得颇为有趣的部分是这个经济系统中货币和商品的关系。

游戏中的生态其实是用商品构成的:人口提供了商品的基本需求,同时人口也用来生产它们。在生产过程中,转换关系又产生了对原料的需求。为了提高生产力,需要建造和升级建筑,这些建筑本身又是由商品转换而来。所以这么看来,是这些商品构成了这个世界,从这个角度完全不涉及货币。

但货币是什么呢?货币是商品扭转的中介。因为原产是固定在世界的各个角落的,必须通过市场和贸易通达各处。

建立一个超大的单一市场可以避免贸易,它们都直接计入市场中心。但远离市场中心生产出来的商品(非原产)受市场接入度的影响而削弱生产效率,所以这个世界只能本分割成若干市场。不同市场由于供需关系不同而造成了物价波动。价差形成贸易的利润,让商品流动。这很好的体现了货币的本质:商品流动的中介。

政府通过税收和其主导的国家贸易行为获得货币,同时也可以通过铸币获取额外收益(并制造通货膨胀)。再用这些货币去投资引导世界的发展:建造和升级生产建筑光有钱是不行的,必须市场上有足够的商品;没有钱也是不行的,得负担得起市场上对应商品的价格。

注:铸币可以看作是用黄金或白银直接变成货币的生产行为。它的基础产能似乎和人口无关,也不需要分配人口去工作,也没有特定的生产地点 。只需要在国家结余菜单中勾选即可。选择更大的产能会导致货币贬值,本质上是单位黄金/白银兑换的价值变少,但游戏中是以通胀变现的。即,生产出来的货币并没有变少,但国家的通胀增加了。铸币行为表现为在市场中自动消耗一定量的贵金属。如果整个市场中都没有贵金属,也就无法铸币。


11 月 7 日补充:

玩游戏的时候,一直没弄懂游戏中市场间的交易是怎么撮合的。官方 wiki 现在没有详细解释,游戏内的说明也不够完整。昨天和玩友讨论后,又仔细在游戏中研究了一下,发现其实游戏中说明还是很丰富的,只是细节散布在不同界面中。我是这样理解的:

由于同一市场中可能有不同国家的贸易建筑,所以导致了在一个市场中,不同国家分别拥有一定的贸易影响力和贸易容量。注:在友好的外国,可以修建贸易站,这种建筑占用当地的人口,为当地产生税基,但可以为本国带来贸易影响力和贸易容量。这些数值可以在市场界面上看到。

贸易影响力在出口紧俏商品时起作用,即商品的当月节余不能满足当月出口需求时,高影响力的贸易订单先被满足。在订单详情界面中可以看到相关商品的供给和需求细节。我不确定进口商品过多时是否也按影响力排序,游戏内的说明中说贸易影响力不影响进口,但我认为受商品仓库上限的影响,如果当月进口数量太多会超过库存上限,同样需要对进口订单排序。只不过在库存过高时,通常交易是负利润,很难出现这种情况罢了。

即使在一个外国市场内没有贸易容量,也可以发起贸易,只要该市场在你拥有贸易容量市场的贸易范围内。计算贸易距离似乎看的是市场边界的最短距离(而不是市场中心的距离),在贸易订单界面可以看到商品的出口地点和进口地点,很多时候并不是贸易中心地。贸易范围看起来是一个国家相关值。所以,在周边国家建贸易站可以扩展可以交易的市场。但如果在交易对手的市场内没有贸易容量,那么贸易影响力一定为零,对紧俏商品的采购很可能失败。

在两个市场间对某种商品进行贸易,会产生订单,同一商品只会有一张。订单上会指定商品数量,数量越多,消耗的贸易容量越大。每种商品有一个基础消耗比例,根据贸易距离乘一个系数。所以距离越远的贸易,消耗的贸易容量越大。在下订单时,并不能立刻确定是否能成交。如果商品库存不够,需要根据贸易影响力排序。低影响力国家发起的订单可能不能完全满足,甚至为 0 。这种情况下,订单所属的贸易容量就被浪费掉了。

每个订单可以看作是一支商队,贸易容量就是商队的规模。长途贸易需要支付额外的费用,可以理解为商队的工资以及海峡过路费等。这个费用以贸易容量为比例支付,而不是实际成就额。所以,即使订单数量为 0 ,这些额外费用也是要支付的。所以订单有亏本的风险。所以大容量订单(未能成交的)风险更大。如果是采购战略物资,采用多个小订单分散去不同市场采购获得足够成交数量更安全。

订单可以被人工固定,每月固定执行,但有更大的可能无法成交。大部分情况下,采用系统自动生成的订单。系统看起来会按利润多寡分配贸易容量。但似乎也不会产生过大的订单,具体规则没有写。总之,系统自动生成的订单浪费贸易容量的机率更小。

人口自身也有一定的贸易容量(独立于国家可以控制的贸易容量)自发进行贸易。对应的订单在游戏界面中无法查到。但在商品界面详情中可以看到一个合计数值,显示该商品由人口自发贸易行为产生的输入或输出总量。人口如何产生这些贸易的规则不清楚,从文字说明看,和人口自身的需求相关。我怀疑也和贸易利润相关。

关于桌游设计大赛的介绍

作者 云风
2025年10月12日 17:57

这一篇是前几个月研究桌游规则期间的另一篇小结。因为最近两个多月都在制作 Deep Future 的数字版,没空整理笔记。现在闲下来,汇总整理这么一篇记录。

今年夏天,我迷上了 DIY 类型的桌游。这类桌游最显设计灵感。商业桌游固然被打磨的更好,但设计/制作周期也更长。通常,规则也更复杂,游戏时间更长。我经常买到喜欢的游戏找不到人开。阅读和理解游戏规则也是颇花精力的事情。所以,我近年更倾向于有单人模式的游戏。这样至少学会了规则就能开始玩。但为单人游玩的商业桌游并不算多(不太好卖),而我对多年前玩过的几款 PnP (打印出来即可玩)类单人桌游印象颇为深刻:比如 Delve 和同期的 Utopia Engine (2010)

在 7 月初我逛 bgg 时,一款叫做 Under Falling Skies 的游戏吸引了我。这是一个只需要 9 张自制卡片加几个骰子就可以玩的单人游戏,规则书很短几分钟就理解了游戏机制,但直觉告诉我在这套规则下会有很丰富的变化。我当即用打印机自制了卡片(普通 A4 纸加 9 个卡套)试玩,果然其乐无穷。尤其是高难度模式颇有挑战。进一步探索,我发现这个游戏还有一个商业版本,添加了更长的战役。当即在淘宝上下了单(有中文版本)。

从这个游戏开始,我了解到了 9 卡微型 PnP 游戏设计大赛。从 2008 年开始,在 bgg (boardgamegeek) 上每年都会举办 PnP 游戏设计大赛。这类游戏不限于单人模式,但显然单人可玩的游戏比例更高。毕竟比赛结果是由玩家票选出来,而单人游戏的试玩成本更低,会有更多玩家尝试。据我观察,历年比赛中,单人游戏可占一半。近几年甚至分拆出来单人游戏和双人游戏,多人游戏不同的设计比赛。

根据使用道具的限制条件,比赛又被细分。从 2016 年开始,开始有专门的 9 卡设计大赛。这是众多比赛中比较热门的一个。我想这是因为 9 张卡片刚好可以排版在一张 A4 纸上,只需要双面打印然后切开就完成了 DIY 制作。加上每个桌游玩家都有的少许米宝和骰子,阅读完说明书就可以游戏了。

如果嫌自己 DIY 麻烦或做出来的卡片不好看,在淘宝上有商家专门收集历年比赛中的优秀作品印出来卖,价格也非常实惠。比赛作品中特别优秀的,也会再完善和充实规则,制作大型的商业版本。例如前面介绍的坠空之下就是一例。我觉得,阅读规则书本身也很有意思。不要只看获奖作品,因为评奖只是少量活跃玩家的票选结果,每个玩家口味不同,你会有自己的喜好。而且我作为研究目的,更爱发现不同创作者的有趣灵感。

如果对这个比赛有兴趣,可以以关键词 2025 9-Card Nanogame Print and Play Design Contest 搜索今年的比赛历程。

我花了几周时间玩了大量的 9 卡桌游。喜欢的非常多,无法一一推荐。除了前面提到的坠空之下,让我推荐的话,我会选择 2023 年的 Survival Park (Dinosaurs game) 。倒不是我自己特别偏爱这款,而是我介绍给云豆后,他也很喜欢。

其实,除了 9 卡游戏,还有 18 卡,54 卡等。卡片数量限制提高后,设计者可以设计出更丰富的玩法。例如著名的 Sprawlopolis (无限都市) 一开始就是一款 18 卡桌游,但后来已经出了相当多的扩展。反过来,也有用更少卡片来设计游戏。比如 1 卡设计大赛就限制设计者只使用一张卡片(的正反面)。


在 bgg 上,你可以在 Design Contests 论坛找到每年举办的各种类型设计大赛。除了传统的 各种 PnP 类型外,我很喜欢的还有传统扑克设计比赛。用 2025 Traditional Deck Game Design Contest 就可以搜索到今年的。这个比赛开始的比较晚,2022 年才开始的第一届。

这个比赛限制设计者围绕传统扑克牌来设计游戏玩法。如果你想玩这些游戏,成本比 PnP 游戏更低:你甚至不需要 DIY 卡片,家中找出 1/2 副扑克就可以玩了。我小时候(1980 年代)特别着迷扑克的各种玩法,在书店买到过一本讲解单人扑克玩法的书,把上面介绍的游戏玩了个遍。所以在多年之后见到 Windows 后,对纸牌游戏的玩法相当亲切。

可以说扑克发展了几百年,单人玩法就没太脱离过“接龙”;多人玩法的核心规则也只有吃墩(桥牌)、爬梯(斗地主)、扑克(Poker 一词在英文中特指德州扑克)等少量原型。

但自从有了这种比赛,设计者的灵感相互碰撞,近几年就涌现出大量依托扑克做道具的新玩法。往往是头一年有人想出一个有趣的点子,后一年就被更多设计者发扬光大。电脑上 2024 年颇为好评的小丑牌也是依托德州扑克的核心玩法,不知道是否受过这个系列比赛作品的启发,但小丑牌的确又启发了这两年的诸多作品:例如我玩过的 River Rats 就特别有小丑牌的味道,同时兼备桌游的趣味。

单人谜题类中,我特别喜欢 2024 年的 Cardbury :它颇有挑战,完成游戏的成功率不太高,但单局游戏时间较短,输了后很容易产生再来一盘的冲动。

多人游戏,我向身边朋友推广比较顺利的有 Chowdah 。它结合了拉米和麻将的玩法。我只需要向朋友介绍这是一款使用扑克牌玩的麻将,就能勾起很多不玩桌游的人的兴趣。而玩起来真的有打麻将的感觉,具备一定的策略深度。

我自己曾经想过怎样用传统扑克来模仿一些经典的卡片类桌游,但设计出来总是不尽人意。比如说多年前我很喜欢的 Condottiere 佣兵队长,如果你没玩过它的话,一定也听过或玩过猎魔人 3 中的 Gwent 昆特牌。昆特牌几乎就沿用了佣兵队长的核心规则。而 2024 年的 Commitment 相当成功的还原了佣兵队长的游戏体验。

还有 MOLE 则很好的发展了 Battle Line 。

如果想体验用扑克牌玩出 RPG 的感觉,可以试试 2022 年的Kni54ts :有探索地图、打怪升级捡装备等元素;多人对抗的则有 Pack kingdoms

有趣的游戏规则还有很多,我自己就记了上千行规则笔记。这里就不再一一列出给出评价,有兴趣的同学可以自己探索。

SetWindowText 引起的死锁

作者 云风
2025年8月9日 10:51

最近发现我在写的小游戏在启动时有很小的概率黑屏。我使用的是 ltask 多线程框架,在黑屏时感觉 ltask 并没有停止工作,似乎只是管理窗口的部分(线程/服务)卡死了。

窗口管理使用的是 sokol_app 做的多平台封装,这只是一个很浅的封装层,但已经够用。我觉得美中不足的是,sokol_app 的 frame 回调函数是放在 WinProc 中,由 Windows 的消息循环被动调度,而不是放在外层的主动 GetMessage 循环中。

即,在 Windows 程序中,线程通常会在最外面写一个这样的 while 循环:

for (;;) {
    while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) {
        if (msg.message == WM_QUIT)
            return;
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // 在这里,我们可以做一些额外的工作,比如渲染游戏画面、处理游戏逻辑。
}

但我们也可以选择在窗口的 WinProc 中,通过响应 WM_TIMER 等消息的方式来做这些工作:

LRESULT CALLBACK wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_TIMER :
            // 在这里,可以定时做一些工作。
        break;
    }
    return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}

// 外面的消息处理循环则可以使用 GetMessage 而不是 PeekMessage

while(GetMessage(&msg, NULL, 0, 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

无可厚非,后一种方法显得更正规一点:让 Windows 自身调度所有任务,系统如果做的正确,和系统的窗口系统本身契合的更好一点。这个模式是 Window 的历史设计造成的。把窗口系统的工作流程放在用户线程内,用户的程序其它部分配合它,换取交互的流畅度。

但是,一旦采用多线程设计,就变得有点不同了。窗口只是多线程任务的一部分,需要一个更高阶的框架来调度任务,例如 ltask 干的那些。通过在 WinProc 中处理对应消息,在没有消息进入的时候,线程会堵塞在 GetMessage 函数中。这对 ltask 这样的调度器来说非常的不友好。通常一个任务调度器需要的行为是:每个任务要么完成,要么让出,而不是阻塞。Windows 的 GetMessage/DispatchMessage 也是这样的循环,只不过是单线程的。

ltask 处理这样的模块,也不是完全没有办法。这得益于 ltask 的任务都运行在 lua 虚拟机上,和 C 层有一定的隔离。对于 C 代码来说,stack 是绑定在线程上的,所以无法在一个线程运行一半,然后在另一个线程继续工作(因为 stack 不同);但 Lua 的 stack 在 heap 上,迁移完全没有问题。

我曾经做过类似的尝试 ,但最终又从 ltask 主干上撤销了这个特性。倒不是实现的不对,而是配合它使用的 C 代码如果重入问题解决不好,隐藏的 bug 很难发现。这需要 C 部分最好在设计时就考虑过并行/重入问题。sokol 显然不是这样设计的。


为了让 sokol 可以在 ltask 下工作,我做了不少工作。sokol_gfx 的图形 api 部分倒是简单,我只需要保证在同一个服务中调用就可以了;比较麻烦的是 sokol_app 中处理窗口的部分。直接让 frame 回调函数运行在 ltask 的一个服务中非常困难。原因上面已述:这个回调函数结束后线程会挂起在 Windows 的消息处理循环中,而没有将控制权归还 ltask 。虽然可以通过 ltask 那个实验特性解决这个问题,但 sokol 并没有为多线程设计,很可能隐藏多线程 bug ,一旦出现难以调试。

我试过几个方案后,最终采用了最简单粗暴的方法:利用锁来同步任务。也就是在 frame callback 开始时抛出一个消息,并阻塞在一个锁上。这个消息会开启另一个 ltask 掌握的线程中对应的 render 服务;而在 render 服务渲染完当前帧,解开这个锁,frame callback 就会顺利返回。

在绝大多数场景中,这个方案工作的很好。但我最近偶尔发现在启动程序时,会有很小的概率,锁并没有解开。

一开始我并不为意,觉得或许是一些同步代码没有写好,因为有更想做的特性要开发,这种偶发死锁 bug 出现概率很低,且只出现在启动阶段,想着有空稍微复查一下启动代码就能解决。

这两天感觉的确“有空”了,花了一晚上,终于定位了问题。

问题出在游戏启动阶段改变窗口的标题上。固然,可以在窗口创建时就把标题设置好。但标题需要根据多语言环境设置不同的文本,处理多语言文本的这块逻辑不算简单,我不想放在启动的最初阶段(创建窗口之前),所以窗口创建时使用了一段默认文本,之后才修改它。

sokol_app 的 api 只是间接调用了 SetWindowTextW() 。显然不是 sokol_app 的封装问题。我查阅了 msdn ,发现 SetWindowTextW 只是给 WinProc 发送了一个 WM_SETTEXT 消息。也就是说,等价于调用 SendMessageW()

如果在 WinProc 所在线程中调用它当然没有问题,只是引起了 WinProc 重入:调用方在 frame callback 内,而 frame callback 处于 WinProc 的 WM_TIMER 的消息处理环节。这时调用 SetWindowTextW 等于递归再运行一次 WinProc 本身,但消息变成了 WM_SETTEXT ,新的调用返回后窗口的标题栏就被改变了。

可是,我现在在另外一个线程调用 SetWindowTextW 行为有所不同。这时 WM_SETTEXT 被投递到窗口消息处理线程,它需要排队等待 WinProc 再次被处理,也就是外层循环的下一次 DispatchMessage 调用。但是,这个时候当下的 DispatchMessage 还阻塞在 frame callback 的锁上面无法返回。这就是死锁产生的原因:

  1. DispatchMessage 调用 WinProc 处理 WM_TIMER 消息,它调用了 sokol 的 frame callback 。我的程序在 frame callback 中发出消息唤醒真正的处理流程,并等待在锁上。
  2. 真正的处理流程运行在另外线程,它调用了 SetWindowTextW ,其通过 SendMessageW 投递 WM_SETTEXT 到窗口线程的消息队列,等待返回。
  3. 窗口线程需要等当前的 WM_TIMER 处理完毕才 DispatchMessage 才可以结束,后续的 GetMessage 才可以拿到 WM_SETTEXT 消息处理它。

了解了死锁的原因后,最直接的解决方案是在窗口线程调用 SetWindowTextW 。因为这样会直接运行设置文本的逻辑,消息不需要进入消息队列,当然就没有锁的问题。但这个方案不适合现在的 ltask 框架。目前窗口线程不在 ltask 的管辖之下,也就无法在 lua 服务中调用 SetWindowTextW ,也无法直接通过 ltask 内部的消息把这个任务传递过去。

比如容易想到的是:“改变窗口标题”这个行为并不需要等待结果。那么是不是可以改用 PostWindowTextW 发送 WM_SETTEXT 就可以不阻塞调用方了呢?

答案是不行,原因在这里有解释 。因为这条消息发送了一个字符串,这里存在这个字符串生命期管理的问题,为了减少使用错误,Windows 禁止用 PostMessage 发送这样有生命期管理问题的系统消息。只有 SendMessage 可以在结果返回后正确释放消息文本所占用的内存。

所以,我们可以用独立线程通过 SendMessage 投递这个消息,并等待其返回后做完后续(生命期管理)工作。在 C 中创建新线程非常麻烦,但在 ltask 中却非常容易。只需要用一个独立的服务调用 SetWindowTextW 就够了。frame 的处理流程所在的服务/线程向它投递一个 ltask 消息,通知这个独立服务改变窗口标题,就不会阻塞 frame 流程。

极度未来( Deep Future )给我的启发

作者 云风
2025年7月13日 12:50

最近我在 bgg 上闲逛时了解到了“Make-as-You-Play”这个游戏子类型,感觉非常有趣。它是一种用纸笔 DIY (或叫 PnP Print and Play)的游戏,但又和传统 DIY 游戏不同,并不是一开始把游戏做好然后再玩,而是边做边玩。对于前者,大多数优秀的 PnP 都有专业发行商发行,如果想玩可以买一套精美的制成品;但 Make as You Play 不同,做的过程是无法取代的,做游戏就是玩的一部分。

极度未来 Deep Future 是“做即是玩”类型的代表作。它太经典了,以至于有非常多的玩家变体、换皮重制。我玩的官方 1.6 版规则。btw ,作者在 bgg 上很活跃,我在官方论坛八年前的规则讨论贴上问了个规则细节:战斗阶段是否可以不损耗人口“假打”而只是为了获得额外加成效果。作者立刻就回复了,并表示会在未来的 1.7 规则书上澄清这一点。

读规则书的确需要一点时间,但理解了游戏设计精神后,规则其实都很自然,所以游戏进程会很流畅。不过依然有许多细节分散在规则书各处,只有在玩过之后才会注意到。我(单人)玩了两个整天,大约玩了接近 100 局,酣畅淋漓。整个游戏的过程有如一部太空歌剧般深深的刻印在我的脑海里,出生就灭亡的文明、离胜利只有一步之遥的遗憾、兴起衰落、各种死法颇有 RogueLike 游戏的精神。难怪有玩家会经年玩一场战役,为只属于自己战役的科技和文明设计精美的卡片。

在玩错了很多规则细节后,我的第一场战役膨胀到了初始卡组的两倍,而我也似乎还无法顺利胜利哪怕一局。所以我决定重开一盒游戏。新的战役只用了 5 盘就让银河推进到了第二纪元(胜利一次),并在地图上留下了永久印记,并制作了第一张文明卡片。这些会深刻的影响同场战役的后续游戏进程。

我感觉这就是这类游戏的亮点:每场游戏都是独特的。玩的时间越长,当前游戏宇宙的特点就有越来越深刻的理解:宇宙中有什么特别的星球、科技、地图的每个区域有不同的宜居星球密度,哪里的战斗强度会更大一些…… 虽然我只玩了单人模式,但游戏支持最多三人。多人游戏可以协作也可以对抗。你可以邀请朋友偶尔光临你的宇宙玩上两盘,不同的玩家会为同一个宇宙留下不同的遗产。和很多遗产类游戏不同,这个游戏只要玩几乎一定会留下点什么,留不下遗产的游戏局是及其罕见的。也就是说,只要玩下去哪怕一小盘都会将游戏无法逆转的改变。


下面先去掉细节,概述一下游戏规则:

游戏风格类似太空版文明,以一张六边形作为战场。这是边长为 4 的蜂巢地图(类似扩大一圈的卡坦岛),除去无法放置人口方块的中心黑洞,一共是 36 个六边形区格。玩家在以一个母星系及三人口开局,执行若干轮次在棋盘上行动。可用行动非常类似 4X 游戏:生产、探索、繁殖、发展、进攻、殖民。

每个玩家有 4 个进度条:文化 C、力量 M 、稳定 S 、外星 X。除去文化条外,其余三个条从中间开始,一旦任意一条落到底就会失败;而任意一条推进到顶将可能赢得游戏。文化条是从最底部开始,它推进到顶(达成文化胜利)需要更多步数,但没有文化失败。

另外,控制 12 个区域可获得疆域胜利,繁殖 25 个人口可获得人口胜利。失去所有星球也会导致失败。在多人模式中,先失败的玩家可以选择在下个回合直接在当前游戏局重新开始和未失败的玩家继续游戏(但初始条件有可能比全新局稍弱)。

游戏以纯卡牌驱动,每张卡片既是行动卡,又是系统事件卡,同时卡片还是随机性的来源。抽取卡片用于产生随机性的点数分布随着游戏发展是变化的,每场战役都会向不同的方向发展,这比一般的骰子游戏的稳定随机分布会多一些独有的乐趣。

玩家每轮游戏可作最多两个独立且不同的行动:

  • POWER 抽两张卡
  • ADVANCE 发展一项科技
  • GROW 繁殖两个人口
  • EXPAND 向临接空格移动任意数量人口,但至少在出发地留一个
  • BATTLE 和临接空格交战,或(当没有任何邻接敌人时)推进任意进度条
  • SETTLE 在有人口的区域殖民一个星球
  • EVOKE 打出一张文明卡
  • PLAN 制造一张新的指定行动卡

在执行这些行动的同时,如果玩家拥有更多的科技,就可能有更多的行动附加效果。这些科技带来的效果几乎是推进胜利进度条的全部方法,否则只有和平状态的 BATTLE 行动才能推进一格进度条。

在行动阶段之后,系统会根据玩家帝国中科技卡的数量产生不同数量的负面事件卡。科技卡越多,面临的挑战越大。但可以用手牌支付科技的维护费来阻止科技带来的额外负面事件,或用手牌兑换成商品寄存在母星和科技卡上供未来消除负面事件使用。

负面事件卡可能降低玩家的胜利进度条,最终导致游戏失败;也可能在地图增加新的野生星球及野怪。后者可能最终导致玩家失去已殖民的星球。但足够丰富的手牌以及前面用手牌制造的商品和更多的殖民星球可以用来取消这些负面事件。

每张星球卡和科技卡上都有三个空的科技栏位,在生成卡片时至少会添加一条随机科技,而另两条科技会随着游戏进程逐步写上去。

游戏达成胜利的必要条件是玩家把母星的三条科技开发完,并拥有至少三张完成的科技卡(三条科技全开发完毕),然后再满足上面提到的 6 种胜利方式条件之一:四个胜利进度条 C T S X 至少一条推进到顶,或拥有 12 区域,亦或拥有 25 人口。

胜利的玩家将给当局游戏的母星所在格命名,还有可能创造 wonder ,这会影响后面游戏的开局设定。同时还会根据这局游戏的胜利模式以及取得的科技情况创造出一张新的文明卡供后续游戏使用。

游戏以 36 张空白卡片开始。一共有 6 种需要打出卡片的行动,(EVOKE 和 PLAN 不需要行动卡),每种行动在6 张空白卡上画上角标 1-6 及行动花色。太阳表示 POWER ,月亮表示 SETTLE ,爱心表示 GROW ,骷髅表示 ADVANCE ,手掌表示 BATTLE ,鞋子表示 EXPAND 。这些花色表示卡片在手牌上的行动功能,也可以用来表示负面事件卡所触发的负面事件类别(规则书上有一张事件查阅表)。

数字主要用来生成随机数:比如在生成科技时可以抽一张卡片决定生成每个类别科技中的 6 种科技中的哪一个(规则书上有一张科技查阅表),生成随机地点时则抽两张组成一个 1-36 的随机数。


我初玩的时候搞错了一些规则细节,或是对一些规则有疑惑,反复查阅规则书才确定。

  • 开局的 12 个初始设定星球是从 36 张初始卡片中随机抽取的卡片随机生成的,而不是额外制作 12 张卡片。

  • 如果是多人游戏,需要保证每个玩家的母星上的初始科技数量相同。以最多科技的母星为准,其余玩家自己补齐科技数量。无论是星球卡还是科技卡,三个科技的花色(即科技类别)一定是随机生成的。这个随机性通过抽一张卡片看角标的花色决定。通常具体科技还需要再抽一张卡,通过角标数字随机选择该类别下的特定科技。

  • 每局游戏的 Setup 阶段,如果多个野生星球生成在同一格,野怪上限堆满 5 个即可,不需要外溢。但在游戏过程中由负面事件刷出来的新星球带来的野怪,放满格子 5 个上限后,额外的都需要执行外溢操作:即再抽一张卡,根据 1-6 的数字决定放在该格邻接的 6 格中的哪一格,从顶上面邻格逆时针数。放到版图外面的可以弃掉,如果新放置的格也慢了,需要以新的那格为基准重复这个操作,直到放完规定数量。放在中心黑洞的野怪暂时放在那里,直到所有负面事件执行外,下一个玩家开始前再弃掉。

  • 开始 START 阶段,玩家是补齐 5 张手牌,如果超过 5 张则不能抽牌但也不需要丢到 5 张。超过 10 张手牌则需要丢弃多余的牌。是随机丢牌,不可自选。

  • 殖民星球的 START 科技也可以在开始阶段触发且不必丢掉殖民星球。但在行动阶段如果要使用殖民星球的科技,则是一次性使用,即触发殖民星球上的科技就需要弃掉该星球。

  • 在 START 阶段触发的 Explorarion 科技可以移动一个 cube 。但它并不是 EXPAND 行为,所以不会触发 EXPAND 相关科技(比如 FTL),也无法获得 Wonder 。和 EXPAND 不同,它可以移动区域中唯一的一个 cube ,但是失去控制的区域中如果有殖民星球,需要从桌面弃掉。

  • 玩家不必执行完两个行动、甚至一个行动都不执行也可以。不做满两个行动在行动规划中非常普遍。两个行动必须不相同。

  • PLAN 行动会立刻结束行动阶段,即使它是第一个行动。所以不能利用 PLAN 制造出来的卡牌在同一回合再行动。

  • 行动的科技增益是可选发动的。同名的科技也可以叠加。母星和桌面的科技卡上提供的科技增益是无损的,但殖民星球和手上的完整科技卡提供的科技是一次性的,用完就需要弃掉。

  • 完成了三项科技的科技卡被称作完整科技卡,才可以在当前游戏中当手牌使用。不完整科技卡是不能当作手牌提供科技增益的。

  • SETTLE 行动必须满足全部条件才可以发动。这些条件包括,你必须控制想殖民的区域(至少有一个人口在那个格子);手上需要有这个格子对应的星球卡或该星球作为野生星球卡摆在桌面。手上没有对应格的星球卡时,想殖民必须没有任何其它星球卡才可以。这种情况下,手上有空白卡片必须用来创造一张新的星球卡用于殖民,只有没有空白卡时,才创造一张全新的星球卡。多人游戏时,创造新的星球卡的同时必须展示所有手牌以证明自己没有违反规则。如果殖民的星球卡是从手牌打出,记得在打出星球卡后立刻抽一张牌。新抽的牌如果是完整科技卡也可以立刻使用。如果星球卡是新创造的,或是版图上的,则不抽卡。

  • SETTLE 版图上的野生星球的会获得一个免费的 POWER 行动和一个免费的 ADVANCE 行动。所谓免费指不需要打出行动手牌,也不占用该回合的行动次数。这视为攻打野生星球的收益,该收益非常有价值,但它是可选的,你也可以选择不执行

  • SETTLE 的 Society 科技增益可以让玩家无视规则限制殖民一个星球。即不再受“手牌中没有其它可殖民星球”这条限制,所以玩家不必因此展示手牌。使用 Society 科技额外殖民的星球总是可以选择使用手上的空白卡或创造一张新卡。这个科技不可堆叠,每个行动永远只能且必须殖民一个星球。

  • SETTLE 的 Goverment 科技增益可以叠加,叠加时可以向一科技星球(星球卡创建时至少有一科技)添加两个科技,此时玩家先添加两个随机花色,然后可以圈出其中一个选择指定科技,而不需要随机选择。

  • GROW 的 Biology 科技增益必须向不同的格子加人口,叠加时也需要每个人口都放在不同格。如果所控区域太少,可能浪费掉这些增益。

  • 如果因为人口上限而一个人口也无法增加,GROW 行动无法发动。所以不能打出 GROW 卡不增加人口只为了获得相关科技增益。

  • 未完成的科技卡在手牌中没有额外功能。它只会在 ADVANCE 行动中被翻出并添加科技直到完成。如果 ADVANCE 时没有翻出空白卡或未完成的科技卡,则创造一张新科技卡。新创建的科技卡会立刻随机生成三个随机花色。玩家可以选择其中一个花色再随机出具体科技。在向未完成的科技卡上添加新科技时,如果卡上没有圈,玩家可以选择圈出一个花色自主选择科技,而不必随机。一张卡上如果圈过,则不可以再自主选择。

  • ADVANCE 的 Chemistry 科技增益可以重选一次随机抽卡,可以针对花色选择也可以针对数字选择。但一个 Chemistry 只能重选一次,这个科技可以叠加。

  • ADVANCE 的 Physics 科技增益只针对科技卡,不能针对星球卡。所以,无论 Physics 叠加与否,都最多向科技卡添加两条科技(因为科技卡一定会至少先生成一条)。当 Physics 叠加两次时(三次叠加没有意义),科技卡上的三条科技都可以由玩家自主选择(每一科技卡原本就可以有一条自由选择权,叠加 Physics 增加了一次选择权)。注意,花色一定是随机生成的。这个增益增加的是玩家对科技的选择权。

  • 只有在所有邻接格都没有敌人(野怪和其他玩家)时,才可以发动 BATTLE 行动的推进任意胜利条的功能。战斗默认是移除自己的人口,再移除敌人相同数量的人口。但可以选择移除自己 0 人口来仅仅发动对应增益。所以 BATTLE 行动永远都是可选的。

  • BATTLE 的 Military 科技增益新增的战场可以重叠,即可以从同一己方格攻打不同敌人格,也可以从多个己方格攻打同一敌人格。和 Defence 科技增益同时生效时,可以一并结算。

  • EXPAND 行动必须移动到空格或己方控制格,但目的地不可以超过 5 人口上限。永远不会在同一格中出现多个颜色的人口。移动必须在出发地保留至少一个人口。当永远 FTL 科技增益时,可以移动多格,途经的格不必是空格,也可以是中心黑洞。

  • EXPAND 行动移动到有 Wonder (过去游戏留下来的遗产)的格子,且该格为空时,可以通过弃掉对应花色的手牌发动 Wonder 能力,其威力为弃牌的角标数字。Wonder 只能通过 EXPAND 触发,不会因为开局母星坐在 Wonder 格触发。

  • BATTLE 的 spaceship 科技增益需要选择不同的目的地,多个叠加也需要保证每个目的地都不相同。

  • PLAN 行动制造新卡时,只有花色是自选的,数字还是随机的。PLAN 会结束所有行动。

  • 行动阶段后的 Payment 阶段可以用来消除之后 Challenge 阶段的负面事件数量。方法是打出和母星及科技卡上科技增益的花色。针对母星以及每张科技卡分别打出一张。如果卡片上有多个科技增益花色,任选其中一个即可。科技卡上未填上的增益对应的花色则不算。每抵消一张就可以减少一张事件卡,但事件卡最后至少会加一张。每次抵消一次事件,都可以所在卡片(母星或科技卡)上添加一个 upkeep 方块。每张卡上的方块上限为 3 ,不用掉就不再增加。但到达上限后,玩家依旧可以用手牌抵消事件,只不过不再增加方块。

  • 挑战阶段,一张张事件卡翻开。玩家可以用对应花色的手牌取消事件,也可以使用桌面方块取消,只需要方块所在卡片上有同样花色。还可以使用殖民星球取消,需要该星球上有对应花色的科技(不是星球卡的角标花色)。但使用殖民星球需要弃掉该星球卡。不可使用母星抵消事件卡。

  • 事件生效时,如果需要向版图添加野怪。这通常是增加随机方块事件,和增加野外星球事件(带有 5 方块)。增加的方块如果在目标格溢出,需要按规则随机加在四周。

  • 如果增加的方块所在格有玩家的方块,需要先一对一消除,即每个增加的野怪先抵消掉一个玩家方块。如果玩家因此失去一个区域,该区域对应的桌面星球也需要扔掉,同时扔掉牌上面的方块。如果母星因此移除,玩家可以把任意殖民星球作为新的母星。移除的母星会变成新的野外星球。如果玩家因此失去所有星球就会失败。在多人游戏中,失败的玩家所有人口都会弃掉,同时在哪些有人口的格放上一个野怪。

  • 游戏胜利条件在行动阶段达成时就立刻胜利,而不需要执行后续的挑战行动。在单人游戏中,除了满足常规的胜利条件外,还需要根据版图上的 Wonder 数量拥有对应数量的殖民星球(但最多 4 个)。这些殖民星球需要在不同的区格,且不在母星系。玩家胜利后应给当前母星所在格标注上名字,这个格子会在后续游戏中刷多一个野怪。玩家可以创建一张文明卡,文明卡的增益效果和胜利条件以及所拥有的科技相关,不是完全自由选择。

  • 不是每局胜利都会创造 Wonder 。需要玩家拥有至少 5 个同花色科技,才能以此花色创造 Wonder 。每个 Wonder 还需要和胜利模式组合。Wonder 以胜利玩家的母星位置标注在版图上,胜利模式和科技花色的组合以及 Wonder 地点不能在版图中重复。


这个游戏给我的启发很大。它有很多卡牌游戏和电子游戏的影子,但又非常独特。

不断制作卡牌的过程非常有趣,有十足的创造感。读规则书时我觉得我可能不会在玩的过程中给那些星球科技文明起名字,反正不影响游戏过程,留空也无所谓。但实际玩的时候,我的确会给三个半随机组合起来的完整科技卡起一个贴切的名称。因为创造一张完整的科技卡并不容易,我在玩的过程中就不断脑补这是一项怎样的科技,到可以起名的时候已经水到渠成了。

更别说胜利后创建文明卡。毕竟游戏的胜利来得颇为艰难。在失败多次后,脑海中已经呈现出一部太空歌剧,胜利的文明真的是踏着前人的遗产(那些创建出来的独有卡片)上成功。用心绘制一张文明卡真的是乐趣之一。我在 bgg 上看到有玩家精心绘制的带彩色头像的文明卡,心有戚戚。

游戏的平衡设计的非常好,有点难,但找到策略后系统也不是不可战胜的。关键是胜利策略会随着不断进行的游戏而动态变化:卡牌角标会因新卡的出现而改变概率分布,新的科技卡数量增加足以影响游戏策略,卡组里的星球科技会进化,星球在版图上的密度及分布也会变化…… 开局第一代策略和多个纪元的迭代后的策略可能完全不同,这让同一个战役(多局游戏的延展)的重玩价值很高。

用卡牌驱动随机性是一个亮点:以开始每种行动都是 6 张,均匀分布。但会因为星球卡打在桌面(从卡堆移除)而变化;更会因为创造新卡而变化。尤其是玩家可以通过 PLAN 主动创建特定花色卡片,这个创造过程也不是纯随机的,可以人为引导。负面事件的分布也会因此而收到影响。

用科技数量驱动负面事件数量是一个巧妙的设计。玩家获得胜利至少需要保有 6 个科技,即使在游戏后期纪元,也至少需要创造一个新科技,这会让游戏一定处于不断演变中。强力的桌面卡虽然一定程度的降低了游戏难度,但科技越多,每个回合潜在的负面事件也越多。以 3 科技开局的母星未必比单科技开局更容易,只是游戏策略不同而已。

每局游戏的科技必须创造出来(而不是打出过去游戏创造的科技牌)保证了游戏演变,也一定程度的平衡了游戏。即使过去的游戏创造出一张特别强力的科技,也不可以直接打在本局游戏的桌面。而只能做一次性消耗品使用。

一开始,负面事件的惩罚远高于单回合能获得的收益。在不太会玩的时候,往往三五回合就突然死亡了。看起来是脸黑导致的,但游戏建议玩家记录每局游戏的过程,一是形成一张波澜壮阔的银河历史,二是当玩家看到自己总是死于同一事件时有所反思,调整后续的游戏策略。

而战役的开局几乎都是白卡和低科技星球,一定程度的保护了新手玩家,平缓了游戏的学习曲线。边玩边做的模式让战役开局 setup 时间也不会太长,玩家也不会轻易放弃正常战役。

单局失败是很容易接受的,这是因为:单局时间很短,我单刷时最快 3 分钟一局,长局也很少超过 10 分钟。每局 setup 非常快。而游戏演化机制导致了玩家几乎不可能 undo 最近玩的一局,因为卡组已经永久的改变了。不光是新卡(因为只增加新卡的话,把新制造的卡片扔掉就可以 undeo ),还会在已有的卡牌上添加新的条目。

虽然我只玩了单人模式(并用新战役带朋友开了几局多人模式),但可以相像一个战役其实可以邀请其他玩家中途加入玩多人模式。多人模式采用协作还是对抗都可以,也可以混杂。协作和对抗会有不同的乐趣,同时都会进化战役本身。这在遗产类桌游中非常少见:大多数遗产类游戏都有一个预设的剧本和终局条件,大多推荐固定队伍来玩。但这个游戏没有终局胜利,只有不断创造的历史和不断演化的环境,玩家需要调整自己的策略玩下一局。

育儿的一些日常

作者 云风
2025年6月15日 18:52

以下从我最近两个月发的推文中整理。

可可

  1. 晚上可可读着书在床上睡着了。我没叫醒她,给她盖了被子就睡了。早上醒来时她跟我说,完蛋了,昨天晚上我没有洗澡。我说没关系,别被妈妈发现就好了。然后她蹑手蹑脚的偷偷起来换了身衣服,然后又躺回来睡觉。

  2. 可可的同学喊她联机 minecraft ,我在隔壁看书,听她们在微信上聊了半天硬是鸡对鸭讲。我过去跟她说,你把电脑屏幕拍个照片给她看不就好了。结果对面喊了句:你怎么玩国际版啊,我玩的是网易中国版,然后安慰可可说,你自己玩也是可以的。我想:到底谁是正版受害者?

  3. 可可最近在读的一本小说突然就找不到了,她怎么都想不起来在哪里。我花了许多时间引导她回忆,终于想起是周三的课外班落在隔壁班的教室里了。如果缺乏引导,她的记忆是绝对不可能打开的。周五家长会,我特地提前了 5 分钟到学校,和隔壁班主任解释了一番,在教室仔细搜寻,果然找到了。可可很开心。

  4. 可可最近读书挺认真的,问了好多问题。前几天问了好几个成语的意思,又比问了什么时候用——(破折号),还讨论了为什么小说里要写那么多她觉得并不精彩的情节。

  5. 可可二年级,我最近发现她数学是真的不行 :( 今天检查作业错了一道题,引导了半天才发现她对 100 以上的数字概念都没建立起来。比如知道 10 个 100 是 1000 ,但她觉得 20 个 100 是一万,而且讲了一个小时才纠正过来。果然,人类天生的感知就是对数的么?本福特定理诚不我欺。

  6. 我觉得文字阅读能力对人的一生非常重要,而现在的小孩娃很难自发练习了 :( 试过很多方法培养兴趣,还是很难。最近一个月试着强制每天半小时文字阅读。两个娃都还听话,虽然觉得是个负担,但也认了这个任务。但经过一段事件,感觉阅读能力真的有提高(从阅读速度判断)。

  7. 可可看了几部关于老鼠的小说后,已经开始跟同学说地球上最聪明的动物是老鼠,地球就是老鼠造的计算机了。

  8. 可可迷上和我一起玩 rimworld 。假期跟妈妈出去旅行,回到家第一件事就是让我打开电脑继续抓一只豚鼠当宠物。知道游戏可以通过存档回退时间后,她让我试了一下在婚礼上把除新娘之外的人全部杀掉。等她长大,我一定推荐她看一遍杀死比尔。

  9. 可可说看到短视频中说 switch 的卡带是苦的。我说你要不要试试,她挑了一张舔了一下说好苦啊。云豆说我也试试。过了一会,可可又换了一张再舔了一下,这下她相信每张 switch 卡带都非常苦了。

  10. 可可说,我们玩个游戏,我问你问题,你必须马上回答。我说好。可可问:你最喜欢哥哥还是我?看我没说话,她说,算了,这个问题不好,下一个问题……

  11. 应朋友邀请去扬州玩。在扬泰机场跟可可讲李白的诗,我说古时候送别朋友远行,可能就一辈子不会再见了。可可问,不能打电话吗?

云豆

  1. 云豆说,什么时候买 switch 2 啊。我说你又不喜欢马车,买它作甚。但还是下了单。第一天试了 switch 秘密展和马车,玩得很开心。周末他的一帮同学闻讯都来了我们家。但是摸了一下 switch 2 以后,又围在 pc 上玩《小飞船大冒险》去了。

  2. 云豆家长会上,班主任展示了一张同学自制的贺卡。粉色的封面上画着一颗爱心,写着“我喜欢你”。打开后,内面用透明胶贴满了一整面秘密麻麻的蚊子。看来广州的夏天蚊虫真多,难为孩子能攒这么多。

  3. 云豆很兴奋的告诉我,他在科学课上学到埃菲尔铁塔是古斯塔夫造的。他前段玩了 33 号远征队,古斯塔夫是他最喜欢的角色。我告诉他我对埃菲尔铁塔那一带的路很熟,因为我玩了鬼武者。

  4. 给云豆买了本《猫和少年魔笛手》,我自己先读了一遍,非常喜欢。不过我怀疑他可能不太看得懂。云豆最近主动看书了,《哈利波特》已经快把《凤凰社》读完了。前几年我给他读过前四本,这次是他自己主动从第一本开始读的。

  5. 云豆问我,100 以内哪个数的因数最多,它有多少个因数?我想到一个问题:取一个足够大的整 n ,比 n 小的整数中因数最多的数大约有多少个因数?云豆说他找到 100 以内因数最多的是 96 ,有 12 个因数。我说 72 也是 12 个,我问,你能不能证明没有更多的了?1000 以内最多因数的是 900 ,有 27 个(其实 840 的有 32 个因数更多)。我给他讲了应该怎么找到这个数,以及应该怎样快速计算因数的个数。他发现分解质因数有实际的用途,还是挺开心的。

  6. 云豆学校最近查视力,一边 5.0 一边 4.4 ;去年都是 5.0 。前同事介绍了个医生,我们就去看眼科了。配了 OK 镜,前两天佩戴颇费事,第三天开始就很顺手了。

  7. 买了一本《在数学的雨伞下》。我先读完觉得内容不错,然后在睡前给云豆读了三次共三个小时左右。出乎意料,接受度还不错。不过每次不能太长时间,小孩得慢慢来,时间长一点他就犯困。

  8. 云豆看了 switch 2 预告的直面会后非常开心,因为樱井政博又回来做卡比了。

  9. 和云豆把双影奇景通关了,然后在隐藏关死了几百次才打到第三小关。云豆强迫症犯了,一定要我陪他练习直到把隐藏关通关。

  10. 云豆同学来家里玩,我说你们打游戏水平都不错,不如一起玩双影奇境。玩了两关后,同学说没意思,我们还是玩蛋仔派对吧。

  11. 晚上给云豆讲了一晚上勾股定理,用的总统证法。娃还没开窍,累死他也累死我了。最后他终于自己想通了等量加等量还是等量;我一开始以为是公理所以没办法教,这个必须自己想明白。“三角形的内角和是 180 度” 这个可以有疑惑的定理却很快接受了,只因为老师在课堂上讲过。去年花了 8 个周末给云豆讲质数,质因数,公约数等等。虽然花了比我预想得多的时间,但我确定他最后是懂了。今年课堂上开始教了,他说很轻松。我看课堂速度比我去年教的快多了,如果靠老师教,估计要学个一知半解。

  12. 云豆拿了语文测验卷子回来,错了好多。我给他讲卷子发现他阅读能力真的是很差。三国演义的半白话自然是完全不懂的;而一篇白话的百草园,也是一半没看懂。很多书面语的词完全不明白意思。

  13. 云豆的好朋友过生日,我帮他选了个礼物 RG28XX 。

  14. 云豆一大早起来背 100 以内的质数表,课本上还有口诀,说是今天数学老师要抽查。我说不需要这么背的。你心里顺着数数,把个位是 1379 的挑出来,去掉乘法口诀里出现的数字比如 49 ,再检查一下是不是 3 的倍数。最后记住 9 字头的只有 97 就好了。他试了一次就开心的上学去了。

电梯的交互和调度

作者 云风
2025年6月9日 14:02

今天在网上和人闲扯,说到电梯的交互设计或许是有问题的。一般在楼宇的电梯区,会设置上下两个按钮,让乘客表达自己是要上行还是下行。如果在电梯区显示电梯当前所在楼层的话,就会有人理解为:上是指让电梯轿厢向上运行,下是指让其向下。一旦这样理解,就会输入错误的指令。

几乎每个有过在高层办公室上班经历的程序员都参与过电梯调度算法的讨论。看来,在饭点挤电梯是程序员们的共同记忆。(另一个永恒话题是怎样提高厕所的使用效率)我也不例外,20 多年里,我曾经反反复复和人讨论过这个问题。现在再也不用挤电梯了,似乎可以把过去考虑过的方案记录一下。

先说说现实存在过的方案:

大多数方案都是为了提高电梯的运营效率。要么为了节能,要么为了更快的满足乘客需求,要么为了提高高峰时的吞吐量。

大部分高层建筑都把多部电梯按楼层分区。有些电梯只服务低层,有些服务高层。如果楼层更多时可能还会分出更多区间。对于超高层建筑,也有把顶楼超高区单独分割出来,需要转电梯的。

这个设计显然是因为对于高层建筑,电梯的需求按楼层分布是金字塔型的。乘客永远都需要从地面进入,越往上,乘客越少,而路程越长。乘客的目的地却是接近均匀分布的,如果让乘客随机进入任意轿厢,最终电梯会在更多楼层停留开门,将乘客预先分组就显得很有必要。

另外,为了解决繁忙时段的吞吐量问题,电梯也可能按单双层分组。这样可以把一部分运力转嫁到楼梯上,减少每部电梯的停留时间。类似的方案还有针对乘客类型设置专用电梯,比如让饭点运输食物的乘客走专用梯,而减少乘客使用电梯的需求量;让领导们使用专门电梯,提高他们的幸福指数以获得重要工作上的效能增益,等等。

也有一些办公楼会让乘客预先输入自己的目的地,而不是简单的选择上行还是下行。这样,系统理论上可以统筹安排。我也使用过这样的系统,效果嘛,一言难尽。只能说理想很丰满,现实很骨感。电梯公司想乘着软件系统升级多赚点钱无可厚非,但复杂系统就是这样:很难把它实现得正确。


回到文章开头的话题,我的观点是,与其做一个交互更复杂的系统妄想提高效率,还不如进一步简化它。

其实,电梯的外部控制按钮或许并不需要上下两个?只要一个召唤按钮就够了。

首先,这样的交互设计是没有歧义的:我需要使用电梯,就召唤它过来。

其次,用上下行来对乘客预分类过于粗糙,实际中对效率的提升非常有限。

如果建筑只有一部电梯,看起来对乘客分类的意义最少:反正乘客都必须乘坐这唯一一部电梯去目的地的,即使电梯目前运行方向相反,提前进轿厢的区别也仅仅是在里面等待还是在外面等待。

有同学说,不对啊,假设电梯目前从 1 楼向上运行到 10 楼顶楼,5 楼的人想下去,按了召唤按钮,电梯就可能在上行过程中做无谓的停留。如果电梯按钮分开上下,需求就明确了,电梯只会上去后下行时才会停下来。

我的观点是,其实电梯在上行时停下来,乘客就可以进去了。这样电梯之后下来时就不用再次在 5 楼停下来。无论是电梯运行时间,还是乘客抵达 1 楼的时间,差别都微乎其微。

而且就我的实际经历:在饭点想乘电梯下楼的话,往往是只要电梯开门了就进去,哪管它上行还是下行。你不进去等下就进不去了。稍低楼层的人更多的是反向坐电梯,不然就可能要等到餐厅快打烊了才吃得上饭:因为你不反向乘电梯的话,电梯下行的第一站永远是最高层,比你楼层高的乘客会优先使用。电梯的设计运力难以满足高峰期的需求。

在这种使用场景下,略低楼层的乘客往往上下两个按钮都会按下,在电梯上行时就先进入轿厢,而当电梯折返下行路过同一楼层时,电梯再次开门,外面却已经没有乘客了。效率反而降低了。如果电梯只设一个召唤按钮,这个问题就可以回避掉。

ps. 真要在高峰期保证公平的话,电梯需要设置成:单趟只停一个楼层,然后循环这个目的地。例如,一部高层电梯可以依次循环停 30, 29, 28, .... ,每一趟只在目的地停一次。这个运行模式可以在电梯不满载时自动取消,或是按高峰时间段固定开启。可惜我工作过的办公楼还没有见过电梯系统能设置成这种公平模式的。

这种单按钮召唤的设计,只要算法合理也并不比上下两按钮的低效。上下两按钮只是预先把乘客分开上行组和下行组,避免只有一组乘客时电梯反向停留(例如在上行阶段为下行乘客开门)。单按钮系统可以在载荷超过阈值时拒绝响应外部召唤请求,在完全已进入轿厢的乘客都送达目的地后,再根据外部召唤情况跑下一趟。这样就可以做到:延迟处理外部请求队列,以延长乘客外部等待时间为代价,增加单趟满足的乘客数量,从而提高整体的运行效率。

那么,如果电梯系统有多部电梯时怎样处理呢?

我觉得也可以不用上下行按钮预分类乘客。而是将电梯本身分为上行开门和下行开门。和高低层分类一样,乘客应该自行去上行区和下行区召唤电梯。鉴于除了地面的乘客趋向于上行外,其实绝大多数楼中乘客使用电梯都是下行的。多部电梯的系统只要保留一部梯为上行开门就够了。而且,即使你需要下行,其实也可以进入这部梯,它或许并不慢。因为一旦反向抵达有乘客召唤的最上一层,之后它下到一楼是直达的(下行不开门)。

一个简单的 A star 寻路算法实现

作者 云风
2025年5月14日 12:05

我需要一个接口简单的寻路模块,所以今天写了一个 。其实之前也写过很多版本,在我上传代码时就发现我自己的 github 账号下早有同名仓库。不过,之前的版本的接口设计不太满意,直接删掉了,用这次的新版本复用老的仓库名字。

我希望达到的目标是,C 接口简单易用,且和地图本身的数据结构无关,只提供寻路功能。这样容易拓展到不同应用场景。

数据结构简单,内存开销固定,在算法执行过程中不额外分配内存。这可以方便的在多线程环境运行。

我不需要处理特别复杂和规模巨大的地图,那种场景应该额外做一些预处理。但在起点和终点的路线结果不长时(即使在大规模地图上),应该有较好的性能。

原始的 A star 算法实现最为简单,在大多数情况下有不错的表现,所以我选择了它。我知道算法可以有很多改进方法,但我觉得代码简单最为重要。

通常 A star 算法依赖一个优先队列,但我没有选择使用诸如平衡二叉树等复杂结构来实现它,而使用了最简单的单向链表。因为这样可以轻松的把全部数据全部塞在一块平坦内存中。

基础数据结构是一个用数组实现的闭散列 hash 表,使用者来决定使用多大的数组,通常使用预期路径长度的平方大小会比较合适。为了减少每次寻路的初始化成本,使用了一个 version 值表示每个 slot 的初始状态,每次调用寻路,都会把 version 递增( O(1) 操作),这样就可以让整个 hash 表的所有 slot 复位。

寻路过程中每个尝试的节点都会加入 hash 表中,在 hash 表使用率超过一半就会中止算法,防止性能恶化。但接口在这种情况下依然会返回已经找到的离目标最近的中途点。

在不复杂的大规模地图上,通常可以通过多次调用找到完整路径。但依然建议针对大地图做更高层次的预处理。在 Youtube 上有一个 Rimworld 作者讲解 Rimworld 中区域分割系统的视频值得一看,搜索 "RimWorld Technology - Region System" 可以找到。

A star 工作中的待展开节点集是用单向链表的形式串起了 hash 表中的 slot ,而没有使用额外的优先队列结构。虽然单向链表的插入操作是 O(n) 的,但我猜想在大部分场景中,这个 n 并不算大。尤其是估价函数理想工作状态下(朝着目标直线移动),新插入的节点都是在链表一端附近的。这个猜想需要足够多的测试数据验证。

为了调试算法工作中的内部状态,模块提供了一个函数可以输出整个 hash 表的当前状态图(仅限于每个 slot 的 gscore ,即离起点的路程)。合理使用这张图,可以把算法的内部状态可视化表现。test 中使用 ascii 字符展示,但用灰度图输出图像效果会更好。


代码刚写好,尚未充分测试。但我觉得接口设计还算通用,应该会有人愿意使用。期待有更多人使用而让代码的质量提升。

卡牌构筑类桌游核心规则之七

作者 云风
2025年5月8日 14:48

纯单人游戏在桌面游戏中不太多见,但我很喜欢这种。毕竟,找人一起玩桌游太不容易,虽然多人协作桌游总可以一个人操控多方进行 solo ,但终究不是为单人游戏设计的。今天介绍的两款单人卡牌游戏,我没买到实体版,都只是在桌游模拟器上玩过几盘。

第一款是 Legacy of Yu (2023) 大禹治水。老实说,这不是一款卡牌“构筑”游戏。虽然在游戏过程中玩家还是需要从市场列“购买”新卡片,但游戏过程并不是围绕构筑进行的。这些卡牌更像是消耗品。

游戏中的工人卡有三种用法:

  1. 打出后获得卡片上标注的资源,然后进入弃牌堆。

  2. 销毁一张工人卡,获得卡片上额外标注的一次性资源,卡片将移出游戏。这样获得的资源一定比前一种方法获得的多。

  3. 把卡牌(常驻)押在版图中已经盖好的房子上,此后的回合每回合获得持续资源奖励。

和一般的卡牌构筑游戏不同,卡堆不是越少越好。一般的卡牌构筑游戏,精简卡堆总是好的,因为这样可以加快卡堆循环,能更快抽到自己需要的强力卡。而这个游戏中,当抽牌堆耗尽,游戏进程就会向前推进。一旦准备不足,推进游戏会加快失败进程。虽然,游戏过程中,除非迫不得已,都不要销毁工人卡获得额外资源。

游戏中有砖头、木头、粮食、货币(贝壳)四种资源,以及白色劳工、红色战士、黄色弓箭手、黑色骑士、蓝色枪兵五种工人。

资源可以用来做建设:砖头+木头+劳工=农场(三个待建),砖头+3x木头+劳工=前哨(四个待建),3x砖头+木头+劳工=房屋(四个待建),运河。

农场效果是固定的,为后面的轮次每回合增加固定产能。三个农场分别对应粮食、劳工、其它任意工人;前哨可以让四种特殊工人和白色劳工相互替代;房屋的触发效果是随机的,标注在房屋卡片背面,大多数是触发说明书上的事件。建成房屋还可以为工人提供工作地(将工人换成对应资源)以及常驻资源产地(减少卡组中的工人卡换取持续产能)。

游戏需要玩家以不断增加的成本修建六段运河。成本会逐步递增,一二段需要两个劳工及两个贝壳;三四段需要三个劳工及两个贝壳;五六段需要两个劳工两个指定颜色工人及三个贝壳。每段运河修建成功后,可获得一次性奖励并摧毁部分抽牌堆中的工人卡,以及触发事件。并可以为之后的游戏回合带来一些贸易选项:

贸易选项是固定的:

  1. 两个贝壳换一个粮食

  2. 一张弃牌换两个贝壳

  3. 劳工及粮食转换为任意工人

  4. 四个贝壳换一个劳工

  5. 两个粮食及两个贝壳增加一张工人卡

  6. 砖头或木头换一个贝壳

玩家需要在修完六段运河后存活到回合结束才能赢得游戏。

在游戏版图上方由预备工人卡和蛮族卡共享市场列。蛮族卡越多,可选择的工人卡就越少。一旦市场列全部挤满蛮族卡,游戏就会失败。

市场列的最左端位置上的工人卡总是免费的。玩家可以选择拿取放在弃牌堆,也可以直接销毁获得一次性资源。而蛮族卡会随着修建运河的进程逐步进入市场列。未消灭的蛮族,在每个回合结束会收取贡品,通常贡品可以用销毁一张工人卡抵消,但如果工人卡被销毁光也会导致游戏失败。击败蛮族需要支付卡片上标注的指定种类的工人,击败蛮族后会获得卡片上标注的一次性奖励。

这个游戏有传承机制,熟悉基础规则后,可以根据说明书逐步解锁新的玩法:游戏难度会随着游戏进程慢慢加强,并引入一些新的游戏机制。因为根据单局游戏失败或成功,导向不同的游戏进程,难度也是动态调节的。

这个游戏不是很热门,可能是因为它只能单人游玩,在国内很难买到实体版。但这个游戏我非常喜欢,桌游模拟器上有汉化过的电子版。


Kingdom Legacy: Feudal Kingdom (2024) 是一个较新的游戏。它和很多传承类游戏一样,几乎只能玩一次。因为游戏过程中会涂改卡牌,这个过程是不可逆的。而且卡堆一开始在包装内的次序是设计过的,一旦打乱还原比较麻烦。后期的一些卡片一旦被巨头,也会失去一些探索未知的乐趣。

可能是因为每开一局都需要新买一盒游戏的远古,这个游戏各处都缺货,不太好买到。好在其官网有所有卡片的电子版本,可以方便做研究。

游戏规则简单有趣,整个游戏过程是升级卡牌。大部分卡片有四个状态:两面每面上下两端。卡牌升级指支付一些资源,让卡片旋转到另一端或翻面(根据卡片上的指示),同时结束当前回合;卡片上也可能有一些标注的效果让卡片状态变化。资源不使用额外指示物,而是弃掉当前的卡片获得(同样标注在卡面),资源必须立刻使用,无法保留到下一回合。

这个游戏的“构筑”过程颇有新意。它没有主动挑选购买新卡的环节,但一旦抽牌堆为空,就会结束一大轮,会从卡堆中新补充两张新卡(以设计过的次序,没有随机元素)。而玩家的主动构筑在于对已有卡片的变化(升级)。

每一个回合,抽四张手牌,用其中三张的资源换取一张卡的升级。在玩的过程中,如果当前回合资源不足以升级卡片,可以选择增加两张新卡继续当前回合;也可以选择 pass ,放弃当前所有手牌,重新补四张。游戏不会失败,不管怎么玩,游戏都在抽到第 70 号卡后结束,并结算得分。玩家可以以得分多少评价自己玩的成绩。游戏包装内不只 70 张卡片,超过编号 70 的卡片会在游戏进程中根据卡片上描述的事件选择性加入游戏。

游戏中有六种资源:金币、木材、石头、金属、剑、货物。它们对应了不同卡片的升级需求。通过卡牌升级,把牌组改造为更有效的得分引擎是这个游戏的核心玩点。部分卡片是永久卡,可以在游戏过程中生效永久驻留在桌面提供对应功能。有些卡片会被永久销毁,如前所述,一盒游戏只能玩一次,所以一旦一张卡片被销毁就再也用不到了。按游戏规则的官方说法,你可以把已销毁的卡片擦屁股或是点火。

为了方便初次游戏熟悉规则,在翻开第 23 号卡片之前,可以反复重玩,玩家可以不断的刷开局;直到 23 号卡片之后,游戏才变得不可逆。而在游戏后期,玩家牌堆会越来越大(因为每个大轮次,即抽完牌堆后后会加入两张新卡),这时就引入了支线任务 ,在游戏术语中叫做扩展(expansions)。触发支线时,需要清洗(销毁)一张桌面的永久卡,并执行一次 purge 12 动作。即洗掉牌堆,抽出 12 张卡,选一张保留,并销毁另外 11 张卡。但 purge 的这 11 张卡上的分数可以保留下来,记录在支线得分中。支线会让牌堆维持在一个较小的规模。

每个支线都会持续 4 轮(对应卡片的四个状态),每轮旋转或反转支线卡推进任务。支线的每个状态都会有一个当轮生效的效果。游戏的基础包中有三个内置支线卡,额外的扩展包增加了许多任务(以及额外的卡片)。一局游戏可以最多触发 10 个支线。


8 月 11 日补充:

Kingdom Legacy: Feudal Kingdom 的卡片翻转机制我是第一次玩到,感觉挺有趣。但我最近又玩了一款老一点的游戏 Palm Island (2018) 也是同样的机制,玩起来也颇为有趣。

不过棕榈岛没有遗产机制,只有一些有限的成就卡(勉强也能算遗产)。它的资源储存和消耗方法比较独特,最有趣的一点是:虽然是一款桌面游戏,它被设计成没有桌面也能玩,只需要把握在手中即可,并不需要打在桌面上。

❌
❌