Claude of Duty 소개
AI 에이전트에게 규모 있는 코드베이스를 맡길 때 먼저 문제가 생기는 쪽은 코드 작성이 아니라 품질 판정입니다. 빌드가 통과하고 화면이 뜨는 지점까지는 에이전트가 스스로 도달합니다. 그런데 "이 정도면 됐다"를 언제, 어떠한 기준으로 선언해야 하는지는 어려운 문제입니다. 이러한 판정 기준이 주관적이면 에이전트를 몇 바퀴 더 돌려도 결과가 나아졌는지 나빠졌는지 말할 수 없고, 루프는 수렴하는 대신 취향 사이를 왕복합니다.
Claude of Duty는 품질 판정을 사람의 취향에서 측정 도구로 옮긴 사례입니다. Matt Shumer(@mshumer)가 공개한 이 저장소는 브라우저에서 실행되는 1인칭 슈팅(FPS, First-Person Shooter) 게임으로, Three.js r180과 WebGL2 기반의 11개 서브시스템, 약 55,000줄 규모의 코드로 작성됐습니다. 코드를 쓴 주체는 오케스트레이션 아래 움직인 여러 AI 에이전트들이고, 사람이 넣은 입력은 저장소의 prompt.md 에 그대로 남아 있는 지시문 한 개뿐입니다. 아트 자산은 저장소에 하나도 없습니다. 텍스처와 메시, 애니메이션, 사운드가 전부 로드 시점에 코드로 생성되며, 런타임 의존성은 three 하나입니다.
즉, 이 저장소에서 눈여겨봐야 할 부분은 게임보다는 게임과 함께 커밋된 검증 장치입니다. 저자 스스로 "이 저장소에서 흥미로운 부분은 게임이 아니라 하네스일 것이다" 라고 적었고, 실제로 프레임을 채점하는 적대적 비평가 에이전트, 픽셀 단위로 회귀를 잡는 게이트, 프레임 시간 분포를 보는 프로파일러가 저장소에 들어 있습니다. 본 게시물에서는 이 하네스의 구성과 저자가 기록한 측정값, 그리고 목표에 끝내 도달하지 못한 지점을 정리합니다.
Claude of Duty를 만든 지시문 한 개
저장소의 prompt.md 에는 이 코드베이스를 만들어 낸 지시문 전문이 그대로 공개되어 있습니다.
I want you to build a first-person shooter at the level of the most recent
Call of Duty games. It should be utterly perfect, visually beautiful, with
every single thing done at AAA quality—from textures to physics to anything
you could think of.
Fan out sub-agents and have sub-agents tackle each one individually so that
the game is utterly perfect. You should /loop on each item and have a separate
sub-agent check it visually to ensure it looks triple A. That separate
sub-agent should be a really harsh critic, and if it doesn't look triple A,
it should keep going.
Don't stop until each sub-agent is utterly wowed with the quality when
compared with the actual Call of Duty game. It should literally compare them
side by side blind and say which one looks better. Do this in ThreeJS. /loop
until it's utterly perfect. Fan out sub-agents and ultracode.
지시문이 요구한 항목은 네 가지입니다. 최신 Call of Duty 수준의 품질, 항목마다 서브에이전트를 팬아웃(fan out)해 각각 담당시키기, 각 항목에서 루프를 돌리며 별도의 서브에이전트가 결과를 시각적으로 검사하기, 그리고 그 검사 에이전트를 "정말 혹독한 비평가" 로 두고 실제 게임과 나란히 놓은 블라인드 비교에서 어느 쪽이 나은지 말하게 하기입니다. 분량의 절반이 만드는 방법이 아니라 판정하는 방법에 배정되어 있고, 이 배분이 저장소에 남은 결과물의 성격을 결정했습니다.
Claude of Duty의 에이전트 협업 계약
여러 에이전트가 한 저장소를 동시에 건드리면 서로의 수정을 덮어쓰거나 상대의 전제를 깨뜨립니다. Claude of Duty는 그 조정을 회의나 중재자가 아니라 문서 한 장으로 처리했습니다. 저장소의 ARCHITECTURE.md 는 첫 줄에서 자신을 "유일한 조정 메커니즘(the only coordination mechanism)" 이라고 선언하고, 모든 에이전트가 코드를 쓰기 전에 이 문서를 읽도록 요구합니다.
문서가 못 박은 규칙 중 협업 구조를 만든 것은 다음 항목들입니다.
- 디렉토리 소유권: 각 에이전트는 자기 디렉토리만 수정하고 다른 디렉토리는 건드리지 않습니다. 다른 디렉토리에는 다른 소유자가 있어 수정이 덮어써지거나 상대를 깨뜨립니다.
- 모듈 직접 import 금지: 다른 서브시스템을 정적으로 가져오지 않고 런타임에
ctx.get('fx')로 조회합니다. 문서는 이 규칙이 병렬 작업을 안전하게 만드는 근거라고 적고 있습니다. - 의존성 추가 금지:
three외의 npm 패키지, CDN 요청, 외부 이미지·HDRI·모델·오디오 파일을 쓰지 않습니다. 게임은 완전히 오프라인으로 실행되어야 합니다. Math.random()금지: 게임플레이와 시각 요소의 난수는ctx.rng또는 그것을fork()한 값에서만 얻습니다. 캡쳐 재현성이 여기에 걸려 있습니다.- 프레임당 할당 금지: 벡터와 행렬, 배열은
init()에서 미리 잡아 재사용하고,update()안의new THREE.Vector3()는 버그로 취급합니다. - 빌드와 캡쳐 통과: 변경 후
npm run build가 통과하고node tools/capture.mjs가 프레임을 만들어야 합니다. 부팅을 깨뜨리면 다른 에이전트가 아무 작업도 못 하기 때문입니다.
서브시스템은 아래 인터페이스를 구현하는 클래스로 통일되어 있습니다. 정적 필드 id 로 서로를 찾고 deps 로 초기화 순서를 선언하므로, 에이전트는 남의 파일을 열지 않고도 상대를 호출할 수 있습니다.
export class MySystem {
static id = 'mysystem'; // unique; how others reach you
static deps = ['render']; // ids that must init before you
async init(ctx) {} // build resources; may await
fixedUpdate(h, ctx) {} // optional, 120 Hz, deterministic gameplay
update(dt, ctx) {} // optional, once per frame
lateUpdate(dt, ctx) {} // optional, after all update()
resize(w, h, ctx) {} // optional
dispose() {} // optional
}
Claude of Duty의 품질 검증 하네스
저장소의 tools/ 에는 게임 코드가 아니라 게임을 검사하는 스크립트가 여섯 개 들어 있습니다. 이 도구들이 지시문의 "혹독한 비평가" 를 취향 문제에서 수치 문제로 끌어내립니다.
| 도구 | 하는 일 |
|---|---|
tools/capture.mjs |
GPU 백엔드 헤드리스 Chromium으로 지정한 장면 한 컷을 캡쳐 |
tools/shotset.mjs |
11개 장면을 한 세션에서 모두 캡쳐하는 빠른 검토용 세트 |
tools/baseline.mjs |
장면마다 페이지를 분리하고 프레임 예산을 고정한 재현 가능한 캡쳐. 실행마다 비트 단위로 동일 |
tools/imagediff.mjs |
픽셀 단위 게이트. 한 픽셀이라도 움직이면 0이 아닌 코드로 종료 |
tools/profile.mjs |
실제 디바이스 픽셀 비율에서 도는 게임플레이 프로파일러. 프레임 시간 분포와 프레임별 WebGL 프로그램 수로 히치 원인 지목 |
tools/playtest.mjs |
이동과 발사를 스크립트로 돌리는 스모크 테스트 |
저자는 이 도구들을 만들면서 앞선 측정을 무효로 만든 발견 두 가지를 기록해 두었습니다. 둘 다 에이전트 루프의 판정 기준이 어디서 새는지 보여줍니다.
첫째, 프레임 시간의 중앙값은 문제를 가립니다. 카메라를 고정한 벤치마크는 94fps를 보고했지만 실제로는 플레이가 불가능한 상태였습니다. Retina 픽셀 비율(내부 해상도 3.34MP, 2.07MP가 아님)에서 실제로 플레이해 보면 12~17fps였고, 프레임 중간에 34개 이상의 WebGL 프로그램이 지연 컴파일되면서 728~1236ms 멈춤이 발생했습니다. profile.mjs 가 p50과 p95, p99를 함께 보고하고 각 히치의 원인을 지목한 뒤에야 이 문제가 드러났습니다.
둘째, 캡쳐가 재현되지 않았습니다. shotset.mjs 는 11개 장면에서 페이지 하나를 재사용하기 때문에 파티클 수명과 데칼 버퍼, 노출 상태가 다음 장면으로 흘러갑니다. 같은 조건으로 두 번 돌린 결과가 11개 중 10개에서 달라졌습니다. baseline.mjs 는 장면마다 새 페이지를 쓰고, 그래서 결과가 비트 단위로 같아지며, 그 덕분에 imagediff.mjs 가 쓸 만한 게이트가 됩니다. 회귀 검사 도구가 먼저 결정적(deterministic)이어야 비평가의 점수도 의미를 갖는다는 순서입니다.
Claude of Duty의 병렬 팬아웃과 순차 단일 소유 비교
지시문은 서브에이전트를 팬아웃하라고 요구했지만, 저자가 남긴 기록은 그 방식이 이 작업에서 밀렸다고 말합니다. 6개 에이전트가 각자 디렉토리 하나를 소유하는 라운드를 세 번 돌렸을 때 점수는 0.46점 올랐고, 프레임을 망치는 결함 수는 오히려 시작보다 늘었습니다(60 → 47 → 66). 톤매핑과 하늘, 간접광이 하나로 얽힌 시스템인데 분리된 에이전트들이 서로의 전제를 계속 깨뜨렸기 때문입니다. 반대로 얽힌 관심사마다 소유자를 한 명으로 두고 순차로 한 번 돌린 패스는 점수를 1.00점 올리고 결함을 66개에서 26개로 줄였습니다.
| 항목 | 병렬 팬아웃 3라운드 | 순차 단일 소유 1패스 |
|---|---|---|
| 구성 | 6개 에이전트가 각자 디렉토리 1개 소유 | 얽힌 관심사마다 소유자 1명 |
| 점수 변화 | +0.46 | +1.00 |
| 프레임 결함 수 | 60 → 47 → 66 | 66 → 26 |
가장 값진 결과 하나는 에이전트가 자기 지시를 정면으로 거스른 데서 나왔습니다. 세 라운드 동안 모든 비평가가 무기에 텍스처가 없다고 보고했지만 실제로는 텍스처가 있었고, 정반사가 지배하는 상태였습니다. 확산 성분은 출하 값 L=67에 대해 L=26으로 측정됐습니다. 앞선 라운드들은 밝은 부분 지적에 대응하려고 알베도를 계속 낮추고 있었는데, 그 조치가 확산광을 죽여 상황을 악화시키고 있었습니다. 해법은 요청받은 방향의 반대였습니다.
Claude of Duty의 측정된 성능
성능 수치는 Apple 실리콘 노트북에서 1512×982 해상도, DPR 2(내부 3.34MP), ultra 프리셋, 3회 실행, AI와 발사가 동작하는 실제 플레이 조건으로 측정한 값입니다.
| 항목 | 최적화 전 | 최적화 후 |
|---|---|---|
| fps p50 | 12~17 | 28~30 |
| fps p99 | 4~9 | 14~17 |
| 최악 프레임 | 728~1236ms | 66~82ms |
| 플레이 중 셰이더 컴파일 | 34~35 | 0 |
| 부팅 시간 | 약 9~12초 | 3.7~4.6초 |
멈춤을 없앤 것은 저장소의 src/core/prewarm.js 의 셰이더 선행 컴파일입니다. 이 최적화 패스에는 시각적 변화를 0으로 유지하라는 제약이 붙었고, 그 제약을 주장이 아니라 imagediff.mjs 로 강제했습니다. 출하된 빌드는 11개 장면 전부에서 최적화 전 기준 이미지와 비트 단위로 동일합니다. 픽셀 중립성을 증명 가능한 형태로 만들려면 엔진 시계 대신 performance.now() 로 애니메이션하던 서브시스템들을 먼저 고쳐야 했는데, 그러지 않으면 부팅 시간이 바뀔 때마다 출력이 함께 밀려서였습니다.
Claude of Duty가 도달하지 못한 지점
저자는 목표 달성 여부를 명확히 적었습니다. 최신 Call of Duty에 맞서는 것이 목표였고, "도달하지 못했다(It does not)" 는 것이 결론입니다. 독립적인 적대적 비평가 11명이 그 기준으로 프레임을 채점한 점수는 10점 만점에 3.59 → 4.14 → 4.05 → 5.05로 움직였습니다. 두 장면이 "CLOSE" 판정을 받았고 나머지는 "AMATEUR" 로 남았으며, 블라인드 A/B 비교에서는 모든 라운드에서 모든 비평가가 실제 Call of Duty 프레임을 골랐습니다.
부족한 지점도 항목별로 적혀 있습니다. 손은 무기를 제대로 쥐지 못하는 각진 덩어리로 보이고, 표면 재질은 근거리에서 촬영된 실물이 아니라 절차적 노이즈로 읽힙니다. 저자는 이 지점을 코드로 텍스처를 생성하는 방식의 천장이라고 표현합니다. 적은 원거리에서 마네킹처럼 보이고, 간접광은 실제 전역 조명이 아닌 근사이며, 프레임률은 Retina에서 28~30fps에 머무릅니다. 아트 패스들이 지오메트리 비용을 3배로 늘린 뒤(삼각형 590만 개 → 1130만 개) 최적화가 절반 정도를 회수한 상태입니다.
원인이 밝혀졌는데도 고치지 못한 문제 하나가 남아 있습니다. 저장소의 src/render/index.js 에 있는 뷰모델 조명 리그가 알베도 단위당 약 20배의 조도를 전달합니다. 뷰 장면의 순수 검은 재질이 F0=0.04만으로 배경 91에 대해 L=110으로 렌더되는 상태입니다. 모든 무기 알베도를 물리 값의 3분의 1로 속여 이를 보정하고 있고, 그 대가로 게임에서 가장 오래 보게 되는 물체의 재질 구분이 눌려 있습니다.
Claude of Duty는 누구에게 유용한가
이 저장소는 바로 쓸 수 있는 게임 엔진이나 FPS 템플릿을 찾는 사람에게는 맞지 않습니다. 저자 본인이 목표 미달을 선언했고, Retina 환경 28~30fps에 손·재질·캐릭터의 한계를 항목으로 남겨 두었기 때문입니다. 아트 파이프라인이 이미 있는 팀에게도 절차적 생성 전제가 오히려 제약입니다.
반대로 여러 에이전트에게 코드베이스를 나눠 맡기는 구조를 설계하는 사람에게는 참고할 만한 자료가 됩니다. 조정을 문서 한 장으로 못 박은 방식, 판정을 픽셀 게이트와 프레임 분포로 옮긴 방식, 병렬 팬아웃과 순차 단일 소유를 같은 지표로 비교한 기록이 실제 수치와 함께 남아 있습니다. WebGL2 렌더링 파이프라인이나 절차적 텍스처 생성을 읽을거리로 찾는 경우에도 의존성 하나로 완결된 코드베이스라는 점이 도움이 됩니다.
Claude of Duty 설치와 실행
Node 환경에서 저장소를 받아 개발 서버를 띄우면 브라우저에서 바로 실행됩니다.
npm install
npm run dev # http://127.0.0.1:5173
캔버스를 클릭하면 커서가 잠기고, 이동은 WASD, 조준은 마우스, 발사는 좌클릭, 정조준은 우클릭입니다. R은 재장전, Shift는 달리기, Ctrl은 앉기, Space는 점프, Q와 E는 기울이기, Esc는 커서 해제입니다.
package.json 의 스크립트에는 검증용 진입점도 함께 정의되어 있어, 캡쳐 한 장을 뽑아 보는 것으로 하네스 쪽을 먼저 확인할 수 있습니다.
npm run shot # node tools/capture.mjs
npm run build # vite build
Claude of Duty의 라이선스
Claude of Duty는 MIT 라이선스로 공개되어 있어 개인 및 상업적 목적으로 자유롭게 사용할 수 있습니다.
Claude of Duty 프로젝트 GitHub 저장소
더 읽어보기
-
Gas Town: 20-30개 AI 코딩 에이전트를 안정적으로 조율하는 Git 기반 멀티 에이전트 오케스트레이션 시스템
-
WebGPU Claude Skill: TSL을 사용하여 WebGPU 기반 Three.js 애플리케이션 개발을 위한 Claude 스킬(Skill)
이 글은 GPT 모델로 정리한 초안을 바탕으로 한 것으로, 원문의 내용 또는 의도와 다르게 정리된 내용이 있을 수 있습니다. 관심있는 내용이시라면 원문도 함께 참고해주세요! 읽으시면서 어색하거나 잘못된 내용을 발견하시면 댓글로 알려주시기를 부탁드립니다. ![]()
이 도구를 직접 설치해 사용해보셨다면, 파이토치 한국 사용자 모임
회원들을 위해 경험이나 팁을 댓글로 남겨주세요! ![]()

