# AI 꾸러미 — 입력 줄 세우기 (컴퓨터 차례에 누른 것 기억) — Input buffering (input queue)
> 컴퓨터 차례에 누른 것을 버리지 않고 줄에 세워 두었다가, 차례가 오자마자 처리해 「다시 눌러야 하는」 답답함을 없앤다.  
> 견본: https://ai-techstudio.web.app/#t/i393

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

## 주문서

### 만들어 줘: 입력 줄 세우기 (컴퓨터 차례에 누른 것 기억) — Input buffering (input queue)

#### 1. 목표
컴퓨터와 두는 대결 게임에서 컴퓨터 차례(입력 잠금) 동안 누른 것을 버리지 말고 마지막 하나를 기억해 줘 — 「상대가 생각 중이에요」를 보여 주고, 차례가 오면 바로 그 수를 처리. 분위기는 기억한 칸에 옅은 표시.

#### 2. 핵심 기술 용어
- **Input buffering (input queue)** — 입력 줄 세우기 — 잠긴 동안 누른 것 기억
- **Input lock** — 입력 잠금 — 상대 차례 · 연출 중
- **Turn-based state machine** — 차례 상태 기계

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

#### 4. 조건
- 잠긴 동안 누름은 마지막 하나만 기억 (여러 개 쌓지 않기)
- 차례가 오면 기억한 수가 아직 둘 수 있는지 다시 검사하고, 안 되면 조용히 버리기
- 기억하는 동안 「상대가 생각 중이에요」 같은 안내와 기억한 칸 표시
- 판을 새로 시작하거나 게임을 닫으면 대기 줄을 비우기

#### 5. 완성 기준 (이게 보이면 성공)
- 끔 줄은 생각 중 누른 손가락이 무시되고 「다시 누름」 뒤에야 말이 움직인다
- 켬 줄은 「기억해 둠」 막대가 생기고 생각이 끝나자마자 말이 움직인다
- 누르는 때 슬라이더를 생각 시간보다 늦게 하면 둘이 같아진다

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

## 원리
- 잠긴 동안 누름을 그냥 버리면, 사용자는 눌렀는데 안 됐다고 느끼고 차례가 온 뒤 다시 눌러야 한다.
- 잠긴 동안 누른 것을 「대기 줄」에 하나 기억하고, 「상대가 생각 중이에요」를 보여 준다.
- 차례가 오는 순간 대기 줄을 꺼내 바로 처리한다 — 그 수가 이제도 둘 수 있는지 다시 검사.
- 견본: 생각 1.2초 · 0.6초에 누름 — 끔은 다시 누르기까지 기다림, 켬은 생각이 끝나자 0.05초 뒤 바로.

## 핵심 코드 — 잠긴 동안 누른 것을 기억했다가 차례가 오면 처리
(발췌: 새로 씀 (demos/demosPerf.ts demo393 의 「눌러 둔 것을 줄에 세워 두고 차례가 오면 바로」))
```ts
let locked = false;            // 컴퓨터 차례 · 연출 중
let queued: number | null = null; // 잠긴 동안 누른 칸 (마지막 하나만)

function onTap(cell: number): void {
  if (locked) {
    queued = cell;
    showHint('상대가 생각 중이에요');
    markQueued(cell); // 기억한 칸에 옅은 표시
    return;
  }
  play(cell);
}

async function computerTurn(): Promise<void> {
  locked = true;
  const reply = await think(board);
  board = apply(board, reply);
  await animateMove(reply);
  locked = false;
  // 차례가 오자마자 — 아직 둘 수 있는 칸인지 다시 검사
  const q = queued;
  queued = null;
  markQueued(-1);
  if (q !== null && isLegal(board, q)) play(q);
}

function resetGame(): void {
  queued = null; // 새 판에는 넘기지 않는다
}
```

## 흔한 실수 · 확인 목록
- [ ] **잠긴 동안 누름을 모두 쌓으면 차례가 오자 여러 수가 한꺼번에 나간다** — 마지막 하나만 기억한다.
- [ ] **기억한 칸이 컴퓨터 수로 차 버렸는데 그대로 두면 오류가 난다** — 꺼낼 때 isLegal 로 다시 검사하고, 안 되면 버린다.
- [ ] **이유 없이 입력을 막으면 사용자는 고장이라고 느낀다** — 막힌 입력에는 까닭(「상대가 생각 중이에요」)을 보여 준다 — 이 사이트의 「차례는 판 위에 크게」 규칙.

## 완성 기준 체크리스트
- [ ] 끔 줄은 생각 중 누른 손가락이 무시되고 「다시 누름」 뒤에야 말이 움직인다
- [ ] 켬 줄은 「기억해 둠」 막대가 생기고 생각이 끝나자마자 말이 움직인다
- [ ] 누르는 때 슬라이더를 생각 시간보다 늦게 하면 둘이 같아진다

## 이 기술 정보
- id: `i393` · 분류: 게임 시스템 · AI › 속도 기법 · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 변수 하나 · 검사 한 번. 비용 없음.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i393
- 쓰면 좋을 때: 빠르게 누르는 사용자가 「씹혔다」고 느낄 때 / 상대 차례 · 연출이 0.5초 넘게 걸리는 게임
- 쓰지 말 때: 상대 수를 보고 판단해야 하는 수 — 미리 누른 수가 위험할 수 있다 (대신 기억한 수를 표시만 하고 확정은 한 번 더) / 실시간 액션 — 대신 짧은 입력 버퍼(몇 프레임)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### demo393 — `src/demos/demosPerf.ts:1369`
```ts
function demo393() {
  let think = 1.2;
  let tap = 0.6;
  const MAXT = 2.4;
  const CYC = 3.3;
  return {
    draw(g: G, w: number, h: number, tt: number) {
      const t = Math.min(tt % 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 retap = Math.max(think + 0.65, tap + 0.6);
        const moveAt = bad ? retap : think + 0.05;
        const bars: Bar[] = [{ t0: 0, t1: think, row: 0, color: C.amber, label: '상대가 생각 중' }];
        if (!bad) bars.push({ t0: tap, t1: think, row: 1, color: C.cyan, label: '기억해 둠' });
        const X = ganttLane(g, y, kind, bad ? '잠긴 동안 누르면 무시' : '눌러 둔 것을 줄에 세워 두고 차례가 오면 바로', bars, t, MAXT);
        const taps = bad ? [tap, retap] : [tap];
        for (const tp of taps) {
          if (t < tp) continue;
          finger(g, X(tp), y + 40, 6);
        }
        if (bad && t >= tap) {
          txt(g, '✕ 무시됨', X(tap) + 5, y + 34, 6, C.red, 'left', 900);
          if (t >= retap) txt(g, '다시 누름', X(retap) - 4, y + 34, 5.4, '#ffb0b8', 'right', 700);
        }
        if (!bad && t >= tap && t < think) {
          pill(g, '상대가 생각 중이에요', X(tap) + 4, y + 43, 5.2, '#2b3560', '#cfe9ff');
        }
        dashV(g, X(moveAt), y + 15, y + 50, bad ? C.red : C.green);
        txt(g, `내 수 ${moveAt.toFixed(2)}초`, 268, y + 8.3, 7, bad ? C.red : C.green, 'right', 900);
        miniBoard(g, 16, y + 18, 42, 0, t >= think ? Math.min(1, (t - think) / 0.2) : 0, t < think, tt, t >= moveAt ? Math.min(1, (t - moveAt) / 0.2) : 0);
      }
      g.restore();
    },
    controls: [
      { type: 'range', label: '컴퓨터 생각 (초)', min: 0.6, max: 1.8, step: 0.1, value: think, on: (v) => { think = v; } },
      { type: 'range', label: '내가 누르는 때 (초)', min: 0.1, max: 1.0, step: 0.1, value: tap, on: (v) => { tap = v; } },
    ] as Control[],
  };
}
```

### i393 견본 항목 — `src/demos/demosPerf.ts:1620`
```ts
  i393: { kind: '2d', caption: '컴퓨터 차례에 누르면 무시되어 다시 눌러야 함 vs 기억해 두었다가 차례가 오자마자', make: demo393 }
```

## 관련 기술
- 먼저 알면 좋은 기술: [미리 생각하기 (상대가 두는 동안)](https://ai-techstudio.web.app/ai/t/i389.md) `i389`
- 다음에 해 볼 기술: [손가락 크기 누름 영역](https://ai-techstudio.web.app/ai/t/i418.md) `i418`
