어느 비 오는 오후, 할머니와 부엌 식탁에 마주 앉아 카드 놀이를 하고 있어요. 이 게임은 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 | Player B: 4 |
| 1 | 7 8 Q 10 | 5 6 K 9 J | 2 3 A 4 | Player B: 3 |
| 1 | 7 8 Q 10 | 6 K 9 J | 2 3 A 4 5 | Player B: 2 |
| 1 | 7 8 Q 10 | K 9 J | 2 3 A 4 5 6 | Player B: 1 |
| 1 | 7 8 Q 10 | 9 J | 2 3 A 4 5 6 K | Player A: 3 |
| 1 | 8 Q 10 | 9 J | 2 3 A 4 5 6 K 7 | Player A: 2 |
| 1 | Q 10 | 9 J | 2 3 A 4 5 6 K 7 8 | Player A: 1 |
| 1 | 10 | 9 J | 2 3 A 4 5 6 K 7 8 Q | Player B: 2 |
| 1 | 10 | J | 2 3 A 4 5 6 K 7 8 Q 9 | Player B: 1 |
| 1 | 10 | - | 2 3 A 4 5 6 K 7 8 Q 9 J | Player 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 | Player 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 | Player 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 | Player 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가지(약 6.54해)나 되는데, 아직 다 계산하지 못했어요!
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 함수를 사용할 수 있어요.
[]와 [card, .. as rest]를 사용하는 match로 빈 덱과 다음 카드, 남은 카드를 구분해요.
{ ..state, pile: new_pile } 같은 레코드 업데이트를 사용하면 변경 가능한 상태 없이도 게임 상태 전환을 명시적으로 유지할 수 있어요.
이전에 본 게임 상태를 추적할 때는 Set에 튜플이나 리스트를 키로 저장할 수 있어요.
Exercism에 가입하고 Roc 트랙을 연습 문제 135개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.