一個下著雨的午後,你坐在廚房的餐桌旁,和奶奶一起玩牌。 你們玩的是她版本的 Camicia。
一開始,這感覺就像又一場友誼賽:牌啪地甩在桌上,餐桌對面傳來笑聲,Nonna 偶爾露出勝利的笑容。 但隨著遊戲拉長,奇怪的事發生了。 同樣的牌不斷循環回來。 你一張接一張地出牌,卻好像永遠等不到結束。
你開始納悶。 這局牌到底會不會結束? 還是我們可以一直玩下去?
後來,出於好奇,你上網查了一下,驚訝地發現剛才發生的不只是運氣不好。 你和奶奶可能無意間碰上了數一數二長的牌序! 突然間,你著迷了。 原本只是一場隨興的遊戲,如今卻成了一場探索:這種遊戲究竟能持續多久? 你能找到比廚房餐桌上那局更長的牌序嗎? 或許甚至長到能刷新世界紀錄?
於是,你只帶著一副牌和一些演算法上的巧思,決定一探究竟……
在這個練習中,你將模擬一個和經典紙牌遊戲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(大約 654 百萬兆)種可能性,而我們還沒把它們全部算完喔!
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可以把元組或陣列存成鍵。