무승부를 수집하는 봇을 만들었다
종료된 대회의 부검이다. 세 번 떨어진 기록을 순서대로 쓰는 연작의 2편.
NYPC 2026 마스터 부문 예선 최종 성적은 447위, 937팀 중이다. 결선에 가는 건 상위 스무 팀이고 컷은 실력점수 2300 근처였다. 상위 2%다. 내 최종 점수는 1830에서 1910 사이를 진동했다.
숫자만 보면 그냥 실력이 부족한 것처럼 보인다. 그런데 궤적을 보면 다른 게 보인다.
| 날짜 | 실력점수 | 순위 |
|---|---|---|
| 06-29 | 1610 | 52위 / 334팀 |
| 07-01 | 1930 | 45위 / 539팀 (상위 8.4%) |
| 07-08 (최종) | 1830~1910 | 447위 / 937팀 |
7월 1일에 1930이었고, 여드레 뒤 마감일에 1830에서 1910이었다. 그 여드레 동안 봇을 스물여섯 대 만들었다. 순증은 사실상 0이다. 순위가 45위에서 447위로 떨어진 건 내가 나빠져서가 아니라 제자리에 서 있는 동안 참가팀이 539에서 937로 늘었기 때문이다.
즉 이건 정체의 기록이다. 그리고 정체의 원인 세 개가 전부 대회 첫날에 결정돼 있었다.
원인 1: 쉬운 지표가 진짜 목표를 납치했다
이 대회에는 두 가지 채점이 있었다. 샘플 문제 여덟 개는 제출하면 즉시, 무제한으로 결과를 알려준다. 실력점수는 다른 참가자와 자동 매칭돼서 하루 세 라운드만 돌아간다.
샘플 여덟 문항을 만점 받았다. 8.0 / 8. 그런데 결선 진출은 샘플이 아니라 실력점수로 정해진다.
첫날 밤에 이걸 이미 알고 있었다. 노트에 "진짜 목적함수는 참가자 순위다"라고 적어놨다. 그리고 열흘 중 엿새를 샘플 그라인드에 썼다.
왜 그랬는지는 지금 보면 명백하다. 샘플은 피드백이 즉시 오고 무제한이다. 실력점수는 하루 세 번, 매칭 상대에 따라 ±100씩 흔들린다. 인간은 반응이 빠른 지표에 끌린다. 그게 목표가 아닐 때도.
Goodhart의 법칙이 보통 "지표를 목표로 삼으면 지표가 망가진다"로 인용되는데, 내 경우는 좀 달랐다. 지표는 안 망가졌다. 내가 망가진 지표를 만든 게 아니라, 멀쩡한 지표에 잘못 끌려간 것이다. 이건 측정의 문제가 아니라 주의력의 문제다.
규칙으로 바꾸면 이렇게 된다. 매 사이클 한 번씩 자문한다. "이 지표를 정복하면 목표가 달성되나?" 아니라면 그 지표는 목표가 아니라 캘리브레이션 도구다. 도구는 도구 자리에 둬야 한다.
원인 2: 지지 않는 봇을 만들었고, 그게 문제였다
봇 아키텍처가 이렇게 진화했다.
economy-turtle → 패배를 무승부로 → 절대 지지 않기 → 비열등 게이트
각 단계는 앞 단계의 합리적인 개선이었다. 자원을 아끼는 전략을 짰고, 지는 판을 비기는 판으로 바꿨고, 지지 않는 걸 목표로 세웠고, 새 버전이 기존 챔피언보다 나쁘면 채택하지 않는 게이트를 걸었다.
그렇게 만들어진 것이 무승부 수집기였다. 후반 버전들의 전적이 7승 7무 6패다.
한편 상위권 봇들의 리플레이를 뜯어봤다. 실력점수 2150 이상 봇들은 25턴에서 70턴 사이에 선공으로 보드를 장악하고, 첫 분기점이 오기 전에 게임을 끝냈다. 평균 50.7턴이다. 내 봇은 상대의 수를 보고 대응하는 반응형 레이어를 쌓아 만든 구조였다. 그 구조로는 50턴 전에 게임을 결정할 수 없다. 반응하려면 상대가 먼저 움직여야 하니까.
천장이 낮은 코어로 시작해서 여드레 동안 그 위에 레이어를 스물여섯 겹 얹은 셈이다.
원인 3: 보상함수가 계단인데 연속함수처럼 최적화했다
이게 가장 큰 오판이고, 지금은 새 대회를 시작할 때 첫날에 확인하는 항목이 됐다.
이 대회의 보상은 계단이다. 상위 스무 팀은 전부를 받고 스물한 번째는 0을 받는다. 21위와 447위의 보상 차이는 없다. 둘 다 0이다.
그런데 내 봇의 모든 게이트는 순위 보존을 최적화하고 있었다. 지는 판을 비기는 판으로 바꾸는 것, 기존 챔피언보다 나쁘면 채택하지 않는 것, 회귀를 막는 것. 전부 "지금 위치를 잃지 않는 것"에 자원을 쓰는 규칙이다.
컷 위에서 방어할 때는 그게 정답이다. 컷 아래에서는 정반대다. 컷 아래에서 분산은 비용이 아니라 자산이다.
기댓값으로 쓰면 이렇게 된다.
현상 유지: 100% × 0 = 0
백지 재건: 15% × 전부 > 0
내 점수 1900은 컷 2300에 못 닿는다. 그 상태를 안전하게 유지하는 데 성공하면 확정적으로 0을 받는다. 성공률 15%짜리 재설계가 그것보다 낫다. 산수가 이렇게 단순한데도 나는 여드레 동안 분산을 줄이는 쪽으로 일했다.
대회가 끝나고 이 얘기를 하다가 이런 말을 들었다. "400등 밖으로 갈 거면 도박이라도 했어야." 정확한 지적이다. 그리고 여기서 알아야 할 게 하나 더 있는데, 보수적으로 굴자고 결정한 사람은 없었다. 보수성은 내가 짜놓은 게이트 기계 안에 들어 있었다. 챔피언보다 나쁘면 버린다는 규칙이 자동으로 모든 탐색을 죽였다.
백지 재건은 실제로 있었다. 이틀 만에 사형당했다
여드레째에 반응형 구조를 버리고 처음부터 다시 짠 봇을 만들었다. 템포를 잡는 쪽으로 완전히 다른 코어였다.
그 봇은 두 번의 반복만 받고 폐기됐다. 폐기 근거는 기존 챔피언과의 직접 대결 전적 0승 2무 38패였다.
이 판정은 두 가지 이유로 틀렸다.
첫째, 평가축이 틀렸다. 백지에서 다시 짠 봇을 성숙한 챔피언과 붙이면 당연히 진다. 그 봇을 평가하는 옳은 방법은 상위권 리플레이의 템포 곡선과 얼마나 일치하는지를 보는 것이었다. 목표 코퍼스와의 거리를 재야 하는데 나는 현재 챔피언과의 거리를 재고 있었다.
둘째, 예산이 틀렸다. 여드레에 봇 스물여섯 대를 만든 속도로 계산하면, 엿새째에 갈라섰을 경우 재건 봇에 여덟에서 열두 번의 반복을 줄 수 있었다. 두 번은 판정에 필요한 최소치에도 못 미친다.
그래서 회고에 "재건은 불가능했다"고 쓰지 않는다. "선택하지 않았다"고 쓴다. 실현 가능성과 선택은 다른 얘기고, 둘을 섞으면 다음에 같은 판단을 반복한다.
다음 대회에 넣은 네 가지 규칙
- 첫날에 보상함수 판별. 계단인가 연속인가를 요강에서 확정한다. 계단이고 내가 컷 아래면 순위 보존 전략을 금지한다.
- 탐색 레인은 게이트를 면제한다. 챔피언 보호 게이트는 챔피언 레인 전용이다. 재건 레인은 초기 회귀를 전제로 하고, 평가는 자기 챔피언과의 대결이 아니라 목표 코퍼스와의 일치도로 하며, 최소 반복 횟수를 보장한 뒤에만 판정한다.
- 기간 50% 지점에 중간 체크포인트를 캘린더에 박는다. "현 아키텍처의 기대 도달점 대 컷"의 갭을 계산하고 "레이어냐 재건이냐"를 명시적으로 결정한다.
- 피드백 대역폭보다 작은 델타는 측정하지 않는다. 매칭 노이즈가 ±100인 환경에서 기대 이득 ±20짜리 변경을 스물여섯 번 반복하면, 스물여섯 번의 측정 전부가 무의미하다.
남은 질문
여드레 동안 나는 열심히 일했다. 봇 스물여섯 대는 게으름의 결과가 아니다. 매일 뭔가를 만들고 붙이고 측정했다. 그런데 그 여드레의 순증이 0이다.
성실함은 방향이 맞을 때만 자산이다. 그리고 방향이 틀렸다는 신호는 대개 "노력 대비 결과"라는 비율에 먼저 나타난다. 나는 그 비율을 매일 보고 있었는데도 며칠 더 같은 일을 했다.
다음에 물어야 할 건 이거다. 지난 사흘의 작업이 목표 지표를 얼마나 움직였나? 그 값이 측정 노이즈보다 작으면, 나는 지금 일하는 게 아니라 바쁜 것이다.