대부호 CPU 플레이어 만들기 Part 7: 처음 보는 판에서 재다
이 글은 Claude Fable 5.1 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.
Part 6 끝에서 적었습니다. 튜닝 뒤의 숫자는 시험 성적이 아닙니다. 그 숫자를 보고 가중치를 골랐으니까요. 시험은 미리 정한 문제 로, 한 번만, 처음 보는 딜 에서 봅니다. 이번 편이 그 시험이고, 시리즈의 마지막입니다.
1. 시험 문제를 열기 전에#
1.1 합격선을 먼저 커밋한다#
held-out1 시험을 돌리기 전에 합격선을 문서에 적고 커밋해서 push 했습니다. 결과를 본 뒤에 조건을 고치고 싶어지는 것은 사람의 일이고, 그것을 막는 가장 단순한 방법은 결과를 보기 전의 기록을 남기는 것입니다.
flowchart LR
A["합격선 6개를<br/>문서에 적고 커밋 · push"] --> B["held-out seed<br/>3,000,001 ~ 3,000,128<br/>한 번만 실행"]
B --> C{"건강 조건<br/>대체 · 미완주 · 불변식 = 0?"}
C -- "아니오" --> D["판정 불가<br/>미리 정한 절차로 재시도<br/>(같은 seed 를 다시 쓰지 않음)"]
C -- "예" --> E{"여섯 조건<br/>모두 통과?"}
E -- "예" --> F["기본 봇으로 승격"]
E -- "아니오" --> G["승격하지 않음<br/>easy 가 그대로 기본"]
style A fill:#FFD700,color:#000000
style B fill:#D3D3D3,color:#000000
style D fill:#FF9999,color:#000000
style F fill:#90EE90,color:#000000
style G fill:#FF9999,color:#000000
조건은 여섯입니다.
| 조건 | 한도 | 왜 |
|---|---|---|
| 건강 — easy 대체·미완주·불변식2 위반 | 0 | 하나라도 있으면 성적을 내지 않고 판정 불가 |
| 전체 Δ3 의 95% 신뢰구간4 상한 | < 0 | 우연으로 좋아 보이는 것을 받지 않는다 |
| 4인 Δ 점 추정5 | ≤ −0.15 | 약한 hard 를 기본값으로 올리지 않는다 |
| 4·5·6인 각각의 신뢰구간 상한 | < 0 | 한 인원에서만 센 봇을 막는다 |
| 아홉 칸 모두의 점 추정 | ≤ +0.10 | 어느 칸에서도 easy 보다 뚜렷이 나쁘지 않다 |
| hard 결정 시간 p996 (시야 포함) | ≤ 50ms | 운영에서 사람을 기다리게 하지 않는다 |
셋째 줄의 −0.15 가 Part 1 에서 말한 숫자입니다. 달무티 hard v1 은 easy 대비 −0.07 로 기본 봇이 됐고, 숙련자에게 한 달 만에 읽혔습니다. 그 두 배 넘는 차이가 아니면 올리지 않기로 했습니다. 넷째 줄은 Part 6 의 실험을 다 본 뒤, 그러나 held-out 을 돌리기 전에 더한 조건입니다. 인원별로 따로 신뢰구간을 요구할지는 처음 문서에서 “실행 전에 정한다” 로 남겨 두었던 것입니다.
판정 불가가 나왔을 때 어떻게 할지도 미리 정했습니다. 같은 seed7 로 다시 돌리지 않고, 일꾼8 수를 줄여 기계 경합을 없앤 뒤 다른 namespace9 의 seed 를 씁니다. 결과를 본 뒤 유리한 표본만 늘리는 길을 막기 위해서입니다.
1.2 seed 의 영역을 나눈다#
튜닝에 쓴 seed 는 1~999,999 영역, held-out 은 3,000,001 부터입니다. 달무티의 held-out(1,000,001·2,000,001 부터)과도 겹치지 않게 했습니다. 영역을 나누는 것은 “이 seed 는 한 번도 본 적이 없다” 를 말로 하는 것이 아니라 번호로 보증 하기 위해서입니다.
2. 결과#
2026년 10월 5일, hard-3 대 easy 를 held-out seed 128개에서 한 번 돌렸습니다. 아홉 칸 × 양방향 × 자리 회전 = 92,160판, 2분 47초.
2.1 판정 — 통과#
| 조건 | 값 | 한도 | |
|---|---|---|---|
| 건강 | 0 | 0 | ✅ |
| 전체 Δ 신뢰구간 상한 | −0.731 | < 0 | ✅ |
| 4인 Δ 점 추정 | −0.689 | ≤ −0.15 | ✅ |
| 4인 신뢰구간 상한 | −0.666 | < 0 | ✅ |
| 5인 신뢰구간 상한 | −0.727 | < 0 | ✅ |
| 6인 신뢰구간 상한 | −0.769 | < 0 | ✅ |
| 아홉 칸 중 가장 큰 점 추정 | −0.687 (4인 12판) | ≤ +0.10 | ✅ |
| hard 결정 p99 | 1.49ms | ≤ 50ms | ✅ |
2.2 필수 보고 — 방향별#
| 범위 | Δ [95% CI] | 방향 A (hard 1 + easy N−1) | 방향 B (easy 1 + hard N−1) |
|---|---|---|---|
| 전체 | −0.746 [−0.761, −0.731] | −0.841 | −0.650 |
| 4인 | −0.689 [−0.712, −0.666] | −0.790 | −0.588 |
| 5인 | −0.751 [−0.776, −0.726] | −0.833 | −0.669 |
| 6인 | −0.797 [−0.826, −0.769] | −0.901 | −0.694 |
두 방향이 모두 음수이고, 방향 A 가 더 큽니다. hard 하나가 easy 셋 사이에 앉으면 평균 순위가 0.84 등 좋고, easy 하나가 hard 셋 사이에 앉으면 0.65 등 나쁩니다. Part 6 에서 본 “약한 봇들만 앉은 탁자” 의 왜곡은 없습니다.
2.3 칸마다 — 1등, 반칙패, 나락#
혼자 앉은 쪽의 판당 비율입니다.
| 인원 | 1등 — hard / easy | 반칙패10 — hard / easy | 나락11 — hard / easy |
|---|---|---|---|
| 4인 | 0.61 / 0.08 | 0.00 / 0.07~0.08 | 0.09 / 0.05 |
| 5인 | 0.47 / 0.06 | 0.00 / 0.07~0.08 | 0.09 / 0.04 |
| 6인 | 0.41 / 0.05~0.06 | 0.00 / 0.07~0.08 | 0.10 / 0.03 |
판 수(4·8·12판)에 따른 차이는 소수 둘째 자리 안이라 합쳤습니다.
4인 판에서 공정한 1등 비율은 25% 입니다. easy 셋 사이의 hard 는 61% 를 1등으로 끝냈고, hard 셋 사이의 easy 는 8% 였습니다. 반칙패는 hard 가 92,160판에서 한 번도 없었습니다. 나락이 hard 쪽에서 높은 것은 Part 6 에서 적은 그대로입니다. hard 가 대부호인 판이 많고, 나락은 대부호만 당합니다.
2.4 튜닝 seed 에 맞춘 흔적이 없다#
held-out 의 Δ −0.746 은 Part 6 의 확인 seed(101~164)에서 잰 −0.754 와 거의 같습니다. 튜닝에 쓴 seed 와 쓰지 않은 seed, 그리고 처음 보는 seed 에서 같은 값이 나왔습니다.
3. 눈으로 읽기#
숫자가 좋아도 봇이 이상한 수 를 두면서 이기는 것일 수 있습니다. 그래서 held-out 영역 밖의 seed(3,000,500) 하나로 “hard 1 + easy 3” 4인 매치 세 판을, hard 의 손패·필드·상대 장수·결정 단계(Part 5 의 ①~⑤)와 함께 수마다 읽었습니다.
- 리드12는 가장 약한 카드부터 털었습니다(♣3). 받기는 묶음을 깨지 않는 수로 받았습니다(♠10, 그리고 ♣8 로 8컷13).
- 2 를 아끼다가 확정 종료 줄14 에서 썼습니다. 2 석 장 → ♣K 로 정상 종료.
- 조커가 든 계단15을 마지막 직전 에 내고 ♦A 로 끝냈습니다. 조커로 끝내면 반칙입니다.
- 확정 줄은 8 쌍의 8컷과 아무도 못 받는 9 석 장으로 리드를 되찾았습니다.
- 조공16은 가장 약한 3·4 를 돌려줬습니다.
받을 수 있는데 이유 없이 패스한 수, 반칙 종료로 가는 수는 없었습니다. 패스처럼 보인 한 국면은 ♠ 깔맞춤17이 걸려 ♣2 로 받을 수 없던 자리였습니다. 사람이 보기에도 설명이 되는 수들이었습니다.
이것은 세 판입니다. 증명이 아니라 표본 조사 이고, Part 4 에서 “7장까지 완전탐색18과 맞췄으니 13장에서도 맞을 것” 이라 적은 추론을 긴 손패에서 한 번 더 눈으로 확인한 것입니다.
4. 승격#
통과했으므로 혼자하기의 기본 봇을 hard 로 바꿨습니다.
PolicyDefault가hard입니다. 혼자하기를 열면 봇 3~5명이 모두 hard 입니다.- easy 는 지워지지 않습니다. Part 1 의 fallback 이자, 다른 세 게임과 같은 숨은 뒷길(
/games/fugohime/solo/new?easy=true)로 고를 수 있습니다. - 봇이 둔 수마다
hard·hard-3이 기록됩니다. easy 가 대신 둔 수가 있으면 그 사유도 함께 남습니다. - 모르는 정책 이름으로 혼자하기를 시작하려 하면 서버가 거절합니다.
그리고 기준선을 얼립니다. hard-3 의 코드·가중치·장부·계획기는 이 시점의 것으로 고정이고, 기준 결정 fixture19 60 국면이 그것을 지킵니다. 앞으로 hard 를 고치는 PR 은 easy 가 아니라 “후보 1 + hard-3 N−1” 맞대결을 시험으로 봅니다. 달무티 Part 8 에서 한 것과 같은 방식입니다.
5. 말할 수 있는 것과 아직 말할 수 없는 것#
이 시험으로 말할 수 있는 것은 정확히 이것입니다.
easy 봇만 앉은 탁자에서, 한 번도 쓰지 않은 딜 128개 × 아홉 칸에서, hard 는 easy 보다 판당 평균 순위가 0.75 등 좋다.
말할 수 없는 것이 더 많습니다.
사람 상대 강도는 모릅니다. 대부호는 2026년 10월 3일에 공개됐고, 이 글을 쓰는 시점에 사람 대 봇 기록이 거의 없습니다. 달무티에서는 봇끼리의 성적이 좋았어도 사람이 열 판 중 여섯 판을 이겼습니다. 봇끼리 대칭으로 앉힌 결과는 사람에게 읽히지 않는다는 결론을 주지 않습니다. 기록이 쌓이면 달무티 Part 6·Part 7 처럼 사람의 1등 비율과 사람 결정 재생으로 다시 봐야 합니다. 그때 쓸 재료(딜·조공·낸 수의 종류·봇 정책 버전)가 보존되는지는 이번에 확인해 두었습니다.
상대가 easy 뿐입니다. hard 가 hard 를 상대로 어떤지는 Part 6 의 맞대결에서 변형끼리만 봤습니다. 다른 설계의 봇, 예컨대 몬테카를로로 상대 손패를 뽑아 끝까지 두어 보는 봇과 견준 것이 아닙니다.
위험 점수는 작은 상태에서만 정답과 맞췄습니다. Part 3 에서 적은 대로 큰 상태의 오차는 검증하지 않았습니다. 그래서 봇은 그것을 확률로 쓰지 않고 순서와 확정 안전만 씁니다.
6. 남은 것#
- 통제20와 온존21이 묶여 있습니다. Part 6 에서 좌표 하강22이 이 쌍을 옮기지 못했습니다. 둘을 한 축으로 묶어 움직이거나, 정의를 갈라야 합니다.
- 위협 항의 정의. 지웠지만 생각은 맞았습니다. “상대가 이 필드로 끝낼 수 있는가” 에 정상 종료인가 를 더해 다시 정의하면 달라질 수 있습니다. 다시 넣으려면 Part 6 의 켜기 실험부터입니다.
- 패스 기록을 쓰지 않습니다. 장부는 자리마다 패스한 필드를 적어 두지만 확률에는 섞지 않습니다. 다음 필드에서 “그 자리에 센 싱글이 없다” 는 약한 증거로 쓸 수 있고, 그 무게는 실험으로 정해야 합니다.
- 깔맞춤 선호가 따로 없습니다. 내 수가 거는 깔맞춤은 통제 항의 위험 점수에 이미 들어가지만(응수자가 그 무늬로만 받아야 하니까), “곧 끝낼 수 있을 때 깔맞춤을 걸어 두는” 종류의 판단은 없습니다.
- 사람 결정 재생. 기록이 쌓이면 합니다.
마치며#
PR 열다섯 개, 나흘이었습니다. easy 봇이 혼자하기와 함께 나온 것이 10월 2일, hard 가 held-out 을 통과해 기본 봇이 된 것이 10월 5일 새벽입니다. 그 사이의 순서는 Part 1 의 그림 그대로였습니다. 문서, 시뮬레이터, 장부, 계획, hard 1차, 실험과 튜닝, 시험.
일곱 편을 한 문장씩으로 줄이면 이렇습니다.
| 편 | 한 문장 |
|---|---|
| Part 1 | 대부호는 패스·8컷·혁명·깔맞춤·♠3·계단·반칙패·나락·블라인드·조공 때문에 달무티 봇의 판단을 그대로 못 쓴다 |
| Part 2 | easy 는 규칙 네 줄이고 시간 초과 자동 수와 같은 함수이며, 열 판에 한 판을 반칙패로 끝낸다 |
| Part 3 | hard 는 더 보지 않고 잊지 않는다 — 54장을 네 칸에 나눈 장부와, 확률이라 부르지 않는 위험 점수 |
| Part 4 | 반칙은 마지막 수와 그때의 혁명 상태로 정해지므로, 혁명 묶음의 홀짝 하나로 손패 계획을 세울 수 있다 |
| Part 5 | hard 는 끝낼 수 있으면 끝내고, 끝내지 못하게 되는 수를 버리고, 그 뒤에야 easy 의 수를 기본으로 점수를 본다 |
| Part 6 | 규칙 세 개가 개선의 91% 였고, 두 항은 해로워 지웠고, easy 에서 바꾸는 문턱은 네 배로 올랐다 |
| Part 7 | 처음 보는 딜 128개에서 easy 대비 −0.746, 4인 1등 61%, 반칙패 0 — 통과. 사람 상대는 아직 모른다 |
이 시리즈의 가장 큰 교훈은 Part 6 의 한 줄입니다. 대부호에서 hard 를 만드는 일의 본체는 정교한 점수가 아니라, 끝낼 수 있는 길을 처음부터 지키는 것이었습니다. 점수 층은 그 위에 얹은 1/10 입니다. 규칙 입문에서 “대부호에서 2 와 조커는 종반 직전에 리드를 가져오는 데 쓰고, 마지막 한 수는 K 나 A 같은 강하지만 합법인 카드로 남겨야 한다” 고 적었는데, 봇이 숫자로 같은 말을 했습니다.
사람 상대 성적은 기록이 쌓이면 다시 씁니다. 그때까지는, 혼자하기에서 hard 셋을 상대로 한 판 해 보시고 어디서 봇이 읽히는지 알려 주시면 그것이 다음 글의 재료가 됩니다.
시리즈 목록#
- 대부호 CPU 플레이어 만들기 Part 1: 달무티 봇을 그대로 못 쓰는 이유
- 대부호 CPU 플레이어 만들기 Part 2: 규칙 네 줄짜리 easy 봇
- 대부호 CPU 플레이어 만들기 Part 3: 본 것을 잊지 않는 장부
- 대부호 CPU 플레이어 만들기 Part 4: 혁명을 따라가는 손패 계획
- 대부호 CPU 플레이어 만들기 Part 5: easy 와 hard 는 어디서 갈라지는가
- 대부호 CPU 플레이어 만들기 Part 6: 항목을 하나씩 꺼 보니
- 대부호 CPU 플레이어 만들기 Part 7: 처음 보는 판에서 재다 (이 글)
References#
- 달무티를 아는 사람을 위한 대부호(大富豪) 입문 — 이 시리즈가 쓰는 규칙
- 달무티 CPU 플레이어 만들기 Part 6 · Part 7 · Part 8 — 사람 상대 측정, 결정 재생, 얼린 기준선과의 맞대결
- 훌라 CPU 플레이어 만들기 Part 7 · Part 8 — 시뮬레이터가 다른 봇을 재던 사고, 사전 등록한 합격선
- ASCII.jp, 日本発の世界統一ルール! 大人が大富豪で大盛り上がり · ねとらぼ, これで遊べば「大富豪のローカルルール多過ぎ問題」が解決 — 일본대부호연맹 룰(조커 2장·4인·블라인드·계단·반칙 올라가기 카드)의 출처
held-out(따로 떼어 둔 시험 문제): 봇을 만들고 조정하는 동안 한 번도 쓰지 않고 남겨 둔 판(딜)들입니다. 공부할 때 본 문제로 시험을 보면 실력을 알 수 없으므로, 마지막 시험은 처음 보는 문제로만 봅니다. ↩︎
불변식: 어떤 일이 일어나도 늘 참이어야 하는 조건입니다. 54장이 보존된다, 순위가 1~N 을 빠짐없이 한 번씩 쓴다 같은 것이고, 깨지면 프로그램 어딘가가 틀린 것입니다. ↩︎
Δ(델타): 두 봇의 «판당 평균 순위» 차이입니다. 음수면 후보 봇의 순위가 더 좋다는 뜻이고, −0.5 는 평균 반 등 앞선다는 뜻입니다. ↩︎
95% 신뢰구간: 측정한 값이 우연에 따라 흔들릴 수 있는 범위입니다. «상한이 0 보다 작다» 는 것은 운이 나쁜 쪽으로 봐도 여전히 후보가 낫다는 뜻입니다. ↩︎
점 추정: 범위(신뢰구간)가 아니라 측정한 값 하나(평균)를 말합니다. ↩︎
p99: 측정값을 작은 순으로 줄 세웠을 때 99% 지점의 값입니다. «결정 시간 p99 1.5ms» 는 결정 100번 중 99번이 1.5ms 안에 끝났다는 뜻으로, 평균보다 «느린 경우» 를 보는 데 씁니다. ↩︎
seed: 무작위로 섞은 덱을 다시 똑같이 만들어 내기 위한 번호입니다. 같은 seed 는 언제 돌려도 같은 순서로 카드가 나뉘므로, «같은 딜에서 봇만 바꿔 앉혀» 비교할 수 있습니다. ↩︎
일꾼(worker): 시뮬레이션을 동시에 나눠 돌리는 실행 단위입니다. 일꾼이 넷이면 네 판을 동시에 돕니다. ↩︎
namespace: seed 번호의 «영역» 입니다. 조정용과 시험용 번호 영역을 미리 나눠 두면, 시험에 쓴 seed 를 조정에 썼는지를 번호만 보고 알 수 있습니다. ↩︎
반칙패: 조커·2(혁명 중에는 3)·8 만으로 된 수·♠3 한 장으로 손패를 비우면 그 판 꼴찌가 되는 규칙입니다. ↩︎
나락: 지난 판 대부호가 남아 있는데 다른 사람이 먼저 손패를 비우면 대부호가 그 즉시 탈락해 꼴찌가 되는 규칙입니다. ↩︎
리드 / 받기 / 필드: 필드는 테이블에 놓여 있는 지금의 카드(들)입니다. 필드가 비어 있을 때 첫 수를 내는 것이 «리드», 놓인 필드보다 센 수로 이어 내는 것이 «받기» 입니다. 모두가 패스하면 필드가 정리되고 마지막에 낸 사람이 다시 리드합니다. ↩︎
8컷: 8 이 든 싱글이나 같은 숫자 묶음을 내면 즉시 필드가 정리되고 낸 사람이 다시 리드하는 규칙입니다(계단 속 8 은 예외). ↩︎
확정 종료 줄: 첫 수부터 마지막 수까지 아무도 끼어들 수 없다는 것이 증명된, 손패를 합법으로 다 비우는 순서입니다. Part 4 에서 설명합니다. ↩︎
계단: 같은 무늬의 연속한 숫자 3장 이상(예: ♠5 ♠6 ♠7)을 한 수로 내는 조합입니다. 조커가 빈 자리를 채울 수 있습니다. ↩︎
조공: 판을 시작할 때 아래 계급이 위 계급에게 가장 강한 카드를 주고, 위 계급은 자기 손패에서 같은 장수를 골라 돌려주는 규칙입니다. ↩︎
깔맞춤: 같은 무늬의 수가 두 번 이어지면 그 필드에서는 그 무늬로만 받을 수 있게 되는 규칙입니다. ↩︎
완전탐색: 가능한 경우를 하나도 빼지 않고 전부 세어 정답을 구하는 방법입니다. 느리지만 틀릴 수 없어서, 빠른 근사 계산이 맞는지 확인하는 «정답지» 로 씁니다. ↩︎
fixture(기준 결정 고정 파일): 미리 정해 둔 상황 60개에서 봇이 낸 수를 적어 둔 파일입니다. 코드를 고쳐 수가 달라지면 테스트가 실패해 «행동이 바뀌었다» 를 알려 줍니다. ↩︎
통제(Control): 이 수가 필드에 놓인 뒤 아무도 받지 못해 다음 리드가 다시 내게 올 가능성을 재는 점수 항입니다. 장부의 위험 점수를 1 에서 뺀 값이고, 8컷처럼 필드를 바로 정리하는 수는 1 입니다. ↩︎
온존(溫存): 강한 카드를 아껴 두는 것입니다. 이 시리즈에서는 «지금 서열의 상위 카드와 조커를 쓰는 비용» 을 재는 점수 항으로, 손패가 많을수록 비용이 크고 종반에는 거의 0 이 됩니다. ↩︎
좌표 하강: 조정할 값이 여럿일 때 한 번에 하나만 바꿔 보고, 좋아지면 옮기고, 다음 값으로 넘어가는 것을 되풀이하는 단순한 조정 방법입니다. ↩︎