FPS Arena에 폭파 모드를 넣었습니다. 라운드제, 부활 없음, 공격이 설치하고 수비가 해체합니다. 이 게임에서 제일 큰 규칙 변경이라 검증이 중요했습니다.
그래서 헤드리스 스모크에 확인 절차를 넣었습니다. 20초 돌리면서 봇이 폭탄을 심는지, 라운드가 실제로 끝나는지를 봅니다.
같은 맵에서 결과가 계속 달랐습니다.
규칙 검증: 설치 true · 기폭으로 라운드 종료 true
규칙 검증: 설치 true · 기폭으로 라운드 종료 false
규칙 검증: 설치 true · 기폭으로 라운드 종료 true
첫 번째 오진: 라운드가 안 끝난다
처음 로그는 이랬습니다.
폭파 모드: 라운드 0회 · 설치 0회 · 결과 공격 전멸(1팀)
라운드 0회인데 결과에는 사유가 적혀 있습니다. 앞뒤가 안 맞습니다.
원인은 GDScript 였습니다. 람다가 바깥 지역변수를 값으로 복사해 갑니다.
var rounds := 0
GameState.round_ended.connect(
func(team: int, why: String) -> void:
rounds += 1 # 바깥 rounds 는 영원히 0
reasons.append(...) # 배열은 참조형이라 이건 된다
)
reasons 는 배열이라 동작했고 rounds 는 정수라 안 됐습니다. 그래서 "사유는 있는데 횟수는 0" 이라는 모순된 로그가 나온 겁니다. 사전에 담아 고쳤습니다.
두 번째 오진: 설치가 무시된다
다음엔 설치가 성공했는데 라운드가 안 끝났습니다. 로그를 더 넣어 보니:
라운드 진행중=true · 설치됨=false · 기폭까지 0.0
설치를 분명히 호출했는데 bomb_planted 가 false 입니다.
plant_bomb() 의 첫 줄이 이랬습니다.
if bomb_planted or not round_live:
return
관찰 단계에서 한 팀이 전멸하면 라운드가 이미 끝나 있고, 그 뒤로는 설치가 조용히 무시됩니다. 게임 규칙으로는 맞습니다 — 끝난 라운드에 폭탄을 심을 수는 없으니까요. 틀린 건 "라운드가 살아 있다" 를 확인 안 하고 검증을 시작한 제 테스트였습니다.
세 번째 오진: 타이머가 안 돈다
라운드를 새로 열고 검증하게 고쳤습니다. 그래도 가끔 실패했습니다.
(진단) 라운드=true 설치=true 기폭까지 0.60 매치종료=true
매치종료=true. 관찰 단계에서 라운드가 5번 쌓여 매치가 끝나 있었고, GameState._process 의 첫 줄이 if match_over: return 이라 기폭 타이머가 아예 안 돌고 있었습니다.
진짜 문제는 검증 방식이었다
세 번 다 게임 코드는 멀쩡했습니다. 저는 매번 게임을 의심하고 파고들었습니다.
한 발 물러나 보니 문제가 명확했습니다. 저는 봇이 우연히 3초를 안 끊기고 버티기를 기다리고 있었습니다. 7명이 뒤엉킨 맵에서 그건 매번 결과가 달라지고, 그러면 "규칙이 맞나" 를 영영 판정할 수 없습니다.
그래서 검증을 둘로 갈랐습니다.
| 무엇을 재는가 | 어떻게 | |
|---|---|---|
| 1부 | 봇이 목표를 향해 움직이는가 | 20초 관찰, 지표만 수집 |
| 2부 | 규칙이 맞는가 | 전투를 끄고 설치·기폭·해체를 직접 호출 |
2부에서는 봇의 감각을 통째로 끕니다. 아무도 안 싸우니 라운드가 중간에 안 끝나고, 규칙만 남습니다. 세 번 연속 통과했고 그 뒤로 흔들리지 않습니다.
규칙 시험은 규칙만 봐야 합니다. 창발적 행동이 섞이면 그건 시험이 아니라 관찰입니다. 둘 다 필요하지만 같은 자리에서 하면 안 됩니다.
그 사이에 실제로 있던 버그
이 소동 덕분에 진짜 버그도 하나 나왔습니다. 봇이 폭탄 지점 1.2m 앞까지 가 놓고 30초 내내 설치 진행이 0.0초였습니다.
"적이 안 보일 때만 목표 행동" 으로 두었기 때문입니다. 7명이 싸우는 맵에서 3초 동안 아무도 안 보이는 순간은 오지 않습니다. 지금은 한 번 손을 대면 적이 11m 밖일 때는 끝까지 가고, 코앞이면 총이 먼저입니다.
남은 자국
같은 람다 함정에 이 프로젝트에서 두 번 걸렸습니다. 폭파 모드 카운터에서 한 번, 그리고 며칠 뒤 되감기 검증에서 또 한 번 — 이번에는 Vector3 였습니다.
두 번째로 걸린 뒤에야 README 에 적었습니다.
카운터는 사전이나 배열 같은 참조형에 담는다.
한 번 당했을 때 적었으면 두 번째는 없었을 겁니다.