---
type: reference
title: 리플레이 계약
summary: 게임을 재생 가능하게 만드는 필수 run/seed/verdict 계약, run 함수가 무엇을 받고 반환하는지, 시드가 트레이스를 한 라운드에 어떻게 묶는지, 그리고 서버가 그것을 어떻게 리플레이하는지.
---

리플레이 계약은 **재생 가능한** 게임이 플랫폼과 합의하는 하나의 필수 표면입니다. 그것은 `@caputchin/replay-contract`로 공개됩니다: 일부러 작고 의존성 없는 패키지(봉인된 리플레이 아이솔레이트에 로드되니, 아무것도 끌어들이지 않습니다). 당신의 게임에 대한 그 밖의 모든 것, 엔진, 렌더러, 트레이스 형식은 당신의 것입니다.

적합한 `run`을 내는 게임은 **재생 가능**하고 따라서 사이트 키를 **게이트**할 수 있습니다; 그것 없는 게임은 여전히 임베드 가능하지만, 오직 UX로만입니다(**재생 불가**로 보입니다). 그 구분은 [마켓플레이스에 게임 출시하기](/docs/marketplace-game-development/overview#replayable-vs-not-replayable)를 보세요.

## `run` 함수

재생 가능한 게임은 `run`이라는 함수 하나를 export하는 JS 또는 WASM 모듈을 냅니다:

```ts
run(seed, config, trace) -> { passed, score, durationMs }
```

```ts
type RunFn<C = unknown> = (
  seed: Seed,
  config: C | null,
  trace: Uint8Array | string,
) => Verdict | Promise<Verdict>
```

| 인자 | 소스 | 의미 |
|---|---|---|
| `seed` | 서버 | 라운드별 [시드](#the-seed). 라운드의 서버 소유 설정. |
| `config` | 서버 | 게임플레이 구성, **nullable이고 불투명**: 플랫폼은 그것을 결코 검사하지 않으며, 리플레이 시 서버 측에서 다시 해소되지 결코 클라이언트가 주장하지 않음. `null`은 "run 자체의 기본값을 쓰라"는 뜻. MVP에서는 서버가 `null`을 넘김; 사이트별 구성은 미뤄진 단계. |
| `trace` | 클라이언트 | 플레이어의 클라이언트가 낸, 이 함수만이 해석하는 불투명 블롭. 플랫폼은 그것을 결코 파싱하거나 타입 지정하지 않음; 그것은 날 바이트나 문자열이며, 크기 상한과 아이솔레이트 CPU 제한으로만 한정됨. |

인자 순서가 신뢰 모델을 인코딩합니다: `seed`와 `config`는 라운드의 서버 설정입니다; `trace`는 플레이어의 입력입니다. **게이트에 영향을 주는 값(통과 임계값, 목숨)은 `config`에서 읽으면 안전하지만 클라이언트 `trace`에서 읽으면 우회가 됩니다.**

모듈은 함수를 `run`이라는 이름(상수 `RUN_EXPORT_NAME`)으로 export해야 합니다. 리플레이 호스트가 그것을 호출하고 결과를 기다립니다(WASM 인스턴스화는 첫 호출에서 비동기입니다).

## 시드 [#the-seed]

```ts
type Seed = readonly [number, number, number, number]   // 128 bits, 4 u32 words, MSW first
```

시드는 `SHA-256(sessionId : gameId : roundIndex)`의 하위 128비트로, 네 부호 없는 32비트 워드로, 최상위 워드 먼저 실립니다. 패키지의 `deriveSeed(sessionId, gameId, roundIndex)`가 그것을 계산합니다; 서버는 라운드를 발급할 때와 리플레이할 때 다시, 둘 다 그것을 도출하니, 그것은 결코 신뢰된 클라이언트 입력으로 와이어를 타지 않습니다.

결정적으로, 시드는 **트레이스를 한 세션, 게임, 라운드에 묶습니다**: 다른 시드 아래에서 외부 또는 이전 트레이스를 리플레이하면 `passed: false`를 냅니다. 그 묶음이 트레이스 주입과 리플레이 공격을 방어하는 방법이니, 라이브 플레이와 서버 리플레이가 일치하도록 모든 무작위성을 그것에서 시드해야 합니다.

## 판정

```ts
interface Verdict {
  readonly passed: boolean      // drives the verification decision
  readonly score: number        // game-defined, any finite number
  readonly durationMs: number   // finite, non-negative
}
```

`passed`는 captcha 결정을 모는 유일한 필드입니다; `score`와 `durationMs`는 발급된 토큰(그리고 미래의 점수판)에 실립니다. 패키지의 `parseVerdict(value)`는 신뢰되지 않는 반환 값을 검증하고 **잘못된 결과를 통과 라운드가 아니라 거부된 라운드로 다룹니다**, 그래서 던지거나 쓰레기를 반환하는 `run`은 닫힌 채 실패합니다.

## 결정론이 요건 전부입니다 [#determinism-is-the-whole-requirement]

계약의 하나의 단단한 규칙: `run`은 **두 런타임에 걸쳐 순수하고 결정론적**이어야 합니다, 플레이어의 브라우저와 서버 아이솔레이트. 동일한 `(seed, config, trace)`는 둘 다에서 동일한 판정을 내야 합니다. 이것을 깨는 흔한 방법들, 모두 공개 자체 확인이 `run-not-conforming`으로 잡습니다:

- `Date.now()`, `performance.now()`, `Math.random()`, 또는 다른 비결정론적 글로벌 읽기;
- 리플레이 아이솔레이트가 제공하지 않는 외부 상태(DOM, 네트워크, 스토리지) 읽기;
- 런타임 사이에 다른 부동소수점 연산에 의존하기.

당신이 결정론을 어떻게 달성하는지(고정소수점, WASM 스펙 부동소수점, 또는 IEEE-754에 중립화 심)는 당신의 선택입니다; 플랫폼은 리플레이만 호스팅합니다. [엔진 키트](/docs/marketplace-game-development/engine-kit)는 당신이 직접 굴리고 싶지 않으면 기성 결정론적 프리미티브를 제공합니다.

## 서버가 그것을 쓰는 방식

```mermaid
sequenceDiagram
  participant P as 플레이어 브라우저
  participant S as Caputchin 서버
  participant R as 리플레이 아이솔레이트
  S->>S: deriveSeed(sessionId, gameId, round)
  S->>P: 시드와 함께 라운드 발급
  P->>P: 플레이, 트레이스 기록
  P->>S: 통과 시 트레이스 제출
  S->>S: 같은 시드 다시 도출
  S->>R: run(seed, config, trace)
  R-->>S: 판정 { passed, score, durationMs }
  S->>S: parseVerdict, 결정
```

아이솔레이트가 로드하는 산출물은 당신의 `run` 번들이며, 라이브 `entry`이거나 매니페스트의 전용 [`run` 산출물](/docs/marketplace-game-development/the-caputchin-json-manifest#the-run-artifact)입니다.

## 함께 보기

- [engine-kit](/docs/marketplace-game-development/engine-kit): 평이한 리듀서에서 적합한 `run`을 내기, 결정론은 당신을 위해 처리됨.
- [caputchin.json 매니페스트](/docs/marketplace-game-development/the-caputchin-json-manifest#the-run-artifact): `run` 산출물과 그 모듈 선언하기.
- [마켓플레이스 게임 빌드하기](/docs/marketplace-game-development/build-a-marketplace-game): 이 계약이 리플레이하는 트레이스 기록하기.
- [공개 오류 레퍼런스](/docs/marketplace-game-development/publish-failed-reference): `run-not-conforming` 자체 확인 결과.
