# PatchMe Game Testing Methodology

## 개요

모든 게임에 대해 자동화된 플레이 테스트를 수행하는 시스템.
Sub Agent가 게임 소스코드를 분석하고, 해당 게임 전용 BOT 프로그램을 작성하여 실행한다.

## 아키텍처

```
Main Agent (나)
  └─ Sub Agent (게임당 1개)
       ├─ Phase 1: 게임 학습 (소스코드 분석)
       ├─ Phase 2: BOT 작성 (게임 전용 자동화 프로그램)
       ├─ Phase 3: BOT 실행 + 결과 관찰
       ├─ Phase 4: BOT 수정 (버그/실패 대응)
       └─ Phase 5: 테스트 리포트 작성
```

## Sub Agent 동작 사이클 (Continuous Hot-Swap)

### 핵심 원리: BOT은 멈추지 않는다

```
[기존 방식 — 비효율]
bot A 실행 → 멈춤 → 학습 → bot B 작성 → bot B 실행 → 멈춤 → ...
                ↑ 게임이 멈추는 동안 데이터 수집 불가

[Hot-Swap 방식 — 채택]
bot A 실행 ─────────────────────────────────→ (계속 데이터 수집)
     ↓ (스냅샷/로그 스트리밍)                       ↓
Sub Agent: 학습 → bot B 작성                   bot A 데이터도 수집됨
     ↓                                         ↓
bot B로 교체 (hot-swap) ──────────────────────→ (계속 데이터 수집)
     ↓ (B 스냅샷 + A 후반 데이터 종합)              ↓
Sub Agent: A+B 종합 학습 → bot C 작성          bot B 데이터도 수집됨
     ↓
bot C로 교체 ...
```

### 구현 방법

**BOT은 무한 루프로 돌아간다.** Sub Agent가 새 BOT 파일을 작성하면 프로세스를 교체한다.

```bash
# runner.sh — BOT 무한 실행기
GAME_SLUG=$1
BOT_FILE="/tmp/bot_${GAME_SLUG}.js"
BOT_VERSION=1

while true; do
  echo "[RUNNER] Starting bot v${BOT_VERSION}: $BOT_FILE"

  # BOT 실행 (백그라운드)
  cd ~/sites/arcade
  NODE_PATH=./node_modules node "$BOT_FILE" &
  BOT_PID=$!

  # 새 버전 파일이 생길 때까지 대기
  NEW_FILE="/tmp/bot_${GAME_SLUG}_v$((BOT_VERSION+1)).js"
  while [ ! -f "$NEW_FILE" ]; do
    sleep 5
  done

  # Hot-swap: 기존 BOT 종료, 새 BOT 시작
  echo "[RUNNER] Hot-swapping to v$((BOT_VERSION+1))"
  kill $BOT_PID 2>/dev/null
  wait $BOT_PID 2>/dev/null

  BOT_VERSION=$((BOT_VERSION+1))
  BOT_FILE="$NEW_FILE"
done
```

**Sub Agent의 사이클:**
```
1. runner.sh 시작 (bot v1 실행)
2. bot v1이 데이터를 /tmp/bot_snapshots/에 쌓는 동안...
3. Sub Agent가 스냅샷/로그를 읽고 분석
4. bot v2를 /tmp/bot_{slug}_v2.js로 작성
5. runner.sh가 자동으로 hot-swap
6. bot v2가 실행되는 동안 Sub Agent는:
   - v1 후반 데이터 + v2 초반 데이터를 종합 분석
   - bot v3 작성
7. 반복 (3~5 버전이면 충분)
```

**데이터 연속성:**
```
bot v1: tick 0-300    → snapshots/v1_tick_000.png ~ v1_tick_300.png
bot v2: tick 301-600  → snapshots/v2_tick_301.png ~ v2_tick_600.png
bot v3: tick 601-900  → snapshots/v3_tick_601.png ~ v3_tick_900.png

Sub Agent가 v3를 만들 때 참조:
- v1 전체 로그 (어디서 죽었나, 어디서 막혔나)
- v2 전체 로그 (수정이 효과가 있었나)
- v1→v2 변경점이 성공했는지 실패했는지
```

### Phase 1: 게임 학습

Sub Agent는 게임 디렉토리의 소스코드를 읽는다.

**읽어야 할 것:**
- `index.html` — 구조, 스타일, canvas 유무
- `game.js` 또는 주요 JS 파일 — 게임 로직
- `src/` 하위 파일들 — 엔티티, 전투, UI 등

**파악해야 할 것:**
- 게임 장르 (액션/퍼즐/전략/타워디펜스 등)
- 조작법 (키보드/마우스 매핑)
- 게임 상태 변수 (state, phase 등)
- 적/아이템/UI 요소의 데이터 구조
- 승리/패배 조건
- 화면 좌표계 (canvas 크기)

### Phase 2: BOT 작성

게임 분석 결과를 바탕으로, **해당 게임 전용 BOT 프로그램**을 Node.js로 작성한다.

**BOT의 구조:**
```js
// /tmp/bot_{game-slug}.js
const CDP = require('chrome-remote-interface');

async function bot() {
  const client = await CDP({ port: 9222 });
  const { Page, Runtime, Input } = client;

  // ── 게임 상태 읽기 (Runtime.evaluate) ──
  // 적 위치, 플레이어 위치, HP, 게임 상태 등
  // 이것은 "조작"이 아니라 "관찰"이다

  // ── 판단 (코드 로직) ──
  // 가장 가까운 적 방향 계산
  // 체력 낮으면 회피
  // 아이템 팝업이면 장착

  // ── 입력 (Input API만 사용) ──
  // 키보드: Input.dispatchKeyEvent
  // 마우스: Input.dispatchMouseEvent

  // ── 스크린샷 (관찰용) ──
  // Page.captureScreenshot
}
```

**BOT이 할 수 있는 것:**
- `Runtime.evaluate`로 게임 상태 **읽기** (적 위치, HP, 상태 등)
- `Input.dispatchKeyEvent`로 키보드 입력
- `Input.dispatchMouseEvent`로 마우스 이동/클릭
- `Page.captureScreenshot`로 스크린샷

**BOT이 하면 안 되는 것:**
- `Runtime.evaluate`로 게임 변수 **수정** (HP 변경, 적 제거 등)
- 게임 파일 직접 수정

**BOT의 핵심 패턴:**

```js
// 패턴 A: 액션 게임 (적 추적 + 공격)
async function actionGameTick(Runtime, Input) {
  // 1. 게임 상태 읽기
  const state = await Runtime.evaluate({
    expression: 'JSON.stringify({ enemies: game.enemies.map(e => ({x:e.x, y:e.y, hp:e.hp})), player: {x:game.player.x, y:game.player.y, hp:game.player.hp}, state: game.state })'
  });
  const data = JSON.parse(state.result.value);

  // 2. 가장 가까운 적 찾기
  const nearest = findNearest(data.enemies, data.player);

  // 3. 적 방향으로 이동 키 입력
  if (nearest.x > data.player.x) holdKey('KeyD', 87);
  else holdKey('KeyA', 65);

  // 4. 적이 사거리 안이면 클릭 공격
  // 적의 월드 좌표를 화면 좌표로 변환
  const screenX = nearest.x - camera.x + canvasWidth/2;
  const screenY = nearest.y - camera.y + canvasHeight/2;
  if (dist < attackRange) clickAt(screenX, screenY);
}

// 패턴 B: 퍼즐 게임
async function puzzleGameTick(Runtime, Input) {
  const board = await Runtime.evaluate({ expression: 'JSON.stringify(game.board)' });
  // 최적 수 계산 후 클릭
}

// 패턴 C: 타워디펜스
async function towerDefenseTick(Runtime, Input) {
  const gold = await Runtime.evaluate({ expression: 'game.gold' });
  const towers = await Runtime.evaluate({ expression: 'JSON.stringify(game.towers)' });
  // 타워 배치 판단 후 클릭
}
```

### Phase 3: BOT 실행 + 결과 관찰

```bash
# BOT 실행 (N 사이클)
cd ~/sites/arcade && NODE_PATH=./node_modules node /tmp/bot_{slug}.js

# BOT은 매 tick마다:
# 1. 게임 상태 읽기
# 2. 판단
# 3. 입력
# 4. 일정 간격으로 스크린샷 저장
# 5. 버그/이슈 로그 기록
```

**BOT이 자동으로 기록해야 할 것:**
- 매 N tick마다 스크린샷 (`/tmp/bot_screenshots/`)
- 게임 상태 변화 로그 (`/tmp/bot_log.jsonl`)
- 감지된 이슈 (`/tmp/bot_issues.md`)

**이슈 감지 기준 (BOT 코드에 내장):**
- JS 에러 발생 → 기록
- 게임 상태가 일정 시간 변하지 않음 (멈춤) → 기록
- HP가 갑자기 0이 됨 (즉사 버그?) → 기록
- 화면에 렌더링되는 요소 수가 0 (빈 화면) → 기록
- 특정 상태에서 입력이 무시됨 → 기록

### Phase 4: BOT 수정

Sub Agent가 BOT 실행 결과(로그, 스크린샷, 이슈)를 확인하고 BOT을 수정한다.

**수정이 필요한 경우:**
- BOT이 죽었다 → 전략 변경 (회피 로직 추가, 포션 사용 등)
- BOT이 막혔다 → 새로운 상태 처리 추가 (팝업, 이벤트 등)
- BOT이 비효율적이다 → 알고리즘 개선
- 새로운 게임 메커닉 발견 → 대응 코드 추가

**수정 사이클:**
```
실행 → 결과 확인 → 코드 수정 → 재실행 → 반복 (3~5회)
```

### Phase 5: 테스트 리포트

최종적으로 Sub Agent가 리포트를 생성한다.

```markdown
# {게임 이름} 테스트 리포트

## 테스트 요약
- 실행 시간: N분
- 도달 진행도: Floor 3, Level 8
- BOT 수정 횟수: 4회

## 발견된 버그
1. [BUG] 크로스헤어가 메뉴에서 안 보임
2. [BUG] 아이템 오버레이가 화면 전체를 가림

## UX 개선 제안
1. 크로스헤어 크기를 키우거나 색상을 밝게
2. 업적 배너가 게임 UI와 겹침

## BOT이 어려워한 부분 = 유저도 어려워할 부분
1. Floor 2에서 적이 너무 많이 몰려옴
2. 아이템 비교가 직관적이지 않음
```

## 파일 구조

```
~/sites/arcade/pipeline/
├── game-controller.js         # 액션 파일 실행기 (범용)
├── play-loop.sh               # Sub Agent 실행 스크립트 (범용)
├── game-tester.js             # 기존 테스터 (파이프라인용)
├── test-and-deploy.sh         # 테스트 후 배포 (파이프라인용)
└── bots/                      # 게임별 BOT 저장
    ├── abyssal-descent.js
    ├── crimson-spire.js
    └── weekly-current.js
```

## Sub Agent 실행 방법

### Main Agent가 Sub Agent를 호출하는 방식

```
Agent(
  prompt: "다음 게임을 테스트하라: {game-slug}
    게임 경로: ~/sites/arcade/games/{game-slug}/
    게임 URL: https://patchme.lol/games/{game-slug}/

    1. 게임 소스코드를 읽고 분석하라
    2. 이 게임 전용 BOT을 /tmp/bot_{slug}.js로 작성하라
    3. BOT을 실행하고 결과를 확인하라
    4. 문제가 있으면 BOT을 수정하고 재실행하라 (최대 5회)
    5. 최종 테스트 리포트를 작성하라
    6. 발견된 버그/이슈를 ~/sites/arcade/pipeline/bots/{slug}_report.md에 저장하라

    모든 작업은 오라클 서버(ssh oracle)에서 실행한다.
    BOT은 게임 변수를 읽기만 하고 수정하지 않는다.
    입력은 반드시 Input API(키보드/마우스)만 사용한다."
)
```

### 여러 게임 동시 테스트

```
# 각 게임을 별도 Sub Agent로 병렬 실행
Agent("abyssal-descent 테스트")  # 탭 1
Agent("crimson-spire 테스트")    # 탭 2 (탭 전환 필요)
```

단, CDP 포트가 하나이므로 동시 실행 시 탭 전환이 필요하다.
Chrome 탭을 여러 개 열고, 각 Agent가 자기 탭 ID를 지정하여 제어할 수 있다.

## BOT 작성 가이드 (Sub Agent용)

### 필수 구조

```js
const CDP = require('chrome-remote-interface');
const fs = require('fs');

const GAME_URL = 'https://patchme.lol/games/{slug}/';
const TICK_INTERVAL = 200; // ms
const MAX_TICKS = 300; // 60초
const SCREENSHOT_INTERVAL = 20; // 20 tick마다 스크린샷

async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

async function run() {
  const client = await CDP({ port: 9222 });
  const { Page, Runtime, Input } = client;
  await Page.enable();
  await Runtime.enable();

  const issues = [];
  const log = [];

  // 게임 로드
  await Page.navigate({ url: GAME_URL });
  await sleep(5000);

  // 게임 시작 (게임마다 다름)
  // ...

  // 메인 루프
  for (let tick = 0; tick < MAX_TICKS; tick++) {
    try {
      // 1. 게임 상태 읽기 (읽기만!)
      const state = await readGameState(Runtime);

      // 2. 판단 + 입력
      await decide(state, Input);

      // 3. 이슈 감지
      detectIssues(state, issues);

      // 4. 로그
      log.push({ tick, state: state.summary });

      // 5. 스크린샷
      if (tick % SCREENSHOT_INTERVAL === 0) {
        const s = await Page.captureScreenshot({ format: 'png' });
        fs.writeFileSync('/tmp/bot_screenshots/tick_' + tick + '.png', Buffer.from(s.data, 'base64'));
      }
    } catch (e) {
      issues.push({ tick, error: e.message });
    }

    await sleep(TICK_INTERVAL);
  }

  // 결과 저장
  fs.writeFileSync('/tmp/bot_issues.json', JSON.stringify(issues, null, 2));
  fs.writeFileSync('/tmp/bot_log.jsonl', log.map(l => JSON.stringify(l)).join('\n'));

  await client.close();
}

run().catch(console.error);
```

### 게임 상태 읽기 예시

```js
// 액션/로그라이크 게임
async function readGameState(Runtime) {
  const r = await Runtime.evaluate({
    expression: `JSON.stringify({
      state: game.state,
      player: { x: game.player.x, y: game.player.y, hp: game.player.hp, maxHp: game.player.maxHp },
      enemies: game.enemies.filter(e => e.hp > 0).map(e => ({ x: e.x, y: e.y, hp: e.hp })),
      camera: { x: game.camera.x, y: game.camera.y },
      canvas: { w: game.canvas.width, h: game.canvas.height },
      floor: game.floor,
      kills: game.kills
    })`
  });
  return JSON.parse(r.result.value);
}
```

### 월드 좌표 → 화면 좌표 변환

```js
function worldToScreen(worldX, worldY, camera, canvas) {
  return {
    screenX: worldX - camera.x + canvas.w / 2,
    screenY: worldY - camera.y + canvas.h / 2
  };
}
```

### 스냅샷 전략 (중요)

BOT은 실행 중 다음 시점에 반드시 스크린샷과 상태를 저장해야 한다:

```js
// BOT 내부에 내장
const snapshots = [];

function snapshot(tick, state, reason) {
  snapshots.push({
    tick,
    timestamp: Date.now(),
    reason,  // 'periodic', 'state_change', 'issue', 'death', 'level_up' 등
    state: {
      gameState: state.state,
      floor: state.floor,
      hp: state.player.hp,
      level: state.player.level,
      kills: state.kills,
      enemyCount: state.enemies.length
    }
  });
}

// 스냅샷을 찍어야 하는 시점:
// 1. 주기적: 매 20 tick (약 4초)
// 2. 상태 변화: state가 바뀔 때 (playing→paused, 레벨업, 층 이동 등)
// 3. 이슈 발생: JS 에러, 멈춤, 예상 밖 상태
// 4. 전투 시작/종료: 적과 첫 교전, 모든 적 처치
// 5. UI 이벤트: 아이템 드롭, 퍼크 선택, 이벤트 팝업
```

**스냅샷 포맷 규칙:**
- 형식: JPEG (quality 40-50) — PNG 대비 80-90% 용량 절감
- Claude가 해석하기 충분한 품질
```js
// 스크린샷 캡처 시
const { data } = await Page.captureScreenshot({ format: 'jpeg', quality: 45 });
fs.writeFileSync(path, Buffer.from(data, 'base64'));
// PNG ~120KB → JPEG q45 ~15KB
```

**스냅샷 정리 규칙:**
- BOT은 스냅샷을 삭제하지 않는다. 쌓기만 한다.
- Sub Agent가 사이클 간 학습을 완료한 후, 해당 사이클의 스냅샷을 삭제한다.
- 즉, 삭제 타이밍 = "Sub Agent가 스냅샷을 다 읽고 분석한 후"
```bash
# Sub Agent가 v2 작성 완료 후 실행
rm -f /tmp/bot_snapshots/{slug}/v1_*.jpg
# v1 스냅샷은 학습 완료했으므로 삭제
# v2 스냅샷은 아직 학습 안 했으므로 유지
```
- BOT은 스냅샷 파일명에 버전을 포함: `v1_tick_100.jpg`, `v2_tick_301.jpg`
- 이렇게 하면 어떤 버전의 스냅샷인지 구분 가능하고, 학습 후 버전 단위로 삭제

Sub Agent는 사이클 간에 이 스냅샷들을 읽고:
- BOT이 올바르게 동작했는지 확인
- 게임 진행이 멈춘 구간이 있는지 확인
- 버그/UX 이슈가 발생한 스냅샷을 식별
- 다음 사이클에서 BOT을 어떻게 수정할지 결정

```
/tmp/bot_screenshots/
├── tick_000.png    # 게임 시작
├── tick_020.png    # 주기적
├── tick_025.png    # state_change: 레벨업
├── tick_040.png    # 주기적
├── tick_042.png    # issue: 화면 멈춤 감지
└── tick_060.png    # 주기적

/tmp/bot_snapshots.jsonl
{"tick":0,"reason":"start","state":{"gameState":"playing","floor":1,"hp":100,...}}
{"tick":20,"reason":"periodic","state":{...}}
{"tick":25,"reason":"state_change","state":{"gameState":"levelup",...}}
{"tick":42,"reason":"issue","state":{...},"issue":"no state change for 10 ticks"}
```

### 입력 헬퍼

```js
async function pressKey(Input, code, kc) {
  await Input.dispatchKeyEvent({ type: 'rawKeyDown', code, windowsVirtualKeyCode: kc });
  await sleep(40);
  await Input.dispatchKeyEvent({ type: 'keyUp', code, windowsVirtualKeyCode: kc });
}

async function clickAt(Input, x, y) {
  await Input.dispatchMouseEvent({ type: 'mouseMoved', x, y });
  await sleep(20);
  await Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
  await sleep(30);
  await Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
}

async function holdKeyFor(Input, code, kc, ms) {
  await Input.dispatchKeyEvent({ type: 'rawKeyDown', code, windowsVirtualKeyCode: kc });
  await sleep(ms);
  await Input.dispatchKeyEvent({ type: 'keyUp', code, windowsVirtualKeyCode: kc });
}
```
