실험 설계
증강 드래프트를 다 만들고 나서 물어야 할 것은 하나였다 —
정말 판이 달라지는가. 같은 난수 씨앗으로 켜고/끄고 돌려
비교했다. 그런데 결과가 상식과 정반대로 나왔다.
기대실제
seed 11 · 증강을 켜면 더 커진다
46,594 → 20,983 (절반)
계수가 세거나 약해서 난 차이다
같은 seed 인데 다른 판이었다
테스트가 잡아 줄 것이다
테스트 29개도 전체 스위트도 전부 초록이었다
고친 뒤
생존판 영토 중앙 36,732 → 108,162 (2.9배)
어떻게 알아냈나
- 증강은 전부 유리한 계수다(병력 상한 +18%, 정복 비용 −14% …).
그런데 켠 쪽이 절반이다 — 계수 조정으로 설명되는 방향이 아니다.
- 그래서 값이 아니라 비교 자체가 성립하는지를 의심했다.
같은 씨앗을 넣었으니 두 판은 같아야 한다.
- 드래프트가 카드를 뽑을 때 판의 난수를 쓰고 있었다.
카드를 뽑느라 난수를 한 번 더 소비하면, 그 뒤로 스폰 위치도 전투 굴림도
AI 판단도 전부 어긋난다.
- 드래프트 전용 난수를 분리했다. ⚠ 켜든 끄든 만들어야 한다 —
조건부로 만들면 판을 만들 때 난수가 한 번 더 불려
고치려던 버그를 같은 자리에 다시 만든다.
검증은 테스트가 아니라 판으로 했다. 첫 드래프트는 750 tick 에
열리므로 그 이전 구간은 양쪽이 같은 판이어야 한다.
[7 off] 400/900 tick 영토 4769
[7 on ] 400/900 tick 영토 4769 ← 정확히 같다
버그가 남아 있었다면 여기서 이미 갈렸다. 진행 로그가 자기 전제를 스스로 검증한다.
A/B 결과가 상식과 반대면 계수보다 난수 소비를 먼저 의심한다.
이 종류는 결과를 봐야만 나온다 — 드래프트 자체는 완벽하게 돌고 있었다.
규칙 설계
기준선을 재는데 한 판이 22,000 tick 에서 안 끝났다.
상한을 25,000 으로 올렸더니 거기서도 안 끝났다.
그럴듯한 해법실제로 필요한 것
tick 상한을 더 올린다
안 풀린다. 두 번 연속 미종료가 그 증거다
승리 문턱에 하한을 20% 쯤 둔다
바닥이 없어야 한다 — 하한을 두면 그 아래에서 영원히 버틴다
기존 테스트는 그대로 통과할 것이다
하나가 깨졌다 — 시간 종료 승리가 도달 불가가 됐다
그대로 가져오면 된다
참고 구현은 기본 꺼짐이고 로비에서만 켠다. 싱글이라 로비가 없어 켤지 말지가 곧 판 규칙이었다
어떻게 알아냈나
- 미종료 판의 생존자 수가 2로 고정돼 있었다. 판이 느린 게 아니라
끝날 수 없는 상태다 — 둘이 남아 서로를 못 이긴다.
- 그러면 상한을 올리는 것은 답이 아니다. 승리 조건 쪽을 봐야 한다.
- 참고 구현에 이미 있었다. 시작 시각을 넘기면 지배 승리에 필요한 점유율이
분당 2%p 씩 내려가고, 바닥이 없다.
지배 승리에 필요한 점유율 · 경과 시간에 따라
0~30분80%
40분60%
50분40%
60분20%
70분0%
0%25%
50%75%
100%
바닥이 없는 것이 핵심이다. 하한을 20% 쯤 두면 그럴듯해 보이지만,
남은 둘이 각자 15% 씩 갖고 있으면 그 판은 여전히 안 끝난다.
0 에 닿는 순간 남은 사람 중 영토가 가장 넓은 쪽이 이기므로
판 길이가 70분으로 못 박힌다.
딸려서 승리 조건 하나가 사라졌다. 문턱이 70분에 0 이면
170분 하드 리밋에 닿을 수가 없다. 테스트가 틀린 게 아니라
규칙이 바뀐 결과였고, 참고 구현도 이 기능을 켜면 마찬가지다. 경로를 지우지 않고
테스트를 둘로 갈랐다 — 하나는 껐을 때 그 경로가 아직 도는지 재고,
하나는 켠 채 도달 불가임을 못 박는다.
교착을 푸는 장치에 하한을 두면 그 장치가 아니게 된다.
하한 아래에서 버티는 판이 정확히 우리가 풀려던 판이다.
변이 6 / 6 잡힘
측정 미완 — 45,000 tick 중 14,000 에서 중단
24b6683
문서 관리
문서가 둘이다. 대조 문서(참고 구현과 우리 코드를 나란히 놓은 것, 8,700줄)와
설계 문서(우리가 얹는 계층, 460줄). 둘 다 같은 규칙을 지키기로
돼 있었는데 한쪽만 썩었다.
설계 문서가 적어 둔 것실제 코드
상한 = 250 + 영토 × 22
2 × (타일^0.6 × 1000 + 50000) + Σ도시레벨 × 250000
지형에 숲이 있다 (방어 1.20)
숲이 없다. 평지·구릉·산악 셋뿐
시간 종료 15분
170분, 그마저 도달 불가
도구 도움말: “안 주면 900초에 판을 자른다”
실제 10,200초
대조 문서 (전투 공식 상수 37개)
불일치 0
어떻게 알아냈나
- 증강 UI 를 만들다 설계 문서의 카드 표를 다시 읽었다.
표에 이미 삭제한 카드 둘이 그대로 있었다.
- 이상해서 절마다 코드와 대조했다. 규칙 절이 한 줄도 안 맞았다.
- 가장 나빴던 것은 “미해결” 로 표시된 항목이었다 —
“할인 두 장이면 비용이 음수가 된다. 하한이 막고 있다.”
실측하니 두 카드는 축이 달라 합산 자체가 안 일어난다.
일어날 수 없는 계산을 미해결 문제로 적어 두고 있었다.
- 그리고 하한도 막고 있지 않았다. 하한은 축마다 걸리는데 코드는
곱을 쓴다 — 실측 배율 0.179 로 하한 아래다.
- 같은 잣대를 대조 문서에도 돌렸다. 불일치 0 이었다.
여기가 이 노트의 요점이다. 두 문서에 같은 규칙이 걸려 있었는데 한쪽만
썩었다. 차이는 규칙이 아니었다 — 대조 문서는 세션마다 절을 붙이며 계속
읽었고, 설계 문서는 방향이 바뀔 때 머리말만 붙이고 본문을 안 열었다.
그리고 설계 문서 마지막 절이 “다음에 할 일” 이었는데,
대조 문서에도 같은 절이 있었다. 계획이 두 자리에서 경쟁하고 있었다.
설계 문서 쪽이 가리키던 상수 넷은 하나도 존재하지 않았다.
문서를 지키는 것은 규칙이 아니라 다시 읽는 빈도다.
그래서 규칙 대신 구조를 바꿨다 — 공식을 지우고 “어디를 보면 되는지” 만
남겼다. 베껴 적은 수식이 썩은 원인이었다.
테스트 방법론
증강 카드는 계수다. 병력 상한 +18%, 정복 비용 −14% 같은 것들이 각자
축에 실린다. 문서와 코드 주석이 둘 다 앞자리에 이 원칙을 적어 뒀다 —
“같은 축에 여러 카드가 실리면 더한다.”
있다고 믿은 것실제
합산 규칙이 돌고 있다
한 번도 돈 적이 없다. 쌓이는 것은 레벨뿐
그 규칙을 재는 테스트가 있다
이름은 same_axis_cards_add 인데 재료는 한 장의 Lv2 였다
주석이 산수를 적어 뒀으니 맞겠지
0.486 이라 적혀 있고 실제는 0.306 — 틀린 주석과 함께 통과하고 있었다
테스트를 더 쓰면 잡힌다
새로 쓴 테스트도 못 잡았다 — 재료를 손으로 만들어 그 코드를 아예 안 탔다
어떻게 알아냈나
- 다음에 카드를 늘린다면 어느 축에 놓을지 보려고 분포를 한 줄로 세었다.
카드 10장이 축 10개에 정확히 하나씩이었다.
- 그러면 합산 규칙은 발동할 조합 자체가 없다.
- 확인차 합산 코드를 덮어쓰기로 바꾸는 변이를 넣었다.
살아남았다 — 두 구현의 동작이 같기 때문이다.
- 축마다 카드가 하나뿐이니 누적값이 늘 0 이고,
0.0 + x 와 x 는 같다.
- 합성 카드를 임시로 얹어 같은 축 두 장을 만들고 나서야 잡혔다.
values[field] = values.get(field, 0.0) + value_at(aug, level)
→ values[field] = value_at(aug, level) ← 결과가 같다
규칙을 지울 수도 있었지만 카드를 늘리면 되살아날 규칙이다. 대신
깨어나는 순간을 알리는 테스트를 뒀다 — 열한 번째 카드를 이미 있는
축에 얹으면 그 테스트가 깨지고, 실패 메시지가
“이제 설계 문서의 할인 계산을 다시 재야 한다” 고 알려 준다.
같은 날 같은 모양을 하나 더 만났다. 할인 하한도 카드로는 절대 닿을 수
없는 값을 막고 있었다. 보호 장치가 있고, 그 장치를 재는 테스트도 있고,
그 상황이 게임에서 일어날 수 없었다.
“막지 않았으면 무엇이 일어났을 것인가” 를 단언해도 빠져나간다.
빠진 것은 다음 질문이다 —
“그 일이 이 게임에서 실제로 일어날 수 있는가.”
변이 1 살아남음 → 재료 문제
고친 뒤 3 / 3 잡힘
22d1284
계측
예전에 카드 둘이 갈 곳을 잃은 적이 있다. 구현하면서 지형과 이동
규칙이 통째로 바뀌었는데 카드는 그대로 남아 있었다 — 항해술(바다 건너기)은
배가 물길만 이어지면 어디든 가서 곱할 자리가 없어졌고,
삼림 순찰대는 지형에 숲이 없어서 갈 곳이 없어졌다.
그래서 모든 계수 축이 코드에 등장하는지 재는 테스트를 뒀다.
이번에는 다른 질문을 던졌다 — 한 판에 몇 번 걸리는가.
한 판(8,258 tick)에서 계수가 곱해진 횟수 · 로그 눈금
강행군497,947
정예 병단342,374
산악병325,415
개척단155,573
징집령104,785
비옥한 땅8,519
교역로213
상륙전9
1100
1만100만
상륙전은 한 판에 9번 걸린다 — 정예 병단의
1/38,000 이다. 그 축은 배가 도착했는데 목적지가 이미 내
땅일 때만 걸리고, 걸려 봐야 퇴각 벌점의 일부를 깎는다.
막으려던 자리가 그 자리에 다시 생겼다. 그때는 축이
없어서 꽝이었고, 지금은 축이 거의 안 걸려서 꽝이다.
그리고 기존 테스트는 초록이다.
여기서 두 번 넘어졌다
- 계수 함수를 감쌀 때 인자 개수를 잘못 짚었다. 비슷한 이름의 다른
함수 시그니처를 썼다 — 즉시 오류라 금방 알았다.
- 사람이 없는 판으로 쟀다. 증강은 사람만 받는데 관전 모드로 돌렸다.
그러면 병력 상한 함수가 봇·AI 분기에서 먼저 반환해 계수 줄에 도달조차
안 한다. 그 축이 0회로 나왔고, 하마터면
“이 카드가 배선 안 됐다” 로 읽을 뻔했다.
둘 다 도구 머리말에 박아 뒀다. 다음에 다시 만들면 또 같은 데서 넘어진다.
축이 있는가와 축이 걸리는가는 다른 질문이다.
다만 횟수만으로 세기를 판정하면 안 된다 — 교역로는 213회지만 한 번에 큰
골드가 걸린다. 결론이 나오는 것은 9회짜리 하나뿐이다.
대조 검증
참고 구현에서 화면에 숫자를 찍는 함수를 열어 보니 이렇게 돼 있었다.
export function renderTroops(troops: number): string {
return renderNumber(troops / 10);
}
참고 구현은 병력을 표시할 때 10으로 나눈다. 우리는 날것을 찍고 있었다 —
시작 병력 25,000 을 참고 구현은 2.5K 라고 쓴다.
화면의 모든 병력 숫자가 실제의 10배였다.
우리가 하던 것참고 구현
한 자리만 고치면 된다
병력이 뜨는 거의 모든 곳에 쓰인다 — 한 자리만 고치면 화면 안에서 축이 갈린다. 열여섯 자리를 한 번에 바꿨다
골드도 같이 나누면 되겠지
골드는 안 나눈다. 합치면 골드가 1/10 이 된다
“적당히 K 로 줄이기”
구간마다 자릿수가 다른 표다 (1K~10K 소수 둘 · 10K~100K 하나 · 100K 이상 정수)
반올림하면 되겠지
내림이 먼저다. 반올림이면 9,999 가 10.00K 가 되어 다음 구간 표기와 섞인다
난이도 impossible 시작 병력 25,000
참고 구현 31,250 — 가장 어려운 난이도가 그 아래와 같았다
어떻게 알아냈나
- 렌더링 폴더는 대부분 셰이더라 규칙이 없다고 보고 넘긴 곳이다.
그런데 파일 하나의 머리말이 스스로
→ CPU progress 라고 적어 뒀다.
- 그래서 같은 폴더 32개의 머리말만 다시 훑었고, 그중 하나가
number formatting 이었다.
- 구간 경계에서 틀리기 딱 좋은 표라 눈으로 옮기지 않고
참고 구현을 실제로 실행해 대조했다.
- 참고 구현 파일을 통째로 불러오면 클라이언트 트리 전체가 딸려 와 실행되지 않는다.
그래서 그 두 함수의 본문만 떼어 돌렸다 — 로직은 한 글자도 안 건드린다.
- 같은 자리에서 시작 병력 표를 대조하다 값 하나가 더 나왔다.
⚠ 테스트가 넷 중 하나만 재고 있었다.
표를 재는 테스트는 모든 칸을 재야 한다.
하나만 재면 나머지가 조용히 틀릴 수 있고, 실제로 그랬다. 넷을 다 재고
단조 증가까지 단언하도록 고쳤다 — 두 난이도가 같아지면 그 난이도는 뜻이 없다.
참고 구현 대조 54건 · 불일치 0
값 대조 도구가 현재 고장 — 백로그
34c330d
훑는 방법
백로그에 이런 항목이 있었다 —
“사일로 재장전 진행바. 값은 이미 있다. 그리기만 없다. 언제든(작다).”
시간이 남아서 집었는데 작지 않았다.
집을 때 생각한 것실제
막대 하나 그리면 끝난다
참고 구현의 막대 하나가 셋을 보여 준다 — 철거 예정 · 건설 진행 · 재장전
재장전만 없다
철거 예정이 아예 없었다. 건물을 철거하면 30초 동안 계속 돌아가는데 화면에 아무 표시가 없었다 — 눌렀는지조차 알 수 없다
그 파일만 보면 된다
같은 폴더 32개 머리말을 마저 읽었더니 사거리 패스 셋이 나왔고, 그게 건설 미리보기(677줄) 로 이어졌다 — 통째로 없던 기능이다
677줄을 다 구현해야 한다
아니다. 규칙은 반경 넷뿐이다 — 나머지는 “이미 있는 것을 어떻게 보여 주나” 라 백로그로 넘겼다
왜 반경이 규칙인가
- SAM — 사거리가 레벨을 탄다(Lv1 70 · Lv3 90).
업그레이드 미리보기에서 Lv1 반경을 그리면 덮는 범위를 과소평가한다.
- 공장 — 역 사거리 110 안에 있어야 철도가 이어진다.
모르고 놓으면 왜 안 이어지는지 알 방법이 없고 골드는 이미 나갔다.
- 핵 — 바깥 반경을 그린다. 안쪽(전멸)을 그리면 원자탄이
12 대 30 이라 피해를 2.5배 과소평가한다.
순수 함수 테스트만으로는 “안 그려진다” 를 못 잡는다
계산을 순수 함수로 옮기고 테스트도 통과했다. 그런데 그리는 쪽이 그 함수를
안 부르면 테스트는 전부 초록인 채 화면에는 아무것도 안 뜬다.
그래서 화면을 실제로 그려 막대 색 픽셀을 셌다.
⚠ 존재가 아니라 개수를 센다. 처음엔 색이 있는지만 봤더니
“채우는 폭이 비율을 안 탄다” 변이가 살아남았다.
25% 와 75% 를 그려 픽셀 수를 견주도록 고치고 나서야 잡혔다.
폴더 이름으로 넘긴 곳은 파일 머리말로 한 번 더 훑는다.
그 파일은 자기가 CPU 계산이라고 직접 적어 뒀다. 그리고 “작다” 고
적어 둔 항목을 믿지 않는다 — 그 판단은 우리에게 무엇이 없는지 모르는 상태에서
내린 것이다.