코드는 GitHub LAMDiceBot·deguri·CrazyArcadeCopy에, 앱은 lamdice.com과 deguri.ai.kr에서 볼 수 있다. 더 많은 글은 radarlog.kr에서.
AI 를 붙였더니 봇이 더 못해졌다.
처치율 67% 에서 53%. 2,840번 호출에 실패는 1번, 비용은 $0.054. 돈은 거의 안 들었다. 효과도 없었다.
이게 두 번째 실패였다. 첫 번째는 AI 가 조종하는 공이 벽에 끼어서 안 내려왔고, 세 번째에서야 "이건 된다" 싶은 자리를 찾았다.
게임 세 개에 같은 모델을 붙여 본 기록이다.
왜 하필 Jev 였나
시작은 쓰레드 반응이었다. "jev ai 를 게임에 접목" 글이 725 조회, "jev 로 게임 AI" 글이 2시간 만에 445. 게임 기능만 소개한 글은 100 을 못 넘었다.
사람들은 AI 가 판단하는 게 화면에 보이는 쪽에 반응했다.
Jev 는 TypeSafe 의 모델인데, 글을 쓰는 LLM 이 아니다. 상태(state)와 타입이 정해진 질문을 보내면 선택지 위의 확률만 돌아온다.
choice 는 "이 중 어느 것", score 는 "어느 단계", noul 은 "참인가". 파싱할 게 없고 코드가 바로 if 로 받는다.
curl -s https://api.typesafe.ai/v1/systemone \
-H "Authorization: Bearer $TYPESAFE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"jev-latest",
"state":"Help! My payouts have been failing for 3 days.",
"questions":{"is_urgent":{"type":"noul","instructions":"Does this convey urgency?"}}}'돌아오는 건 "noul": 0.95 한 줄이다. 긴급할 확률 95%.
가격이 결정타였다. 입력 100만 토큰당 $0.042, 출력은 무료. 한 번 부르는 데 1원 안팎이라 판마다 수백 번 불러도 부담이 없다. 응답은 공식 문서가 "약 100ms", 내 실측은 205~285ms.
게임 프로그래머 눈에는 상태 머신에 꽂는 분기 하나로 보였다. 그래서 가장 실시간인 것부터 붙여 봤다. 거기서 첫 번째 실패가 나왔다.
1차: 타임어택 봇 — 공이 벽에 끼었다
데구리 단독판의 타임어택은 픽셀 동물 공이 언덕을 굴러 내려오는 모드다. 조작은 기울기와 점프뿐, 독수리한테 잡히면 시간을 잃는다.
여기에 Jev 를 조종사로 앉혔다. 120ms 마다 상태를 영어 문장으로 보내고, 기울기 여섯 개 중 하나(choice)와 점프할지(noul)를 받았다.
첫 판부터 공이 오른쪽 벽에 붙어서 안 내려왔다.
원인은 보기 문구였다. 구르기와 걷기에서 기울기의 뜻이 다른데, 걷기용 설명 "통로에서 가장 빠름"이 구르기 보기에 섞여 있었다.
Jev 는 그걸 문자 그대로 읽고 hard_right 만 골랐다.
// Before — 걷기용 설명이 구르기 보기에 섞임
hard_right: "fastest in the corridor"
// After — 모드별로 설명을 나누고, 사실만 적는다
rolling.hard_right: "tilt hard toward the right wall"
walking.hard_right: "walk right along the corridor"
"가장 빠름" 같은 평가 낱말이 선택을 끌고 간다. 설명을 모드별로 나누고, 상태에 "최근 2초 거의 안 내려감(끼었을 것)"을 넣어서야 벽에서 떨어졌다.
고치고 나서 5시드 평균 51.7초. 그런데 가만히 두면 58.1초, 독수리 올 때만 점프하는 규칙 한 줄이면 44.3초였다.
AI 가 규칙 한 줄을 못 이겼다. 독수리한테는 매 판 잡혔다.
요청은 120ms 마다 가는데 응답이 205ms 다. 답이 오면 이미 다음 상황이다. 게다가 Jev 는 타이밍 수치에 약해서, 독수리가 잡기 1.2~0.85초 전에 미리 뛰고 재사용 대기에 걸렸다.
그래서 역할을 바꿨다. 코드가 기울기×점프 후보를 1.5초 앞까지 미리 굴려 결과를 말로 적어 주고, Jev 는 그 중에서 고르기만 한다.
candidate A: reaches the next section, eagle still far away
candidate B: hits the right wall and nearly stops
candidate C: jumps early, lands in the mud
18.56초. 독수리를 전부 피했다. 같은 후보를 코드 점수로 고르면 20.86초, 운영 중 사람 1위가 31.1초(9명). 비용은 한 판에 수백 번 물어서 약 1센트.
사람 랭킹을 압도한 건 Jev 가 아니라 미리 굴리기다. Jev 는 코드 점수보다 2초 남짓 나았을 뿐이다. 후보 평가는 된다, 조종은 안 된다.
2차: 크아 봇 전술 — 위약 효과에 속을 뻔했다
두 번째는 크레이지 아케이드를 따라 만든 물풍선 게임이다. 봇은 0.1초마다 위험 지도를 다시 그려서 움직이고, 이미 꽤 잘한다.
Jev 한테는 전술만 맡겼다. 1.5초마다 상황을 문장으로 보내고 사냥·아이템·파밍 셋 중 하나를 받는다. 상태는 2,000자, 후보 설명은 200자로 잘랐다.
설계하던 날은 API 키가 없어서 Jev 를 한 번도 못 불렀다. 그런데도 "코드 점수 선택" 군 승률이 80%.
파 보니 첫 판단을 7ms 미룬 것만으로 기존 봇 승률이 1.7% 에서 38.3% 로 뛰었다. 14ms 면 93.3%.
왜 그런지는 아직 모른다. 전술이 아니라 타이밍 몇 ms 가 승률을 흔든 것만 분명했다.
위약 폭을 먼저 재지 않으면 어떤 숫자도 믿을 수 없다.
나흘 뒤 키를 받아 실측했다. 3맵×10시드, 고수 봇 처치율 67% 에서 53%. 2,840번 호출에 실패 1번, 응답 중앙값 285ms, 입력 128.9만 토큰, $0.054.
67% → 53%
2,840번 호출. $0.054. 14%p 하락.
−14%p 는 위약 폭 10%p 안이라 "확실히 나쁘다"고는 못 한다. 하지만 나아진 증거도 없다. Jev 가 셋 중 자꾸 "아이템"을 골라 봇을 사냥에서 떼어 놓고 있었다.
코드는 0.1초마다 위험 지도를 보는데 Jev 는 1.5초마다 문장 몇 줄만 본다. 정보가 적은 쪽이 이길 수 없다. 코드가 이미 잘하는 결정에 AI 를 얹은 게 잘못이었다.
그래서 자리를 옮겼다. 선수가 아니라 감독이다. 30초마다 난이도를 up/keep/down 으로, 봇 성향을 choice 로, 10초마다 작전 하나를 받는다.
틱 단위 결정은 코드가 그대로 쥔다. 이건 지금 운영 중이다.
3차: 데구리 하이라이트 — 됐다, 근데 끝 장면만 골랐다
세 번째는 실시간이 아니라 이미 끝난 경주다.
경주가 끝나면 서버가 6초짜리 하이라이트 후보를 만든다. 역전, 독수리 납치, 구멍, 진흙, 낮잠, 몸싸움, 접전.
후보마다 사실을 숫자로 모아 검사한 뒤 영어 문장으로 다시 쓴다. 참가자 이름, 방 이름, 좌표는 안 보낸다. 순번과 동물과 사건만.
그걸 choice 하나로 묻는다.
{
"model": "jev-latest",
"state": {
"task": "Choose the best 6-second highlight of a finished race.",
"race": "8 pixel animals rolled down a hill. The rabbit led most of the way...",
"what_makes_it_fun": "The moment the outcome flips is the core. Everyone already saw the finish."
},
"questions": {
"pick": {
"type": "choice",
"instructions": "Which clip would be the most fun to watch?",
"criteria": {
"c1": "the leader was snatched by the eagle",
"c2": "nothing special happened"
}
}
}
}{
"model": "jev-1.13.0",
"answers": {
"pick": {
"type": "choice",
"choice": "c1",
"probabilities": { "c1": 0.88, "c2": 0.12 },
"confidence": 0.76
}
},
"usage": { "input_tokens": 318, "output_tokens": 34 }
}됐다. 결과 카드에 그 장면이 GIF 로 뜬다.
그런데 20판 돌려 보니 18판이 끝 장면이었다.
후보 문장에 "very end of the race" 같은 위치 단서가 있었다. 1차의 "가장 빠름"과 똑같은 함정이다.
// Before — 위치 단서가 선택을 끌어간다
"the rabbit took the lead at the very end of the race"
// After — 위치를 말하지 않는다
"the rabbit overtook the leader after trailing for most of the race"
위치를 지우고, 코드가 "결정적 계기" 후보를 항상 하나 넣게 했다. 그 뒤로는 접전이었던 판에서만 결정 장면을 고른다.
써 본 사람들은 결정되는 순간만큼 접전이 재밌다고 했다. 그래서 붙어 간 시간과 골인 차이도 후보에 넣었다.
단 숫자가 아니라 낱말로. neckSec: 4 대신 "stayed neck and neck for several seconds". Jev 는 수치 비교에 약하다.
반응도 학습에 쓴다. 결과 카드의 👍/👎 를 사실별 좋아요 비율로 모아 코드 점수 가중치를 0.5~2배로 조정한다. 그리고 5판에 1판은 코드 점수로만 골라 Jev 가 정말 나은지 계속 비교한다.
다른 프로젝트에 가져갈 것
세 저장소의 호출 코드를 함수 하나로 줄였다. 키 없음, 실패, 시간 초과, 엉뚱한 답은 전부 null. 부르는 쪽이 코드 규칙으로 대신한다.
const JEV_TIMEOUT_MS = 3000;
async function askJev(state, questions) {
const key = process.env.TYPESAFE_API_KEY;
if (!key) return null;
try {
const res = await fetch('https://api.typesafe.ai/v1/systemone', {
method: 'POST',
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'jev-latest', state, questions }),
signal: AbortSignal.timeout(JEV_TIMEOUT_MS)
});
const body = await res.json().catch(() => null);
if (!res.ok || !body || !body.answers) return null;
return body.answers;
} catch (err) {
console.warn('[jev] 실패:', err && err.name);
return null;
}
}
const answers = await askJev(state, { pick });
const pick = answers && ['c1', 'c2'].includes(answers.pick.choice)
? answers.pick.choice
: codeFallback();choice 가 내가 준 옵션인지 다시 검사하고, 타임아웃은 2~3.5초. 실시간 경로면 재시도 없이 바로 fallback 이다.
원칙은 세 번이 전부 같은 걸 가리켰다. 흐름은 코드가 쥐고 Jev 는 좁은 판단 하나만. 숫자는 코드가 계산하고 Jev 에는 낱말로.
질문 하나에 판단 하나, 여러 질문은 한 요청에 묶는다. 병렬로 평가되니 시간은 그대로고 비용은 토큰만큼만 든다.
키는 서버 환경변수에서만 읽고, 상한을 둔다. 소켓당 동시 요청 1개, 분당 60회, 하이라이트는 판당 1회. 'Jev' 배지는 실제로 Jev 가 답했을 때만 붙인다.
그리고 비교 실험은 위약군부터. 7ms 가 승률을 1.7% 에서 38.3% 로 바꾸는 세계에서, 대조군 없는 숫자는 숫자가 아니다.
세 번 다 같은 질문이었다. 이 결정을 코드가 이미 잘하고 있나. 그렇다면 AI 는 안 얹는다.
코드가 못 하는 "이 중 뭐가 더 볼만한가"는 이미 일어난 일에 물을 때 가장 잘 맞았다.
"AI 는 선수가 아니라 감독 자리에 앉혀라. 끝난 경기를 보는 자리면 더 좋다."