# AI 꾸러미 — 미리 생각하기 (상대가 두는 동안) — Concurrent AI thinking (overlap with animation)
> 사람 말이 움직이기 시작하는 순간 컴퓨터가 생각을 시작해, 말 이동 연출과 계산이 겹치게 하고 체감 대기를 계산 시간만큼 줄인다.  
> 견본: https://ai-techstudio.web.app/#t/i389

이 문서를 코드 도우미(Claude Code · Cursor · ChatGPT 등)에 그대로 주면 돼요. 「## 주문서」 가 할 일, 나머지는 참고 자료예요.

## 주문서

### 만들어 줘: 미리 생각하기 (상대가 두는 동안) — Concurrent AI thinking (overlap with animation)

#### 1. 목표
체스 · 오목 같은 대결 게임에서 컴퓨터가 「사람 말이 움직이기 시작하는 순간」 생각을 시작하게 바꿔 줘 — 계산은 워커에서, 이동 연출과 동시에. 둘 다 끝나면 컴퓨터 수를 두기. 기다리는 동안 「생각 중」 점 세 개.

#### 2. 핵심 기술 용어
- **Concurrent AI thinking (overlap with animation)** — 연출과 동시에 생각하기
- **Web Worker** — 다른 스레드 — 메인 화면을 막지 않고 계산
- **Perceived latency** — 체감 대기 — 사용자가 실제로 기다린다고 느끼는 시간
- **Promise.all** — 둘 다 끝날 때까지 기다리기

#### 3. 환경
- 플랫폼: TypeScript (브라우저), 라이브러리 없이 — 화면과 떨어진 순수 함수로
- 화면: 브라우저 — PC · 폰 모두

#### 4. 조건
- 사람 수가 확정되는 순간(놓기 · 누르기) 계산을 시작 — 연출을 기다리지 않기
- 계산은 워커에서 (메인 스레드에서 하면 이동 연출이 멈춘다)
- 이동 연출과 계산이 둘 다 끝나야 컴퓨터 수를 보이기
- 판 상태는 복사해 넘기기 — 연출 중 판이 바뀌어도 계산이 흔들리지 않게
- 전/후 체감 대기(이동이 끝난 뒤 기다린 시간)를 숫자로

#### 5. 완성 기준 (이게 보이면 성공)
- 위 줄(끝난 뒤 계산)은 「체감 대기 0.9초」, 아래 줄(동시에)은 「0.3초」로 보인다
- 이동 · 계산 슬라이더를 바꾸면 아래 줄 대기 = max(0, 계산 − 이동) 으로 바뀐다
- 계산이 이동보다 짧으면 아래 줄 대기가 0초가 된다

#### 6. 진행 방식
- 핵심 코드 위주로, 설명은 짧게. 내 프로젝트에 끼워 넣기 쉬운 함수 · 클래스로 나눠 줘.
- 처음 화면에 바로 결과가 보이게, 그리고 켬/끔(또는 전/후) 비교를 할 수 있게 만들어 줘.
- 그림 · 소리 · 모델 파일이 필요하면 코드로 만든 임시 대체물로 먼저 돌아가게 하고, 진짜 파일로 바꿀 자리를 표시해 줘.
- 마지막에 「확인 방법」(무엇을 보면 성공인지)과 「조절할 값」 목록을 짧게 정리해 줘.
- 답변과 코드 주석은 한국어로 해 줘.

## 원리
- 옛 순서: 말 이동(0.6초) → 끝난 뒤 계산(0.9초) — 체감 대기 0.9초.
- 새 순서: 말이 움직이기 시작할 때 계산도 시작 — 체감 대기 = max(0, 계산 − 이동) = 0.3초.
- 계산은 이동 연출을 막지 않게 워커(다른 스레드)에서 한다 — 메인에서 하면 연출이 멈춘다.
- 둘 다 끝날 때(Promise.all) 컴퓨터 수를 보여 준다.

## 핵심 코드 — 이동 연출과 생각을 동시에 시작
(발췌: 새로 씀 (demos/demosPerf.ts demo389 의 「이동과 동시에 계산 (워커)」 순서))
```ts
const worker = new Worker(new URL('./ai.worker.ts', import.meta.url), { type: 'module' });

function think(board: number[]): Promise<number> {
  return new Promise((resolve) => {
    worker.onmessage = (e: MessageEvent<number>) => resolve(e.data);
    worker.postMessage(board.slice()); // 판을 복사해서 넘긴다
  });
}

async function onHumanMove(move: number): Promise<void> {
  board = apply(board, move);
  // 옛 순서: await animateMove(move); const reply = await think(board);  → 대기 = 계산 시간 전부
  const thinking = think(board);          // 1) 말이 움직이기 시작할 때 생각도 시작
  await animateMove(move);                // 2) 연출 (0.6초) — 그동안 워커가 계산
  showThinking(true);                     // 3) 남은 시간에만 「생각 중」
  const reply = await thinking;           //    대기 = max(0, 계산 − 연출)
  showThinking(false);
  board = apply(board, reply);
  await animateMove(reply);
}
```

## 흔한 실수 · 확인 목록
- [ ] **계산을 메인 스레드에서 동시에 돌리면 이동 연출이 멈춘다** — 「동시에」는 다른 스레드여야 한다 — 워커에서 돌린다.
- [ ] **판 배열을 그대로 넘기고 연출 중 바꾸면 계산이 엉뚱한 판을 본다** — 복사본(slice)을 넘긴다. postMessage 도 복사하지만 메인 쪽 공유 배열은 조심.
- [ ] **생각 중 표시를 처음부터 띄우면 오히려 더 오래 기다린 느낌이 든다** — 연출이 끝났는데 아직 계산 중일 때만 보여 준다.

## 완성 기준 체크리스트
- [ ] 위 줄(끝난 뒤 계산)은 「체감 대기 0.9초」, 아래 줄(동시에)은 「0.3초」로 보인다
- [ ] 이동 · 계산 슬라이더를 바꾸면 아래 줄 대기 = max(0, 계산 − 이동) 으로 바뀐다
- [ ] 계산이 이동보다 짧으면 아래 줄 대기가 0초가 된다

## 이 기술 정보
- id: `i389` · 분류: 게임 시스템 · AI › 속도 기법 · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 계산 양은 그대로, 순서만 바꾼다. 워커 하나를 띄우는 비용(처음 한 번)만 든다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i389
- 쓰면 좋을 때: 컴퓨터 차례마다 기다림이 길게 느껴질 때 / 사람 수에 이동 · 잡기 연출이 있는 게임
- 쓰지 말 때: 계산이 아주 짧은 게임(수십 ms) — 이미 연출 안에 끝난다 / 사람 수가 확정되기 전(끌기 중)에 시작 — 수가 바뀌면 헛일 (대신 놓는 순간 시작)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### demo389 — `src/demos/demosPerf.ts:965`
```ts
function demo389() {
  let anim = 0.6;
  let comp = 0.9;
  let t0s = -1;
  return {
    draw(g: G, w: number, h: number, tt: number) {
      if (t0s < 0) t0s = tt;
      const maxT = Math.max(1.8, anim + comp + 0.25);
      const cyc = maxT + 1.1;
      const t = Math.min((tt - t0s + cyc * 100) % cyc, maxT);
      view(g, w, h);
      txt(g, '사람 말이 움직이기 시작하면 바로 계산 시작', 8, 10, 7.2, C.text, 'left', 800);
      const lanes: [number, Kind][] = [[21, 'bad'], [103, 'good']];
      for (const [y, kind] of lanes) {
        const bad = kind === 'bad';
        const cs = bad ? anim : 0;
        const done = cs + comp;
        const wait = Math.max(0, done - anim);
        const bars: Bar[] = [
          { t0: 0, t1: anim, row: 0, color: C.blue, label: `내 말 이동 ${anim.toFixed(1)}` },
          { t0: cs, t1: done, row: 1, color: C.violet, label: `컴퓨터 계산 ${comp.toFixed(1)}` },
        ];
        const X = ganttLane(g, y, kind, bad ? '이동이 끝난 뒤에야 계산' : '이동과 동시에 계산 (워커)', bars, t, maxT);
        // 체감 대기 괄호
        if (wait > 0.01 && t > anim) {
          const xa = X(anim);
          const xb = X(Math.min(t, done));
          g.fillStyle = bad ? 'rgba(255,93,108,0.22)' : 'rgba(61,220,151,0.2)';
          g.fillRect(xa, y + 41, xb - xa, 5);
        }
        txt(g, `체감 대기 ${wait.toFixed(1)}초`, 268, y + 8.3, 7, bad ? C.red : C.green, 'right', 900);
        dashV(g, X(done), y + 15, y + 50, bad ? C.red : C.green);
        miniBoard(g, 16, y + 18, 42, t / anim, t >= done ? Math.min(1, (t - done) / 0.25) : 0, t >= cs && t < done, tt);
      }
      g.restore();
    },
    controls: [
      { type: 'range', label: '내 말 이동 (초)', min: 0.2, max: 1.2, step: 0.1, value: anim, on: (v) => { anim = v; } },
      { type: 'range', label: '컴퓨터 계산 (초)', min: 0.2, max: 1.5, step: 0.1, value: comp, on: (v) => { comp = v; } },
      { type: 'button', label: '다시', on: () => { t0s = -1; } },
    ] as Control[],
  };
}
```

### i389 견본 항목 — `src/demos/demosPerf.ts:1616`
```ts
  i389: { kind: '2d', caption: '이동 끝난 뒤 계산하면 체감 대기 0.9초 vs 이동과 동시에 계산하면 0.3초', make: demo389 }
```

## 관련 기술
- 먼저 알면 좋은 기술: [무거운 표 미리 · 나눠 만들기](https://ai-techstudio.web.app/ai/t/i387.md) `i387`
- 다음에 해 볼 기술: [입력 줄 세우기 (컴퓨터 차례에 누른 것 기억)](https://ai-techstudio.web.app/ai/t/i393.md) `i393`
- 참고 문서: [MDN — Using Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)
