一个下雨的午后,你坐在厨房桌边,和奶奶一起玩牌。 这个游戏是她对 Camicia 的一种演绎。
起初,这感觉不过是又一场友好的对局:甩下纸牌,桌边笑声不断,奶奶偶尔露出得意的笑容。 可随着游戏一点点进行下去,奇怪的事情发生了。 同样的牌不断循环回来。 你一张接一张地出牌,可结局似乎永远不来。
你开始琢磨。 这局牌到底会不会结束? 还是说我们可以一直玩下去?
后来,出于好奇,你在网上搜索,惊讶地发现刚才发生的一切并不只是运气不好。 你和奶奶也许碰巧撞上了最长的序列之一! 你一下子被吸引住了。 原本随意的一场游戏,就这样变成了一场探索:这样的牌局究竟能持续多久? 你能找到比厨房桌边那局更长的序列吗? 甚至长到足以创造一项新的世界纪录?
于是,仅凭一副牌和一点算法上的巧思,你决定去一探究竟……
在这个练习中,你将模拟一个与经典纸牌游戏 Camicia 非常相似的游戏。 你的程序会收到两名玩家牌堆的初始配置,必须模拟游戏直到它结束(或者发现它永远不会结束)。
一个会结束的对局的小示例。
| 回合 | 玩家 A | 玩家 B | 中央牌堆 | 待付惩罚 |
|---|---|---|---|---|
| 1 | 2 A 7 8 Q 10 | 3 4 5 6 K 9 J | - | |
| 1 | A 7 8 Q 10 | 3 4 5 6 K 9 J | 2 | - |
| 1 | A 7 8 Q 10 | 4 5 6 K 9 J | 2 3 | - |
| 1 | 7 8 Q 10 | 4 5 6 K 9 J | 2 3 A | 玩家 B:4 |
| 1 | 7 8 Q 10 | 5 6 K 9 J | 2 3 A 4 | 玩家 B:3 |
| 1 | 7 8 Q 10 | 6 K 9 J | 2 3 A 4 5 | 玩家 B:2 |
| 1 | 7 8 Q 10 | K 9 J | 2 3 A 4 5 6 | 玩家 B:1 |
| 1 | 7 8 Q 10 | 9 J | 2 3 A 4 5 6 K | 玩家 A:3 |
| 1 | 8 Q 10 | 9 J | 2 3 A 4 5 6 K 7 | 玩家 A:2 |
| 1 | Q 10 | 9 J | 2 3 A 4 5 6 K 7 8 | 玩家 A:1 |
| 1 | 10 | 9 J | 2 3 A 4 5 6 K 7 8 Q | 玩家 B:2 |
| 1 | 10 | J | 2 3 A 4 5 6 K 7 8 Q 9 | 玩家 B:1 |
| 1 | 10 | - | 2 3 A 4 5 6 K 7 8 Q 9 J | 玩家 A:1 |
| 1 | - | - | 2 3 A 4 5 6 K 7 8 Q 9 J 10 | - |
| 2 | - | 2 3 A 4 5 6 K 7 8 Q 9 J 10 | - | - |
status: "finished", cards: 13, tricks: 1
这是一个会陷入循环的对局的小示例。
| 回合 | 玩家 A | 玩家 B | 中央牌堆 | 待付惩罚 |
|---|---|---|---|---|
| 1 | J 2 3 | 4 J 5 | - | - |
| 1 | 2 3 | 4 J 5 | J | 玩家 B:1 |
| 1 | 2 3 | J 5 | J 4 | - |
| 2 | 2 3 J 4 | J 5 | - | - |
| 2 | 3 J 4 | J 5 | 2 | - |
| 2 | 3 J 4 | 5 | 2 J | 玩家 A:1 |
| 2 | J 4 | 5 | 2 J 3 | - |
| 3 | J 4 | 5 2 J 3 | - | - |
| 3 | J 4 | 2 J 3 | 5 | - |
| 3 | 4 | 2 J 3 | 5 J | 玩家 B:1 |
| 3 | 4 | J 3 | 5 J 2 | - |
| 4 | 4 5 J 2 | J 3 | - | - |
第 4 回合开始时的情况与第 2 回合开始时相同。 回想一下,数字牌的点数无关紧要。
status: "loop", cards: 8, tricks: 3
"finished"或"loop"
对于那些想接受更激动人心的挑战的人,寻找“有终局的最长对局”其他纪录的努力仍在继续。 共有 653,534,134,886,878,245,000(约 65400 亿亿)种可能,而我们还没有把它们全部算完!
from_quote的优缺点Card.from_quote函数会把字符串解析成Card。
这样就可以写出下面这样的代码:
card : Card
card = "Q"
这段代码会在编译时隐式调用Card.from_quote("Q")。用这种方式(或下文介绍的其他方式)在编译时运行代码,有可能加快程序的运行速度,因为程序无需在运行时处理解析和数据校验。此外,它还能确保无效数据(比如错误的日期格式)在编译时就被拒绝,而不是拖到运行时,让你的应用更加健壮。最后,它也让代码更加简洁易读。例如,下面是本练习中的一个测试:
result = simulate_game({
player_a: ["2", "A"],
player_b: ["3", "4", "5", "6", "K"],
})
不过,阅读这段代码的人可能会以为simulate_game函数接收的是一个包含两个List(Str)的记录,而实际上它接收的是包含两个List(Card)的记录。这可能会导致困惑和错误。因此,你可能更偏好下面这种稍长但更明确的写法:"Q".Card,它同样会在编译时调用from_quote。
在你自己的程序中,你也许更愿意改用from_str:
Card := { ... }.{
from_str : Str -> Try(Card, [InvalidCard])
from_str = |card_str| ...
}
你也可以在编译时调用from_str(或其他任何纯函数),就像这样:
Ok(card) = Card.from_str("Q")
注意:把这个定义放在顶层会让它在编译时运行(因为该函数是纯函数,且它的输入在编译时已知)。而在函数内部,你可以在运行时使用from_str。
使用match搭配[]和[card, .. as rest],区分空牌堆与它的下一张牌及剩余牌。
像{ ..state, pile: new_pile }这样的记录更新,可以在不使用可变状态的情况下,让游戏状态的转换保持明确。
在追踪先前出现过的游戏状态时,Set可以把元组或列表作为键来存储。