작성: 테크니컬 디렉터(TD), 2026-10-03, 최종본(라운드 3). 조율 결과는 7장(라운드 2 판정)과 8장(최종 조율 상태). 기준 코드 4dc02f4(측정 체크아웃 4e4fd93, 코드 동일).
이 문서는 엔진이 지키는 보증, 그 보증을 실제로 지키는 검사, 예산과 실측, 그리고 게임 디렉터 브리프의 TD 질문 네 개에 대한 답이다. 숫자에는 모두 측정 방법을 붙였다. "(추정)"은 측정 근거가 없는 것.
측정 환경: ~/workspace/hg-directors, node 24.18, 8코어 WSL2. 하니스 수치는 결정론이라 어느 기계에서나 같고, 시간(ms) 수치는 기계 상태에 따라 달라진다(3.5절).
1. 요약 — 이번 라운드에서 새로 확인한 것
아파치 대리 100%는 "아파치가 강하다"의 증거가 아니다. 대리는 플레이어 경로가 아니라 봇 유닛(world.spawnUnit('c_apache'))이고, 봇끼리 통계 사격(vsUnits)을 맞는다. 적이 경기당 평균 46.7초 동안 조준해도 맞히는 것은 경기당 평균 53.5 hp(체력 300), 사망 0.05회. 명중률을 1/4로 낮춰도(skill 0.25) 승률 70%다.
보병 영향력이 0인 첫 원인은 "거점에 없다"이다. 보병 대리는 비소유 거점 원 안에 한 번도 들어가지 못한다(CD 측정 0%, 아군 거점 포함으로는 평균 19.9초·2.8%). 원인은 D 북쪽 270~290 m의 단(#187)과, 대리의 흐름장·조향·실제 이동이 서로 다른 경사 기준(32°·30°·35°)과 표본 간격을 쓰는 불일치다(7.2절). 흐름장은 갈 수 있다고 하고 이동 규칙은 못 간다고 해서, 대리는 같은 단 앞에서 경기 끝까지 맴돈다.
20시드로는 브리프의 띠(idle + 20pp)를 판정할 수 없다. idle 승률의 95% 신뢰구간은 20시드에서 1552%다. 같은 idle을 시드 110과 1120으로 나누면 10%와 50%가 나온다. 판정에는 한 쪽당 60100시드가 필요하다(4.1절).
실패할 수 없는 검사가 네 개 있다. 경로별 번들 보고(경고만), 지연 로드 에셋 예산(상수만 있고 쓰는 곳 없음), 프레임·틱 예산(벽시계라 기계 상태가 결과를 정함), 하니스 "proxy ≥ 60%"(대리가 사실상 무적).
기준선 문서가 낡았다. 전장 첫 출격 번들은 baseline.md의 333.9 KB가 아니라 348.8 KB다. 남은 여유는 66 KB가 아니라 51.2 KB다.
2. 지킬 보증과 각각을 지키는 검사
"실패 가능"은 보증이 깨졌을 때 검사가 실제로 빨간불을 내는지를 뜻한다. "게이트"는 npm run check(CI와 같음)에 들어 있는지를 뜻한다.
const r = Math.random 처럼 호출 없이 참조하면 통과한다. Date.now·performance.now도 금지 목록에 없다(현재 sim 사용 0건, grep)
결정론(같은 시드 → 같은 결과)
runtime.test.ts 10분 재생 2회 비교(양 진영), hooks.test.ts 고정 해시 792702309
예
안
재생 비교는 같은 프로세스 안 재현성만 본다(결과가 바뀌어도 통과, 의도된 설계). 고정 해시는 아파치·적 AI 경로 변화를 잡는다
전장 코드는 지연 청크
arch.test.ts "loads the battle code only as a lazy chunk"
예
안
render는 sim에 쓰지 않는다
arch.test.ts 대입 정규식
부분
안
world.x = … 꼴만 잡는다. 지역 변수로 받은 뒤 쓰는 경우는 못 잡는다
PWA 무캐시
src/sw.test.ts
예
안
sw.js의 동작만 본다. main.tsx의 등록(updateViaCache: 'none')은 검사 밖
아파치 계통 회귀
baseline.md 회귀 기준 목록
예
안
맵 배치 원칙
schema.test.ts(원칙 통과 + 깨진 배치가 실패하는지 확인하는 낙제 시험)
예
안
좋은 예: 낙제 시험이 있다
외부 에셋 출처·라이선스
credits.test.ts
예
안
첫 로드 에셋 ≤ 600 KB
credits.test.ts
예
안
현재 578.5 KB, 여유 21.5 KB
글꼴이 모든 문자열을 덮는다
fonts.test.ts(content JSON의 비ASCII 문자 전부)
예
안
소스 코드에 한국어 문자열 0건(grep)이라 범위가 맞다
글꼴 ≤ 160 KB
fonts.test.ts
예
안
현재 113 KB
경로별 JS 번들(A1)
scripts/bundle-report.mjs
아니오
안
넘어도 경고만 내고 종료 코드 0. 강제는 B10-3(#174)
경기당 지연 로드 에셋 ≤ 1.5 MB(9.9)
MISSION_BUDGET_BYTES(1 MB, 낡은 값)
아니오
없음
상수를 쓰는 코드가 없다. 현재 유닛 235.1 + FARP 110.7 = 345.8 KB
자기 기체 메시 수
perfBudget.test.ts ≤ 125
예
안
아파치만. 전장 장면 드로우콜 검사는 없다
프레임·틱 시간 예산
battle:bench 수동
아니오
밖
벽시계. 3.5절
밸런스 띠(idle 30~50%, proxy ≥ 60%)
battle:harness 수동
아니오(proxy), 우연(idle)
밖
4.1절
모바일 30fps
소유자 실기기(#172)
측정 없음
밖
플레이어와 봇의 통행 규칙이 같다(브리프 P2)
없음
—
—
#187
2.1 실패할 수 없는 검사 — 고칠 순서
하니스 "proxy ≥ 60%" — 대리를 실제 플레이어 경로로 옮기기 전까지 이 띠는 아무것도 증명하지 않는다(4.1절). B5-8(#144) 완료 조건에도 같은 띠가 있다.
번들 보고 — B10-3까지 기다리지 말고, 지금 수치에 여유 5 KB를 둔 "회귀 방지선"을 테스트로 건다(예: 첫 출격 ≤ 355 KB). 한도(400)는 그대로 두고, 늘릴 때마다 커밋에서 선을 올리게 한다. 늘어난 것을 사람이 보게 만드는 장치다.
지연 로드 에셋 — 죽은 상수를 1.5 MB 검사로 바꾼다(경기 경로에서 읽는 그룹 합계).
벤치 — 시간 대신 결정론적 작업량(1Hz 틱의 A* 확장 노드 수, 가시선 질의 수, 효용 계산 횟수)을 세어 고정 시드에서 상한을 건다. 작업량은 기계와 무관하니 게이트에 넣을 수 있다. 시간은 참고값으로만 남긴다.
2.2 검사가 다른 것을 재고 있는 곳
하니스 --set은 최상위 수치만 바꾸고, 없는 키를 조용히 무시한다(scripts/battle-harness.ts: runtime.rules[k] = Number(v)). --set strength.player=1.2는 아무것도 바꾸지 않고 정상 결과표를 낸다. CD 제안을 숫자로 판정하려면 중첩 키 지원과 "없는 키면 오류"가 먼저다(코드 읽기로 확인).
대리 아파치의 처치 수는 "내 표적이 죽은 횟수"다(proxy.ts). 다른 유닛이 죽여도 센다. 실제 처치자(unitDestroyed.by)로 다시 세면 skill 1에서 13.7 → 12.8(7%), skill 0.25에서 6.45 → 5.05(28%) 부풀려져 있다.
플레이어 위치가 RNG 소비량을 바꾼다.combat.ts는 플레이어 2 km 안의 빗나간 봇 사격에만 예광탄 산포용으로 world.rng()를 더 뽑는다. 결정론은 유지되지만, 플레이어가 다르면 같은 시드라도 첫 사격 근처에서 경기 전체가 갈라진다. 그래서 idle과 soldier를 같은 시드로 돌려도 짝지은 비교가 되지 않는다. 브리프의 "4시드 겹침, 2시드 교체"는 영향이 아니라 이 분기다.
3. 예산과 실측
3.1 JS 번들 (gzip, npm run check 끝의 bundle-report.mjs)
경로
지금
baseline.md(B1-16)
한도(A1)
여유
첫 로딩(타이틀·설정·전장 설정)
144.3 KB
143.5
210
65.7
전장 첫 출격
348.8 KB
333.9
400
51.2
모든 청크
348.8 KB
333.9
440
91.2
"첫 출격"과 "모든 청크"가 같다 — 지금은 첫 출격에 안 쓰는 코드가 한 바이트도 없다. 이후 모든 기능이 첫 출격 한도(51.2 KB)를 먹는다.
구성(소스맵으로 원본 크기를 귀속, vite build --sourcemap): Battle 청크 609 KB(원본) 가운데 three 195 KB(32%), 아파치 전용 코드 약 85 KB(14%) — IHADSS·조종석·TADS·자기 기체 모델·로터 소리·무장·센서. 첫 출격이 보병인데 매번 같이 내려받는다. 첫 번들 491 KB(원본) 가운데 three 245 KB·React 214 KB.
B1+B2 16작업이 첫 출격을 약 +15 KB(333.9 → 348.8) 늘렸다. 마일스톤당 같은 속도라면 B3에서 여유가 36 KB로, B4 중간에 한도에 닿는다(추정).
청크 분할안 (브리프 TD 질문 4)
청크
내용
첫 출격에서 빠지는 양
heli
IHADSS·조종석·계기·TADS·자기 기체 모델·로터 소리(렌더·오디오 쪽)
약 12~15 KB gzip(추정: 아파치 전용 85 KB 원본 중 sim 공유분을 뺀 렌더 쪽 약 45 KB × 압축비 0.29)
vehicles
지상 탈것 조종·HUD·모델(B4)
첫 출격 0
jet
전투기 비행 모델·조종석 2종·HUD(B7)
첫 출격 0
heli-extra
수송·VPA 헬기 조종석(B6)
첫 출격 0
조건: sim 쪽 아파치 계통(sim/heli·sim/weapons)은 World가 직접 부르므로 분리하려면 PlayerBody 뒤로 숨기는 리팩터가 필요하다(비용 중). 렌더·오디오 쪽만 먼저 나눈다.
분할하면 bundle-report.mjs의 "첫 출격" 정의를 "보병 첫 출격"으로 바꾸고, 탈것별 경로(보병 → 아파치 전환 등)를 따로 보고한다.
코드 모델은 JS 예산을 먹는다. 탈것 외형을 코드로 만들면 그 기하 생성 코드가 번들에 들어간다(채택 GLB는 에셋 예산으로 간다). AD와 조율(TD-5).
3.2 무캐시 다운로드 (매 실행이 전체 다운로드)
GitHub Pages는 JS·CSS를 gzip으로 보내고, mp3·woff2·png·glb는 이미 압축돼 그대로 간다.
단계
크기
출처
HTML + 첫 JS + CSS
0.45 + 144.3 + 4.3 = 149.1 KB
vite 출력
첫 로드 에셋(boot 그룹)
578.5 KB
manifest.ts 그룹 합계
전장 JS(첫 출격까지 추가)
204.5 KB
348.8 − 144.3
유닛 모델 + FARP 소품
235.1 + 110.7 = 345.8 KB
전장 진입 시 renderer.ts가 로드
첫 출격까지 합계
약 1.28 MB / 실행
boot 그룹의 29%(169 KB)가 아파치 전용 소리다: engine_start.mp3 94.3 KB, rotor_interior_loop.mp3 47.8 KB, hellfire_launch.mp3 18.6 KB, rocket_launch.mp3 8.4 KB. 보병으로 시작하는 사람도 매번 받는다. 이것을 헬기 탑승 시 그룹으로 옮기면 boot는 약 409 KB, 여유는 191 KB가 된다.
ground_detail.png 165 KB는 boot 최대 단일 파일이다. WebP 재인코딩(가공 스크립트, 런타임 의존성 없음)으로 줄일 여지가 있다(추정, 미측정).
아이콘 5장 114.6 KB는 boot 예산 밖이고 브라우저가 manifest를 읽을 때 받는다.
3.3 동시 유닛 (battle:bench, 10분, 시드 1·2)
경기 방식
목록 최대
살아 있는 최대
은퇴
예산(9.9)
빠른 점령전
49~55
37
21~31
≤ 150
점령전(대규모, B5 대상)
97~102
76
54~62
≤ 150
baseline.md(B1-14)의 "목록 최대 40"은 잔해 은퇴 전 측정이라 지금 수치(49~55)와 정의가 다르다. B5 편제 목표(유닛 최대 134)까지 여유가 있다.
3.4 드로우콜
전장 장면은 한 번도 재지 않았다. 마지막 실측은 M7-3(캠페인 시절, 조종석 98·외부 129·높은 곳 60, 08장 8.7). 위키 9.9의 전장 추정 162는 계산값이다.
계측 경로는 있다(?debug&fps, renderer.info.render.calls). 이번 라운드는 브라우저 금지라 재지 않았다. 다음 라운드에서 헤드리스 1280×720 외부·보병 1인칭·조종석 세 시점을 잰다.
예산: 데스크톱 ≤ 200 / 모바일 low ≤ 120(추정) / 삼각형 데스크톱 ≤ 1.2M, 모바일 low ≤ 350k(추정).
3.5 프레임·틱 시간 — 이 예산은 지금 측정 불가능하다
같은 날 같은 기계에서 B1-14 커밋(02c19be)과 지금 코드를 차례로 돌렸다(git archive로 풀어 node_modules만 연결, 코드 수정 없음).
빠른 점령전, 10분 (ms)
baseline.md 기록
오늘 · 옛 코드 02c19be
오늘 · 지금 코드
sim 프레임 평균
0.066
0.174 / 0.209
0.227 / 0.197
10Hz 틱 최악
1.291
9.7 / 25.1
26.8 / 8.8
1Hz 틱 p99
—
5.6 / 5.1
5.0 / 5.4
1Hz 틱 최악 (예산 ≤ 2)
1.401
7.8 / 12.7
7.8 / 13.1
점령전, 10분 (ms)
baseline.md 기록
오늘 · 옛 코드
오늘 · 지금 코드
sim 프레임 평균
0.165
0.578
0.529 / 0.570
1Hz 틱 최악
3.6
9.0
11.1 / 13.5
같은 옛 코드가 기록 당시보다 약 3배 느리게 나온다. 오늘 이 기계가 느린 것이지 코드가 느려진 것이 아니다(옛 코드 대비 지금 코드 차이는 시드 간 차이 안).
"최악 ≤ 2 ms" 예산은 벽시계 최댓값이라 GC·스케줄러 한 번에 결정된다. 같은 실행 안에서 옛 코드도 25 ms를 냈다. 이 수치로는 회귀를 판정할 수 없다.
그래도 상대 비교에서 확실한 것: 점령전 1Hz 틱은 빠른 점령전의 약 2.4배(평균 1.25 vs 0.46~0.53 ms, 같은 세션). 지휘관 효용 계산과 도로 A*가 몰리는 곳이고, B5(#138·#143) 전에 터질 곳이다.
바꿀 것(2.1절 4): 시간 예산을 "고정 시드 작업량 상한"(게이트 안) + "같은 세션에서 기준 커밋과 나란히 잰 상대 시간"(마일스톤마다)으로 나눈다.
3.6 모바일
측정 없음. 30fps 확인은 소유자 실기기(#172, A9)이고 B10에 있다. 헤드리스 모바일 뷰포트 측정은 B5-7(#143).
대규모를 지킬 수 있는지는 B5에서 처음 안다. 그 전에 모바일에서 터질 곳(추정)은 인형 수와 1Hz 틱 스파이크다(점령전에서는 데스크톱도 이미 수 ms).
렌더러에는 그림자 맵 패스가 없다(src/render에서 shadowMap·castShadow 0건, 그림자는 헬기 아래 원판 메시 하나, AD 지적으로 확인). 그래서 위키 09장 9.9의 "모바일 low: 그림자 끔"은 줄일 비용이 없는 항목이다. 그림자 맵을 들이면 그때 새 패스로 따로 예산을 잡는다.
3.7 게이트 시간
단계
시간
측정
tsc
3초
로컬
vitest run
64~112초(74파일·470테스트)
로컬 두 번, 기계 상태 차
vite build + 번들 보고
3초
로컬
CI 전체(빌드+배포)
1분 25초 ~ 2분 17초
최근 15회 gh run list, 최근 100회 중 성공 97
테스트 시간 합(병렬 워커 합계) 499초 중 48%가 전장 테스트 3개: runtime.test.ts 106초(10분 재생 ×4), commander.test.ts 75초, combat.test.ts 59초.
하니스는 게이트에 넣기에 느리다: 빠른 점령전 한 경기에 idle·proxy 약 9초(단독 실행), 보병 대리는 4m 흐름장 생성(지도 115만 칸, 목표마다 수 초)이 더해진다. 역할 표 한 장(역할 8 × 진영 2 × 100시드)은 단일 스레드로 수 시간이다 → 시드 분할 병렬(worker_threads, 새 의존성 없음)과 마일스톤 단위 실행으로 간다. 게이트에는 2시드 "끝까지 돈다" 연기 검사만 넣는다.
4. 브리프 TD 질문에 대한 답
4.1 역할 영향력을 공정하게 재는 하니스 (질문 1)
지금 도구가 불공정한 이유 (측정)
~/workspace/hg-directors에서 하니스와 같은 sim을 부르는 측정 스크립트(작업 폴더, 저장소 밖)로 연합 빠른 점령전을 돌렸다.
연합 · 빠른 점령전
시드
승률 (95% CI)
끝 티켓 차 평균 (SD)
실제 처치 / 사망
받은 피해
idle
1~20
30% (15~52)
−58.9
—
—
idle
1~60
37% (26~49)
−28.2 (98.7)
—
—
soldier · cover
1~20
30% (15~52)
−54.2 (88.1)
0.35 / 3.1
—
proxy(아파치) skill 1
1~20
100% (84~100)
+140.1 (34.6)
12.8 / 0.05
53.5 hp/경기
proxy skill 0.5
1~20
90% (70~97)
+81.8 (74.6)
8.6 / 0.05
41.9
proxy skill 0.25
1~20
70% (48~85)
+48.5 (89.4)
5.1 / 0.1
85.7
skill은 대리 유닛의 명중 배율(Unit.skill)을 측정 스크립트에서 덮어쓴 값이다.
대리 아파치를 조준한 적은 휴대용 대공(시드 1: 52초, 평균 거리 744 m)과 자주 대공포(시드 2: 12초, 1,874 m)다. 시드 1에서 가시선이 트인 사격 판정(10Hz) 409회, 피해 0회. 봇끼리 사격은 units.json의 vsUnits 수치를 쓴다(휴대용 대공 sa_ir 발사율 0.05/초, 기본값은 0.1/초).
보병 대리는 사격이 이미 완벽에 가깝다(탄 낙차 보정 조준). 약한 이유는 실력이 아니라 위치다: 거점 안 평균 19.9초.
설계안 — 하니스 v3 "역할 영향력 표"
모든 대리는 실제 플레이어 경로로. 아파치 대리는 world.player(비행 모델)를 조종한다. 이미 있는 자동 호버(sim/heli/autohover.ts)로 위치·고도·기수를 잡고, 실제 무장 해(rocketSolution·hellfireSolution)로 쏜다. 그러면 적은 플레이어 상대 경로(진짜 탄·미사일)로 쏜다. 지금의 ProxyPilot은 "같은 자리를 봇이 채웠을 때"의 대체 기준선으로 남긴다.
역할 공통 실력 단계(L0~L3). 역할마다 다른 매개변수를 같은 의미의 네 축으로 정의한다: 반응 지연(발견 → 첫 사격), 조준 오차(도), 노출 정책(엄폐·고도·거리), 표적 선택(가까운 것 / 위협 순). L2는 "같은 역할 봇과 같은 처치/사망 비"로 보정한다 — 역할끼리 같은 실력을 맞추는 유일한 공통 눈금이다.
판정은 신뢰구간으로. 결과표에 승률 Wilson 95% 구간과 끝 티켓 차 평균 ± 표준오차를 같이 낸다. 띠 판정은 구간이 띠 안에 있을 때만 "통과", 걸치면 "미판정"(시드 추가).
시드 수. idle 37% 대비 +20pp를 검출하려면(유의 5%·검정력 80%) 승률로는 한 쪽당 약 93시드, 끝 티켓 차(SD 약 95, 20pp ≈ 46티켓으로 skill 단계에서 환산)로는 약 67시드. 기본 100시드, 최소 60.
짝지은 비교를 가능하게. 예광탄 산포 같은 표시용 무작위를 월드 RNG에서 떼어 별도 스트림으로 옮긴다(2.2절). 그러면 같은 시드에서 플레이어 행동만 다른 짝 비교가 되고, 필요한 시드 수가 크게 준다(얼마나 줄지는 구현 후 측정).
처치는 unitDestroyed.by로. 표적이 죽은 횟수가 아니라 실제 처치자로 센다.
레버 분해를 표의 열로. 역할마다 "거점 안 시간", "점령 방향을 바꾼 시간", "실제 처치", "스폿 → 아군 처치", "사망 티켓"을 같은 표에 낸다(4.2절).
실행.--jobs N 시드 분할 병렬, 결과는 JSON 줄로 남겨 표를 다시 그릴 수 있게. 표 한 장(역할 × 양 진영 × 100시드)은 8코어에서 30분 안(추정, idle·proxy 9초/경기 기준).
결과물은 "역할 영향력 표" 한 장: 행 = 역할 × 실력 단계, 열 = 승률(CI)·티켓 차(SE)·레버 열. B3-8(#125)과 B4 각 탈것의 완료 조건이 이 표의 행이 된다.
비용(추정): 아파치 실제 경로 대리 23일, 실력 단계·통계·병렬 12일, RNG 스트림 분리 반나절 + 고정 해시(hooks.test.ts)·결정론 테스트 갱신. 아파치 수치 조정(CD 판단)은 이 도구가 나온 뒤에 하는 것을 권한다.
4.2 보병 영향력 0의 원인 분해 (질문 2)
측정 스크립트가 매 초 각 거점에서 "플레이어를 뺀 세력"을 다시 계산해, 플레이어가 점령 방향(부호)이나 경합 판정을 바꿨는지 셌다(conquest.strengthOf와 같은 식).
연합 · soldier · 20시드 평균
값
거점 영역 안에 있던 시간(아군 시작 거점 포함)
19.9초 / 약 706초(2.8%). 비소유 거점만 세면 0%(CD 측정)
점령 방향을 바꾼 시간
6.05초(시드 5: 102초, 시드 6: 17초, 나머지 0)
경합 판정을 바꾼 시간
0.25초
처치
0.35(한 시드에서 7, 나머지 0)
사망
3.1 → 티켓 3.1(180 중 1.7%)
레버 순서: ① 거점에 가는가(이동·스폰 거리·#187 경사 — 막히는 지점과 원인은 7.2절) → ② 가서 살아 있는가 → ③ 세력·처치. ①이 2.8%인 동안 ②③의 수치를 올려도 결과에 닿지 않는다.
이 측정은 CD 제안을 판정하는 실험의 틀이 된다. 제안 하나마다: 바꿀 규칙 → 예상되는 레버 열의 변화 → 100시드 표. 예를 들어 "주변 분대가 따라온다"는 거점 안 시간과 점령 방향 시간이 같이 올라가야 하고, "스폿 → 아군 화력"은 스폿 기여 처치 열이 올라가야 한다.
선행 작업: --set 중첩 키 + 없는 키 오류(2.2절), 거점 안 시간·결정 시간·스폿 기여 열을 하니스에 정식으로.
4.3 #187 경사 (질문 3)
측정(4m 격자, 지형 법선, 시드 1). 주의: 이 표는 거점 중심에서 1.6r(D는 80 m)까지만 본다. #187의 단(D 북쪽 270290 m, 경사 3746°)은 이 범위 밖이라 D 행이 0%로 나온다. 라운드 1에서 이 한계를 적지 않아, CD 초안이 "D 정체는 경사로 설명되지 않는다"고 읽었다(7.2절에서 정정).
거점
반경
영역 안 > 35°
영역 안 > 45°
둘레(1~1.6r) > 35°
둘레 > 45°
A·G(빠른 점령전 시작 거점)
80 m
14.8%
7.0%
21.4%
4.8%
D(가운데)
50 m
0%
0%
1.4%
0%
C·E(점령전 전용)
50 m
0%
0%
32.4%
24~25%
(a) 보병 한계 45°: 시작 거점 영역의 7%와 C·E 둘레의 약 25%는 여전히 못 오른다. 문제를 줄이지만 없애지 않는다. 비용은 상수 하나 + 이동 테스트 갱신, sim 비용 0.
(b) 봇에도 같은 규칙: 국소 탐침(보병 대리의 walkable과 같은 17방향)이면 이동 유닛 72(점령전 시작) × 17 × surfaceAt 0.26 µs ≈ 10Hz 틱당 0.31 ms(측정, 부하 중). 싸다. 그러나 국소 탐침만으로는 봇이 단 앞에서 막힌다(보병 대리가 몇 분씩 제자리걸음한 것과 같은 현상). 막히지 않으려면 흐름장이 필요한데, 지금 FootField(4m, 지도 전체 115만 칸)는 개당 약 6초(부하 중 측정)라 런타임 생성은 불가능하다. 거점 반경 + 300 m 상자로 줄이면 칸 수 약 1/30이고, 맵 파일에 굽거나 로딩 중 생성할 수 있다(추정).
(c) 지형 완화(평탄화 섞는 폭): 원인 제거이고 sim 비용 0. 단점은 헬기 능선 은폐(GD 지적).
TD 권장: (a) + 맵 검사 조건부 예. 45°로 올리고, 맵 검사는 CD 요청대로 "둘레 N방향"이 아니라 보병 고정 스폰 → 각 거점 원까지 실제 이동 규칙으로 닿는 경로가 있는가로 둔다(낙제 시험 포함). 이 검사와 보병 이동·흐름장·조향은 같은 경사 함수 하나를 써야 한다(7.2절). 다르면 검사가 통과해도 보병이 막힌다. (b)는 B4 탈것 이동(차량도 경사 규칙 필요)과 같이 하면 흐름장 비용을 한 번만 낸다.
4.4 예산 계획 (질문 4)
위험
언제 터지나
대책
지킬 검사
첫 출격 JS 400 KB
B4 중간(추정, 마일스톤당 +15 KB)
3.1절 청크 분할 — heli 렌더 먼저(B3 전), vehicles·jet·heli-extra는 생길 때부터 지연
글꼴 재서브셋 불필요: 후보 "카르다 전선"·"카르다 전장"·"하레크 계곡"·"티켓 제로"의 글자는 모두 지금 서브셋에 있다(content JSON 문자 집합과 대조). 다른 이름이 새 글자를 쓰면 fonts.test.ts가 실패하고, 두 굵기를 각각 다시 서브셋해야 한다.
번들 변화 0. 설치된 PWA의 이름은 브라우저가 manifest를 다시 읽을 때 바뀐다(안드로이드 크롬은 주기적으로, iOS 홈 화면 이름은 다시 설치해야 바뀐다).
지킬 검사(새로): "<title> = manifest name = 타이틀 문자열" 일치 테스트 + 옛 이름("헬기 조종", "AH-64 … 시뮬레이터") 0건. 브리프 문제 3의 성공 측정을 게이트로 만든다.
② 저장소·URL도 변경
vite base: '/heli-game/', sw.test.ts의 URL, README가 바뀐다.
manifest id: "./"가 새 경로로 풀려 PWA 정체성이 바뀐다 — 설치본은 새 앱이 아니라 깨진 옛 앱이 된다.
GitHub Pages는 프로젝트 저장소 이름 변경 시 리다이렉트가 없다. 옛 경로는 사용자 사이트 저장소(kyhsa93.github.io)에 이동 스텁을 두어 받을 수 있다(블로그 쪽 postbuild meta-refresh로 같은 일을 한 전례가 있다). 이때 옛 경로에 sw.js를 두지 않아야 등록된 서비스 워커가 갱신 실패(404)로 해제된다(사양 동작, 실기기 미확인).
localStorage는 출처(kyhsa93.github.io) 단위라 설정·저장(heli-campaign-v1)은 경로가 바뀌어도 남는다. 같은 출처를 블로그·다른 프로젝트와 공유하므로 키 이름은 지금 그대로 둔다.
비용: 코드 몇 줄 + 다른 저장소 작업 + 설치본 손실. TD도 ①을 권장한다.
6. 열린 일 (이 문서가 제안하는 이슈 후보, 등록은 조율 후)
하니스 v3: 실제 경로 아파치 대리, 실력 단계, CI·시드 수, --jobs, by 처치 집계, 레버 열(4.1·4.2)
하니스 --set 중첩 키 + 없는 키 오류(2.2)
표시용 무작위를 월드 RNG에서 분리(2.2·4.1)
번들 회귀 방지선 테스트, 경기당 지연 에셋 1.5 MB 검사(2.1)
벤치 작업량 계수 + 상한 테스트(3.5)
아파치 전용 소리를 boot에서 헬기 그룹으로(3.2), heli 렌더 청크 분리(3.1)
판정 원문은 디렉터 조율 폴더의 responses-td.md. 여기에는 결과와 새 측정만 옮긴다.
7.1 판정
안건
판정
조건·근거
CD-1 대리가 D 둘레에서 막히는 원인
수락, 원인 확정
7.2
CD-2 역할마다 도착 조건
수락
역할별 "레버 도달" 열. 아파치는 실제 경로 전까지 상한값 표기
CD-3 자동 집중 거점 비용
조건부 수락
판정 자체는 무시할 만함. 집중 전환 때 재계획(도로 A*)을 작업량 계수로 재고, 90초 쿨다운을 sim 규칙으로
CD-4 첫 출격 ≤ 600 m
조건부 수락
맵에 고정 전방 집결지가 있어야 정적 검사가 됨. 지금 A → D 1,640 m. 동적 "분대 옆" 스폰은 하니스 지표(첫 진입 시각)로
CD-7 사람 항목 ↔ 하니스 지표
수락
firstContactSec·firstEntrySec·reentrySec·endFlips
AD-1 피아 외형
조건부 수락
드로우콜 +3~4(추정), JS ≤ 1 KB. 은폐 비대칭은 하니스로 못 봄(봇 탐지는 색을 안 봄, sim은 렌더 비의존)
AD-2 지형 점선·#182 실기기
둘로 나뉨
도로 띠는 기하라 실기기에서도 생김(7.3). 경사면 점선은 판별 실험 + 소유자 스크린샷
AD-3 라이선스 고지
수락
첫 로드 비용 0
AD-4 boot·전장 그룹 정리
수락
재유입 막는 검사 조건
AD-5 탈것 규칙 + 실루엣 도구
수락 / 도구는 게이트 밖
산출 PNG는 저장소에 커밋하지 않음
AD-6 경계 말뚝·상대색 깃발
수락
드로우콜 +1
AD-8 보병 최저선
조건부 수락
1.5 m 아군 숨김은 렌더에서만
7.2 보병 대리가 D 북쪽에서 막히는 원인 (CD-1)
시드 1을 추적했다(측정 스크립트 td-stuck.ts, 5초 표본). 대리는 340초에 D 북쪽 (−46, 275), 경사 35° 지점에 닿는다. 이후 120초 동안 적 0·피격 0·전진 입력 100%인데 스텝의 54%에서 움직이지 못한다. 5초마다 우회각을 뒤집으며 (−28−48, 275305) 안을 맴돈다. 지면은 경사 37~46°이고 4 m 안에서 3 m가 오른다. #187이 말한 단이 바로 이곳이다.
누가
경사 기준
표본
흐름장 footpath.ts
32°
4 m 칸 중심 + 칸 사이 중점
조향 탐침 soldierProxy.walkable
30°
4 m 앞 높이차
실제 이동 movement.ts
35°
매 스텝 도착점 법선
흐름장이 내준 길을 0.25 m 간격으로 다시 걸으면, 오르막에서 35°를 넘는 곳이 8곳 나온다(최대 37.1°).
D ±400 m 상자에서 "칸 중심은 완만한데 칸 안에 35° 초과가 숨은 칸"은 1.7%(547/31,674)다.
흐름장 자체는 끊기지 않았다(D 반경 60~500 m 원 위 72방향 모두 도달). 단만 넘으면 길이 있다.
사람 플레이어도 같은 곳에서 풀색인데 못 오르는 띠를 만난다(아트-디렉션 4.2a).
고칠 것: ① 경사 판정 함수 하나(footStep, 35°, 0.5 m 표본)를 보병 이동·흐름장·조향·맵 도달 검사가 같이 쓴다. ② 하니스에 "전진 입력 중 정지 시간"을 넣는다. 대리가 길에 갇힌 것을 "보병이 약하다"로 읽는 오진을 막는다. ③ 지형 단은 4.3절대로 손본다.
7.3 지형 점선과 도로 띠 (AD-2)
도로 띠는 실기기에서도 생긴다. 측정 스크립트(td-road.ts)로 도로 리본(roadMesh, 6.25 m 간격, 지면 +0.25 m)과 근거리 지형 메시(heightAt와 같은 삼각 분할)를 삼각형마다 45점씩 비교했다. 도로 삼각형 6,912개 중 23.2%에서 지형이 도로 위로 솟고, 최대 2.37 m다. 래스터와 무관한 기하다. polygonOffset은 로그 깊이 버퍼에서 효과가 없을 것으로 본다(three.js에서 알려진 동작, 이 코드에서는 미검증).
경사면 점선은 판정 불가. 청크 안은 인덱스 공유 메시라 T-접합이 없고, 스커트는 청크 테두리에만 있다. 남는 후보는 SwiftShader의 로그 깊이 래스터다. 판별: logarithmicDepthBuffer를 끄는 디버그 플래그로 같은 장면을 찍는다. 확정은 소유자 기기 스크린샷(#101).
7.4 그 밖의 확인
진영 비대칭(CD 1.2): idle 60시드씩 쟀다. 연합 37%(95% 구간 2649), VPA 50%(3862), 두 비율 검정 p ≈ 0.14다. 끝 티켓 차는 −28.2 vs −12.7(SD 84~99). 비대칭이라고 말할 근거가 아직 없다. 하니스 v3에서 양 진영 100시드씩 다시 잰다.
스폿 → 봇 표적(CD 4.5): 들어간다. 스폿 키가 Intel.mark로 가고, targeting.ts가 표시된 유닛을 표적 후보에 넣는다. 다만 보병 대리는 스폿 키를 누르지 않아서 하니스가 이 레버를 잰 적이 없다.
쓰지 않는 모델(AD-4): 전장 분대는 코드 인형이라 unitRenderer가 GLB soldier를 쓰지 않는다. fuel_truck·civ_car는 로스터에 없다. 빼면 경기당 지연 에셋 345.8 → 약 245 KB이고, 아파치 소리까지 옮기면 boot 578.5 → 약 409 KB다.
라이선스(AD-3): 고지 파일은 크레딧 화면에서만 받는다(boot 0). 글꼴 name ID 13·14는 fonts.test.ts의 예약 이름 검사 대상(1·3·4·6·16~22)과 겹치지 않는다.
바위색과 이동 한계(아트 4.2a): 셰이더 바위 혼합(31°→46°)이 이동 한계와 따로 정해져 있다. 7.2의 경사 상수 하나에서 렌더가 읽게 하면 한계가 바뀔 때 화면도 같이 바뀐다(render → sim 읽기는 의존 규칙상 허용).
8. 조율 상태 (최종, 라운드 3)
순서는 CD → AD → TD. 원문은 디렉터 조율 폴더 final-td.md. GD 회부 0건, 합의 18건, TD 정정 1건, 소유자 결정 요청 1건.
도로 띠(AD-2): 지형 평탄화가 아니라 렌더 쪽 수정이다. 도로 리본 정점을 지형 삼각형 모서리와 만나는 곳마다 넣고, 높이는 heightAt + 0.25 m로 둔다. sim 지형이 그대로라 결정론 고정 해시와 하니스 결과는 바뀌지 않는다. 드로우콜은 1 그대로, 정점은 약 2~3배(추정). 확인은 측정 스크립트(솟는 삼각형 0%)와 100 m 앞 헤드리스 스크린샷으로 한다(AD 조건 "끊김 없는 한 줄"). 원거리 청크(2.6 km 밖)의 어긋남은 1 px 미만이라 둔다.
타이틀 실시간 배경(CD-6·AD-7): 지연 청크로 두고 단색 배경을 먼저 보인다. 첫 로딩 한도 210 KB(지금 144.3)를 지킨다.
라이선스(AD-3): licenses.txt는 크레딧 화면에서만 받는다. 글꼴 재서브셋 때 fonts.test.ts에 "name ID 13·14에 예약 이름 없음"을 더한다.
WebP(TD-6 대안): 브라우저 디코더(TextureLoader)라 새 의존성 없음. iOS Safari 지원 버전은 재확인(추정 14+).
8.3 정정
3.6의 "그림자 패스"는 틀렸다(AD 지적). 렌더러에 그림자 맵 패스가 없다. 고쳤다.
4.3 경사 표의 측정 범위(1.6r) 때문에 생긴 오해는 7.2에서 정정했고, CD가 위키 4.1 문장을 철회했다.
8.4 소유자 결정 요청
"아파치" 실명(CD·AD와 같은 항목): 채택 차량은 가상 명칭인데 아파치만 실명이고, 상표 위험이 있다(추정). TD 기술 비용: strings.ko.json 슬롯 이름 4곳만 바꾸면 되고 코드 식별자는 그대로 둔다. 새 글자가 나오면 재서브셋하고, fonts.test.ts가 이를 잡는다.
8.5 남은 부채 (합의된 것, 기록용)
봇과 플레이어의 경사 규칙은 B4까지 다르다(P2 공정성).
하니스 v3 전까지 모든 역할 비교 수치는 참고값이다(아파치는 상한, 보병은 도착 실패).