훌라 초보자 전략 가이드 Part 6: 한 판 복기와 초보 체크리스트
이 글은 Claude Fable 5.1 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.
다섯 편의 원칙 가운데 한 판에 나올 수 있는 것들을 모읍니다. 4인 게임이고 나는 좌석 1, 상대는 좌석 순서대로 B · C · D 입니다. 선은 B 입니다. 카드는 전부 손으로 설계한 것이고, 상대의 손패는 그 사람이 공개한 것만 적습니다.
1. 한 판 복기#
딜 — 받은 일곱 장을 분류한다#
| 무리 | 카드 |
|---|---|
| 조합 | 없음 |
| 조각 | (3♥ 또는 6♥), (9 한 장) |
| 반쯤 조각 | (7♥ 가 혼자 놓이면 붙는다) |
| 홑 카드 |
Part 1 의 분류입니다. 조각을 채우는 아웃은 넉 장(3♥ 6♥ 9♥ 9♣)이고, 7 을 뽑으면 단독 등록, 10♦ 를 뽑으면 9♦-10♦-J♦ 가 되니 어떤 형태로든 첫 등록을 여는 아웃은 아홉 장입니다. 는 완전한 홑 카드가 아니라 9♦ 와 10♦ 한 장을 사이에 둔 조각이기도 합니다. 확정 스톱 자격은 없습니다(합 59).
첫 공개 카드 — 6♥ 가 뒤집혔다#
첫 공개 카드는 선 B 부터 우선권이 돕니다. B · C · D 가 부르지 않았고 내가 불렀습니다. 독박이 없는 땡큐이므로 로우·하이 자격만 없으면 부릅니다(Part 4 §4).
내 첫 턴 — 등록하고 버린다#
를 가져왔으니 RUN 4-5-6♥ 를 즉시 등록해야 합니다. 가져온 카드가 이 턴의 뽑기를 대신하니 덱에서는 뽑지 않습니다. 다른 등록 후보는 없습니다.
버릴 후보는 홑 카드 와 입니다(Part 3 원칙 1). 는 7♥ 가 아직 안 보였으니 뒤로 미룹니다. 첫 턴이라 상대 셋이 모두 미등록이지만 아직 아무 카드도 공개되지 않아 누구에게 무엇이 땡큐가 될지 가릴 정보가 없으니 값으로 정합니다. 비싼 를 버립니다. 손패는 네 장, 카드합 37 입니다.
한 가지 짚어 둘 것이 있습니다. 여기서 를 버리면 안 됩니다. 9 가 비싸 보여도 는 조각입니다. 다음 절에서 이 조각이 판을 결정합니다.
B · C · D 의 턴#
| 좌석 | 한 일 | 그 뒤 손패 |
|---|---|---|
| B | 뽑고, SET 등록, 버림 | 4장, 등록함 |
| C | 뽑고, 버림 | 7장, 미등록 |
| D | 뽑고, 등록, 버림 | 6장, 등록함 |
B 가 버린 와 C 가 버린 는 내 손패와 아무 조합도 안 됩니다. 그런데 D 가 를 버렸습니다.
땡큐 — 좌석 순서로 내가 1순위#
D 의 다음 자리는 좌석 1, 나입니다. 누가 같이 부르든 내 것입니다(Part 4 원칙 3). 부릅니다. SET 9 를 등록하고 손패는 두 장이 됩니다.
D 의 입장에서 보면 는 위험한 카드였습니다. 9 가 한 장도 공개되지 않아 SET 방향이 열려 있었고, 바로 다음 자리인 내가 9 를 두 장 들고 있었습니다. 두 방향이 모두 막힌 카드가 손에 있었다면 그쪽이 먼저고, 한쪽만 막힌 카드는 그다음이었습니다(Part 3 §4).
가로챈 턴 — 뽑지 않고 버린다#
땡큐로 가져온 가 이 턴의 뽑기입니다. 덱에서는 뽑지 않습니다. 등록할 것도 붙일 것도 더 없으니 와 중 한 장을 버립니다.
다음 자리 B 는 등록했고 네 장이니 독박은 없습니다. C 는 미등록으로 일곱 장을 들고 있는데 아직 첫 바퀴라 훌라를 완성해 두었을 가능성은 낮습니다. 값으로는 가 먼저이고, 는 의 이웃 입니다. 테이블에 은 나왔어도 7♥ 는 아직입니다. 누가 7♥ 를 혼자 놓는 순간 는 한 장으로 붙습니다(Part 3 의 “6 과 8 은 반쯤 조각”). 그래서 를 버립니다. 손패는 한 장, 카드합 8 입니다.
만약 C 가 등록할 수 있었을 텐데도 여러 바퀴를 미등록으로 버티고 있다면 순서가 바뀝니다. 먼저 C 에게 땡큐가 되는지인데, J 도 8 도 한 장도 공개되지 않아 SET 방향이 둘 다 열려 있습니다. RUN 방향은 가 9-10-J (9♦ 가 내 SET 에 있어 불가) · 10-J-Q · J-Q-K, 가 6-7-8 (6♥ 가 내 RUN 에 있어 불가) · 7-8-9 · 8-9-10 이라 각각 두 경로가 열려 있습니다. 둘 다 안전하지 않고 더 안전한 쪽도 없으니 6·8 규칙보다 값이 앞섭니다. 를 버리는 결론은 같지만 이유가 다릅니다. 독박 위험, 그다음 6·8 규칙, 그다음 값 의 순서입니다.
마지막 바퀴 — B · C · D#
| 좌석 | 한 일 | 그 뒤 손패 |
|---|---|---|
| B | 뽑고, 버림 | 4장, 등록함 |
| C | 뽑고, 버림 | 7장, 미등록 |
| D | 뽑은 을 자기 에 붙이고, 버림 | 5장, 등록함 |
세 장 모두 내 와 조합이 안 됩니다. D 가 8♣ 을 7♣ 에 붙인 것이 Part 3 의 “6 과 8 은 반쯤 조각” 그 장면입니다. 7♥ 는 아직 나오지 않았습니다.
내 다음 턴 — 뽑지 않고 스톱#
내 차례입니다. 손패 한 장, 카드합 8, 등록한 상태. 일반 스톱 자격입니다. 먼저 Part 5 의 확정 구간을 봅니다.
| 상대 | 남은 장수 | 최소 가능 합 | 판정 |
|---|---|---|---|
| B | 4장 | 4 | 확정 아님. 표: 내 합 8 대 4장은 >99% |
| C | 7장 | 10 | 확정. 8 보다 낮거나 같을 수 없음 |
| D | 5장 | 6 | 확정 아님. 표: 내 합 8 대 5장은 >99% |
C 는 확정이고, B 와 D 는 표에서 99% 를 넘습니다. 둘을 곱해도 99% 언저리입니다. 기다리는 쪽의 길은 생각보다 많습니다. 뽑은 카드를 처리하고 를 마지막으로 버려도 끝나기 때문입니다. 7 을 뽑으면 단독 등록(세 장), 는 내 RUN 앞에, 는 D 의 7-8♣ 앞에, 는 내 SET 9 에 붙이고 8♥ 를 버리면 됩니다. 여섯 장이고, 내가 아는 카드 19장을 뺀 33장 중 여섯이라 다음 뽑기로 끝날 확률은 18% 안팎입니다. 그래도 지금 걸면 99% 를 넘으니 스톱이 낫습니다. 기다림의 값을 낮게 세어서 스톱을 고르는 것이 아니라, 제대로 세어도 스톱이 앞섭니다. 지금 스톱 입니다(Part 5 §2.6). 뽑기 전에 선언합니다.
정산 — 손패를 공개해 검산한다#
스톱을 걸면 전원이 손패를 공개합니다.
| 좌석 | 카드합 | 등수 | 계산 | 점수 |
|---|---|---|---|---|
| 나 | 8 | 1등 | 아래 셋의 합 | +11 |
| B | 29 | 2등 | 기본 1 | -1 |
| D | 31 | 3등 | 기본 2 | -2 |
| C | 56 | 4등 (미등록이라 맨 아래) | 기본 4 × 미등록박 2 | -8 |
계산 순서는 등수별 기본 부담 → 판 배율 → 개인 박 입니다. 이 판은 일반 승리라 배율 1 이고, C 에게만 미등록박이 붙었습니다. C 손에 7 이 없으니 세븐박은 없습니다.
1등의 몫 +7 이 미등록박 덕에 +11 이 됐습니다. C 가 손에 7 을 한 장 들고 있었다면 -8 에 세븐박 2배가 곱해져 -16 이고 내 몫은 +19 입니다. 이 판에서 내가 한 것은 첫 공개 카드의 땡큐, 조각을 지킨 버리기, 1순위 땡큐, 7 의 이웃을 남긴 버리기, 한 장 남았을 때의 스톱 다섯 가지입니다. 어느 것도 어렵지 않고, 어느 것도 “가장 긴 조합을 등록하고 가장 비싼 카드를 버린다” 만으로는 나오지 않습니다.
2. 턴마다 보는 체크리스트#
[뽑기 전]
미등록이고 합 22 이하 / 76 이상인가 → 뽑은 뒤 손패를 다 비울 수 있으면 훌라, 아니면 등록하지 않는다
(아래 [뽑은 뒤]의 등록 단계를 건너뛴다). 두 번째 턴부터 로우·하이
확정 스톱 자격이 있는가 (총통 / 두 번째 턴 이후 미등록 로우·하이)
등록했고 합 15 이하인가 → 내 합 < 상대 전원 최소 가능 합이면 확정 스톱. 아니면 Part 5 표
[뽑은 뒤]
7 이 있는가 → 내려놓는다 (세 장이면 같은 턴에 붙일 6·8 이 있을 때 따로, 없으면 SET. RUN 안에 있으면 RUN)
등록 후보가 여럿인가 → 남는 카드를 보고 이 턴에 가장 많이 터는 순서로
등록했는가 → 붙일 수 있는 카드는 지금 붙인다
[버리기 전]
미등록 상대가 있는가 (다음 자리부터 전원) → 그 상대에게 땡큐가 될 카드는 가능한 한 피한다
죽은 조각이 있는가 → 홑 카드로 재분류하고 값·위험으로 비교한다
홑 카드 중 위험하지 않은 것 중 가장 비싼 것
[남의 차례]
버림패 + 내 손패 2장이 조합인가 → 부른다
단, 로우·하이 자격을 지키는 중이거나 한 장이면 훌라인 손패는 참는다
가져가면 손패가 비는가 → 무조건 부른다
처음 몇 판은 위 줄을 다 볼 수 없습니다. 한 판에는 7 과 버리기 만, 다음 판에는 땡큐 만, 그다음에는 스톱 만 보는 식으로 하나씩 늘리면 됩니다.
3. 흔한 실수 열 가지#
| # | 실수 | 어느 편 |
|---|---|---|
| 1 | 7 두 장을 SET 이 될 때까지 쥔다 | 1, 2 |
| 2 | 넉 장짜리 조합을 통째로 내려놓아 다른 조합의 다리를 없앤다 | 2 |
| 3 | 등록하기 전에 남는 카드를 보지 않고 눈에 먼저 띈 조합을 내려놓는다 | 2 |
| 4 | 붙일 수 있는 카드를 손에 쥐고 있는다 | 3 |
| 5 | 가장 비싼 카드라는 이유로 조각을 버린다 | 3 |
| 6 | 짝이 전부 공개된 죽은 조각을 살아 있다고 믿고 지키다 다른 조각을 깬다 | 3 |
| 7 | 더 안전한 카드가 있는데 위험을 확인하지 않고 미등록 상대에게 땡큐가 될 카드를 버린다 (독박) | 3 |
| 8 | 땡큐 자격이 생겼는데 “조합이 작다” 며 참는다 | 4 |
| 9 | 카드합 15 이하라는 이유만으로, 상대 장수와 최소 가능 합을 보지 않고 스톱을 건다 (스톱박) | 5 |
| 10 | 한 장만 오면 훌라라며 7 을 든 채 몇 턴을 기다린다 | 5 |
4. 인원이 다르면#
이 시리즈는 4인 기준입니다. 3인과 5인에서 달라지는 것은 셋입니다.
| 3인 | 5인 | |
|---|---|---|
| 1등의 몫 (박·동점 없을 때) | +3 | +15 |
| 일반 스톱 성공 조건 | 상대 둘 다 나보다 높아야 함. 성공률이 4인보다 높음 | 상대 넷 모두. 한 명만 낮아도 실패라 성공률이 뚝 떨어짐 |
| 스톱박 (박·동점 없을 때) | -3 | -15. 실패의 값이 큼 |
| 땡큐 | 한 바퀴에 남의 버림패가 두 번뿐이지만 나 외의 경쟁자가 하나뿐 | 한 바퀴에 남의 버림패가 네 번이고, 나 외의 경쟁자가 셋이라 우선권이 뒤로 밀리기 쉬움. 내가 꼴찌인 버림패는 인원과 무관하게 바로 다음 사람의 것 |
5인에서는 일반 스톱을 4인보다 훨씬 보수적으로 걸어야 합니다. 확정 구간이 아니면 상대 넷 전원이 4장 이상일 때만 겁니다. 3인에서는 상대가 둘뿐이라 같은 표로도 성공률이 높게 나옵니다.
봇 실험에서는. hard 봇이 easy 봇보다 얼마나 더 버는지를 인원별로 재면 3인 판당 약 +3.6, 4인 약 +8.2, 5인 약 +18.1 이었습니다. 인원이 늘수록 판단의 값이 커집니다. 1등의 몫과 스톱박이 함께 커지기 때문입니다.
5. 다음 단계#
이 여섯 편은 큰 손실을 피하는 법입니다. 그다음은 상대를 읽는 것입니다. 상대가 땡큐로 무엇을 가져갔는지, 어느 무늬를 계속 버리는지, 미등록으로 몇 장을 버티는지를 세면 버리기와 스톱의 정확도가 한 단계 오릅니다. 그리고 그 셈을 컴퓨터가 하면 어떻게 되는지가 궁금하시면 훌라 CPU 플레이어 만들기 시리즈로 이어집니다. 이 가이드의 “봇 실험에서는” 상자가 전부 거기서 왔습니다.
부록: 확률표 스크립트#
Part 5 의 표를 만든 스크립트입니다. 표준 라이브러리만 씁니다. 상대 한 명의 카드합 분포는 경우의 수를 전수로 세고(균등), 치우친 가정은 낮은 카드에 무게를 준 가중 추출을 6만 회 표본으로 구합니다. 실행에 9초쯤 걸립니다.
#!/usr/bin/env python3
"""훌라 초보자 전략 가이드의 암기용 표를 계산한다. 표준 라이브러리만 쓴다.
1. 아웃 확률: 안 보이는 카드 U 장 중 아웃 k 장. 한 번 뽑을 때 / 두 번 뽑을 때 적어도 한 장.
2. 일반 스톱 성공률: 상대 한 명이 n 장을 들고 있을 때 카드합이 내 합 S 보다 클 확률.
(a) 균등 가정 — 안 보이는 카드가 표준 덱 분포(각 값 4장)에서 무작위로 뽑힌다. 전수 계산(DP).
(b) 치우친 가정 — 상대는 높은 카드를 먼저 버리므로 낮은 카드일수록 손에 남을 무게가 크다.
무게 w(v) = 1 + (13 - v) / 12 (2 는 K 의 약 2배). 비복원 가중 추출을 Monte Carlo 로 근사.
3. 4인 스톱 기대값: 성공 시 +7, 실패 시 -7 (스톱박 = 패자 전원 몫). 박(미등록·세븐)과 패자 간 동점이 없다고 가정.
"""
import random
from fractions import Fraction
from itertools import product
VALUES = list(range(1, 14)) # A=1 … K=13
DECK = {v: 4 for v in VALUES}
def outs_table():
print('## 아웃 확률 (안 보이는 카드 U 장, 아웃 k 장)')
print('| U | k=1 한 번 | k=2 한 번 | k=3 한 번 | k=4 한 번 | k=2 두 번 | k=4 두 번 |')
print('|---|---|---|---|---|---|---|')
for U in (40, 35, 30, 25, 20, 15, 10):
row = [str(U)]
for k in (1, 2, 3, 4):
row.append(f'{k / U * 100:.0f}%')
for k in (2, 4):
p2 = 1 - (1 - k / U) * (1 - k / (U - 1))
row.append(f'{p2 * 100:.0f}%')
print('| ' + ' | '.join(row) + ' |')
print()
def sum_dist_uniform(n, deck=DECK):
"""deck(값→장수)에서 n 장을 비복원 균등 추출했을 때 합의 분포. 경우의 수 DP."""
# dp[j][s] = j 장 골라 합 s 인 조합 수
from math import comb
dp = [dict() for _ in range(n + 1)]
dp[0][0] = 1
for v, cnt in deck.items():
for j in range(n, 0, -1):
for take in range(1, min(cnt, j) + 1):
ways = comb(cnt, take)
for s, w in list(dp[j - take].items()):
dp[j][s + v * take] = dp[j][s + v * take] + w * ways if (s + v * take) in dp[j] else w * ways
total = sum(dp[n].values())
return {s: Fraction(w, total) for s, w in dp[n].items()}
def p_opp_above_uniform(S, n):
d = sum_dist_uniform(n)
return float(sum(p for s, p in d.items() if s > S))
def p_opp_above_biased(S, n, rng, trials=60000):
cards = [v for v in VALUES for _ in range(4)]
weights = [1 + (13 - v) / 12 for v in cards]
hit = 0
for _ in range(trials):
pool = cards[:]
wts = weights[:]
total = 0
for _ in range(n):
i = rng.choices(range(len(pool)), weights=wts, k=1)[0]
total += pool[i]
pool.pop(i)
wts.pop(i)
if total > S:
hit += 1
return hit / trials
def stop_tables():
rng = random.Random(7)
print('## 상대 한 명이 내 카드합보다 높을 확률 — 균등 가정')
print('| 내 합 \\ 상대 장수 | 2장 | 3장 | 4장 | 5장 | 6장 | 7장 |')
print('|---|---|---|---|---|---|---|')
uni = {}
for S in (5, 8, 10, 12, 15):
row = [str(S)]
for n in range(2, 8):
p = p_opp_above_uniform(S, n)
uni[(S, n)] = p
row.append(f'{p * 100:.0f}%')
print('| ' + ' | '.join(row) + ' |')
print()
print('## 상대 한 명이 내 카드합보다 높을 확률 — 치우친 가정 (낮은 카드가 손에 남는다)')
print('| 내 합 \\ 상대 장수 | 2장 | 3장 | 4장 | 5장 | 6장 | 7장 |')
print('|---|---|---|---|---|---|---|')
bia = {}
for S in (5, 8, 10, 12, 15):
row = [str(S)]
for n in range(2, 8):
p = p_opp_above_biased(S, n, rng)
bia[(S, n)] = p
row.append(f'{p * 100:.0f}%')
print('| ' + ' | '.join(row) + ' |')
print()
print('## 4인: 상대 셋이 (n1,n2,n3) 장일 때 스톱 성공률 (상대별 확률의 곱 — 근사) 과 기대값 (성공 +7 / 실패 -7)')
print('| 내 합 | 상대 장수 | 균등 성공률 | 균등 기대값 | 치우침 성공률 | 치우침 기대값 |')
print('|---|---|---|---|---|---|')
for S in (8, 10, 12, 15):
for sizes in ((3, 3, 3), (2, 4, 5), (4, 5, 6), (5, 6, 7)):
pu = 1
pb = 1
for n in sizes:
pu *= uni[(S, n)]
pb *= bia[(S, n)]
print(f'| {S} | {sizes} | {pu * 100:.0f}% | {7 * (2 * pu - 1):+.1f} | {pb * 100:.0f}% | {7 * (2 * pb - 1):+.1f} |')
print()
def seven_table():
print('## 7 을 들고 판이 끝났을 때 배수 (4인 4등 -4점 기준)')
print('| 손에 남은 7 | 등록함 | 미등록 |')
print('|---|---|---|')
for k in range(0, 4):
reg = -4 * (2 ** k)
unreg = -4 * 2 * (2 ** k)
print(f'| {k}장 | {reg} | {unreg} |')
print()
if __name__ == '__main__':
outs_table()
seven_table()
stop_tables()
시리즈 목록#
- Part 1: 이 게임은 꼴찌를 피하는 게임이다
- Part 2: 등록과 붙이기 — 한 턴에 최대로 턴다
- Part 3: 버리기 — 가장 비싼 카드가 답이 아니다
- Part 4: 땡큐 — 거의 언제나 부르되, 부르지 말아야 할 때
- Part 5: 스톱과 훌라 — 걸 것인가 기다릴 것인가
- Part 6: 한 판 복기와 초보 체크리스트 (이 글)
References#
- 훌라 CPU 플레이어 만들기 Part 1 — 룰셋
- 훌라 CPU 플레이어 만들기 Part 8 — 인원별 easy 대 hard 차이 (3인 +3.6, 4인 +8.2, 5인 +18.1)
- John McLeod, Hoola