普通视图

发现新文章,点击刷新页面。
昨天以前云风的 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》小说的最后三卷,不需要等美剧了 :)

和 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 代码的实现。

欧陆风云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 ,这些额外费用也是要支付的。所以订单有亏本的风险。所以大容量订单(未能成交的)风险更大。如果是采购战略物资,采用多个小订单分散去不同市场采购获得足够成交数量更安全。

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

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

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) 也是同样的机制,玩起来也颇为有趣。

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

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

作者 云风
2025年4月8日 21:10

这次介绍两款在国内人气不高的卡牌构筑类桌游。游戏都还不错,可能是因为没有中文版,所以身边没见什么朋友玩。

首先是 XenoShyft 。它的最初版全名为 XenoShyft: Onslaught (2015) ,后来又出了一个可以独立玩的扩展 XenoShyft: Dreadmire (2017) 。

故事背景有点像星河舰队:由人类军士抵抗虫子大军。简单说,这是一个塔防游戏:游戏分为三个波次,每个波次三轮,一共要面对九轮虫群的冲锋。

游戏中有四类卡片:部队、敌人、物品、矿物,另有一组表示玩家所属部门的能力卡,每局游戏每个玩家可以分到一张,按卡片上所述获得能力

矿物就是游戏中的货币,用来在市场购买部队卡和物品卡。敌人卡分为三组,对应到三次波次,洗乱后形成系统堆。玩家需要在每轮击败一定数量的敌人,撑过三个波次就可以取得游戏胜利。

玩家基础起始牌组 10 张,4 张最低级的士兵和 6 张一费的矿物。根据玩家的部门,还会得到最多 2 张部门所属的特殊卡。

市场由部队卡和物品卡构成,其中部队卡是固定的,分三个波次逐步开放购买。物品卡一共 24 种(基础版),但同时只会有 9 种出现在市场上。玩家部门可能强制某种物品一定出现在市场上,其它位置则是每局游戏随机的。在游戏过程中,当一种物品全部买空后,会在市场中随机补充一堆新的物品卡。普通敌人卡按波次分为三组洗乱,然后根据波次再从 6 张 boss 随机分配到三个波次中。

每个轮次,玩家先抽牌将手牌补齐到 6 张,然后打出所有的矿物卡,并根据波次额外获得 1-3 费,然后用这些费用从市场购买新卡片,花不完的费用不保留。新购得的卡片直接进入手牌(而不是弃牌堆)。这是一个合作游戏,所以玩家可以商量后再决定各自的购买决策。

然后,玩家把手牌部署到战区。每个玩家把部队卡排成一行(最多四个位置),物品中的装备可以叠在部队卡上增强单位的能力。玩家可以给队友的部队卡加装备(但不可以把自己的部队卡部署在队友战区)。部署环节玩家之间可以商量,同时进行。

之后进入战斗环节。这个环节是一个玩家一个玩家逐个结算。翻开敌人队列上的敌人卡(在部署环节是不可见的)、在敌人卡片翻开时可能有一次性能力,发动该能力、然后(所有)玩家都有一次机会打出手牌中的物品卡或使用部署在战场上的卡片能力。之后,双方队列顶部的两张卡片结算战斗结果。卡片只有攻击和 HP 两个数值,分别将自己的 HP 减去对手的攻击点。一旦有一方(或双方)的 HP 减到 0 ,战斗结束,把战斗队列卡片前移,重复这个过程。直到一方队列为空。

如果己方部队全灭,每场战斗的反应阶段(每个玩家都可以打出一张手牌或使用战斗卡片能力)依然有效,但改由基地承受虫子的攻击。基地的 HP 为所有玩家共享,总数为玩家人数乘 15 。可以认为基地的攻击无限大,在承受攻击后,一定可以消灭敌人。一旦基地 HP 降为 0 ,所有玩家同时输掉游戏。

游戏中的死亡效果有两种,毁掉(burning)和弃掉(discarding)。毁掉一张卡指把这张卡片退回市场(如果市场上还有同类卡)或移出游戏(市场上没有对应位置),而弃掉一张卡指放去玩家的弃牌堆。

通常,敌人卡片效果一次只会结算一张(即当前战斗的卡片)。但有些卡片效果会对场上敌人队列中尚未翻开的卡片造成伤害。这种情况需要先将所涉及的敌方卡片都翻过来,并全部结算卡片出场能力。对于需要同时结算多张敌人卡片出场能力时,玩家可以讨论执行次序。

如果对这款游戏有兴趣,又找不到人玩的话,可以试试它的电子版,在 steam 上就有。不过看评论,据说电子版 bug 有点多。


另一个游戏是 G.I. JOE Deck-Building Game (2021) 。G.I. JOE 特种部队是孩之宝(也就是变形金刚品牌的拥有者)旗下的一个品牌,除了玩具,有衍生的漫画、电影和动画片。这个桌游也是这个玩具品牌的衍生品。我认为这个 DBG 里的某些设定(不同的游戏剧本、同一剧本中不断推进的故事任务、队员的多种技能)也影响了星际孤儿那个电子游戏。

游戏有很多剧本、以及若干扩展。不同的剧本在规则细节上有所不同(这一点和星际孤儿很相像),这里只减少核心共通的规则。

这是个多人协作游戏。当然,只要是协作游戏,就一定可以单人玩,只需要你轮流扮演不同角色的玩家即可。每个玩家一开始有一张特殊的领袖卡,然后配上 9 张固定的初始牌组成了起始卡组。每个回合摸 5 张卡,用不完的卡会弃掉,不能保留到下一回合使用。每张领袖卡都对应了一个升级版本,可以在游戏进程中购买替换。

市场由一组卡片洗乱,随机抽出 6 张构成。每当玩家购买一张卡,就会补充一张新卡。但如果卡堆耗尽尚未结束游戏,游戏失败。在游戏过程中,可能有敌对卡片出现,会盖掉市场中的卡。玩家需要解决掉敌人,否则盖掉的卡片无法购买。如果 6 张市场卡片都被盖掉也会导致游戏失败。

当每个玩家执行完一轮行动,即为一大轮游戏。在每大轮开始,都会推进一个全局的威胁指示条。一旦威胁指数上升到某一程度,就会发生一些固定事件。维护指数走到头会导致游戏失败。

游戏故事由三幕构成,每幕随机选取两张对应的故事任务卡和一张固定的终局局故事卡,一共 9 张故事任务卡构成了整局游戏。永远有一个故事任务呈现在场景中,它有可能触发一个回合效果,需要在每个玩家回合开始时结算。

每一幕开始都会洗混所有的系统事件卡堆(包括之前解决完弃掉的事件卡),故事卡和威胁进度条会触发这些事件。这些事件会给玩家增加一些负面效果,或是在场上增加一些任务让玩家解决。

游戏任务分两种:团队任务和支线任务。故事卡一定是团队任务,事件产生的 boss 卡也是团队任务。团队任务可以在当前玩家决定去进行任务时,其他玩家提供协作;而支线任务只能由当前玩家独立完成。任务有地形、难度、技能需求、持续效果等元素构成。

地形指玩家开启任务需要使用怎样的载具,分陆海空三类。技能要求则限制了玩家可以派出的队员。难度数字决定了玩家最终需要在此技能上获得多少点才能完成任务。持续效果则会在该任务完成前,对玩家造成的负面效果。

开启一个任务需要玩家从机库派出一个对应地形的载具以及至少一个队员(从手牌打出)。该载具是在玩家回合开始时从手牌打在机库中的,VAMP 作为默认载具总可以保证一个回合使用一次。高级载具可以从市场购买。对于团队任务,所有玩家都可以协商派出队员,但队员总数不能超过载具的容量。

任务上标注了所需技能,派出的队员卡如果有符合的技能,则可以把技能点加到任务中。如果技能不匹配,也可以视为一个通用技能。任务卡最多要求两种技能,如果是 & 标记,则表示可以只要符合两种技能中的任意一种都可以生效;如果是 or 标记,则需要当前玩家选择其中一种技能,所有队员都需要匹配这种技能。

最终参与任务的技能总数决定了最终可以用几个六面骰。每个六面骰有三个零点,两个一点,一个两点;扔出对应数量的骰子,把最终点数相加,如果大于等于任务的难度值,则任务成功并或许成功奖励,否则任务失败。对于故事任务,失败需承受任务卡上的失败惩罚,并结束任务;对于其它任务,失败会让任务继续保留在场上。

有一类叫做 Precision Strikes 的任务,在翻出时需玩家讨论后决定放在谁的面前,变成它代做的支线任务。每个玩家最多只能放两张 Precision Strikes 在面前,到他的行动回合,必须先处理掉 Precision Strikes 任务。

在玩家做完所有想做的任务后,剩余的手牌可以作为够买新卡的费用。每张卡都标有一个自身的价格,以及一个在购买阶段可以当成几点费用使用。没有用完的费用不会积累到下个回合。购买的载具卡会在购买后直接进入机库,供后续回合所有玩家使用。其它卡片则放在抽牌堆顶。

整个回合结束后,弃掉所有手牌,以及回合中使用过的卡牌以及载具,重新抽五张。

玩家可以共建一个基地。这个基地由五部分构成,在游戏过程中逐步升级,升级也时通过购买完成的。在游戏过程中,以升级的部分也可能被摧毁或重建。这五个部件如下:

Repair Bay 会让任务中使用的载具都放在该处而不是弃牌堆。在回合结束(或被摧毁),Repair Bay 中的载具都会回到机库。这样,载具的利用率会大大提升。

Stockade 建成后,击败的 boss 卡会进入这里而不会重复进入游戏。

Battlestation 可以在团队任务中重掷一个骰子。

Laser Cannon 可以在支线任务中增加一个骰子。

Command Room 把手牌增加到 6 张。

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

作者 云风
2025年3月12日 09:30

最近的兴趣重心转移,没怎么研究桌游。不过前几个月的笔记还有一点,今天继续整理一下。继续谈谈 PvE 向的卡牌构筑类桌游。

Aeon's End (2016) 末日决战是一款偏传统卡牌对战规则的游戏:一开始手牌中只有水晶 (gem) 和基础法术 (spell) 卡。游戏过程中,在一个固定市场(9 种)用水晶购买更强力的卡片升级自己的卡组。游戏的目标是合作(或 solo)击败 boss 或 boss 的仆从。如果被敌人攻击太多次,自己的 HP 减到 0 就失败了。

法术卡其实更像是炉石/万智牌中的玩家仆从,只不过是一次性消耗品。需要先部署在桌面,然后才可以攻击。而玩家卡组内的第三种遗物卡(relic)则更像是一般意义的法术:可以即时产生效果。

打出法术卡或遗物卡是没有直接费用的,水晶主要用来支付从市场购买新卡的费用。另外,法术卡要生效,需要桌面有空闲的位置(叫做 breach 裂隙)。所以即使手牌中的很多强力法术卡,也不能无限制的一次摆出来。

裂隙需要花费水晶费用打开才能使用(摆放法术)。打开 (open) 裂隙在一局游戏中是一次性的,一旦裂隙开启,就可以一直使用(放置法术)。但开启费用较高,也可以 focus 一个关着的裂隙一次性使用。focus 费用较低,但下次使用还需要再次支付水晶 focus 。focus 同一个裂隙 4 次后会自动开启。在开启的裂隙上摆放的法术,玩家可以选择再后续的任意回合生效;但通过 focus 预备的法术则必须在下一回合用掉。

每个玩家角色有一个独特的能力,这个能力需要 charge 后才可以使用。每次 charge 的费用是 2 水晶,不同角色使用能力需要的 charge 数量各不相同。

这个游戏规则中比较独特的是:玩家回合结束后,未使用的卡牌不会自动丢弃,但摸牌是将手牌补齐五张。即,打的牌少,卡组循环也会变慢。而弃牌是有次序的,正面朝上依次置入弃牌堆。抽牌堆用完后,弃牌堆不洗牌,直接反过来形成新的抽牌堆。

游戏的随机性不来源于抽牌堆的乱序,而是每个轮次的次序。每个轮次,玩家和敌人的指代物会放在一起洗混,其随机次序决定了这个轮次中,哪个玩家(如果有多个)以及敌人谁先行动谁后行动。

末日决战这几年口碑不错,一直在发新的扩展。尤其是新的版本加入了传承机制,也就是游戏可以一局局玩下去,玩家可以升级和继承过去的能力。玩家主要的升级是角色本身的技能,敌人也会逐步加强。


另一款同期发行,口碑不错的合作类 DBG 是 Clank!: A Deck-Building Adventure (2016) 。

这款游戏的玩法不是纯粹的打牌,而是加入了版图移动机制。玩家需要在版图上移动获得神器和宝藏(计分)。它并不算是一个合作游戏,更像是玩家有一个共同的敌人(系统),而玩家之间则需要比拼谁拿的分更多。

系统会攻击玩家,当至少有一个玩家 HP 减到零时,游戏会进入结束倒计时阶段。获得一个神器并逃离游戏的玩家会获得额外计分奖励。玩家需要在版图上获得一个神器的基础上尽可能的得分(获取宝藏)并在游戏结束前脱离。玩家如果没能获得神器,得再多分也不计算。游戏的结束条件是由第一个被击倒的玩家开启,但第一个被击倒的得分损失很大,所以玩家应尽可能的活得更久。

游戏规则中比较有特色的是 Clank 系统(也就是游戏的名称)。有些卡牌效果会给玩家增加 Clank 方块的数量。而系统发起攻击时,会把所有玩家获得的 Clank 方块以及系统方块一起放入抽取袋再抽出来。抽到 Clank 数量决定了对应玩家会掉多少 HP ,所以 Clank 越多(尤其是比队友多)意味着会更快挂掉。系统方块更多意味着玩家受伤害的概率越少。不过,每次系统发动攻击抽取出的黑色方块(系统方块)都不会再次放入抽取袋中,这就意味着系统攻击造成的伤害会越来越大。

市场(地城)上有三类卡片:一次性效果卡、小怪、新的可以加入卡组的卡片。这和 Ascension 非常的类似:玩家可以选择不同路线增强自己的能力,杀怪或购买更强力的卡片。

市场上除了随机卡堆翻出的六张卡外,还有固定卡堆:一个技能点及两点攻击(剑符号)的雇佣兵,两个技能点及一点移动力(腿符号)的探索卡、价值 7 分的宝藏卡、需要 2 点攻击击败的小怪哥布林。

每个玩家行动结束后,都会把市场补全六张。当市场上得卡上出现龙符号时,系统发起攻击。

玩家的牌主要有三种能力:

  • 技能点,用来在市场上购买卡片。
  • 攻击力(剑符号),用来杀掉市场上出现的怪物卡。
  • 移动力(腿符号),用来在版图上移动。

击败怪物可以获得金币,这是和技能点不同的第二种游戏货币。金币在游戏结束时可以用于计分,也可以在游戏过程中花掉购买物品。物品有固定的三种,价格均为 7 金币:

  • 钥匙,用来在版图上快速移动(使用通道)。
  • 背包,可以携带额外的神器,每个额外的神器价值 5 分。
  • 皇冠,价值 8,9,10 分。(先买的玩家收益更高)。

打牌过程和传统的领土一样:每个回合的手牌必须全部打出,不可保留到下一回合。每个回合抽取新的 5 张手牌。抽牌堆用完后,洗混弃牌堆形成新的抽牌堆。

Clank! 在最近几年也一直在出各种扩展,同样发行了传承版本。

传承机制加入后,游戏更有故事性。每个章节会加入一些事件卡以及每局游戏的合约(单局游戏目标)。玩家需要扮演不同角色(有一点不同的能力),并可以随着游戏进程增强能力,游戏版图也会慢慢开放。

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

作者 云风
2025年2月16日 14:44

这篇谈谈 PvE (玩家对抗环境)向的卡牌构筑类游戏。

在桌游中,PvP 向(玩家对抗玩家)的游戏数量明显超过 PvE。我认为这是因为桌游需要靠玩家自己驱动游戏规则,扮演环境的同样是玩家,其规则不能设计的太复杂,通常只能靠简单机械的逻辑驱动。或者,起源于桌游的 RPG ,比如 D&D ,则由一个玩家扮演环境(城主),这样就可以增加游戏的深度。但毕竟这种不对称规则下,玩家方和环境方很难调配平衡。RPG 这样的游戏,城主也并非玩家的对立方,大家只是在一起享受游戏过程。而在电脑上,则可以通过程序实现更复杂的环境。所以在电脑游戏中,大量的游戏转向 PvE 。毕竟,找到可以一起玩的游戏搭档并不容易。

所以,对于卡牌构筑这个具体类别,电脑上的游戏几乎一开始就是 PvE 性质的:比如杀戮尖塔,玩家一直在挑战系统而获得乐趣;而桌游中,从领土(Dominion)开始,就是基于玩家对抗设计的规则。

传奇 Legendary 系列是一个比较早的 PvE 向卡牌构筑类桌游系列。最早可以追述到 Legendary: A Marvel Deck Building Game (2012) ,后续几年发布了大量系列作品,并衍生出 Legendary Encounters 系列。关注 Legendary 系列是因为星际孤儿(Stellar Orphans)这个电脑游戏,我特别喜欢。在星际孤儿的玩家社区,有玩家指出这个游戏明显受到了 Legendary 系列桌游的启发。

传奇系列的每个作品都围绕一个题材展开,以最初的漫威系列为例,下面我介绍一下它的核心玩法规则。


游戏是由玩家对抗系统。每个玩家以开始会拿到一叠基础的英雄卡(12 张),系统则由一叠系统卡设定。

每个回合,从系统卡堆中抽出一张卡片出来推动游戏发展。如果从系统卡堆中抽到坏人卡,则表示这张坏人卡会入侵城市(摆放在桌面区);如果抽到旁观者,则绑定在坏人卡上,变成坏人的俘虏;如果抽到事件卡,则引发特殊事件。

而在系统卡结算完毕后,玩家从手牌中打出卡片组合,产生本回合的攻击点、招募点以及特殊能力。攻击点可以用来消灭城市中的坏人卡(并解决俘虏);招募点则从桌面的 HQ 列(从英雄卡堆中翻出)购买新的英雄卡增强自己的卡组。而传统的卡牌构筑类游戏规则一致,每个回合的卡片都必须全部用完,不可保留到下个回合,溢出的攻击点和招募点会作废。购买的新卡片也会先置入弃牌堆。每个回合,玩家从自己的卡组中抽取新的手牌(6 张);一旦卡组抽完,洗混弃牌堆,形成新的抽牌堆。

初始卡组由 8 张基础的招募点卡(每张 1 点)以及 4 张基础的攻击卡(每张一点)构成;英雄卡堆(形成市场)则由玩家选择的几个英雄对应的卡组混在一起。每个英雄卡组有 14 张,其中两组各 5 张相同的普通卡,三张强力卡,以及一张稀有卡。市场上永远展示其中的五张,当玩家购买后会补齐。一旦英雄卡堆用玩,游戏结束。另外,市场上永远有固定的高级招募卡(每张 2 点)可供选购。它类似于 Dominion 中的银币。玩家每个回合还可以花 2 点招募点买到一张 sidekick ,该卡只能使用一次(用完回到市场),效果是抽两张卡。

每局游戏,系统存在一个终极 boss ,在 setup 阶段需要随机选择 boss 对应的一张 scheme 卡。scheme 卡上描述了系统胜利的方法。当系统达成条件,玩家就会输掉。

攻击 boss 他需要大量攻击点,每次成功的攻击会结算一次随机的 Tactics 卡(例如,有些 Tactics 卡会增加下一次攻击 boss 的难度)。每个 boss 对应 4 张 Tactics 卡,四次成功攻击后,玩家就赢得游戏。

系统每个回合翻出的坏人卡会以队列形式在桌面推进。桌面一共有 5 个位置(地点),坏人卡从最右侧进场,在进场时需要结算坏人卡上的 Ambush 效果(若有)。如果玩家一直对翻出的坏人卡置之不理的话,坏人卡会一步步向左推进,直到离开桌面。每离开一张坏人卡,都会摧毁掉市场上的一张卡片(由玩家自己选择);如果坏人带着俘虏离开,则还要弃掉一张手牌。如果逃离的坏人卡上标注有 escape 效果,还需要额外结算。

系统卡堆中存在一些地点卡,翻出后在场上并列一排从右至左一张张排列直到排满。摆满后,新的地点卡会替换掉场上最弱的那张。地点卡会改变存在这个地方的坏人卡的能力。地点卡本身可以作为攻击目标。

系统卡堆还包含一些特殊卡(坏人卡和旁观卡之外),它们不推进坏人卡。其中:

Trap 卡给玩家一个需立刻结算的挑战。

Twist 卡会推进当前 Scheme 卡上系统胜利的某种进度。

Strick 卡会触发 boss 的攻击。

玩家的手牌没有打出费用(类似杀戮尖塔的行动点限制),但受招募费用的限制,强力卡在一开始无法从市场购得。玩家倾向于在每个回合打出所有手牌(不打出的卡也无法带到下个回合)。基本卡片只是一个招募点和攻击点,把这些点数累加起来就是当前回合可以用于购买新卡以及消灭坏人的费用。但强力卡片的打牌次序是有选择的。因为卡片会有专门的特殊能力,比如,有的卡片需要弃掉别的手牌才能使用;有的需要前置打出某些类型的卡,就有额外的能力加成(形成 combo)。

此外,击败的坏人卡、拯救的俘虏(旁观者)、Tactics 卡都附带有 VP 。当以多人协作形式进行游戏时,每个玩家单独计算 VP ,在游戏结束后,可以比较在游戏过程中获得的 VP 总数来觉得谁表现得更好(但玩家在游戏过程中依旧是合作关系,而不应该为 VP 竞争。


在 Legendary 系列之后,同一家公司推出了新的 Legndary Encounters 系列。这是换了新设计师后在 Legendary 规则上的进一步发展。系列的第一作是 Legendary Encounters: An Alien Deck Building Game (2014) 。给我的感受是,Legendary Encounters 更加注重叙事(而不仅仅是围绕一个主题),通过卡片和游戏规则的设计,玩家可以更好的融入游戏故事中。不同的题材会给玩家不同的感受,Legendary Encounters 在今年(2025 年)还会有新作推出,最新的故事看起来会在冰与火之歌的世界中展开。

我在桌游模拟器中尝试了一下最初的异形(Alien)三部曲。和异形电影体验非常接近,代入感很强。和 Legendary 漫威不同的地方是:

敌人(坏人卡)是背面朝上在桌面(场景)中推进的。玩家需要主动 scan 。这和星际孤儿的设定非常相像(应该是它启发了星际孤儿)。玩家对敌人的推进置之不理的话,敌人牌移到头会触发攻击,而受到太多攻击后,玩家会死亡而输掉游戏。

多人协作模式下,不同的玩家会扮演不同的角色。不同角色的能力是不同的。另外,玩家在游戏过程中有可能因为不敌敌人而被感染,当玩家以这种形式被杀死后,会重新以异形的立场加入游戏,变成对抗其它玩家。

每局游戏会有固定的几个阶段,每个阶段有固定的目标。这些阶段性目标被设计成和电影情节一致。玩家需要完成每个阶段的目标推进游戏,直到所有阶段达成获得游戏胜利。btw, 星际孤儿也采用了这种形式,并进一步加长了游戏故事。

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

作者 云风
2025年2月3日 20:39

这一篇,我想先谈一个在中文社区比较小众(没有出中文版),但我个人非常喜欢的卡牌构筑游戏:核心世界 Core Worlds (2011) 。

相对 Dominion 来说,它的规则和体验已经非常不同了。

它的单位牌需要先部署到桌面(战区),而后在从桌面打出,用于征服星球(用于增强能力)。这可以促使玩家做更多跨回合的规划。而不是仅考虑当前回合的手牌怎样打出漂亮的 Combo 。每个回合,玩家可以在回合结束时保留一张手牌(同时也会减少抽牌数量),这点也是为了促进玩家更多的考虑回合间的联系。

游戏采用固定轮数,一共 10 轮 5 个阶段。每个阶段的市场牌堆都是独立的。所以,玩家会在每两轮看到不同的牌进入市场,这样会减少卡组构建的随机性,让强牌逐步出现。游戏节奏被设计的更好。 每个轮次翻到市场的卡很少,只玩一两盘恐怕大多数卡片都不会见到,这加强了重玩性。每局游戏都有不同的变化。虽然游戏有 10 轮,但游戏节奏其实是很快的。尤其是到了终局的第五阶段,节奏被刻意加快了:前 8 轮积累的大量能量,而手牌也增加了。没有新的单位卡,而换成了得分用的声望卡以及核心世界卡需要竞争。终局两轮实际是考验的是玩家前面的组卡和布局成果。

而在前面的回合,玩家每轮可以做的事情并不算太多。市场上的卡片分为星球卡和单位/行动卡。星球卡需要玩家用之前部署的部队去征服,被征服的星球卡直接放到玩家桌面提供永久能力(通常会增加能量点);而单位/行动卡则需要能量点购买,和传统的卡牌构筑规则相同,新购入的卡片需要先进入弃牌堆。市场上的卡片张数是受玩家人数限制的,每轮固定添加新卡片上场。每轮新增加的卡片如果没人搭理,则会增加一点能量奖励下一轮选它的玩家。而两轮不选的卡片则自动弃掉。这样,市场上永远都只有有限几张卡片(根据玩家人数不同而不同),玩家不会陷入选择焦虑。

在战斗征服部分,玩家单位被分为地面战和空中战两种能力。针对不同的星球,需求不同。但这些能力有可以被战术(行动)卡所改变。因为用于征服星球的单位需要花一轮部署在桌面(战区),所以,玩家可以通过观察对手部署的部队,提前了解对手的意图。实际玩的时候,先做什么再做什么,会随着对手的行动而不断变化。加上市场资源(无论是星球还是行动卡)都极为有限,游戏的对抗性就变得很强。

在实际玩游戏的时候,除了自己行动需要决策外,观察对手行动在做什么也相当有意义。这让轮转行动时等待对手行动的时间也不会枯燥。


游戏每轮分为 4 个阶段:抽牌、补充能量、补充市场、行动、结束。前三阶段玩家的选择非常有限,所以进度会很快。游戏的核心时间会花在行动阶段上。

在行动阶段,玩家受行动点和能量的双重限制。每类行动都会花掉一个行动点,玩家以行动为单位轮转。而不同行动的能量点开销各不相同,玩家可以做的是:

  1. 根据市场上的单位/行动卡标注的能量费用购买一张新卡(置入弃牌堆,无法立刻使用)。

  2. 把手上的单位卡部署在桌面战区(支付对应的能量点),只要能量够,一次可以部署多张单位卡。

  3. 使用已经部署好的单位卡去征服一个星球。同时可以利用手牌中的战术行动卡增强能力。

  4. 有部分战术卡可以单独作为一个行动使用。

玩家可以在行动阶段 pass ,能量点不会保留到下个回合,且需要弃掉手牌。和同类游戏相比,核心世界在弃掉手牌时可以保留最多一张卡片。但由于回合开始抽牌阶段的规则时补充手牌到规则上限,所以,保留手牌会减缓卡组轮替。

在最后的结束阶段(所有玩家 pass 行动),不需要玩家做行动。只是清理桌面:玩家在本回合获得的额外能量点(通常是因为选择了上一轮每人要的卡片)会累加在能量条上供下一轮使用。而两轮都没人过问的卡片则被移除游戏。

在 10 轮之后,玩家统计各自的 VP 决定谁最终胜利。大部分的星球会有 VP ,而在最后一个阶段的两轮,市场上会出现大量的 VP 卡。声望卡只需要能量就可以购买,它需要玩家在前面累积大量的能量产能;核心星球则提供多样化的 VP (和玩家卡组里的其它卡片组合算分),核心星球通常需要玩家在前 8 轮部署足够的战力才能拿下。


这个游戏提供了有限的卡组瘦身机制。我们知道,一般的卡牌构筑游戏,卡组越精简,卡组循环就越快,卡组实力便越强。把单位卡部署在桌面是一种临时缩减卡组的办法;而征服星球则可以从征服部队中选一张单位卡殖民到该星球上,这可以让玩家从卡组中永久去掉一张卡不参与循环(但依旧参与计分)。

另外,在熟悉游戏后,初始会有两张不同的单位卡(一张地面战力,一张空中战力)采用轮抽机制替换。这也增加了多局游戏的多样性。

核心世界还有两个扩展,可惜我没买到。这里就无法评说了。


接下来谈的一款游戏在中文社区就比较大众化了:星域奇航 Star Realms (2014) 。

这是一款快节奏的双人对战卡牌构筑游戏。我有一套正版的基础版,它出了很多扩展,但现在都很难买到正版了。所以我只好买了大盒装的盗版体验。

星域奇航上手非常简单。就是抽牌,从市场购买卡片、打牌攻击对手。和 Dominion 不同,它并不靠积累 VP 获胜,而是攻击打掉对手的血获胜。每个人初始有 50 点血(官方称为权威点),在初始手牌中,便有一点攻击力的卡片,抽到即可攻击。初始手牌中大部分卡片是钱币卡。10 张卡片组成的标准起始卡组为 8 (钱币) + 2 (攻击) ,这和 Dominion 的初始卡组非常类似。

市场区和 Ascension 类似,采取买一张补一张的轮换制。但市场区没有怪物卡,全部是单位卡。因为游戏的战斗全部是攻击对手,不靠打怪得分。btw, 星域奇航的作者本身就设计了 Ascension 的很多扩展,所以就不难理解这个游戏和 Ascension 的很多相似之处了。

单位卡除了可以在当前回合发动能力的飞船(提供钱币/攻击/补血等)之外,还有一种要塞卡,可以在打出后永久停留在桌面。要塞卡通常会提供一些持久能力,也可以用于阻挡对手的攻击。星域奇航创造的一条独特规则:部分卡片可以在打出后将自己销毁,提供额外的一次性能力。这种销毁发动一次性能力的卡片也包括要塞卡。所以玩家在打牌时就多了一种选择:要不要触发销毁能力。

(基础)游戏中有四种势力,分属为四种卡牌。通常单一势力的卡组更容易建立起强大的 combo 。组卡方面留下的策略空间不错,堆整个卡池越熟悉,就越容易组出强大的卡组赢得游戏。

总的来说,我还是很喜欢这个游戏的。因为一局时间比较短,规则又简单,可以经常在家里和孩子一起玩。这里再次强调游戏节奏很快,是因为它相比那些卡牌构筑前辈,可以用更短的回合数构筑出攻击引擎,精简卡组也相对容易,往往一个有趣的组合就可以打爆对手。我们玩一局的时间很少超过半个小时。

在加了扩展后,游戏还提供协作模式:由系统制造出一个强大的 boss ,两个人不再相互攻击,而是要协力击败系统。云豆更喜欢协作模式一些,只是把系统设定的几个 boss 都击败后,重玩不够有趣。


在写了这么多对抗类的卡牌构筑游戏后,下一次我想改谈协作类型了。由玩家对抗系统,或许更接近我自己要做的电脑游戏一些。

❌
❌