# AI 꾸러미 — 강 흘려보내기 (내리막 따라) — Flow accumulation (D8 drainage)
> 높이 지도의 웅덩이를 먼저 메운 뒤 칸마다 빗물 1 을 가장 낮은 이웃으로 넘겨 모으면, 물줄기가 합쳐져 강이 되고 메운 곳은 호수가 된다.  
> 견본: https://ai-techstudio.web.app/#t/i251

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

## 주문서

### 만들어 줘: 강 흘려보내기 (내리막 따라) — Flow accumulation (D8 drainage)

#### 1. 목표
잡음으로 만든 섬 지도에 강 흘려보내기를 넣어 줘 — 웅덩이를 메우고, 높은 칸부터 빗물을 가장 낮은 이웃으로 모아 물이 많은 곳을 강으로, 메운 곳을 호수로. 그림은 종이 지도 색.

#### 2. 핵심 기술 용어
- **Flow accumulation (D8 drainage)** — 흐름 모으기 — 칸마다 물을 낮은 이웃으로
- **Priority-flood depression filling** — 바다에서부터 낮은 순서로 넓혀 웅덩이 메우기
- **Min-heap (priority queue)** — 가장 낮은 칸을 먼저 꺼내는 큐
- **Lakes from filled depressions** — 메운 깊이 = 호수

#### 3. 환경
- 플랫폼: HTML Canvas 2D (TypeScript), requestAnimationFrame 루프, 라이브러리 없이
- 화면: 2D · 브라우저 — PC 와 폰(390px 폭) 모두, devicePixelRatio 맞춰 또렷하게, 60fps 목표

#### 4. 조건
- 물 모으기 전에 반드시 웅덩이 메우기 — 안 그러면 강이 중간에서 끊긴다
- 이웃은 8방향, 메운 높이에 아주 작은 기울기(1e-5)를 더해 평평한 곳에서도 방향이 정해지게
- 강 기준값은 해상도에 맞춰 (견본: 기준 × (res/120)²) — 해상도를 올려도 강 굵기가 비슷하게
- 강은 굵기별로 묶어 한 번씩 stroke — 선분 수만 개를 하나씩 그리지 않는다

#### 5. 완성 기준 (이게 보이면 성공)
- 높은 곳부터 물이 모이는 띠가 내려가고, 끝나면 갈래가 합쳐지며 굵어지는 강이 바다까지 이어진다
- 오목한 곳엔 호수가 생기고, 강이 호수를 지나 계속 흐른다
- 「강이 되는 물의 양」을 줄이면 가는 지류가 많이 보이고, 늘리면 큰 강만 남는다
- 물방울 점들이 강을 따라 바다로 흘러간다

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

## 원리
- 그냥 「가장 낮은 이웃」만 따라가면 웅덩이에 물이 갇혀 바다로 못 간다. 그래서 바다 칸부터 우선순위 큐(가장 낮은 것 먼저)로 넓혀 간다.
- 새로 닿는 칸의 메운 높이 = max(제 높이, 온 칸의 메운 높이 + 아주 조금). 그 칸의 「물 내려갈 곳(down)」은 자기를 꺼내 준 칸이다.
- 큐에서 꺼낸 순서를 거꾸로 읽으면 높은 곳 → 낮은 곳 순서. 그 순서로 칸마다 빗물 1 을 더하고 내 물을 down 칸에 넘긴다.
- 모인 물이 기준(6칸 몫) 넘는 칸을 down 쪽으로 선을 그으면 강, 굵기는 log2(물 ÷ 기준). 메운 높이 − 원래 높이 > 0.004 인 칸은 호수.

## 핵심 코드 — 웅덩이 메우기 + 흐름 모으기
(발췌: demos/demosMapA.ts i251 start() · step() 을 정리 (작은 힙은 새로 씀))
```ts
// h: 높이 (W×H), sea: 바다 높이. down[k] = 물이 내려갈 칸, acc[k] = 모인 물
function flow(h: Float32Array, W: number, H: number, sea: number) {
  const n = W * H, filled = new Float32Array(n), seen = new Uint8Array(n);
  const down = new Int32Array(n).fill(-1);
  const heap: [number, number][] = [];                       // [높이, 칸] 가장 낮은 것 먼저
  const push = (v: number, i: number) => { heap.push([v, i]); heap.sort((a, b) => b[0] - a[0]); }; // 짧게 쓴 판 — 크면 이진 힙으로
  for (let i = 0; i < n; i++) if (h[i] < sea) { seen[i] = 1; filled[i] = h[i]; push(h[i], i); }
  const pops: number[] = [];
  while (heap.length) {
    const c = heap.pop()![1];
    pops.push(c);
    const cx = c % W, cy = (c / W) | 0;
    for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
      const x = cx + dx, y = cy + dy;
      if ((!dx && !dy) || x < 0 || y < 0 || x >= W || y >= H) continue;
      const k = y * W + x;
      if (seen[k]) continue;
      seen[k] = 1;
      filled[k] = Math.max(h[k], filled[c] + 1e-5);            // 웅덩이는 메워진다
      down[k] = c;
      push(filled[k], k);
    }
  }
  const acc = new Float32Array(n), lake = new Uint8Array(n);
  for (let p = pops.length - 1; p >= 0; p--) {                 // 높은 곳 → 낮은 곳
    const c = pops[p];
    if (h[c] < sea) continue;
    acc[c] += 1;                                               // 빗물 1
    if (down[c] >= 0) acc[down[c]] += acc[c];
    if (filled[c] - h[c] > 0.004) lake[c] = 1;
  }
  return { down, acc, lake };                                  // acc ≥ 기준 → 강
}
```

## 흔한 실수 · 확인 목록
- [ ] **웅덩이를 안 메우면 강이 산 중턱에서 뚝 끊긴다** — 가장 낮은 이웃이 자기보다 높은 칸(웅덩이)에서 물이 갇힌다. 바다에서부터 우선순위 큐로 넓히며 메운 높이를 쓴다.
- [ ] **평평한 곳에서 물이 갈 곳을 못 정한다** — 메운 높이에 1e-5 씩 더해 나온 쪽으로 아주 살짝 기울게 한다.
- [ ] **해상도를 올리니 강이 온통 굵어졌다** — 칸이 늘면 모이는 물도 늘어난다. 기준값을 (해상도 비)² 로 함께 키운다.
- [ ] **배열 정렬로 큐를 만들면 큰 지도에서 아주 느리다** — 견본은 이진 힙(Heap 클래스)을 쓴다. 위 짧은 코드의 sort 판은 작은 지도용.

## 완성 기준 체크리스트
- [ ] 높은 곳부터 물이 모이는 띠가 내려가고, 끝나면 갈래가 합쳐지며 굵어지는 강이 바다까지 이어진다
- [ ] 오목한 곳엔 호수가 생기고, 강이 호수를 지나 계속 흐른다
- [ ] 「강이 되는 물의 양」을 줄이면 가는 지류가 많이 보이고, 늘리면 큰 강만 남는다
- [ ] 물방울 점들이 강을 따라 바다로 흘러간다

## 이 기술 정보
- id: `i251` · 분류: 지도 · 길찾기 › 지도 생성 (절차) · 2D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 200 × 125 = 2만 5천 칸을 큐에 한 번씩 넣고 뺀다 (n log n). 지도를 만들 때 한 번이면 되고, 화면엔 걸러 그린다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i251
- 쓰면 좋을 때: 잡음 지도에 그럴듯한 강 · 호수를 덧붙일 때 / 「물은 낮은 곳으로, 모이면 강」을 보여 주는 지형 · 과학 체험
- 쓰지 말 때: 물이 실제로 출렁이며 흘러야 할 때 — 얕은 물 시뮬레이션 같은 유체 계산이 맞다 / 흙이 깎이는 모습까지 필요할 때 — 물방울 침식(i253)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i251 견본 항목 — `src/demos/demosMapA.ts:1981`
```ts
  i251: {
    kind: '2d',
    caption: '칸마다 빗물 1 — 높은 곳부터 가장 낮은 이웃으로 넘겨 모으면 물줄기가 합쳐져 강, 고인 웅덩이는 호수',
    make() {
      let T: Terrain | null = null;
      let W = 0;
      let H = 0;
      let base = mkCanvas(1, 1);
      let order = new Int32Array(0);
      let down = new Int32Array(0);
      let acc = new Float32Array(0);
      let lake = new Uint8Array(0);
      let done = new Uint8Array(0);
      let pi = 0;
      let per = 1;
      let thr = 6;
      let drops = true;
      let res = 120;
      const P: { c: number; k: number }[] = [];
      const heap = new Heap();
      const run = runner({
        rate: 60,
        hold: 2.2,
        start(seed) {
          W = res;
          H = Math.round(res * 0.625);
          T = makeTerrain(W, H, seed, 0.56, 1.05);
          const n = W * H;
          const filled = new Float32Array(n);
          const seen = new Uint8Array(n);
          down = new Int32Array(n).fill(-1);
          heap.clear();
          for (let i = 0; i < n; i++)
            if (T.h[i]! < T.sea) {
              seen[i] = 1;
              filled[i] = T.h[i]!;
              heap.push(T.h[i]!, i);
            }
          const pops: number[] = [];
          while (heap.size) {
            const c = heap.pop();
            pops.push(c);
            const cx = c % W;
            const cy = (c / W) | 0;
            for (const [dx, dy] of N8) {
              const x = cx + dx;
              const y = cy + dy;
              if (x < 0 || y < 0 || x >= W || y >= H) continue;
              const k = y * W + x;
              if (seen[k]) continue;
              seen[k] = 1;
              filled[k] = Math.max(T.h[k]!, filled[c]! + 1e-5);
              down[k] = c;
              heap.push(filled[k]!, k);
            }
          }
          const land: number[] = [];
          for (let k = pops.length - 1; k >= 0; k--) if (T.h[pops[k]!]! >= T.sea) land.push(pops[k]!);
          order = Int32Array.from(land);
          lake = new Uint8Array(n);
          for (const i of land) if (filled[i]! - T.h[i]! > 0.004) lake[i] = 1;
          acc = new Float32Array(n);
          done = new Uint8Array(n);
          pi = 0;
          per = Math.max(1, Math.ceil(order.length / 150));
          base = terrainImage(T);
          P.length = 0;
        },
        step() {
          for (let k = 0; k < per && pi < order.length; k++, pi++) {
            const c = order[pi]!;
            acc[c] = acc[c]! + 1;
            done[c] = 1;
            const d = down[c]!;
            if (d >= 0) acc[d] = acc[d]! + acc[c]!;
          }
          return pi >= order.length;
        },
      });
      run.restart();
      return {
        draw(g, w, h, _t, dt) {
          reset(g);
          const want = w > 420 ? 200 : 120;
          if (want !== res) {
            res = want;
            run.restart(run.seed);
          }
          run.tick(dt);
          if (!T) return;
          drawCover(g, base, w, h);
          const s = Math.max(w / W, h / H);
          const ox = (w - W * s) / 2;
          const oy = (h - H * s) / 2;
          const X = (i: number): number => ox + ((i % W) + 0.5) * s;
          const Y = (i: number): number => oy + (((i / W) | 0) + 0.5) * s;
          const u = ui(scaleOf(w, h));
          const k = res / 120;
          // 호수
          g.fillStyle = '#4a9fd6';
          g.beginPath();
          for (let p = 0; p < pi; p++) {
            const c = order[p]!;
            if (!lake[c]) continue;
            g.moveTo(X(c) + s * 0.8, Y(c));
            g.arc(X(c), Y(c), s * 0.8, 0, TAU);
          }
          g.fill();
          // 강 — 굵기별로 묶어 그리기
          const T0 = thr * k * k;
          const buckets: number[][] = [[], [], [], [], []];
          for (let p = 0; p < pi; p++) {
            const c = order[p]!;
            const a = acc[c]!;
            if (a < T0 || down[c]! < 0) continue;
            const b = Math.min(4, Math.floor(Math.log2(a / T0) * 0.9));
            buckets[b]!.push(c);
          }
          g.lineCap = 'round';
          for (let pass = 0; pass < 2; pass++)
            buckets.forEach((list, b) => {
              if (!list.length) return;
              g.beginPath();
              for (const c of list) {
                const d = down[c]!;
                g.moveTo(X(c), Y(c));
                g.lineTo(X(d), Y(d));
              }
              const wd = (0.35 + b * 0.32) * s * (res > 150 ? 1.3 : 1);
              g.strokeStyle = pass ? '#86d0f5' : '#2a78c0';
              g.lineWidth = pass ? Math.max(0.6, wd * 0.4) : Math.max(1, wd);
              g.stroke();
            });
          // 막 물이 지나간 높이 띠
          if (!run.holding) {
            g.fillStyle = 'rgba(255,255,255,0.28)';
            for (let p = Math.max(0, pi - per * 8); p < pi; p++) {
              const c = order[p]!;
              g.fillRect(X(c) - s / 2, Y(c) - s / 2, s, s);
            }
          }
          // 물방울
          if (drops && pi > 0) {
            while (P.length < 70) P.push({ c: order[Math.floor(Math.random() * pi)]!, k: Math.random() });
            g.fillStyle = 'rgba(220,245,255,0.9)';
            for (const q of P) {
              q.k += dt * 7;
              while (q.k >= 1) {
                q.k -= 1;
                const d = down[q.c]!;
                if (d < 0 || !T || T.h[d]! < T.sea) {
                  q.c = order[Math.floor(Math.random() * pi)]!;
                  break;
                }
                q.c = d;
              }
              const d = down[q.c]!;
              if (d < 0) continue;
              const x = lerp(X(q.c), X(d), q.k);
              const y = lerp(Y(q.c), Y(d), q.k);
              g.beginPath();
              g.arc(x, y, 1.3 * u, 0, TAU);
              g.fill();
            }
          }
          vignette(g, w, h, 0.25);
          const pct = Math.round((pi / Math.max(1, order.length)) * 100);
          pill(g, run.holding ? '모든 빗물이 바다로 — 강 · 호수 완성' : `높은 곳부터 물 모으기 ${pct}%`, 8 * u, 13 * u, 9 * u, 'rgba(10,30,50,0.8)');
        },
        controls: [
          seedCtl(run),
          speedCtl(run),
          { type: 'range', label: '강이 되는 물의 양', min: 2, max: 30, step: 1, value: 6, on: (v) => { thr = v; } },
          { type: 'toggle', label: '흐르는 물방울', value: true, on: (v) => { drops = v; } },
        ],
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [잡음 섬 지도 (높이 → 생물군)](https://ai-techstudio.web.app/ai/t/i245.md) `i245`
- 다음에 해 볼 기술: [침식 지형 (물방울 침식)](https://ai-techstudio.web.app/ai/t/i253.md) `i253`
