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


Part 7 에서 추 열한 개를 하나씩 빼 보고, 조정하고, 갈리는 자리를 확인했습니다. 이번 편은 세 가지로 끝맺습니다. 값을 동결 하고, 한 번도 쓰지 않은 판 에서 합격 여부를 재고, Part 4 의 여덟 자리를 결산 합니다.


1. 동결 — 실험이 고른 값을 그대로 쓰지 않은 자리#

Part 7 의 조정은 두 걸음을 옮겼습니다. 땡큐 템포 추를 0 으로, 상대 손패 치우침을 0.25 에서 0.5 로. 동결한 값은 이렇습니다.

추조정이 고른 값동결한 값
땡큐 템포0추 자체를 삭제
상대 손패 치우침0.50.25 유지
나머지 아홉처음 값처음 값

두 자리 모두 조정 결과와 다릅니다. 이유가 서로 다릅니다.

0 으로 두지 않고 지운다#

템포 추는 빼 보기, 조정, 그림자 저울이 모두 같은 방향을 가리켰습니다. 있으면 조금 나쁘고, 없애면 창 2,300개 중 26개만 바뀝니다. 그러면 값을 0 으로 두면 될 것 같은데, 추 자체를 지웠습니다.

값이 0 인 채로 남겨 두면 “왜 0 인가” 를 아는 사람만 알고, 다음 조정이 그 축을 다시 탐색합니다. 지우면 그런 일이 없습니다. 땡큐 판단은 Part 6 의 두 값을 그대로 비교하는 것으로 남았습니다.

같은 이유로 “덮지 못하는 카드의 값” 추도 지웠습니다. Part 7 에서 16,394번의 결정에서 갈린 자리가 0 이었던 그 추입니다. 지우기 전에 한 번 더 확인했습니다. 무게를 0.15 에서 3.0 으로, 스무 배 키우고 그림자 저울을 돌렸습니다. 16,702번의 결정에서 갈린 자리가 여전히 0 이었습니다. “다른 추에 눌린다” 보다 강한 사실입니다. 이 추가 재는 값은 같은 결정 안의 후보들 사이에서 순서를 바꿀 만큼 갈리지 않습니다. 완전히 관성입니다. 지워도 봇의 행동이 하나도 안 바뀌므로 아래의 검증을 다시 돌릴 필요도 없었습니다.

실험이 고른 값을 일부러 안 쓴다#

치우침은 반대입니다. 실험은 0.5 가 낫다고 했고, Part 7 의 그림자 저울은 그것이 잡음이 아니라 스톱박의 꼬리를 피한 것이라고 확인했습니다. 그런데도 0.25 를 그대로 두었습니다.

이유는 실험 밖에 있습니다. 실제로 플레이해 보니 봇이 일반 스톱을 거의 걸지 않았습니다. 0.25 에서도 손패가 한 장 남았을 때조차 뽑기를 골랐고, 0.5 는 그것을 절반으로 더 줄입니다.

치우침스톱 선언율스톱박 비율판당 성적
052.7%36.3%+6.5
0.224.9%19.2%+10.1
0.2521.3%15.9%+10.2
0.316.3%11.9%+10.3
0.510.9%3.1%+10.5

0.2 에서 0.5 사이에서 성적 차이는 판당 0.4점 입니다. 그런데 스톱 선언율은 두 배 넘게 갈립니다.

일반 스톱은 이 게임에서 변수를 만드는 장치입니다. 봇이 걸지 않으면 사람은 스톱박이라는 규칙이 있는 줄도 모른 채 판을 끝냅니다. 강한 봇이 재미없는 봇이 되는 자리입니다. 판당 0.4점은 그 재미의 대가로 쌉니다.

flowchart LR
    A["조정이 보는 것<br/>판당 정산 포인트 하나"] --> B["늘 스톱을 덜 거는<br/>쪽을 고른다"]
    C["조정이 못 보는 것<br/>재미"] --> D["사람이 값을<br/>눌러 둔다"]
    B --> E["0.5"]
    D --> F["0.25"]
    style A fill:#87CEEB,color:#000000
    style B fill:#FFD700,color:#000000
    style C fill:#FF9999,color:#000000
    style D fill:#FFD700,color:#000000
    style E fill:#D3D3D3,color:#000000
    style F fill:#90EE90,color:#000000

이것은 실험을 무시한 것이 아니라 목적 함수의 한계 를 메운 것입니다. 조정이 보는 것은 판당 포인트 하나뿐이라 재미를 볼 수 없고, 그래서 늘 스톱을 덜 거는 쪽을 고릅니다. 값을 사람이 눌러 둔 것은 그 눈먼 자리를 메운 것입니다. 이 판단은 코드 주석과 동결 문서 두 곳에 같은 표와 함께 적어 두었고, 동결한 값은 테스트가 고정합니다. 바꾸려면 근거 문서를 함께 고쳐야 합니다.

동결한 뒤 조정용 seed 에서 다시 재니 판당 +10.27 이었습니다. 이 숫자는 아직 합격의 근거가 아닙니다. 시험 문제로 공부한 값이기 때문입니다.


2. 시험 문제를 열기 전에 합격선을 정한다#

이제 한 번도 쓰지 않은 딜 에서 잽니다. 조정에 쓴 seed 와 절대 겹치지 않는 별도의 이름 공간에서 딜을 뽑습니다.

중요한 것은 순서입니다. 시험 문제를 열기 전에 규모와 합격선을 못 박고, 연 뒤에는 고치지 않습니다. 열고 나서 정하면 결과를 보고 기준을 고른 것이 됩니다.

미리 정한 것값
딜 수256
칸3·4·5인 × 3·5·10판 = 9칸
스톱 빈도높음 (제품 기본값)
칸마다 통과 조건차이의 95% 신뢰구간 하한 이 −2.0 보다 위
전체 통과 조건차이가 0 보다 크고 신뢰구간 하한도 0 보다 위
건강 조건정책 오류·시간 초과·상한 도달 전부 0
실행 횟수한 번. 실패하면 그 딜은 폐기하고 새 이름 공간에서 다시

칸마다 −2.0 이라는 한계는 “어느 인원·판 수에서도 easy 보다 판당 2점 이상 나쁘지 않아야 한다” 는 뜻입니다. 렉시오에서는 이 값이 −0.2 였는데 복사하지 않았습니다. 훌라는 등수 사다리에 4배·8배 배율과 박이 곱해져 정산의 크기 자체가 다릅니다. 첫 측정의 분산을 보고 우위 +10 의 20% 로 잡았습니다.

판정은 평균이 아니라 신뢰구간의 하한 으로 합니다. 운 좋게 평균이 높은 것으로는 통과하지 못합니다.

돌리기 전에 병목부터#

256 딜에 9칸이면 11만 판입니다. 돌리기 전에 얼마나 걸릴지 재 보니 이상한 것이 보였습니다. 일꾼을 늘리면 오히려 느려졌습니다. 일꾼 4개에 초당 49.8판, 8개에 40.1판, 14개에 36.9판이었습니다. CPU 는 바쁜데 처리량은 떨어지니 무언가 서로 밟고 있는 것입니다.

메모리 정리 주기를 늦추자 32% 빨라졌고, 그래도 일꾼을 늘리면 느려지길래 프로세스를 셋으로 나누고 각각 일꾼 4개 를 주었습니다. 초당 107.6판으로 1.7배가 됐습니다. 이 설정으로 256 딜이 약 17분입니다. 512 딜은 34분이었지만 어느 한 칸도 10분을 넘지 않는 쪽을 골랐습니다.

그리고 Part 7 의 40분짜리 조정이 중간에 죽어 날아갔던 경험대로, 칸이 끝날 때마다 결과를 저장 합니다.


3. 결과#

통과했습니다. 실행 시간 17.8분, 110,592판, 봇의 결정 3,815,537번에서 건강 지표 전부 0.

값
전체 차이 (판당)+10.09
95% 신뢰구간+9.88 ~ +10.31
가장 낮은 칸의 하한+3.36

칸별로는 이렇습니다.

인원3판5판10판
3인+3.6+3.6+3.8
4인+8.2+8.2+8.3
5인+18.1+18.4+18.6
flowchart LR
    A["3인<br/>+3.6"] --> B["4인<br/>+8.2"]
    B --> C["5인<br/>+18.6"]
    style A fill:#90EE90,color:#000000
    style B fill:#FFD700,color:#000000
    style C fill:#FF9999,color:#000000

읽을 것이 둘입니다.

인원이 늘수록 hard 가 거의 두 배씩 셉니다. 상대가 많을수록 easy 가 흘리는 카드가 많아지고, 미등록박·세븐박·독박으로 갈 수 있는 자리도 늘어납니다. Part 1 의 등수 사다리가 5인에서 가장 길다는 것도 한몫합니다.

판 길이는 거의 아무것도 바꾸지 않습니다. 4인에서 3판·5판·10판의 차이가 8.20, 8.23, 8.26 입니다. 우위가 판마다 고르게 남고, 판 수는 흩어짐만 줄입니다.

외운 것은 아닌가#

조정용 seed 에서 +10.27, 처음 보는 seed 에서 +10.09. 차이 0.17 입니다. 조정이 자기 seed 를 외웠다면 처음 보는 seed 에서 눈에 띄게 낮아야 하는데 그렇지 않습니다.

스톱 빈도를 표준으로 바꾼 별도 실행에서는 +9.32 로 조금 작았습니다. 기준선이 엄해 스톱 기회가 줄어서입니다. 재미있는 것은 기회가 줄었는데 선언율은 올랐다 는 점입니다. 3인에서 31% 가 36% 로. 문턱이 엄하면 넘기만 하면 걸 값어치가 있는 자리가 대부분이기 때문입니다.

봇이 실제로 한 일#

3인4인5인
일반 스톱 선언율31%23%18%
스톱박 비율7~10%
땡큐 부른 비율91~94%
결정 시간 (100번 중 99번)2.2~2.4ms

상대가 많을수록 누군가 먼저 끝낼 확률이 커지므로 스톱을 덜 겁니다. 결정 시간은 가장 오래 걸린 한 번이 46ms 로 100ms 예산 안이었습니다.

정직하게 적어 둘 것 둘#

첫째, 규모를 정할 때 계산을 틀렸습니다. 칸마다 신뢰구간의 반폭을 ±6.6 으로 예상했는데 실제는 ±0.15 에서 ±1.3 이었습니다. 첫 측정의 매치 총점 표준편차를 판 수로 나누지 않고 그대로 쓴 착오입니다. 결정은 그대로 옳았습니다. 256 딜은 필요한 것보다 정밀도가 남았을 뿐이고, 남는 정밀도는 버려지지 않습니다. 다음에 규모를 잡을 때는 판당 눈금으로 환산한 뒤 세야 합니다.

둘째, 순서가 뒤집혔습니다. 원래 계획은 이 검증을 통과한 뒤 혼자하기의 기본 봇을 hard 로 올리는 것이었습니다. 실제로는 베타 사용자가 이미 시험하고 있어서 기본 봇 승격과 공개가 이 검증보다 먼저 이루어졌습니다. 그래서 이 검증은 “승격할 것인가” 가 아니라 “이미 배포된 봇이 기준을 만족하는가” 의 사후 확인이 되었습니다. 통과했으니 결과는 같지만, 통과하지 못했다면 되돌리는 배포가 필요했을 것입니다.


4. 여덟 자리 결산#

Part 4 에서 정리한 easy 봇의 여덟 자리입니다. hard 봇이 각각을 어떻게 했는지 결산합니다.

flowchart LR
    H0[" "]:::hdr
    H1["메웠다"]:::hdr
    H2["일부만"]:::hdr
    H3["후보로만"]:::hdr
    R1["자기 차례"]:::hdr
    A["① 한 수 greedy<br/>② 등록·붙이기 순서<br/>⑥ 공개 카드 기억"]
    B["⑤ 버림패 선택<br/>⑦ 상대 종료 위험"]
    C["③ 등록 중단<br/>④ 7 전략 보유"]
    R2["선언"]:::hdr
    D["⑧ 땡큐·확정 스톱"]
    E["⑧ 일반 스톱"]
    F[" "]:::hdr
    H0 ~~~ H1 ~~~ H2 ~~~ H3
    R1 ~~~ A ~~~ B ~~~ C
    R2 ~~~ D ~~~ E ~~~ F
    classDef hdr fill:none,stroke:none,color:#dddddd
    style A fill:#90EE90,color:#000000
    style B fill:#FFD700,color:#000000
    style C fill:#D3D3D3,color:#000000
    style D fill:#90EE90,color:#000000
    style E fill:#FFD700,color:#000000
자리결과근거
① 한 수 greedy메웠다턴 전체를 그려 본다. 2♥ 3♥ 4♥ 4♣ 4♦ 4♠ K♠ 에서 훌라
② 등록을 붙이기보다 먼저메웠다등록·붙이기·중단이 같은 저울에 오른다
③ 등록·붙이기 중단후보로만“여기서 그만둔다” 가지는 있으나 저울이 고른 기록은 드물다
④ 7 전략적 보유후보로만추 둘이 5만 결정에서 수를 하나도 안 바꿨다. 사실상 즉시 등록
⑤ 버림패의 카드값만일부만카드값·조합 조각·죽은 카드는 본다. 땡큐 위험은 안 본다
⑥ 공개 카드 기억메웠다장부
⑦ 상대 종료 위험·점수 구조일부만스톱 판단은 등수 사다리와 남은 장수를 본다. 버리기 판단은 안 본다
⑧ 땡큐·스톱대부분 메웠다땡큐 92~93%, 확정 스톱 무조건, 일반 스톱은 기대값. 곧 끝낼 국면의 스톱은 못 건다

여덟 중 셋을 온전히 메웠고, 둘은 일부만, 둘은 후보로만 존재하며, 하나는 대부분 메웠습니다. 그런데도 easy 보다 판당 10점을 더 법니다. Part 4 의 끝에서 “여덟 개를 다 메우면 강한 봇이 되는가” 를 물었는데, 답은 “다 메우지 않아도 강한 봇이 되고, 메운 줄 알았던 것이 실은 아무 일도 안 하고 있을 수 있다” 입니다. 후자는 재 보지 않았으면 몰랐을 것입니다.


5. 남은 것#

봇 단계는 여기서 끝났습니다. 남겨 둔 구멍을 적어 둡니다.

  • 버림패의 땡큐 위험. Part 5 §5 에서 적은 대로, 이 봇은 던지는 카드가 상대의 땡큐를 완성시키는지 보지 않습니다. 좌석마다 확률을 따로 구해 합치는 계산이 렉시오에서 봇을 망친 전례가 있어, 정확한 계산 없이는 넣지 않았습니다.
  • 곧 끝낼 국면의 일반 스톱. Part 6 §5 의 구조적 한계입니다. 기다림의 값이 한 턴만 봅니다.
  • 손잡이 하나가 두 일을 합니다. 상대 손패 치우침이 실제로 하는 일은 스톱박의 꼬리를 피하는 것입니다. 비대칭을 기대값 식이 직접 다루도록 고치는 편이 옳습니다.
  • 재미는 목적 함수 밖에 있습니다. §1 에서 사람이 값을 눌러 둔 그 자리입니다. 스톱 빈도를 목적 함수나 제약으로 넣는 것이 다음 조정의 숙제입니다.

마치며#

이 블로그에서 CPU 플레이어 시리즈는 세 번째입니다. 달무티 에서는 hard 봇이 easy 보다 약했고, 렉시오 에서는 hard 봇이 아무것도 하지 않았습니다. 훌라에서는 처음 만든 hard 봇이 손으로 정한 값 그대로 easy 보다 셌습니다.

운이 아니라 앞선 두 번의 실패가 미리 막아 준 것입니다. 즉시 승리를 저울에 올리지 않은 것, 시뮬레이터를 봇보다 먼저 만든 것, 좌석마다 확률을 곱하지 말라고 설계 문서에 경고를 적어 둔 것 모두 앞의 두 시리즈에서 배운 것입니다.

그래도 재 보기 전에는 몰랐던 것이 남았습니다. 추 넷이 놀고 있었고, 스톱 확률이 틀려 있었고, 실험이 고른 값이 재미없는 봇을 만들었습니다. 봇을 만드는 것보다 봇이 무엇을 하고 있는지 알아내는 것이 어렵다는 사실은 세 번째에도 같았습니다.


시리즈 목록#

References#