이 글은 Claude Opus 5 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.


Part 3 의 easy 봇은 규칙 다섯 줄로 판을 굴리고, 미등록박과 세븐박이라는 훌라 최대의 함정을 전략 없이 피합니다. 기준선으로서는 훌륭합니다.

그런데 몇 판만 붙어 보면 한계가 드러납니다. 이번 편에서는 그 한계를 여덟 자리로 정리합니다. 이것이 hard 봇의 요구사항 명세가 됩니다.

미리 못 박아 둘 것이 하나 있습니다. 아래 여덟 개는 “고치면 봇이 강해지는 것” 이 아니라 “hard 봇이 개선을 시도할 후보” 입니다. 이 구분이 왜 중요한지는 마지막 절에서 다루겠습니다.


1. 한 수 greedy 라 손패 전체를 깨뜨린다#

가장 큰 한계입니다. 실례를 보는 편이 빠릅니다. 손패 일곱 장이 이렇습니다.

3♠ 4♠ 5♠ 3♥ 3♦ 5♥ 5♦

여기서 등록 가능한 조합은 셋입니다.

후보장수등록 후 남는 카드이어서 더 털 수 있는 것
RUN 3♠ 4♠ 5♠3장3♥ 3♦ 5♥ 5♦없음. 3 두 장, 5 두 장
SET 3♠ 3♥ 3♦3장4♠ 5♠ 5♥ 5♦SET 5♠ 5♥ 5♦ — 3장 더
SET 5♠ 5♥ 5♦3장3♠ 4♠ 3♥ 3♦SET 3♠ 3♥ 3♦ — 3장 더

세 후보가 모두 3장입니다. Part 3 의 longest 는 장수만 보므로 셋을 구분하지 못하고, 서버가 만들어 준 목록에서 먼저 나온 것을 집습니다. 그 순서는 손패 입력 순서와 부분집합 열거 순서에 달려 있습니다.

RUN 을 먼저 집으면 이 턴에 세 장 을 텁니다. SET 을 먼저 집으면 여섯 장 을 텁니다. 한 장만 남으므로 다음 턴에 끝날 가능성이 크게 열립니다.

flowchart LR
    H["3♠ 4♠ 5♠<br/>3♥ 3♦ 5♥ 5♦"] --> A["RUN 3-4-5<br/>먼저 등록"]
    H --> B["SET 3-3-3<br/>먼저 등록"]
    A --> A2["남은 카드 4장<br/>더 못 텀"]
    B --> B2["SET 5-5-5<br/>이어서 등록"]
    B2 --> B3["남은 카드 1장"]
    style H fill:#87CEEB,color:#000000
    style A fill:#FFD700,color:#000000
    style B fill:#FFD700,color:#000000
    style A2 fill:#FF9999,color:#000000
    style B2 fill:#90EE90,color:#000000
    style B3 fill:#90EE90,color:#000000

가장 긴 조합 하나는 그 턴 전체에서 가장 많은 카드를 터는 계획과 다릅니다. easy 봇은 첫 행동 뒤의 상태를 비교하지 않으므로 이 차이를 원리적으로 볼 수 없습니다.

그리고 지금 이 봇이 어느 쪽을 고르는지는 아무도 설계하지 않았습니다. 결정적이기는 합니다. 같은 손패면 늘 같은 쪽을 고릅니다. 하지만 그 결정의 근거가 전략이 아니라 열거 순서입니다.


2. 등록을 붙이기보다 무조건 먼저 한다#

Part 3 의 우선순위를 다시 보면, 등록 후보가 하나라도 있으면 붙이기는 아예 보지 않습니다.

그런데 같은 카드를 쓰는 방법이 여러 가지입니다.

  • 새 조합으로 등록한다
  • 이미 놓인 조합에 붙인다
  • Part 1 에서 봤듯 7 한 장을 먼저 내려놓아 붙이기 자격을 연 뒤, 다른 카드를 거기에 잇는다

세 번째가 특히 그렇습니다. 7♠ 를 등록하면 그 조합은 6♠ 도 8♠ 도 받을 수 있는 자리가 됩니다. 손에 6♠ 가 있다면 7 을 내려놓는 행동은 “한 장 등록” 이 아니라 “두 장 배출의 첫 단계” 입니다. easy 봇은 이 셋을 같은 저울에 올리지 않습니다.


3. 등록·붙이기를 중단할 줄 모른다#

합법 후보가 있으면 반드시 실행합니다. 멈추는 선택지가 코드에 없습니다.

그런데 훌라에는 멈춰야 할 이유가 여럿 있습니다.

  • 다음 턴의 더 좋은 조합을 위해 카드를 남긴다
  • 조합을 공개하면 상대에게 붙일 자리를 준다. Part 1 에서 봤듯 내가 등록한 조합에도 남이 붙일 수 있습니다
  • 미등록 상태를 유지해 훌라(×4) 를 노린다

세 번째가 가장 큽니다. Part 1 의 배율표를 다시 보면 훌라는 4배이고, 조건은 한 번도 등록하지 않은 채 한 턴에 손패를 비우는 것 입니다. easy 봇은 등록할 수 있으면 무조건 등록하므로, 이 봇은 훌라를 원리적으로 낼 수 없습니다.

Part 3 에서 easy 봇이 미등록박을 잘 피한다고 했는데, 같은 규칙의 다른 얼굴이 이것입니다. 가장 큰 손실을 피하는 대가로 가장 큰 배당을 포기 하고 있습니다. 어느 쪽이 이득인지는 계산해 봐야 아는 문제인데, easy 봇은 계산하지 않고 한쪽을 택해 두었습니다.


4. 7 을 전략적으로 보유하지 못한다#

7 한 장은 언제나 등록 후보이므로 easy 봇은 사실상 즉시 내려놓습니다. Part 3 에서 봤듯 그 덕에 세븐박과 미등록박을 피합니다.

하지만 7 한 장을 내려놓는 결정에는 실제로 여섯 가지가 걸려 있습니다.

즉시 등록의 이익즉시 등록의 비용
손패가 한 장 준다미래 훌라(×4) 자격을 잃는다
세븐박·미등록박을 벗는다상대에게 붙일 발판을 공개한다
내 붙이기 자격이 열린다상대가 붙이면 RUN/SET 타입이 확정된다

세 번째 비용은 Part 1 에서 짚었습니다. 7♠ 에 상대가 6♠ 를 붙이는 순간 그 조합은 RUN 으로 굳어, 내가 나중에 7♥ 7♦ 를 모아 SET 으로 키우려던 계획이 막힙니다.

그리고 그 계획은 원래도 비쌌습니다. RUN 쪽은 6♠ 한 장이면 붙지만, SET 쪽은 7♥ 와 7♦ 를 한 번에 두 장 붙여야 합니다. 둘이 다 모일 때까지 손에 쥐고 있어야 한다는 뜻입니다. 같은 7♠ 를 두고 두 방향의 값이 이렇게 다릅니다.

“7 이니까 무조건 내려놓는다” 도 “7 이니까 무조건 쥔다” 도 답이 아닙니다. 상황마다 저울이 기웁니다. easy 봇은 저울을 갖고 있지 않습니다.


5. 버림패의 카드값만 본다#

Part 3 의 highestCard 는 손패에서 가장 비싼 카드를 버립니다. 근거는 “판이 갑자기 끝나면 남은 카드값이 실점” 이었고, 그 자체로는 맞습니다.

문제는 그것만 본다는 점입니다. 네 가지가 빠져 있습니다.

  • 내 조합을 완성시킬 카드인지 보지 않습니다. ♠9 를 버렸는데 그게 ♠8 ♠10 을 잇는 유일한 다리였을 수 있습니다.
  • 이미 죽은 카드인지 보지 않습니다. 값이 싸서 남겨 둔 카드가, 짝이 될 카드가 전부 남의 조합에 들어가 있어 영영 조합이 못 될 수도 있습니다.
  • 상대에게 땡큐를 주는 카드인지 보지 않습니다.
  • 아무도 쓸 수 없는 카드와 위험한 카드를 구분하지 않습니다.

세 번째와 네 번째가 훌라에서 특히 비쌉니다. Part 1 의 마지막 규칙을 떠올려 보십시오.

땡큐로 가져간 카드로 상대가 훌라 를 내면, 그 카드를 버린 사람이 독박 으로 모두의 몫을 떠안습니다.

상대가 등록을 한 번도 안 한 채 카드 두 장만 남기고 있는데, 그 두 장과 조합이 되는 카드를 던지면 어떻게 되는지 봅시다. 상대는 땡큐로 그 카드를 가져와 세 장을 한꺼번에 내려놓고 손패가 비며, 그것은 미등록 상태에서 한 턴에 손패를 비운 것 이므로 훌라입니다. ×4 가 붙고, 그 카드를 던진 사람이 전부 뭅니다.

카드 한 장을 잘못 던져 판 전체를 혼자 무는 자리 인데, easy 봇은 그 카드가 K 가 아니라는 이유로 안심하고 던집니다.


6. 공개 카드를 기억하지 않는다#

스냅샷에는 버림패 맨 위 한 장만 있습니다. 그 아래 쌓인 카드들, 그리고 지금까지 등록·붙이기로 테이블에 공개된 카드들은 봇이 따로 세지 않으면 사라집니다.

easy 봇은 세지 않습니다. 그래서 매 턴 판을 처음 보는 것처럼 판단합니다.

기억이 있으면 알 수 있는 것들이 이렇습니다.

  • 내 조합을 완성할 카드가 아직 살아 있는가, 이미 다 나왔는가
  • 이 카드를 버렸을 때 위험이 실제로 얼마인가
  • 7 이 몇 장이나 이미 나왔는가. 총통이 나올 수 있는 판인가
  • 상대가 무엇을 모으는 중인가

여기서 짚어 둘 것이 있습니다. 이건 Part 2 의 공정성 경계를 넘는 일이 아닙니다. 테이블에 공개된 카드와 버려진 카드는 그 자리에 앉은 사람이 눈으로 볼 수 있는 것입니다. 사람이 기억하기 귀찮아서 안 할 뿐입니다. 봇이 완벽하게 기억하는 것은 더 많이 보는 것이 아니라 같은 것을 더 잘 세는 것 입니다.


7. 상대의 종료 위험과 점수 구조를 보지 않는다#

스냅샷에는 상대들의 남은 장수 와 등록 여부 가 들어 있습니다. easy 봇은 이 필드를 한 번도 읽지 않습니다.

그래서 이런 상황들이 구분되지 않습니다.

  • 상대가 두 장 남았을 때와 여섯 장 남았을 때
  • 덱에 30장 남았을 때와 3장 남았을 때
  • 상대가 미등록 상태일 때(훌라 위험)와 이미 여러 조합을 깔았을 때

Part 1 의 정산 구조도 보지 않습니다. 등수 사다리가 아래로 갈수록 두 배씩 커지고, 그 위에 배율과 박이 곱해진다는 사실이 봇의 결정에 아무 영향을 주지 않습니다. easy 봇은 “빨리 손패를 비운다” 는 하나의 목표만 갖고 있고, 그건 훌라의 목적 함수가 아닙니다.

Part 1 에서 정리했던 문장을 다시 씁니다. 훌라는 더 자주 이기고도 누적 포인트가 마이너스일 수 있는 게임 입니다. 3등을 자주 하는 봇이 가끔 -32점을 무는 봇보다 낫습니다.


8. 땡큐와 스톱을 쓰지 않는다#

Part 3 에서 일부러 뺐다고 했던 것들입니다. 포기하고 있는 것을 값으로 적어 보면 이렇습니다.

포기한 것값
총통확정 승리 · 8배
로우 · 하이 스톱확정 승리 · 4배
일반 스톱유리할 때 판을 접는 선택지
땡큐남의 버림패로 조합을 완성하며 차례까지 가로채기

앞의 셋은 판단이 필요 없거나(총통·로우·하이) 계산이 필요합니다(일반 스톱). 어느 쪽이든 자기 차례에 일어나는 일 이라 Part 2 에서 정한 깨움 구조로 그대로 닿습니다. 정책 함수가 해당 커맨드를 돌려주기만 하면 됩니다.

땡큐는 다릅니다. Part 2 에서 예고한 대로, 봇은 자기 차례에만 깨어나는데 땡큐는 남의 차례에 끼어드는 행위입니다. 창은 기본 5초이고, 그 안에 자격 있는 봇을 모두 깨워 판단시키고 좌석 순서로 임자를 가려야 합니다. 정책의 문제가 아니라 배선의 문제 입니다.

여덟 개 중 일곱 개는 NextMove 안에서 해결되고, 마지막 하나만 그 바깥을 건드립니다. 요구사항을 정리할 때 이런 것을 갈라 두면 일감의 크기를 잘못 재지 않습니다.


9. 여덟 개를 다 메우면 강한 봇이 되는가#

이 질문에 답할 자료가 이 블로그에 이미 있습니다. 같은 구조로 두 번 해 봤기 때문입니다.

달무티 봇은 더 약했습니다. Part 4: 만들어 보니 hard 봇이 더 약했다 에 기록해 두었습니다. 설계안의 고급 기능을 전부 구현한 최초의 hard 봇은 easy 봇보다 평균 등수가 0.6601등 나빴습니다. 그리고 easy 봇의 “단순화” 라고 소개했던 규칙 두 개가, 정정 후에는 hard 봇의 guardrail 로 되돌아왔습니다.

렉시오 봇은 아무것도 하지 않았습니다. Part 5: 봇이 아무것도 하지 않는 이유 입니다. 설계한 hard 봇을 구현했더니 거의 모든 턴을 패스했습니다. 개별 평가항은 전부 맞았고, 항을 합치는 방식이 틀렸습니다.

그리고 그 다음이 더 중요합니다. Part 6: 봇이 약한지 어떻게 아는가 에서, 브라우저로 한 판 돌려서는 “조금 약하다” 를 찾을 수 없다는 것을 확인하고 시뮬레이터를 만들었습니다. 그리고 봇이 약한 이유를 세 번 틀리고 네 번째에 찾았습니다. 평가 함수는 처음부터 옳았고, 확률이 틀렸습니다. 여러 상대가 같은 미공개 카드 풀을 나눠 갖는데 좌석별 확률을 독립이라고 가정하고 곱한 것이 원인이었습니다.

훌라에도 같은 함정이 그대로 있습니다. Part 1 을 다시 보면,

  • 일반 스톱의 성패는 “모든 상대의 카드합이 나보다 큰가” 인데, 상대들의 카드합은 서로 독립이 아닙니다. 한 명이 좋은 카드를 들면 다른 사람은 못 듭니다.
  • 버릴 카드의 땡큐 위험도 마찬가지입니다. 어느 상대가 그 카드를 받쳐 줄 두 장을 들고 있을 확률을 좌석마다 따로 구해 곱하면, 렉시오에서 봇을 망친 것과 똑같은 계산을 하게 됩니다.

그래서 이 시리즈의 hard 봇은 위 여덟 개를 요구사항으로 삼되, 그것을 다 구현하는 것을 목표로 삼지 않습니다. 목표는 하나입니다. easy 봇보다 실제로 강한지를 수치로 증명하는 것 입니다. 강해지지 않는 기능은 아무리 정교해도 빼야 하고, 그걸 가리려면 봇을 만들기 전에 재는 방법부터 준비해야 합니다.

Part 5 와 Part 6 에서는 hard 봇이 무엇을 아는가 와 무엇을 결정하는가 를, Part 7 과 Part 8 에서는 정말 강해졌는지 어떻게 쟀는가 를 다루겠습니다. 마지막 질문에 두 편을 씁니다. 앞선 두 번 모두, 봇을 만드는 것보다 봇이 약하다는 사실을 알아내는 것이 어려웠기 때문입니다. 그리고 Part 8 의 끝에서 이 여덟 자리로 돌아와, 어느 것이 실제로 메워졌고 어느 것이 메운 줄 알았는데 아니었는지를 결산하겠습니다.


시리즈 목록#

References#