# AI 꾸러미 — 시드 지도 (같은 수 = 같은 지도) — Seeded procedural generation
> 지도를 만드는 모든 무작위를 시드 숫자 하나에서 뽑아, 같은 시드면 누가 어디서 만들어도 똑같은 섬이 나오고 한 자리만 달라도 전혀 다른 섬이 된다.  
> 견본: https://ai-techstudio.web.app/#t/i255

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

## 주문서

### 만들어 줘: 시드 지도 (같은 수 = 같은 지도) — Seeded procedural generation

#### 1. 목표
오늘의 지도 (날짜 = 시드)을(를) 시드 숫자 하나로 정해지게 만들어 줘 — 같은 시드면 똑같은 지도, Math.random 은 쓰지 않게. 보이는 모습은 시드 다이얼이 돌아가는 비교 화면.

#### 2. 핵심 기술 용어
- **Seeded procedural generation** — 시드로 정해지는 절차 생성
- **Seeded PRNG (mulberry32)** — 시드 있는 난수 — 같은 시드 = 같은 수열
- **Integer hash noise** — 좌표 · 시드를 섞은 정수 해시로 만든 잡음
- **Deterministic output** — 같은 입력이면 늘 같은 결과

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

#### 4. 조건
- 지도 만드는 코드 안에서 Math.random 을 한 번도 쓰지 않는다 — 모든 무작위는 시드 난수 · 해시에서
- 난수를 뽑는 순서가 바뀌면 결과도 바뀐다 — 생성 순서를 고정하고, 꾸미기용 무작위는 다른 난수기로
- 시드는 화면에 보이게 (네 자리) 하고, 복사 · 입력할 수 있게
- 만든 지도는 시드를 열쇠로 캐시 — 같은 시드를 다시 만들지 않게

#### 5. 완성 기준 (이게 보이면 성공)
- 친구 A · B 에 같은 시드를 넣으면 두 지도가 픽셀까지 똑같고 「=」 표시가 뜬다
- 시드 한 자리만 바꾸면 전혀 다른 섬이 되고 「≠」 표시가 뜬다
- 「시드 고정」에 숫자를 넣고 새로고침해도 같은 섬이 나온다
- 다른 기기 · 브라우저에서 같은 시드로 같은 섬이 나온다

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

## 원리
- 컴퓨터 난수는 사실 「씨앗(시드)에서 시작해 정해진 계산을 되풀이」한 수열이다. 시드가 같으면 수열도 같다.
- 지도의 잡음은 좌표와 시드를 정수 곱셈 · XOR 로 섞는 해시(hash2)로 만든다 — 같은 (x, y, 시드)면 늘 같은 값.
- 잡음을 읽는 시작 위치도 시드로 정한다 (ox = hash2(seed, 1) × 200) — 시드가 바뀌면 잡음의 전혀 다른 곳을 읽는다.
- 그래서 시드 1234 와 1235 는 숫자로는 가깝지만 지도는 전혀 다르다. 친구에게 네 자리 숫자만 알려 주면 같은 판을 공유한다.

## 핵심 코드 — 시드 난수 · 정수 해시 · 시드로 정해지는 지형
(발췌: demos/demosMapA.ts mulberry · hash2 · makeTerrain 을 정리)
```ts
/** 시드 → 0~1 난수 수열 (mulberry32 꼴) */
function mulberry(seed: number): () => number {
  let a = (seed * 2654435761) >>> 0 || 1;
  return () => {
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
/** (i, j, 시드) → 0~1. 같은 입력이면 어느 기기에서도 같은 값 */
function hash2(i: number, j: number, s = 0): number {
  let h = (Math.imul(i | 0, 0x27d4eb2d) ^ Math.imul((j | 0) + 0x165667b1, 0x85ebca6b) ^ Math.imul((s | 0) + 0x3c6ef372, 0xc2b2ae35)) | 0;
  h = Math.imul(h ^ (h >>> 15), 0x2c1b3c6d);
  h = Math.imul(h ^ (h >>> 12), 0x297a2d39);
  h ^= h >>> 15;
  return (h >>> 0) / 4294967296;
}
/** 지형 높이: 시드로 잡음 읽는 자리를 옮기고, 가운데가 높은 섬 모양을 뺀다 */
function heights(W: number, H: number, seed: number, fbm: (x: number, y: number, s: number) => number) {
  const h = new Float32Array(W * H), sc = 3.4 / H;
  const ox = hash2(seed, 1) * 200, oy = hash2(seed, 2) * 200;   // 시드마다 잡음의 다른 곳
  for (let y = 0; y < H; y++)
    for (let x = 0; x < W; x++) {
      const nx = (x / (W - 1)) * 2 - 1, ny = (y / (H - 1)) * 2 - 1;
      h[y * W + x] = fbm(x * sc + ox, y * sc + oy, seed) - 0.62 * (nx * nx * 0.9 + ny * ny);
    }
  return h;
}
const cache = new Map<number, Float32Array>();                    // 시드 → 지도 (8장까지)
```

## 흔한 실수 · 확인 목록
- [ ] **생성 중 한 군데라도 Math.random 을 쓰면 같은 시드가 다른 지도를 만든다** — 지도 생성 함수에는 시드 난수만 넘기고, 연출(반짝이 · 흔들림)은 따로 Math.random 으로.
- [ ] **난수를 뽑는 순서가 바뀌어 예전 시드 지도가 달라졌다** — 생성 단계 순서를 바꾸면 수열이 밀린다. 단계마다 hash2(seed, 단계번호) 로 따로 시드를 나누면 서로 영향이 없다.
- [ ] **시드 0 이 이상한 지도를 만든다** — 곱셈 해시에서 0 은 0 이 되기 쉽다. 견본은 >>> 0 || 1 로 0 을 피한다.
- [ ] **바다 높이를 고정값으로 두면 시드마다 육지 넓이가 들쭉날쭉하다** — 높이를 정렬해 아래 (1 − 육지 비율) 지점을 바다 높이로 — 어느 시드든 육지 비율이 같다.

## 완성 기준 체크리스트
- [ ] 친구 A · B 에 같은 시드를 넣으면 두 지도가 픽셀까지 똑같고 「=」 표시가 뜬다
- [ ] 시드 한 자리만 바꾸면 전혀 다른 섬이 되고 「≠」 표시가 뜬다
- [ ] 「시드 고정」에 숫자를 넣고 새로고침해도 같은 섬이 나온다
- [ ] 다른 기기 · 브라우저에서 같은 시드로 같은 섬이 나온다

## 이 기술 정보
- id: `i255` · 분류: 지도 · 길찾기 › 지도 생성 (절차) · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 128 × 80 지도 한 장 만들기는 한 번만, 만든 그림은 시드별로 8장까지 캐시해 둔다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i255
- 쓰면 좋을 때: 「오늘의 퍼즐」 · 주간 도전처럼 모두가 같은 판을 받아야 할 때 / 버그가 난 판을 다시 만들어 보려 할 때 (시드만 기록)
- 쓰지 말 때: 판마다 저장할 내용이 작으면 그냥 판 자체를 저장해도 된다 / 실수(부동소수) 계산이 기기마다 조금씩 다를 수 있는 아주 긴 시뮬레이션 — 결과 자체를 저장한다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### mulberry — `src/demos/demosMapA.ts:59`
```ts
function mulberry(seed: number): () => number {
  let a = (seed * 2654435761) >>> 0 || 1;
  return () => {
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
```

### makeTerrain — `src/demos/demosMapA.ts:204`
```ts
function makeTerrain(W: number, H: number, seed: number, land = 0.45, isl = 1, oct = 5): Terrain {
  const h = new Float32Array(W * H);
  const raw = new Float32Array(W * H);
  const m = new Float32Array(W * H);
  const sc = 3.4 / H;
  const ox = hash2(seed, 1) * 200;
  const oy = hash2(seed, 2) * 200;
  for (let y = 0; y < H; y++) {
    for (let x = 0; x < W; x++) {
      const i = y * W + x;
      const fx = x * sc + ox;
      const fy = y * sc + oy;
      const nx = (x / (W - 1)) * 2 - 1;
      const ny = (y / (H - 1)) * 2 - 1;
      const d2 = nx * nx * 0.9 + ny * ny;
      const n = fbm(fx, fy, seed, oct) + (fbm(fx * 2.3 + 9, fy * 2.3 + 3, seed + 5, 3) - 0.5) * 0.12;
      raw[i] = n;
      h[i] = n - isl * 0.62 * d2;
      m[i] = fbm(fx * 0.8 + 40, fy * 0.8 + 20, seed + 77, 3);
    }
  }
  const s = Array.from(h).sort((a, b) => a - b);
  return { W, H, h, raw, m, sea: s[Math.floor((1 - land) * (s.length - 1))]!, lo: s[0]!, hi: s[s.length - 1]! };
}
```

## 관련 기술
- 먼저 알면 좋은 기술: [잡음 섬 지도 (높이 → 생물군)](https://ai-techstudio.web.app/ai/t/i245.md) `i245`
- 다음에 해 볼 기술: [파동 함수 붕괴 (WFC) 타일 맞춤](https://ai-techstudio.web.app/ai/t/i249.md) `i249` · [포아송 원판 흩뿌리기](https://ai-techstudio.web.app/ai/t/i254.md) `i254`
