테크니컬 디렉션 - kyhsa93/karda GitHub Wiki

테크니컬 디렉션

작성: 테크니컬 디렉터(TD), 2026-10-03, 최종본(라운드 3). 조율 결과는 7장(라운드 2 판정)과 8장(최종 조율 상태). 기준 코드 4dc02f4(측정 체크아웃 4e4fd93, 코드 동일). 이 문서는 엔진이 지키는 보증, 그 보증을 실제로 지키는 검사, 예산과 실측, 그리고 게임 디렉터 브리프의 TD 질문 네 개에 대한 답이다. 숫자에는 모두 측정 방법을 붙였다. "(추정)"은 측정 근거가 없는 것.

측정 환경: ~/workspace/hg-directors, node 24.18, 8코어 WSL2. 하니스 수치는 결정론이라 어느 기계에서나 같고, 시간(ms) 수치는 기계 상태에 따라 달라진다(3.5절).


1. 요약 — 이번 라운드에서 새로 확인한 것

  1. 아파치 대리 100%는 "아파치가 강하다"의 증거가 아니다. 대리는 플레이어 경로가 아니라 봇 유닛(world.spawnUnit('c_apache'))이고, 봇끼리 통계 사격(vsUnits)을 맞는다. 적이 경기당 평균 46.7초 동안 조준해도 맞히는 것은 경기당 평균 53.5 hp(체력 300), 사망 0.05회. 명중률을 1/4로 낮춰도(skill 0.25) 승률 70%다.
  2. 보병 영향력이 0인 첫 원인은 "거점에 없다"이다. 보병 대리는 비소유 거점 원 안에 한 번도 들어가지 못한다(CD 측정 0%, 아군 거점 포함으로는 평균 19.9초·2.8%). 원인은 D 북쪽 270~290 m의 단(#187)과, 대리의 흐름장·조향·실제 이동이 서로 다른 경사 기준(32°·30°·35°)과 표본 간격을 쓰는 불일치다(7.2절). 흐름장은 갈 수 있다고 하고 이동 규칙은 못 간다고 해서, 대리는 같은 단 앞에서 경기 끝까지 맴돈다.
  3. 20시드로는 브리프의 띠(idle + 20pp)를 판정할 수 없다. idle 승률의 95% 신뢰구간은 20시드에서 1552%다. 같은 idle을 시드 110과 1120으로 나누면 10%와 50%가 나온다. 판정에는 한 쪽당 60100시드가 필요하다(4.1절).
  4. 실패할 수 없는 검사가 네 개 있다. 경로별 번들 보고(경고만), 지연 로드 에셋 예산(상수만 있고 쓰는 곳 없음), 프레임·틱 예산(벽시계라 기계 상태가 결과를 정함), 하니스 "proxy ≥ 60%"(대리가 사실상 무적).
  5. 기준선 문서가 낡았다. 전장 첫 출격 번들은 baseline.md의 333.9 KB가 아니라 348.8 KB다. 남은 여유는 66 KB가 아니라 51.2 KB다.

2. 지킬 보증과 각각을 지키는 검사

"실패 가능"은 보증이 깨졌을 때 검사가 실제로 빨간불을 내는지를 뜻한다. "게이트"는 npm run check(CI와 같음)에 들어 있는지를 뜻한다.

보증 검사 실패 가능 게이트 비고
sim은 렌더·DOM·React에 의존하지 않는다 src/arch.test.ts 정규식(import 경로, document., window., localStorage) 예 안 import 문만 본다. 같은 규칙을 우회하는 간접 경로는 못 잡는다
sim 안 무작위는 월드 RNG만 arch.test.ts Math.random( 금지 예 안 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 실패할 수 없는 검사 — 고칠 순서

  1. 하니스 "proxy ≥ 60%" — 대리를 실제 플레이어 경로로 옮기기 전까지 이 띠는 아무것도 증명하지 않는다(4.1절). B5-8(#144) 완료 조건에도 같은 띠가 있다.
  2. 번들 보고 — B10-3까지 기다리지 말고, 지금 수치에 여유 5 KB를 둔 "회귀 방지선"을 테스트로 건다(예: 첫 출격 ≤ 355 KB). 한도(400)는 그대로 두고, 늘릴 때마다 커밋에서 선을 올리게 한다. 늘어난 것을 사람이 보게 만드는 장치다.
  3. 지연 로드 에셋 — 죽은 상수를 1.5 MB 검사로 바꾼다(경기 경로에서 읽는 그룹 합계).
  4. 벤치 — 시간 대신 결정론적 작업량(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 "역할 영향력 표"

  1. 모든 대리는 실제 플레이어 경로로. 아파치 대리는 world.player(비행 모델)를 조종한다. 이미 있는 자동 호버(sim/heli/autohover.ts)로 위치·고도·기수를 잡고, 실제 무장 해(rocketSolution·hellfireSolution)로 쏜다. 그러면 적은 플레이어 상대 경로(진짜 탄·미사일)로 쏜다. 지금의 ProxyPilot은 "같은 자리를 봇이 채웠을 때"의 대체 기준선으로 남긴다.
  2. 역할 공통 실력 단계(L0~L3). 역할마다 다른 매개변수를 같은 의미의 네 축으로 정의한다: 반응 지연(발견 → 첫 사격), 조준 오차(도), 노출 정책(엄폐·고도·거리), 표적 선택(가까운 것 / 위협 순). L2는 "같은 역할 봇과 같은 처치/사망 비"로 보정한다 — 역할끼리 같은 실력을 맞추는 유일한 공통 눈금이다.
  3. 판정은 신뢰구간으로. 결과표에 승률 Wilson 95% 구간과 끝 티켓 차 평균 ± 표준오차를 같이 낸다. 띠 판정은 구간이 띠 안에 있을 때만 "통과", 걸치면 "미판정"(시드 추가).
  4. 시드 수. idle 37% 대비 +20pp를 검출하려면(유의 5%·검정력 80%) 승률로는 한 쪽당 약 93시드, 끝 티켓 차(SD 약 95, 20pp ≈ 46티켓으로 skill 단계에서 환산)로는 약 67시드. 기본 100시드, 최소 60.
  5. 짝지은 비교를 가능하게. 예광탄 산포 같은 표시용 무작위를 월드 RNG에서 떼어 별도 스트림으로 옮긴다(2.2절). 그러면 같은 시드에서 플레이어 행동만 다른 짝 비교가 되고, 필요한 시드 수가 크게 준다(얼마나 줄지는 구현 후 측정).
  6. 처치는 unitDestroyed.by로. 표적이 죽은 횟수가 아니라 실제 처치자로 센다.
  7. 레버 분해를 표의 열로. 역할마다 "거점 안 시간", "점령 방향을 바꾼 시간", "실제 처치", "스폿 → 아군 처치", "사망 티켓"을 같은 표에 낸다(4.2절).
  8. 실행. --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는 생길 때부터 지연 회귀 방지선 테스트(지금), 한도 강제(B10-3을 앞당김)
첫 로드 에셋 600 KB 다음 boot 에셋 22 KB 아파치 소리 169 KB를 헬기 그룹으로 credits.test.ts(이미 있음)
점령전 1Hz 틱 B5-2(#138) 대규모 편제 지휘관 효용 계산을 틱 사이에 나누기(소대 단위 순번), 도로 A* 결과 캐시 고정 시드 작업량 상한(새로)
드로우콜 B4(탈것 유형 +10)~B5(인형 LOD) 유형당 인스턴스 메시 1, 재질 수 제한 헤드리스 측정 표(다음 라운드), 이후 B5-7
모바일 B5-7 측정 전엔 모름 A9 단계 #143·#172

5. 이름을 바꿀 때의 기술 비용 (소유자 결정 O1·O2 자료)

① 화면 이름만 변경, heli-game URL 유지 (GD 권장)

바꿀 곳 비용
index.html <title>·apple-mobile-web-app-title 2줄
public/manifest.webmanifest name·short_name·description 3줄
src/content/strings.ko.json title.name·sub·battleSub 3줄
README·CLAUDE.md·위키 Home·_Sidebar·디렉터 에이전트 설명 문서
아이콘(public/icons 5장, 114.6 KB, boot 예산 밖) AD 산출물 교체
  • 글꼴 재서브셋 불필요: 후보 "카르다 전선"·"카르다 전장"·"하레크 계곡"·"티켓 제로"의 글자는 모두 지금 서브셋에 있다(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)
  • 전장 드로우콜 헤드리스 측정(3.4)
  • baseline.md 번들 수치 갱신(348.8 KB), 08장 8.7 "JS 번들 ≤ 350KB" 행을 A1로 교체
  • 화면 이름 일치 테스트(5장)
  • 보병 경사 판정 함수 하나로 통일(이동·흐름장·조향·맵 도달 검사), 하니스 "전진 중 정지 시간" 지표(7.2)
  • 도로 리본이 지형에 묻히는 문제(도로 삼각형 23.2%) — 렌더 쪽에서 리본 정점을 지형 삼각형 모서리마다 넣어 표면을 따르게(8.2), 로그 깊이 끄는 디버그 플래그로 점선 판별(7.3)
  • 타이틀 실시간 배경은 지연 청크로, 이름 일치 테스트에 apple-mobile-web-app-title 포함(8.2)
  • fonts.test.ts에 "name ID 13·14에 예약 이름 없음" 추가(8.2)
  • 전장 그룹 모델은 로스터가 쓰는 것만 — 검사 추가 후 soldier·civ_car·fuel_truck 제외(7.4)
  • 라이선스 고지: licenses.txt(boot 밖), 글꼴 name ID 13·14(7.4)
  • 맵 rally 전방 집결지 + "고정 스폰 → 비소유 거점 ≤ 600 m, 도달 가능" 검사(7.2)

7. 조율 반영 (라운드 2)

판정 원문은 디렉터 조율 폴더의 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건.

8.1 합의

안건 결과 TD가 지킬 검사·조건
TD-1 아파치 판단은 하니스 v3 뒤 합의 봇 대공 vsUnits 조정도 v3 뒤
TD-2·TD-3 보병 레버·비용 등급 합의(CD 후보 P1~P5) 레버 열 + 100시드 표
TD-4 #187 합의: 45° + 도달 경로 맵 검사, 봇 규칙은 B4 공통 footStep, 낙제 시험. 봇·플레이어 규칙 차이는 P2 부채로 기록
TD-5·TD-7 탈것 예산·드로우콜 합의 코드 모델 ≤ 3 KB gzip, 탈것별 지연 청크, 재질 ≤ 2
TD-6 boot 정리 합의(+ AD-4 101 KB 제외, WebP는 전후 스크린샷 조건) 로스터 대조 검사
TD-8 이름 ① 합의 이름 일치 테스트(apple-mobile-web-app-title 포함)
CD-1 D 북쪽 정체 원인 합의(7.2) footStep 먼저, 하니스 "전진 중 정지 시간"
CD-2 역할별 레버 도달 열 합의 아파치 행은 실제 경로 전까지 상한값
CD-3 자동 집중 거점 합의 재계획 작업량 계수, 90초 쿨다운 sim 규칙
CD-4 전방 집결지·첫 출격 ≤ 600 m 합의 rally 좌표 + 도달 검사, firstEntrySec. 집결지는 거점 아님(티켓·효용 밖)
CD-5·AD-6 경계 말뚝·상대색 깃발 합의 드로우콜 +1
CD-6·AD-7 타이틀·아이콘·부제 합의 실시간 배경은 지연 청크, 첫 로딩 210 KB 유지
CD-7·AD-10 사람 관문 합의 firstContactSec·firstEntrySec·reentrySec·endFlips
AD-1 피아 외형 합의 드로우콜 +3~4, JS ≤ 1 KB, 하니스 영향 0(사람 관문에서 진영 비교)
AD-2 도로 띠·점선·#182 합의 8.2
AD-3 라이선스 합의 boot 0, ID 13·14 예약 이름 검사
AD-5 실루엣 도구 합의 게이트 밖, PNG 미커밋
AD-8 보병 최저선 7가지 합의 아군 숨김은 렌더만, 스폰 지도는 기존 미니맵 재사용, 커밋마다 번들 보고

8.2 TD가 정한 구현 방향

  • 도로 띠(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 전까지 모든 역할 비교 수치는 참고값이다(아파치는 상한, 보병은 도착 실패).
  • 프레임·틱 ms 예산은 작업량 계수가 들어오기 전까지 판정 불가다(3.5).
⚠️ **GitHub.com Fallback** ⚠️