---
type: tutorial
title: Ein selbst gehostetes Spiel bauen
summary: Bau ein gesandboxtes Spiel-Bundle mit dem Spiel-SDK, hoste es selbst, richte das Widget mit game-src darauf und produzier den deterministischen Trace, den der Server abspielt.
---

Am Ende dieses Tutorials hast du ein echtes Caputchin-Spiel: ein JavaScript-Bundle, das im gesandboxten Iframe des Widgets läuft, mit dem Host über das Spiel-SDK spricht und einen **Trace** meldet, den der Server erneut laufen lassen kann. Anders als der [manuelle Modus](/docs/custom-game-development/manual-mode) kann ein selbst gehostetes Spiel einen [Site-Key gaten](/docs/site-keys/games), weil seine Runde abspielbar ist. Dieses Tutorial behandelt das Bauen und Hosten des Spiels; [Replay und Gating](/docs/custom-game-development/replay-and-gating) behandelt das Artefakt, das das Gate tatsächlich einschaltet.

Lies [dein eigenes Spiel betreiben](/docs/custom-game-development/overview) dafür, wo das hineinpasst. Du brauchst einen Ort, um eine statische JavaScript-Datei zu hosten (jedes CDN oder jeder statische Host), und das Widget auf deiner Seite.

## Die Form eines selbst gehosteten Spiels

Ein selbst gehostetes Spiel ist ein in sich geschlossenes JavaScript-Bundle, das:

1. Die `register`-Funktion von `@caputchin/game-sdk` importiert und eine einzelne **Spiel-Factory** registriert.
2. In den Container rendert, den das Widget der Factory gibt, mit der aufgelösten Sprache, dem Skin und der Konfiguration, die daneben übergeben werden.
3. Deterministisch unter dem Pro-Runde-Seed abläuft und das Spiel als **Trace** aufzeichnet.
4. Bei einem Sieg `bridge.pass({ trace })` mit diesem Datensatz aufruft.

Das Widget lädt dein Bundle in seinen Iframe und ruft deine Factory auf; dass die Factory aufgerufen wird, *ist* das Startsignal (es gibt kein separates Start-Event, auf das man warten müsste).

## 1. Eine Spiel-Factory registrieren

Installier das SDK und ruf `register` einmal mit deiner Factory. Du übergibst das Manifest **nicht**; der Server löst Presets auf und reicht sie der Factory.

```js
import { register } from "@caputchin/game-sdk";

register((container, bridge, ctx) => {
  // container — a DOM element inside the sandboxed iframe; render into it.
  // bridge    — push-only channel to the host (pass / error / setSize).
  // ctx       — the per-round context (seed + resolved locale/skin/config).

  const cleanup = startGame(container, bridge, ctx);

  // Return an optional cleanup function; the widget calls it on unmount.
  return cleanup;
});
```

```bash
npm install @caputchin/game-sdk
```

## 2. Den Pro-Runde-Kontext lesen

Das dritte Argument der Factory, `ctx`, trägt alles, was dein Spiel für diesen Besucher braucht:

- `ctx.seed`: der Pro-Runde-Replay-Seed. **Leite jede Zufälligkeit hieraus ab.** Null, wenn das Spiel außerhalb einer verifizierten Session läuft.
- `ctx.locale`: das aufgelöste Sprach-Objekt, `ctx.locale._lang` plus deine übersetzten String-Schlüssel.
- `ctx.skin`: das aufgelöste Skin-Objekt, `ctx.skin._theme` (immer `light` oder `dark`) plus deine Farb- und Asset-Schlüssel.
- `ctx.config`: die aufgelöste Gameplay-Konfiguration (oder null).

Das sind die **aufgelösten Presets** für diesen Besucher, von Caputchin aus deinem dashboard-definierten [Schema und Presets](/docs/custom-game-development/dashboard-schema) produziert. Du liest deine eigenen Schlüssel von ihnen ab; du löst nichts selbst auf.

Braucht dein Layout eine explizite Größe, sag es dem Host einmal nach deinem ersten Paint:

```js
bridge.setSize(360, 480);
```

## 3. Das Spiel deterministisch machen

Das ist die Regel, die ein Spiel gatebar macht: **das Spiel muss deterministisch sein, gegeben den Seed und die Eingaben des Spielers.** Ruf nicht `Math.random()` auf und lies nicht die Wall-Clock für irgendetwas, das das Ergebnis beeinflusst; leite jede Zufallswahl aus `ctx.seed` ab. Der Server lässt deine Logik unter demselben Seed und demselben aufgezeichneten Trace erneut laufen und muss zum selben Urteil kommen.

```js
// A tiny seeded PRNG; feed it ctx.seed so the server reproduces the run.
function makeRng(seed) {
  let s = hashToInt(seed);
  return () => (s = (s * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;
}
```

Zeichne die Eingaben auf, die das Ergebnis treiben (welche Ziele der Spieler traf, in Reihenfolge, mit Timing, wenn es zählt), während du spielst. Serialisier diesen Datensatz zu einem String: dieser String ist dein Trace.

## 4. Mit dem Trace bestehen

Wenn der Besucher gewinnt, reich dem Host den Trace. `pass` nimmt ein Objekt mit einem einzelnen `trace`-String:

```js
function onWin(traceString) {
  bridge.pass({ trace: traceString });
}
```

Der Trace ist **opak für die Plattform**: er ist, was auch immer für ein String dein Spiel allein definiert, sodass deine Logik, kombiniert mit dem Seed, das Ergebnis reproduziert. Das Spiel meldet hier keine Punktzahl; Scoring, falls es welches gibt, ist deine eigene In-Iframe-Angelegenheit.

Zwei weitere Brücken-Fakten:

- Das erste `pass` löst die Runde ein; eine fehlgeschlagene oder abgebrochene Runde wird signalisiert, indem man `pass` einfach **nicht aufruft** (es gibt keine `fail`-Methode auf dieser Brücke).
- `bridge.error({ code, message })` ist für ein spiel-internes Versagen (ein Asset konnte nicht laden, eine Exception), keinen Spieler-Verlust. Es bringt ein `error`-Event zum Host.

Nachdem der Host das Pass eingelöst hat, gibt er das Widget-Token frei, und dein Backend [verifiziert dieses Token](/docs/integration-guides/verify-on-your-backend) wie üblich.

## 5. Für die Sandbox bündeln

Das Widget lädt genau eine Skript-URL, also muss alles in dieser einen Datei sein. Konfigurier deinen Bundler (esbuild, rollup, vite, webpack; einer von ihnen) für eine in sich geschlossene Ausgabe:

- Assets inline als Data-URLs (Sprites, Sounds, Schriften);
- Code-Splitting deaktivieren;
- nutzt du WASM, bette es als base64 ein und instanziiere aus Bytes.

Muster, die in der Sandbox **nicht** funktionieren, weil der Iframe Opaque-Origin mit einer strikten CSP ist:

- pfad-relatives `fetch('./sprite.png')`, es gibt keinen Pfad, von dem abgerufen werden kann;
- dynamisches `import('./chunk.js')`, die zweite URL ist blockiert;
- `new Worker('./worker.js')`, starte Worker stattdessen aus Inline-`Blob`-URLs;
- externe CDN-Abrufe zur Laufzeit, `connect-src` blockiert sie.

## 6. Das Bundle hosten und das Widget darauf richten

Serviere die gebaute Datei von deinem eigenen statischen Host über `https` (Loopback-`http` ist für lokale Entwicklung erlaubt), dann richte das Widget darauf:

```html
<caputchin-game
  sitekey="cpt_pub_..."
  game-src="https://cdn.example.com/my-game/game.js"
></caputchin-game>
```

Das Widget lädt dein Bundle in seinen gesandboxten Iframe, ruft deine Factory mit dem Kontext auf und wartet auf dein `pass`. An diesem Punkt läuft und verifiziert das Spiel, aber es darf einen Key noch nicht **gaten**.

## 7. Es gatebar machen

Um dein selbst gehostetes Spiel als Verifizierungs-Gate zu nutzen, sind zwei weitere Dinge nötig, beide als Nächstes behandelt:

1. **Registrier es als Custom-Spiel** im Dashboard (eine id, die du wählst) und definier sein Feld-[Schema und Presets](/docs/custom-game-development/dashboard-schema), sodass der Kontext echtes Locale/Skin/Config trägt.
2. **Lade ein Replay-Artefakt hoch**: einen Headless-Build deiner Spiel-Logik, den der Server ausführt, um das Urteil aus Seed und Trace neu abzuleiten. Sieh dir [Replay und Gating](/docs/custom-game-development/replay-and-gating) an.

Bis das Replay-Artefakt hochgeladen ist und seine Selbstprüfung besteht, zeigt sich das Custom-Spiel als **Nicht abspielbar** und kann nicht gaten.

## Siehe auch

- [Replay und Gating](/docs/custom-game-development/replay-and-gating): das Artefakt, das das Gate einschaltet.
- [Dashboard-Schema-Referenz](/docs/custom-game-development/dashboard-schema): die Felder definieren, die der Kontext auflöst.
- [Manueller Modus](/docs/custom-game-development/manual-mode): die leichtere, nicht-gatende Alternative.
- [Auf deinem Backend verifizieren](/docs/integration-guides/verify-on-your-backend): das Token bestätigen, das ein Pass produziert.
