달무티 CPU 플레이어 만들기 Part 5: 코드 없이 보는 easy 봇과 hard 봇의 차이
이 글은 Claude Opus 5 를 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.
이 글은 누구를 위한 것인가#
Part 1 부터 Part 4 까지는 확실승수 판정, 패 분해 플랜, 수 평가 함수 같은 이야기로 채워져 있습니다. 달무티는 좋아하지만 개발자는 아닌 분에게는 읽을 이유가 없는 글입니다.
이번 편은 그 네 편의 결론만 을 코드와 수식 없이 옮긴 것입니다. 달무티 규칙을 아는 분이라면 누구나 읽을 수 있습니다.
미리 말씀드리면, 이 글의 결론은 “똑똑한 쪽이 이겼다"가 아닙니다. 여러 편에 걸쳐 설계한 hard 봇이 규칙 세 줄짜리 easy 봇을 겨우 0.07등 차이로 이겼습니다. 이 글의 절반은 왜 그것밖에 안 벌어졌는지에 대한 이야기입니다.
같은 작업을 렉시오에서도 했는데, 거기서는 hard 가 압도했습니다(렉시오 Part 7). 같은 사람이 비슷한 방식으로 만들었는데 결과가 이렇게 다른 이유가 이 글에서 가장 재미있는 부분입니다.
봇 이야기에 필요한 만큼만 규칙을 옮기면 이렇습니다.
- 카드는 숫자 1~12 가 있고, 숫자가 곧 장수 입니다. 1은 1장, 12는 12장, 합쳐서 78장. 여기에 광대 2장을 더해 80장 입니다
- 숫자가 작을수록 강합니다. 1이 최강, 12가 최약입니다
- 리드하는 사람이 같은 숫자를 몇 장 낼지 정하고, 뒤에 오는 사람은 정확히 같은 장수 를 더 강한(작은) 숫자 로 받아야 합니다. 못 내거나 안 내면 패스입니다
- 등수에 따라 다음 판에 세금 을 주고받습니다. 꼴찌는 가장 강한 카드를 1등에게 바칩니다
마지막 줄 때문에 달무티는 한 판을 이기는 게임이 아니라 여러 판에 걸친 게임 입니다. 한 번 밀리면 세금 때문에 계속 밀립니다.
1. 두 CPU 는 어떻게 다른가#
1.1 easy 봇 — 규칙 세 줄짜리 CPU#
easy 는 판을 읽지 않습니다. 지금 낼 수 있는 것만 보고 고정된 규칙으로 고릅니다.
flowchart TD
A["내 차례"] --> B{"테이블에<br/>카드가 깔려 있나?"}
B -- "비어 있다 (내가 리드)" --> C["손에서 가장 약한 숫자를 찾아<br/><b>그 숫자 전부</b>를 낸다"]
B -- "깔려 있다 (받아치는 상황)" --> D{"받을 수 있는<br/>수가 있나?"}
D -- "없다" --> E["패스"]
D -- "있다" --> F["광대를 안 쓰는 수 우선<br/>→ 가장 약한 숫자로 받기<br/>→ 광대를 적게 쓰는 쪽"]
C --> G["제출"]
F --> G
style A fill:#FFD700,color:#000000
style E fill:#FFB6C1,color:#000000
style G fill:#90EE90,color:#000000
세 줄로 줄이면 이렇습니다.
- 리드할 때: 가장 약한 숫자를 통째로 냅니다. 12를 다섯 장 들고 있으면 다섯 장을 한꺼번에 냅니다
- 받을 때: 이길 수 있는 가장 싼 수로 받습니다. 못 받으면 패스합니다
- 광대는 아낍니다: 광대 없이 받을 수 있으면 광대를 쓰지 않습니다
세금을 낼 때는 가장 강한 카드부터 바치고, 돌려줄 때는 가장 약한 카드부터 내려보냅니다. 혁명은 자기가 농노 계급일 때만 선언합니다.
이 규칙이 달무티에서 생각보다 훨씬 좋습니다. 뒤에서 다시 이야기하겠습니다.
1.2 hard 봇 — 점수를 매겨 고르는 CPU#
hard 는 낼 수 있는 모든 수에 점수를 매기고 가장 높은 것을 냅니다. 패스도 하나의 후보로 놓고 같이 점수를 매깁니다.
flowchart TD
A["내 차례"] --> B["판 상태 정리<br/>· 어떤 카드가 아직 안 나왔나<br/>· 각 상대가 몇 장 남았나<br/>· 누가 무엇에 패스했나"]
B --> C["내 손패를 최소 몇 번에<br/>다 낼 수 있는지 계산"]
C --> D["낼 수 있는 수를 전부 나열"]
D --> E["수마다 점수 매기기"]
E --> F["<b>더하는 것</b><br/>· 손패 비우는 계획이 좋아진다<br/>· 이 수를 아무도 못 받을 것 같다"]
E --> G["<b>빼는 것</b><br/>· 이 수를 상대가 받아칠 위험<br/>· 내 손패 정보를 흘린다"]
F --> H["합산 점수"]
G --> H
H --> I["패스에도 점수를 매겨<br/>같이 비교"]
I --> J["가장 점수가 높은 것을 선택"]
J --> K{"상대 중에 이번 트릭으로<br/>손패를 다 털 수 있는<br/>사람이 있나?"}
K -- "있다" --> L["<b>점수를 무시하고</b><br/>그 사람이 못 받는 수로 막는다"]
K -- "없다" --> M["제출 또는 패스"]
L --> M
style A fill:#FFD700,color:#000000
style F fill:#90EE90,color:#000000
style G fill:#FFB6C1,color:#000000
style L fill:#FFA07A,color:#000000
style M fill:#90EE90,color:#000000
easy 에 없는 것이 셋입니다.
첫째, 안 나온 카드를 셉니다. 달무티는 카운팅이 유난히 쉬운 게임입니다. 숫자가 곧 장수이기 때문입니다. 7이 일곱 장뿐이라는 것을 알고 있으니, 7이 다섯 장 나왔으면 남은 건 두 장입니다. hard 는 이걸로 “내가 낸 수를 상대가 받을 수 있을 확률"을 계산합니다.
둘째, 상대의 패스를 기억합니다. 이게 개인적으로 가장 마음에 드는 부분입니다.
3번 자리가 아까 두 장짜리 8 에 패스했다 → 저 사람은 8보다 강한 두 장짜리가 없다 → 앞으로 두 장짜리를 낼 때 저 사람은 위협이 아니다
사람이 달무티를 둘 때 자연스럽게 하는 추론입니다. hard 는 이걸 확률에 반영합니다. 같은 조건에서 패스한 기록이 쌓일수록 그 상대가 받을 확률을 낮춰 잡습니다.
셋째, 나가려는 사람을 막습니다. 점수 계산과 별개로 도는 안전장치입니다. 상대의 남은 장수가 지금 테이블에 깔린 장수와 정확히 같으면, 그 사람은 이번 트릭을 받는 순간 손패가 비어 1등으로 나갑니다. hard 는 이 상황을 감지하면 점수가 어떻든 그 사람이 못 받는 수를 골라 막습니다.
1.3 그런데 리드는 둘이 똑같습니다#
여기가 이 글에서 가장 중요한 대목입니다.
hard 는 리드할 때도 후보마다 점수를 다 계산합니다. 그리고 그 결과를 버리고 easy 와 똑같이 둡니다.
농담이 아니라 코드가 실제로 그렇게 되어 있습니다. 점수를 매기고, 최고점 후보를 고르고, 그다음 줄에서 그 선택을 easy 의 리드로 덮어씁니다.
이유는 Part 4 에 있습니다. 리드를 점수식에 맡겼더니 봇이 더 약해졌습니다. 여러 원인 중 가장 큰 것이었습니다. 점수식이 “지금 이 수가 얼마나 좋은가"는 잘 재는데, 리드는 그것만으로 정해지지 않습니다. 리드는 이번 트릭의 장수를 내가 정한다 는 뜻이고, 그 결정은 남은 손패 전체의 모양과 맞물립니다. 점수식이 그 맞물림을 못 봤습니다.
그래서 되돌렸습니다. 지금 hard 의 점수 계산은 받을 때와 참을 때에만 실제로 쓰입니다.
2. 어떻게 겨루게 했는가#
2.1 문제: 달무티는 운이 크다#
두 CPU 중 뭐가 나은지 재는 것은 까다롭습니다. 좋은 패를 받으면 아무렇게나 둬도 이깁니다. 게다가 달무티는 세금이 있어서 한 판 잘 풀리면 다음 판이 또 유리해집니다. 몇 판 돌려보고 “이쪽이 세 보인다"고 말하는 것은 의미가 없습니다.
2.2 해결: 같은 패를 양쪽에 똑같이 나눠준다#
브리지 대회에서 쓰는 방식입니다. 똑같은 딜을 여러 번 재사용합니다. 같은 패를 두 CPU 가 모두 잡아보게 하면 운이 상쇄되고, 좋은 패라서 이긴 것인지 잘 둬서 이긴 것인지 구분할 수 있게 됩니다.
2.3 조정용 패와 검증용 패를 갈랐다#
시험 문제로 공부하고 그 시험을 보면 점수가 잘 나오는 게 당연합니다. 가중치를 맞출 때 쓴 패로 성능을 재면 실력이 아니라 암기력을 재는 것이 됩니다.
그래서 딜을 완전히 두 묶음으로 갈랐습니다. 조정에 쓴 패는 최종 성적표에 한 번도 등장하지 않습니다.
2.4 조건을 18가지로 쪼갰다#
평균 하나만 보면 속기 쉽습니다. 어떤 조건에서 크게 이기고 다른 조건에서 조금씩 지면 평균은 이기는 것처럼 보입니다. 그런데 인원수와 판 길이는 플레이어가 고릅니다. 4인을 고른 사람에게 약한 CPU 가 붙으면 안 됩니다.
| 나눈 기준 | 값 |
|---|---|
| 인원수 | 4인 / 5인 / 6인 |
| 혁명 빈도 | 기본 / 고빈도 |
| 판 길이 | 3판 / 5판 / 10판 |
3 × 2 × 3 = 18가지 조건 이고, 각 조건마다 128개 딜을 돌려 2,304개 관측 을 얻었습니다. 그리고 18개 조건 전부에서 이겨야 합격으로 봤습니다.
3. 결과#
아래 숫자는 전부 검증용 딜 에서 나온 것입니다. 등수는 낮을수록 좋으므로 음수가 hard 의 승리 입니다.
| 구분 | hard − easy (등) |
|---|---|
| 전체 | −0.0707 |
| 4인 | −0.0985 |
| 5인 | −0.0433 |
| 6인 | −0.0705 |
| 3판 | −0.0619 |
| 5판 | −0.0874 |
| 10판 | −0.0629 |
18개 조건 전부에서 hard 가 앞섰습니다. 방향이 한 번도 뒤집히지 않았고, 통계적으로도 우연이 아닙니다.
그리고 솔직하게 말씀드리면, 0.07등은 아주 작은 차이입니다.
체감으로 옮겨 보겠습니다. 5인 게임에서 등수는 1~5등입니다. 0.07등 차이는 100판을 두면 7번 정도 한 등수 위로 올라간다 는 뜻입니다. 사람이 열 판 두어 보고 “hard 가 확실히 세네"라고 느낄 폭이 전혀 아닙니다.
렉시오와 비교하면 차이가 더 분명합니다.
| 달무티 | 렉시오 | |
|---|---|---|
hard 의 1등 비율 | 거의 차이 없음 | easy 의 두 배 이상 |
| 판이 길어지면 | 격차가 벌어지지 않음 | 격차가 벌어짐 |
| 체감 | 알아채기 어려움 | 분명히 셈 |
같은 사람이 비슷한 방식으로 만들었는데 결과가 이렇게 다릅니다.
4. 왜 이것밖에 안 벌어졌는가#
세 가지로 봅니다. 순서대로 확신의 정도가 낮아집니다.
4.1 리드가 같으니 절반은 같은 봇이다#
가장 확실한 이유입니다. §1.3 에서 본 대로 두 봇의 리드가 완전히 같습니다.
달무티에서 리드는 대단히 중요합니다. 이번 트릭에 몇 장을 낼지 정하는 권한 이기 때문입니다. 한 장씩 내는 트릭과 다섯 장씩 내는 트릭은 완전히 다른 게임이고, 그 선택이 손패를 몇 번에 비울지를 좌우합니다.
그 절반이 동일하니, 두 봇이 갈리는 지점은 받을 때와 참을 때 뿐입니다. 절반만 다른 봇끼리 겨루면 격차가 작은 것이 당연합니다.
4.2 easy 의 규칙이 달무티에 유난히 잘 맞는다#
이쪽이 더 흥미롭습니다.
easy 의 리드 규칙은 “가장 약한 숫자를 통째로 낸다” 입니다. 언뜻 무식해 보이는데, 달무티의 덱 구조 때문에 이게 꽤 좋은 수가 됩니다.
달무티는 숫자가 곧 장수입니다. 약한 숫자일수록 장수가 많습니다. 12는 열두 장, 11은 열한 장 있습니다. 그러니 손에 가장 약한 숫자는 대체로 가장 많이 들고 있는 숫자 이기도 합니다.
즉 “가장 약한 숫자를 통째로 낸다"는 규칙은 자동으로 이렇게 됩니다.
- 약한 카드부터 처리한다 ✓ (강한 카드를 아낀다)
- 한 번에 많이 턴다 ✓ (제출 횟수를 줄인다)
- 손에 어중간하게 남는 카드를 안 만든다 ✓ (숫자 하나를 통째로 비운다)
전략서에 적을 법한 원칙 셋을 규칙 한 줄이 공짜로 달성합니다. 덱 구조가 그렇게 생겼기 때문입니다.
렉시오는 다릅니다. 렉시오에서 낼 수 있는 조합은 싱글·페어·트리플·플러시 등으로 서로 얽혀 있어서, 지금 이 페어를 내면 나중에 만들 플러시가 무너집니다. 한 줄짜리 규칙으로는 도달할 수 없는 판단 이 필요하고, 그래서 계획을 세우는 hard 가 크게 앞섰습니다.
게임의 구조가 단순한 봇에게 얼마나 유리한지가 다릅니다. 이것이 두 게임의 결과 차이를 만든 가장 큰 이유라고 봅니다.
4.3 여러 판 게임이라 한 수의 영향이 희석된다#
확신이 가장 낮은 항목입니다.
달무티는 판마다 세금이 오갑니다. 한 판에서 잘 둬서 등수를 올려도, 그 이득의 상당 부분이 다음 판 세금 이라는 정해진 규칙으로 흡수됩니다. 반대로 못 둬서 밀려도 세금이 어느 정도 보상해 줍니다.
이 구조가 실력 차이를 희석시키는 쪽으로 작용할 가능성이 있습니다. 다만 이건 측정으로 확인하지 않은 추측 입니다. 판 길이별 결과(3판 −0.06, 5판 −0.09, 10판 −0.06)에서 뚜렷한 경향이 보이지 않는 것과 어긋나지는 않지만, 그것만으로 근거가 되지는 않습니다.
5. 무엇을 말할 수 있고, 무엇을 말할 수 없는가#
5.1 말할 수 있는 것#
hard는easy보다 강합니다. 18개 조건 전부에서 이겼고 통계적으로 우연이 아닙니다- 다만 그 차이는 작습니다. 0.07등이고, 사람이 체감하기 어려운 폭입니다
- 조정에 쓰지 않은 패에서 낸 성적입니다. 시험 문제를 외운 결과가 아닙니다
5.2 말할 수 없는 것#
“A 를 이겼다"와 “잘한다"는 다른 문장입니다.
사람과 겨뤄본 적이 없습니다.
easy는 규칙 세 줄짜리 CPU 입니다. 그걸 0.07등 차로 이겼다는 것이 사람 상대로 무엇을 뜻하는지는 알 수 없습니다.두 봇 모두 매번 똑같이 둡니다. 같은 상황에서 항상 같은 수를 냅니다. 사람이 여러 판 하면 패턴을 외울 수 있고, 그러면 체감 난이도는 측정치보다 훨씬 낮아집니다.
hard의 점수 항목이 옳다는 보장이 없습니다. “계획이 좋아진다”, “받힐 위험이 낮다” 같은 항목에 점수를 매겨 고르는데, 이 항목들이 달무티의 실제 좋은 수와 얼마나 맞는지는easy를 근소하게 이겼다는 사실만으로는 알 수 없습니다.리드를 점수식으로 고르는 방법을 아직 못 찾았을 뿐입니다. 지금 리드를
easy규칙에 맡긴 것은 그게 최선이라서가 아니라, 점수식으로 했더니 더 나빠졌기 때문입니다. “규칙이 이겼다"가 아니라 “점수식을 아직 제대로 못 만들었다” 로 읽는 것이 정확합니다.세금과 혁명 판단은 거의 손대지 않았습니다. 여러 판 게임에서 세금은 큰 변수인데, 두 봇의 세금 정책 차이는 크지 않습니다.
6. 앞으로 해볼 만한 것#
우선순위 순입니다.
1. 리드를 다시 공략한다. 격차가 작은 가장 큰 이유가 여기 있으니, 개선 여지도 여기 가장 많습니다. 점수식으로 안 됐다면 다른 방식 — 예를 들어 “손패를 최소 몇 번에 비울 수 있는가"를 직접 최적화하는 방식 — 을 시도해 볼 수 있습니다.
2. 사람과 겨루게 하고 그 판을 기록한다. 지금 있는 근거는 전부 CPU 대 CPU 입니다. 특히 볼 것은 사람이 CPU 의 패턴을 몇 판 만에 읽어내는가 입니다.
3. 세금과 혁명을 제대로 다룬다. 여러 판 게임에서 세금은 매 판의 출발점을 바꿉니다. 지금은 거의 고정 규칙으로 처리하고 있습니다.
4. 무작위성을 조금 넣는다. 점수가 비슷한 후보 중 가끔 다른 것을 고르게 하면 패턴 읽기가 어려워집니다. 다만 강해지려고 넣는 게 아니라 덜 지루하려고 넣는 것이므로, 얼마나 약해지는지를 재고 결정해야 합니다.
5. 난이도를 나눈다. 지금 hard 와 easy 의 차이가 0.07등이라면, 사실상 난이도 선택지가 하나뿐인 셈입니다. 격차를 먼저 벌린 다음에야 중간 난이도가 의미를 갖습니다.
마치며#
세 줄로 줄이면 이렇습니다.
첫째,
easy는 가장 약한 숫자를 통째로 내고 가장 싼 수로 받습니다.hard는 안 나온 카드를 세고, 상대의 패스를 기억하고, 나가려는 사람을 막습니다. 다만 리드할 때는 둘이 똑같이 둡니다.둘째, 같은 딜을 양쪽에 나눠주고 18가지 조건으로 쪼개 쟀습니다.
hard가 전 조건에서 이겼지만 차이는 0.07등 으로, 사람이 체감하기 어려운 폭입니다.셋째, 격차가 작은 이유는 리드가 같아서이기도 하지만, 달무티의 덱 구조가 규칙 한 줄짜리 봇에게 유난히 유리하기 때문 이기도 합니다. 숫자가 곧 장수라서, “가장 약한 숫자를 통째로 낸다"가 저절로 좋은 수가 됩니다.
가장 오래 기억할 만한 것은 세 번째 줄입니다. 좋은 봇을 만드는 난이도는 게임마다 다릅니다. 렉시오에서 통했던 접근이 달무티에서 통하지 않은 것은 접근이 나빠서가 아니라, 달무티에서는 단순한 규칙이 이미 상당히 좋은 답이었기 때문입니다.
그리고 그것을 알아낸 유일한 방법은 만들어서 재보는 것 이었습니다. 설계안만 보고 있었을 때는 hard 가 당연히 이길 줄 알았습니다.
| 편 | 다루는 것 |
|---|---|
| Part 1 | 규칙 기반 easy 봇 |
| Part 2 | hard 봇의 정보 모델과 확실승수 판정 |
| Part 3 | 수 평가와 전략적 패스 |
| Part 4 | 만들어 보니 hard 봇이 더 약했다 |
| Part 5 (이 글) | 코드 없이 보는 결론 |
References#
- 달무티 CPU 플레이어 만들기 Part 1: 규칙 기반 easy 봇
- 달무티 CPU 플레이어 만들기 Part 2: hard 봇의 정보 모델과 확실승수 판정
- 달무티 CPU 플레이어 만들기 Part 3: 수 평가와 전략적 패스
- 달무티 CPU 플레이어 만들기 Part 4: 만들어 보니 hard 봇이 더 약했다
- 렉시오 CPU 플레이어 만들기 Part 7: 코드 없이 보는 easy 봇과 hard 봇의 차이 — 같은 작업을 렉시오에서 한 결과
- 대부호부터 렉시오까지: 클라이밍 카드게임의 세계와 전략
- The Great Dalmuti (BoardGameGeek)
- Duplicate bridge (Wikipedia) — 같은 딜을 재사용해 운을 지우는 방식
- Training, validation, and test data sets (Wikipedia) — 조정용 패와 검증용 패를 가르는 이유