在万智牌实现数字电路

  • # 万智牌

封面:Vexing Puzzlebox - Zezhou Chen

你可能会觉得有翻译腔,这是因为我先写了英语版发到外网,再写的中文版。

 

0 简介

本组合技的目标是只使用指挥官合法的万智牌构建一个数字电路(简称为电路)。首先我们需要澄清一些限制:

  • 电路中涉及的所有触发式异能都是确定性的。对于每个可能的触发式异能,如果​​它需要一个或多个目标、模式等,我们将始终选择一组预先确定的目标、模式等,而不管游戏状态如何。这个限制是必不可少的,否则我们可以直接将游戏操作作为另一个程序的输出来执行。
  • 除了输入信号外,电路的内部信号使用触发式异能传播。结合第一个限制,我认为足以称该电路是自动的
  • 组合技涉及的单卡们的标识仅限蓝色或无色。(我尝试过无色,但没有找到足够直观的组合技方案)(我喜欢纯蓝)

简单来说,我们将使用横置/未横置来表示真/假布尔值,因为这在视觉上很直观,然后构建一系列逻辑运算符和存储单元。其中最棘手的部分是,由于万智牌基于堆叠逐个结算异能,我们必须保证无论信号如何通过电路(也就是异能结算顺序),结果都是正确的。实际上已经有些人尝试过用万智牌编程了,甚至证明万智牌是图灵完备,但是他们的运作方式看起来不是对普通人很友好,我希望构造一个更为直观的组合技路线。

 

1 预备工作

让我们先准备几个基本组合技:

1. 锄犁草人/Scaretiller + 无所属者聚居地/Guildless Common + 撤往珊瑚盔/Retreat to Coralhelm:每当锄犁草人成为横置时,我们可以重置或横置目标生物。

2. 催眠珠/Mesmeric Orb + 夺智魔泽力克斯/Zellix, Sanity Flayer + 歼铁巨像 /Blightsteel Colossus:每当一个我们操控的永久物成为未横置时,派出一个 1/1 惊惧兽衍生生物。

3. 侵蚀构生菌/Encroaching Mycosynth + 博识都哨兵/Lumengrid Sentinel + 组合技 2:每当一个我们操控的永久物成为未横置时,我们可以横置目标永久物。

4. 组合技 1 + 组合技 3:每当一个我们操控的永久物成为未横置时,我们可以横置或重置目标永久物。

现在我们有了两个基础机制:

1. 每当锄犁草人成为横置时,我们可以重置或横置目标锄犁草人

2. 每当锄犁草人成为未横置时,我们可以重置或横置目标锄犁草人

如果你熟悉数字电路的话,你可能已经注意到这与触发器 (flip-flop) 类似。

另外我们需要多播机制 (multicast),使得一个锄犁草人可以驱动多个锄犁草人,可以借助万和琴/Panharmonicon 达成。显然,当我们操控 n 个万和琴时,我们有如下机制:

1. 每当锄犁草人成为横置时,我们可以重置或横置 n+1 个目标锄犁草人

2. 每当锄犁草人成为未横置时,我们可以重置或横置 n+1 个目标锄犁草人

现在给出组合技的完整前置要求:

  1. 牌库中仅有 1 张歼铁巨像
  2. 手牌中有 1 张无所属者聚居地
  3. 我们操控撤往珊瑚盔催眠珠夺智魔泽力克斯侵蚀构生菌博识都哨兵各 1 个。
  4. 我们操控至少 1 个万和琴。

容易看出,在该条件下,我们可以驱动任意数量的锄犁草人的进行横置/重置信号的传递。再次提醒,我们希望电路设计在任意异能结算顺序下都能正确工作。

便捷起见,让我们用 比特 指代锄犁草人,用 指代横置状态,用 称呼未横置状态。我们同时希望定义一些符号描述触发依赖关系:

  • a:一个小写字母表示一个比特。
  • a.T (a.F):比特 a 从假到真(从真到假)的事件(也就是异能触发),我们称呼一个比特的事件时,就是指代这两种中任意一样。
  • A:一个大写字母表示一个信号。这是一个事件元组 (T, F),其中事件 T、F 可以是任何比特的任何事件,我们将使用 A.T 和 A.F 分别指代两个事件。引入信号的意义是,我们可以用两个比特的事件组合出一个信号。比如,(a.T, b.F) 也是一个信号。我们将用信号作为每个模块间传输的单元,而模块内部使用具体的比特。
  • b 的事件 ⇐ A 的事件(或 a 的事件):每当信号 A(或比特 a)的某个事件发生时,尝试触发给定的 b 的事件。例如,b.T ⇐ A.T 意味着 "每当信号 A 的 T 事件结算时,将比特 b 设置为真"。注意将比特 b 设置为真不一定触发 b.T,因为 b 可能已经是真状态了(也就是锄犁草人已经横置了)。这种确定性的触发依赖关系被称为 导线。一个事件能通过导线驱动的事件数量是 1+ 我们操控的万和琴的数量。

 

2 逻辑运算

本章我们实现三个基础逻辑运算,其中涉及的比特初始化问题将在之后解决。

 

2.1 NOT(A)

b.T ⇐ A.F

c.F ⇐ A.T

其中 A 是输入信号,b 与 c 分别初始化为假和真。那么 NOT(A) 的输出信号是 (b.T, c.F)。注意若我们可以安全地忽略中间比特,NOT 可以通过重排导线实现。例如,b.T ⇐ NOT(A).T 与 b.T ⇐ A.F 相同。

 

2.2 AND(A, B).T

因为堆叠机制,信号是异步传播至各个模块的,我们希望得到完整输入前避免输出不必要的信号。因此让我们先只考虑 AND(A, B).T,因为 AND(A, B).F 只要信号 A 和 B 中有一个为假,就可以短路并立刻输出假。AND(A, B).T 的导线如下:

c.F ⇐ A.T

c.T ⇐ B.T

d.T ⇐ A.T

d.F ⇐ B.T

e.T ⇐ c.T

e.T ⇐ d.T

其中 A 和 B 是输入信,c 和 d 初始化为真,e 初始化为假,那么 e.T 就是 AND(A, B).T。接下来我们证明这个模块仅在 A.T 和 B.T 都结算后才会触发 e.T,且与 A.T 和 B.T 的结算顺序无关。

证明:注意到 e.T 仅在 c.T 或 d.T 结算后才触发,让我们先看驱动 c 的导线:由于 c 初始化为真,其必须先被设置为假才能触发 c.T,而由于导线设计,需有 A.T 先于 B.T 结算。同理,d.T 是 B.T 先结算的情况,即 d.T 与 c.T 互斥,这意味着只要 A.T 和 B.T 均结算,其顺序无所谓,且 c.T 与 d.T 中只有一个会触发。

另外注意到这种设计还有一个好处是,A.T 或 B.T 在另一个输入结算前结算多次也可以正确地工作。

 

2.3 OR(A, B)

有了 AND(A, B).T 后,我们可以将或门实现为:

c.T ⇐ A.T

c.T ⇐ B.T

d.F ⇐ AND(NOT(A), NOT(B)).T

其中 A 和 B 是输入信号。c 和 d 分别初始化为假和真,那么输出信号是 (c.T, d.F)。

 

2.4 AND(A, B).F

我们终于可以实现 AND(A, B).F 为:

c.F ⇐ OR(NOT(A), NOT(B)).T

其中 A 和 B 是输入信号,c 初始化为真,那么 c.F 就是 AND(A, B).F。

 

2.5 FALSE(A).F 与 TRUE(A).T

有时我们希望能覆写信号的值,比如将任何信号变成真/假信号。其中始终将输入信号变为假信号的 FALSE(A).F 如下:

b.F⇐A.T

b.F⇐A.F

其中 A 是输入信号,b 初始化为真,那么 b.F 就是 FALSE(A).F。由于 FALSE(A).T 永远不会触发,相当于电路中的悬空(我们不用考虑阻抗!)。

同理,始终将输入信号变为真信号的 FALSE(A).F 如下:

b.T ⇐ A.T

b.T ⇐ A.F

其中 A 输入信号,b 初始化为假,那么 b.T 就是 TRUE(A).T。

类似非门,我们也可以用重排导线实现 FALSE(A) 与 TRUE(A)。

 

3 存储单元

1 比特存储单元实现如下:

a.F ⇐ ENL.T

c.T ⇐ AND(NOT(a), ENL).T

a.T ⇐ c.T

b.F ⇐ ENL.T

d.T ⇐ AND(NOT(b), ENL).T

b.T ⇐ d.T

                                                         

a.T ⇐ AND(A, ENS).T

b.F ⇐ AND(A, ENS).T

a.F ⇐ AND(NOT(A), ENS).T

b.T ⇐ AND(NOT(A), ENS).T

其中 ENL.T 和 ENS.T 分别控制读取与存储,A 为需存储的信号,a 和 b 用于存储信息,c 和 d 用于输出。初始化如下:

  1. a 与 b 可以被初始化为任意状态,只要我们不使用未初始化的值。
  2. 只要我们还想要写入过的值,a 与 b 不应在后续初始化时被刷新。
  3. c 与 d 在每次读取前初始化为假。

最终的结果是,每当 ENL.T 结算时,该存储单元用 (c.T, d.T) 输出存储的值;每当 ENS.T 和 A 结算时,将 A 的值存储至 a 和 b。注意 ENL.T 和 ENS.T 应当互斥。

这一坨看起来还挺吓人的,简单来说我们希望:

  1. 分离存取。
  2. 读取时输出信号。
  3. 读取后内容不变。

所以我使用两个逻辑上互斥的比特来设计,也就是 a 与 b 间只有一个为真,这样同样可以表示真假两种状态。现在分析这种设计的好处为时过早,不妨先看看每个导线的作用。

 

3.1 读取

让我们先看看读取的部分。假设此前我们已经正确地存储了一比特的信息,那么第一个和第四个导线尝试在读取信号 ENL.T 结算时触发 a.F 或 b.F:

a.F ⇐ ENL.T

b.F ⇐ ENL.T

由于此前 a 和 b 中只有一个为真,此处应当恰好有一个事件被触发。接着 c 和 d 检测 a.F 和 b.F:

c.T ⇐ AND(NOT(a), ENL).T

d.T ⇐ AND(NOT(b), ENL).T

还记得 AND(A, B).T 有同步输入信号的作用吗?所以与 ENL 信号进行与运算可以过滤掉非读取导致的 a 和 b 的事件(读取也会导致 a 和 b 变化),这一步避免了环路,也就是信号无限传递下去,这会阻塞只有一个堆叠的万智牌。最后,我们需要将翻转的 a 和 b 复位:

a.T ⇐ c.T

b.T ⇐ d.T

 

3.2 存储

存储操作还挺直接的,我们直接用 A 覆写 a 和 b:

a.T ⇐ AND(A, ENS).T

b.F ⇐ AND(A, ENS).T

a.F ⇐ AND(NOT(A), ENS).T

b.T ⇐ AND(NOT(A), ENS).T

注意此处用到了多播,这就是为什么我们需要至少一个万和琴

至此我们理解了 1 比特存储单元,但是,ENL、ENS 等信号从哪里来呢?解决方案是,我们需要一个全局时钟信号,不妨称为 CLK,来驱动它们,并在每个时钟周期执行一个读取/存储操作,这正和我们的计算机一样。时钟信号的产生我们也将在之后讨论。

 

3.3 存储阵列

只有 1 比特不太能做什么事,所以我们希望能够将其扩展成阵列。我不想展开太多,简单来说,我们只需要模仿现实中的随机访问存储器,其中一种就是常说的内存。我们为一大群比特阵列中的每一个编号,然后用地址掩码选择要读取/写入的比特,触发其 ENL.T/ENS.T 再用一个总线广播写入值,这一步会需要很多万和琴。这些听起来很复杂,实际上用上述的几个逻辑运算就可以完成。

 

4 初始化 & 时钟 & 扩展电路

初始化:只需将锄犁草人横置/重置至正确状态且不为其选择任何触发目标就可以,例如,你可以用已经被玩烂了的等时权杖/Isochron Scepter + 戏剧性逆转/Dramatic Reversal + 铸物勋爵克撒/Urza, Lord High Artificer。克撒也可以利用组合技 1 达成无限费。

变形金刚联动 sld 异画

正好最近变形金刚:起源放映,被迫和女朋友看完了,剧情有点幼稚,但 vfx 不错,还算值。

计算机由神器师操控,很合理。

 

时钟信号:重复横置/重置一个锄犁草人后多播信号即可。但是注意,电路有 "时延",而 "时延" 由电路结构决定。如果你不熟悉这些概念也没关系,简单来说,我们应当在没有更多异能触发时再发出下一个时钟信号。否则两个不同时钟信号驱动的信号可能在同一个逻辑门相遇。

 

扩展电路:如果我们需要某个永久物的多个复制,用任意可回收的复制效应就可以,我们主要需要复制锄犁草人万和琴。我个人最喜欢万形归一欧瓦尔/Orvar, the All-Form + Mind Games,Mind Games 还能用来初始化电路。

欧瓦尔,无限可能!

 

5 总结

至此,我们理论上已经能构造 cpu 和内存的大部分逻辑了,而且基本可以参考现实中的电路设计,也就没必要继续展开。尽管组合技设计上大概有很多冗余,但我认为这可以让逻辑更清晰直观。

那么基于这个组合技我们能做什么呢?那可太多了!随便举一个简单的例子,我们可以构造出一个程序,创造一盘万智牌子游戏,然后修改规则,比如一局不限制牌张数量上限的指挥官对局(如果有玩家不同意参加子游戏就用上面的无限组合技的副作用抬走 ta),并尝试用珍藏的斗智指挥官套牌获胜。听起来就是 Made in Heaven 雪赫拉莎德/Shahrazad!

我希望通过这个组合技,能够让各位玩家意识到万智牌成吨的规则文本下,所隐藏的有趣的事实;同时在胜负之外,我们可以玩些什么。

就是这样,感谢各位的阅读!

10月2日 发布于上海
全部评论 31条
按时间排序

还没有评论

71 31