轨道
/
Roc
Roc
/
练习
/
Camicia
Camicia

Camicia

中等

简介

一个下雨的午后,你坐在厨房桌边,和奶奶一起玩牌。 这个游戏是她对 Camicia 的一种演绎。

起初,这感觉不过是又一场友好的对局:甩下纸牌,桌边笑声不断,奶奶偶尔露出得意的笑容。 可随着游戏一点点进行下去,奇怪的事情发生了。 同样的牌不断循环回来。 你一张接一张地出牌,可结局似乎永远不来。

你开始琢磨。 这局牌到底会不会结束? 还是说我们可以一直玩下去?

后来,出于好奇,你在网上搜索,惊讶地发现刚才发生的一切并不只是运气不好。 你和奶奶也许碰巧撞上了最长的序列之一! 你一下子被吸引住了。 原本随意的一场游戏,就这样变成了一场探索:这样的牌局究竟能持续多久? 你能找到比厨房桌边那局更长的序列吗? 甚至长到足以创造一项新的世界纪录?

于是,仅凭一副牌和一点算法上的巧思,你决定去一探究竟……

说明

在这个练习中,你将模拟一个与经典纸牌游戏 Camicia 非常相似的游戏。 你的程序会收到两名玩家牌堆的初始配置,必须模拟游戏直到它结束(或者发现它永远不会结束)。

规则

  • 一副牌堆在两名玩家之间分成两份。 玩家的牌从左到右读取,最左边的牌就是牌堆顶部。
  • 一个回合中,两名玩家至少各出一张牌。
  • 玩家轮流把自己牌堆最上面的牌放到中央牌堆上。
  • 如果这张牌是数字牌(2-10),出牌权就直接交给另一名玩家。
  • 如果这张牌是支付牌,就必须支付惩罚:
    • J → 对手必须支付 1 张牌
    • Q → 对手必须支付 2 张牌
    • K → 对手必须支付 3 张牌
    • A → 对手必须支付 4 张牌
  • 如果正在支付惩罚的玩家翻出了另一张支付牌,该玩家就停止支付惩罚。 然后另一名玩家必须根据这张新的支付牌支付惩罚。
  • 如果惩罚在没有被打断的情况下全部支付完毕,放置最后一张支付牌的玩家就收走中央牌堆,并把它放到自己牌堆的底部。 然后由该玩家开始下一个回合。
  • 如果一名玩家没有牌了,无法出牌(无论是在支付惩罚的过程中,还是轮到自己出牌时),另一名玩家就收走中央牌堆。
  • 玩家从中央牌堆收走牌的那一刻称为一次收牌。
  • 如果在一次收牌后,所有牌都归某一名玩家所有,游戏就结束。
  • 只要双方的牌堆与游戏中早先某个时刻完全相同(不计数字牌),游戏就进入循环!

示例

一个会结束的对局的小示例。

回合 玩家 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

你的任务

  • 根据输入,按照上面的规则模拟游戏。
  • 确定关于游戏的以下信息:
    • Status:"finished"或"loop"
    • Cards:整局游戏中打出的牌的总数
    • Tricks:中央牌堆被收走的次数
Advanced

对于那些想接受更激动人心的挑战的人,寻找“有终局的最长对局”其他纪录的努力仍在继续。 共有 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可以把元组或列表作为键来存储。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Roc Exercism

准备好开始 Camicia 了吗?

注册 Exercism,借助 135 个练习 和真人导师指导,学习并掌握 Roc,全部免费。