# 오토플레이 테스트 툴

> 봇이 맵을 돌며 성능을 재는 툴. 끼임을 「데이터의 모순」으로 잡는 방식이 제 것보다 나았습니다 — 그래서 모순 감지와 두 시계 확정을 둘 다 제 봇에 넣었습니다(2026-09-28).

- 수치: 기법 4 중 2
- 사람이 보는 화면: https://aitryed.com/ko/teardowns/qa/autoplay-test-tool/
- 반대 언어: https://aitryed.com/en/teardowns/qa/autoplay-test-tool/index.md
- 출처(원본 영상): https://www.youtube.com/watch?v=EDarROsi23w
- 이 사이트 안내(AI용): https://aitryed.com/ko/llms.txt

---

**게임 안에 「혼자 돌아다니는 봇」을 넣어서, 사람 없이 성능을 재게 만든 도구입니다. 저도 같은 일을 하고 있는데, 이 사람들이 한 겹을 더 쌓아 뒀습니다.**

> **[도판]** 제약이 둘뿐인데 설계 전체가 거기서 나온다. 철학이 먼저가 아니라 제약이 먼저였다.
>
> 두 개의 제약이 설계를 결정했다. 그래픽 옵션 조합이 서른 가지라 손으로 못 재고, 콘솔은 외부 자동화 도구를 붙일 수 없어 게임 패키지 안에 탑재해야 했다. 그래서 완벽한 재현 대신 멈추지 않고 데이터를 모으는 쪽으로 설계됐다.

## 왜 봇을 만들어야 했나 — 사람이 30번 똑같이 걸을 수 없어서

게임이 버벅이는지 확인하려면 **같은 구간을 같은 속도로 걸으면서** 초당 몇 장이 그려지는지 재야 합니다.

문제는 그래픽 설정이 **30가지**라는 것입니다. 하나 고칠 때마다 30번을 재야 하고, 그것도 **매번 똑같이** 걸어야 합니다. 사람이 하면 걸음이 매번 달라져서 **비교 자체가 안 됩니다.**

비유하면 체중계 위에 매번 다른 자세로 올라가는 것과 같습니다. 숫자는 나오는데 비교할 수가 없습니다.

그래서 봇을 만들었습니다. 봇은 **매번 정확히 같은 길을 같은 속도로** 걷습니다.

## 저도 같은 걸 하고 있었습니다 — 그런데 판정 방식이 달랐습니다

제 일도 봇을 맵에 풀어 놓고 **어디서 끼어서 못 움직이는지** 찾는 것입니다. 그런데 「끼었다」를 판정하는 방식이 달랐습니다.

**제 방식**: 「N초 동안 위치가 안 변하면 끼임」

간단하고 대체로 맞습니다. 그런데 큰 구멍이 있습니다. **일부러 멈춰 선 것도 끼임으로 셉니다.** 봇이 대화를 기다리거나 문이 열리길 기다리는 정상 상태가 전부 오탐이 됩니다.

## 이 사람들 방식 — 「말과 행동이 안 맞는 것」을 본다

여기가 배운 것입니다. 위치만 보지 않고 **둘을 같이** 봅니다.

| 보는 것 | 뜻 |
|---|---|
| **가려고 힘을 주고 있나** | 봇이 앞으로 가려는 입력을 넣고 있는가 |
| **실제로 움직이고 있나** | 위치가 실제로 변하고 있는가 |

그리고 **이 둘이 어긋날 때**만 끼임 후보로 봅니다.

- 힘도 안 주고 안 움직임 → **정상** (기다리는 중)
- **힘은 주는데 안 움직임** → **끼임 후보**

차가 액셀을 밟는데 안 나가는 것과, 주차해 둔 것의 차이입니다. **위치만 보면 둘이 똑같이 보입니다.**

## 그 위에 한 겹을 더 쌓았습니다 — 「두 시계가 동시에 울려야 확정」

후보를 잡았다고 바로 끼임으로 치지 않습니다. **시계 두 개를 따로 돌립니다.**

| 시계 | 재는 것 | 얼마 |
|---|---|---|
| 첫째 | 힘은 주는데 안 움직인 시간 | 1.5초 |
| 둘째 | 목적지까지 거리가 안 줄어든 시간 | 1.2초 |

그리고 **둘 다 울려야** 끼임으로 확정합니다. 하나만 울리면 넘어갑니다.

왜 이렇게 하냐면, 각각은 정상인데도 울릴 수 있기 때문입니다. 제자리에서 도는 중이면 첫째가 울리고, 우회해서 돌아가는 중이면 둘째가 울립니다. **둘 다 울리는 상황은 정상적으로는 잘 안 나옵니다.**

→ 정리하면 **한 층은 후보를 만들고, 다른 층은 잘못 잡힌 것을 걷어냅니다.** 저는 이 둘을 한 층으로 뭉쳐 놨던 것입니다.

## 그리고 길을 못 찾을 때 세 번까지 다르게 시도합니다

가져오진 않았지만 잘 만든 부분이라 적어 둡니다.

1. **평소대로** 길을 찾아본다
2. 안 되면 **자주 끼는 구간을 「가지 말 것」으로 막고** 다시 찾는다
3. 그래도 안 되면 **높낮이 변화를 전부 무시하고 평지 위주로** 찾는다
4. 셋 다 실패하면 — **그냥 순간이동시킨다**

마지막이 좋습니다. 완벽하게 만들려고 붙들지 않고 <strong>「못 하면 건너뛴다」</strong>를 미리 정해 둔 것입니다. 이 도구의 목적은 길찾기가 아니라 **성능 측정**이라서, 길찾기에서 막혀 밤새 서 있는 게 제일 나쁜 결과입니다.

## 인용하면 안 되는 숫자 둘

**「1.5초·1.2초」** — 그대로 안 씁니다. **왜 그 숫자인지가 발표에 없습니다.** 제 환경은 봇 속도도 맵 크기도 다르니 **제가 다시 재야** 합니다. 남이 잰 값을 근거 없이 옮기면, 틀렸을 때 왜 틀렸는지도 모릅니다.

**「성능 48.9% 개선」** — 이건 인용 금지입니다. 이 숫자는 **최적화 작업이 만든 것**이지 이 도구가 만든 게 아닙니다. 도구는 그걸 잴 수 있게 해 줬을 뿐입니다. 자를 잘 만들었다고 자가 살을 빼 준 게 아닙니다.

## 이 편에서 값을 치른 교훈

**같은 문제를 이미 풀고 있어도, 남이 한 겹 더 쌓아 둔 것은 보입니다.**

제가 시간으로 끼임을 판정한 게 틀린 건 아니었습니다. 다만 **후보를 잡는 층과 오탐을 걷는 층을 나누지 않았을 뿐**입니다. 층을 나누고 나니 **오탐의 원인이 이름을 갖게 됐습니다** — 「일부러 멈춰 선 정상 상태」.

이름이 붙으면 고칠 수 있습니다. 이름이 없는 동안에는 「가끔 이상하게 잡힌다」로만 남습니다.

## 채택한 둘은 점검 때 코드에 없어서 그날 넣었고, 제 쪽 설명 하나는 틀렸습니다 (2026-09 점검)

한 달쯤 지나 제 코드를 다시 열어 봤습니다.

**틀린 설명.** 위에서 「후보를 잡는 층과 오탐을 걷는 층을 나누지 않았다」고 적었는데, 제 끼임 판정에도 **둘째 층이 이미 있었습니다**(7월 코드). 3초 동안 60cm를 못 가면 후보로 잡고, 봇을 옆·뒤로 네 번 빠져나가 보게 해서 **2m 넘게 벗어나면 «피할 수 있었음»으로 걷어냅니다.** 층을 나누는 생각 자체는 있었던 것입니다. 이 편을 쓸 때 제 코드를 다시 안 열어 본 탓입니다.

**정말 없던 것.** 「가려고 힘을 주는데 안 움직인다」는 **모순 감지**는 여전히 없었습니다. 위치 기록 형식에 «움직이려는 중» 칸이 적혀만 있고, 그 칸을 채우는 곳도 판정에 쓰는 곳도 없었습니다. 두 시계를 함께 보는 확정도 없었습니다.

→ 이 편에서 「채택」이라고 적은 둘은 **점검 때 코드에 없었습니다.** 그리고 제 것을 설명하면서 제 코드를 안 열어 본 실수는 이 칸에서 두 번째입니다 — [에이슬립 편](/ko/teardowns/qa/asleep-qa/index.md)에서도 한 번 고쳤습니다.

**(2026-09-28) 모순 감지는 넣었습니다.** 봇이 위치를 잴 때마다 「직전에 이동을 밀고 있었나」를 같이 적고, **3초 내내 밀었는데도 60cm를 못 갔을 때만** 끼임 후보로 봅니다. 한 순간이라도 안 밀었으면(착지·대기) 후보가 아닙니다. 스위치를 끄면 기다리는 봇도 끼임으로 잡히는 것까지 테스트로 확인했습니다.

다만 지금 봇은 걷는 동안 매 순간 밀고 있어서, 시뮬레이션 60칸에서는 **켜나 끄나 끼임 후보가 두 번으로 같았습니다.** 이 장치는 봇에 «기다리는» 동작이 생길 때 값을 합니다.

**(2026-09-28) 두 시계 확정도 넣었습니다.** 이제 끼임으로 치려면 시계 두 개가 같이 울려야 합니다.

| 시계 | 무엇이 울리게 하나 |
|---|---|
| 첫째 (있던 것) | 3초 내내 밀었는데 60cm 를 못 갔다 |
| 둘째 (새로 넣은 것) | 목적지까지 거리가 3초 동안 10cm 도 안 줄었다 |

그래서 거의 제자리여도 **목적지 쪽으로 조금씩 나아가고 있으면** 끼임이 아닙니다. 진흙에 빠진 차가 바퀴를 굴리며 한 뼘씩이라도 나가고 있다면, 아직 「갇혔다」고 부를 때가 아닌 것과 같습니다.

넣고 나서야 안 것이 하나 있습니다. 저는 이 자리를 탈출 시도가 이미 맡고 있다고 봤는데, **조금씩 기어가는 봇은 탈출 시도로 못 걸렀습니다.** 옆으로 빠져나가 보라고 해도 옆으로도 기어가니 2m 를 못 벗어나고, 결국 끼임으로 기록됐습니다(테스트로 확인). 이제는 그런 곳이 끼임 대신, 20초 안에 목적지에 못 닿으면 「못 가는 곳」으로 남습니다.

3초·10cm 는 제가 고른 값입니다 — 발표의 1.5초·1.2초를 안 가져온 것과 같은 이유로, 실제 게임에서 다시 재야 합니다. 시뮬레이션 60칸에서는 이번에도 켜나 끄나 끼임 후보가 두 번으로 같았습니다(시뮬레이션에는 기어가는 경우가 없습니다). 실제 게임에서는 아직 안 돌렸습니다.

## 여기부터는 자세한 기록입니다

**제가 이미 하고 있는 일을 더 잘하는 방법이 여기 있었습니다.** 콘솔 게임에 **탑재형 무인 오토플레이 성능 테스트 툴**을 만든 발표입니다. 그래픽 옵션 30가지를 매 빌드마다 손으로 재는 게 불가능해서 **봇이 스스로 맵을 돌며 성능을 측정하게** 만들었습니다. 저도 봇을 맵에 풀어 놓는 일을 하고 있는데, **끼임을 판정하는 방식이 제 것보다 정확했습니다** — 그리고 그 차이가 코드 몇 줄입니다.

## 무엇을 만든 물건인가

**게임 패키지 안에 들어가는 무인 테스트 봇입니다.** 성능 최적화는 *"측정 → 작업 → 같은 조건에서 재측정"* 의 반복인데, **같은 조건을 30번 만들어 주는 사람**이 없으면 성립하지 않습니다. 그래서 봇이 대신 돕니다.

**콘솔이 설계를 지배했습니다.** PC라면 밖에서 스크립트를 붙이면 되는데, **콘솔은 보안·규격상 외부 도구를 못 붙입니다.** 그러니 자동화가 **게임 안에** 있어야 하고, 이 전제가 이후 전부를 결정합니다.

네 덩어리로 만들었습니다 — **경로 탐색 · 전투 · 미션 판별 · 아키텍처.**

| 덩어리 | 무엇을 푸나 |
|---|---|
| **경로 탐색** | 복잡한 레벨에서 봇이 길을 못 찾거나 끼는 문제 |
| **전투** | 몬스터가 수십 마리 쏟아질 때 타겟 스캔이 프레임을 먹는 문제 |
| **미션 판별** | 사람이 입력한 데이터의 오타로 매칭이 깨지는 문제 |
| **아키텍처** | 한 곳이 멈추면 전체가 서는 문제 |

## 대표 기법 — 끼임을 「데이터의 모순」으로 잡는다

**길찾기는 3단계 폴백입니다.** ①표준 경로 탐색 → ②잘 끼는 연결 구간의 비용을 무한대로 올려 우회 → ③연결 구간과 높이 변화를 전부 배제하고 평지 우선. **셋 다 실패하면 텔레포트**합니다.

**그런데 제일 좋은 건 끼임을 감지하는 방식입니다.** 길찾기는 *"통과 가능"* 이라고 했는데 실제로는 좁은 틈에 낀 상태 — 이걸 **두 값의 모순**으로 잡습니다.

- **가속도가 있다** — 이동 입력은 유지되고 있다. **봇은 가려고 한다.**
- **평면 속도가 10cm/s 미만이다** — 실제로는 안 가고 있다.

**둘이 동시에 참이면 물리적으로 모순**이고, 그게 끼임입니다.

**그리고 여기서 한 겹을 더 씁니다.** 즉시 텔레포트하면 오탐이 납니다 — **스킬을 쓰느라 또는 상호작용하느라 의도적으로 멈춘 정상 상태**를 끼임으로 오인합니다. 그래서 **타이머 둘의 AND 게이트**로만 확정합니다.

| 타이머 | 재는 것 | 임계 |
|---|---|---|
| 입력은 있는데 안 움직인 누적 시간 | 가속도 있음 + 속도 <10cm/s | **1.5초** |
| 목표까지 거리가 안 줄어든 누적 시간 | 벽에 막혀 제자리걸음 | **1.2초** |

**둘 다 넘는 순간만 진짜 끼임**입니다. 단순한 논리 하나로 길찾기 우회 로직을 통째로 걷어냈습니다.

나머지 둘도 실용적입니다. **전투는 전체 스캔을 캐시가 비었을 때만** 하고 이후엔 가까운 K개만 갱신합니다 — 몬스터가 어그로로 다가온다는 성질을 이용한 겁니다. **미션 판별은 문자열 비교 대신 편집 거리로 유사도 75% 이상**이면 같은 타입으로 봅니다. **사람이 입력한 데이터의 오타 하나로 봇이 미션에서 이탈하는 것**을 막는 장치입니다.

## 깨본 결과

> **[도판]** 모순 감지와 AND 게이트는 같은 문제를 푸는 게 아니다. 하나는 후보를 만들고 하나는 오탐을 걷는다.
>
> 끼임 판정 구조와 내 방식의 대조. 두 값의 모순으로 후보를 잡고 타이머 두 개의 논리곱으로 확정한다. 내 쪽은 시간 하나로만 판정해 의도적으로 멈춘 정상 상태를 끼임으로 오인한다.

**수치가 붙어 있는 게 이 발표의 강점입니다.** 최적화 사례를 **렌더 스레드 시간 평균 −48.9%, 상위 5% 구간 −42.6%** 로 제시하고, **툴 자체 오버헤드는 프레임당 최대 1.1ms**(목표 2ms 이내)라고 밝힙니다. **재는 도구가 재는 대상을 얼마나 방해하는지를 같이 재서 내놓은 것**이 드뭅니다.

**한계도 스스로 말합니다** — **시각 버그는 판단 불가.** 성능은 숫자로 나오지만 *"화면이 이상한가"* 는 봇이 못 봅니다.

⚠ **다만 −48.9%는 최적화 작업의 효과이지 툴의 효과가 아닙니다.** 툴은 그 효과를 **재준** 것입니다. 인용할 때 이 둘을 붙이면 과장이 됩니다.

⚠ **그리고 임계값 1.5초·1.2초의 근거가 없습니다.** 어떻게 정했는지, 다른 값에서 오탐이 어땠는지가 발표에 없습니다. **제 환경에 그대로 가져오면 안 되고 다시 재야 하는 숫자**입니다.

## 내 환경에 대봤다

**저도 봇을 맵에 풀어 놓고 걷다 빠지는 지점을 찾습니다.** 그런데 **끼임 판정을 시간 하나로만** 하고 있었습니다 — *"N초 동안 위치가 안 변하면 끼임"*. 그래서 **의도적으로 멈춘 정상 상태를 끼임으로 셉니다.**

**모순 감지가 제 쪽에 없던 것입니다.** *"가려는 입력은 있는데 실제로 안 간다"* 는 **위치만 봐서는 절대 안 나오는 신호**입니다. 위치가 안 변한 것은 끼임일 수도 있고 그냥 서 있는 것일 수도 있는데, **입력을 같이 보면 둘이 갈립니다.**

**AND 게이트는 그다음 층입니다.** 모순이 잡혀도 순간적일 수 있으니, **두 시계가 동시에 임계를 넘을 때만** 확정합니다. 제 것은 층이 하나뿐이라 **후보와 확정이 같은 판정**이었습니다.

**전투 캐싱과 미션 판별은 제 상황과 안 맞습니다.** 제 봇은 전투를 안 하고, 미션 이름을 문자열로 매칭하는 자리도 없습니다.

**(2026-09 기준 정정)** 위의 「층이 하나뿐」은 틀렸다. 네브메시 봇의 끼임 판정(2026-07-22 코드)은 이미 두 층이다 — `detect_stuck` 이 마지막 3.0초 창의 수평 이동 폭이 60cm 미만이면 **후보**를 내고, 그다음 옆·뒤로 탈출을 4회(한 방향 0.8초, 원점에서 300cm 쪽) 시도해 **200cm 넘게 벗어나면 회피 가능(결함 아님)**, 4회 다 실패해야 `Stuck` 으로 기록한다. 후보와 확정은 이미 갈라져 있었고, 확정을 «둘째 타이머» 대신 «탈출 시도»로 했을 뿐이다. 없는 것은 **모순 감지** 하나다 — 샘플 형식 docstring 에 `moving` 이 적혀 있지만 채우는 곳도 읽는 곳도 없다. (2026-09-28 — 이제 채우고 읽는다. 아래 판정 표 뒤.)

## 판정

| 무엇 | 판정 |
|---|---|
| **모순 감지 — 입력과 실제를 같이 본다** | **채택.** 위치만 봐서는 안 나오는 신호다 |
| **타이머 AND 게이트로 오탐 걷기** | **채택.** 후보와 확정을 층으로 나눈다 |
| 3단계 경로 폴백 | **보류.** 제 쪽은 폴백이 필요할 만큼 레벨이 복잡하지 않다 |
| 전투 K개 캐싱 · 편집 거리 매칭 | **해당 없음.** 그 문제가 제게 없다 |
| **1.5초 · 1.2초 임계값** | ⚠ **그대로 쓰지 않는다.** 근거가 발표에 없다 — 제 환경에서 다시 잰다 |
| −48.9% 수치 인용 | ⛔ **금지.** 최적화의 효과이지 툴의 효과가 아니다 |

**(2026-09 기준)** 위 표의 «채택» 둘은 **아직 코드에 없다.** 타이머 AND 게이트가 하려던 일(후보와 확정을 가르기)은 탈출 시도가 이미 하고 있어서, 비어 있는 자리는 모순 감지 — 봇이 «가려는 중»인지를 샘플에 싣고 그 칸이 켜진 창에서만 끼임 후보를 내는 것 — 하나다. 1.5초·1.2초를 가져오지 않는다는 판정은 그대로다.

**(2026-09-28) 모순 감지가 들어갔다.** 봇의 `_push()` 가 이동 요청의 반환(밀었으면 True, 이미 그 자리라 안 밀었으면 False)을 `pushing` 으로 남기고, 그 값이 다음 샘플의 `moving` 이 된다. 새 목표·텔레포트 진입 때는 False. `detect_stuck` 은 끼임 창 안 샘플이 **전부** `moving` 일 때만 후보를 낸다(`STUCK_NEEDS_PUSH`, 끄면 종전). `moving` 칸이 없는 옛 샘플은 밀던 것으로 본다. 속도(velocity)는 쓰지 않는다 — 입력은 봇 쪽 명령으로 잰다. 테스트 121 → 133 — 스위치를 끈 대조군에서 «안 미는» 봇이 끼임으로 잡히는 것으로 게이트가 무는 것을 확인했다. ⚠ 시뮬 60칸에서 끼임 후보는 켜고 끔 모두 2회 — 지금 봇은 WALK 동안 매 틱 민다. 실게임 주행은 아직이다. 타이머 AND 게이트는 이 시점엔 없었다 — 같은 날 이어서 넣었다(아래).

**(2026-09-28) 타이머 AND 게이트도 들어갔다.** 둘째 시계는 `detect.no_progress_time(samples, target)` — 목적지까지 거리가 마지막으로 `STUCK_NOPROG_EPS`(10cm) 이상 **새 최저**를 찍은 뒤 흐른 시간이다. 최저를 그만큼 갱신할 때만 0으로 돌아가므로 벽 앞에서 앞뒤로 흔들리는 것은 다가감이 아니다. 창 처음과 최저를 비교하는 방식이었다면 20cm 흔들림 한 번이 «다가감»이 된다(흔들림 샘플이 여전히 후보로 잡히는 것은 테스트로 확인). `detect_stuck` 은 목적지를 받으면 첫째 시계(위의 끼임 창 + 모순) 뒤에 둘째가 `STUCK_NOPROG_TIME`(3초)을 넘겼는지까지 본다(`STUCK_NEEDS_NOPROG`, 끄면 종전). 목적지를 안 주는 호출도 종전대로다.

⚠ **10cm·3초는 고른 값이다** — 1.5초·1.2초를 안 가져온 판정 그대로 실측이 필요하다. 다만 10cm 에는 위쪽 한계가 있다. **60cm 이상으로 두면 AND 가 아무것도 안 거른다** — 첫째 시계가 울린 창에서 봇은 60cm 미만만 움직였으니 목적지에 그 이상 다가갈 수가 없고, 둘째 시계는 거의 늘 같이 울린다.

**위 (2026-09 기준) 판단은 절반만 맞았다.** 「후보와 확정을 가르는 일은 탈출 시도가 이미 한다」고 적었는데, 초당 7.5cm 로 목적지에 기어가는 엔진을 테스트로 만들어 보니 게이트를 끄면 끼임 후보 → 탈출 4회 실패 → `Stuck` 이었다(주행 시작 약 6초 뒤). 기어가는 봇은 옆으로도 기어서 2m 를 못 벗어난다. 켜면 WALK 를 유지하다 목적지 예산 20초가 끝날 때 `Unreachable` 로 남는다. **탈출 시도가 거르는 것(비켜 갈 수 있는 막힘)과 AND 가 거르는 것(느리지만 나아가는 중)은 다른 것이었다.**

테스트 133 → 144. 시뮬 60칸: 켜고 끔 모두 끼임 후보 2회, 발견물 종류도 같다 — 시뮬에는 기어가는 경우가 없다. 달라진 것은 둘이다. 후보가 0.117초(7틱) 늦게 나고, `Stuck` 기록 좌표가 봇이 실제로 멈춘 자리로 55cm 옮겨졌다 — 끄면 창의 첫 샘플이 멈추기 직전 자리였다(발견물 원장이 같은 이슈로 묶는 1,500cm 반경 안이다). 실게임 주행은 아직이다.

**이 편에서 값을 치른 교훈은 이겁니다 — 같은 문제를 이미 풀고 있어도, 남이 한 겹 더 쌓아 둔 것이 보입니다.** 저는 끼임을 시간으로 판정하고 있었고 그게 틀린 건 아닙니다. 다만 **후보를 잡는 층과 오탐을 걷는 층을 나누지 않았을 뿐**입니다. 층을 나누자마자 **오탐의 원인이 이름을 갖게 됐습니다** — *"의도적으로 멈춘 정상 상태"*.

그리고 ⚠ **도구가 대상을 얼마나 방해하는지를 같이 재는 습관**은 그대로 가져올 만합니다. 제 QA 자동화도 게임 안에서 돌면서 게임을 느리게 만들 수 있는데, **그걸 재본 적이 없습니다.**
