# Your first bundle

From the sample game in the playground to a zip uploaded in the console, previewed, and sent for review.

A bundle is a folder with `index.html` at its root, zipped. This page takes the playground's sample game
through every step; your own game follows the same path.

## The playground

The playground loads a bundle in an iframe, plays each host around it and logs every SDK call and host
message. Open [developers.filudi.com/playground/](https://developers.filudi.com/playground/): nothing to
install. If you have the Filudi repository, the same app runs locally from `apps/sdk-playground`.

<CodePanel title="Run the playground from the repository">
  <Fragment slot="javascript">
    ```bash
    pnpm install
    pnpm --filter @filudi/game-sdk build         # the sample game loads dist/filudi-sdk.iife.js
    pnpm --filter @filudi/sdk-playground dev     # http://localhost:5173
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```bash
    pnpm install
    pnpm --filter @filudi/game-sdk build         # the sample game loads dist/filudi-sdk.iife.js
    pnpm --filter @filudi/sdk-playground dev     # http://localhost:5173
    ```

  </Fragment>
</CodePanel>

Pick "Sample game" and the `filudi` host, press Load. The log shows the handshake: `filudi:ready` from the
bundle, `filudi:init` from the host with a game token that starts with `fg1.`, then `filudi:loaded` when
the game calls `ready()`. Tap the canvas: `filudi:gameplay { running: true }`. Fall: the score goes out,
the board comes back, every third fall asks for an interstitial and the host answers with the result you
set. Tick "Mock API" so saves and scores hit an in-memory API and show as requests with their bearer.
Untick it to use a real API: type its origin in "API URL", or open the playground with `?api=`:
`/playground/?host=filudi&mockApi=0&api=https://api.filudi.com` for production,
`api=http://localhost:8080` for the API you run. That API must list the playground's origin in its
`HUB_URLS`, and the game slug must exist on the site, else the log says `game token refused`.

The other hosts install the portal's global in the frame before the bundle runs: `CrazyGames.SDK`,
`PokiSDK`, Playgama's `bridge`. Each call the game makes is a `game→host` line, each answer a `host→game`
line. Change the ad outcome, the data module, the leaderboard type, and watch what the game does.

## The sample game

The sample game is two files, [`index.html`](https://developers.filudi.com/playground/sample-game/index.html)
and [`main.js`](https://developers.filudi.com/playground/sample-game/main.js) (`apps/sdk-playground/public/sample-game/`
in the repository). `main.js` calls every SDK method at least once; it is the shortest complete
integration. Read it next to the [SDK overview](/docs/sdk/).

<CodePanel title="The sample game's boot, in short">
  <Fragment slot="javascript">
    ```js
    const sdk = window.FiludiSDK;

    async function boot() {
      await sdk.init({ gameId: 'infinijump', version: '1.0.0' });
      corner = { nickname: sdk.player.get().nickname, locale: sdk.i18n.locale() };
      applySave(await sdk.save.get());
      sdk.save.on('replaced', applySave);
      sdk.ads.on('start', () => (muted = true));
      sdk.ads.on('end', () => (muted = false));
      requestAnimationFrame(frame);        // the first frame calls sdk.ready()
    }
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    import { sdk } from '@filudi/game-sdk';

    async function boot(): Promise<void> {
      await sdk.init({ gameId: 'infinijump', version: '1.0.0' });
      corner = { nickname: sdk.player.get().nickname, locale: sdk.i18n.locale() };
      applySave(await sdk.save.get<{ best: number }>());
      sdk.save.on('replaced', applySave);
      sdk.ads.on('start', () => { muted = true; });
      sdk.ads.on('end', () => { muted = false; });
      requestAnimationFrame(frame);        // the first frame calls sdk.ready()
    }
    ```

  </Fragment>
</CodePanel>

## Your own game in the playground

Run your dev server, choose "Bundle URL", paste `http://localhost:5174/index.html`, pick a host, press
Load. The hosted playground frames a `localhost` URL from its https page: browsers treat `localhost` as
secure. Your server must let the playground's origin frame it, `https://developers.filudi.com` (or
`http://localhost:5173` when you run the playground from the repository): no `X-Frame-Options: DENY`,
and `frame-ancestors` must list it if you send a CSP. The portal hosts fetch your `index.html`, so the
server answers that origin with CORS. Vite's default CORS allows `localhost` origins only: add
`server: { cors: { origin: ['https://developers.filudi.com'] } }` to its config while you test.

Add `<script src="https://developers.filudi.com/playground/sdk-trace.js"></script>` after the SDK script
while you work (`http://localhost:5173/sdk-trace.js` for the local playground): the log then shows each
SDK call. Remove it before the build.

Tick "Production CSP" on the `filudi` host. The playground's server then sends the policy filudi.com
applies to bundles it serves itself, and the log gives you the header to send from your own server. Each
violation shows as a `CSP violation` line. An inline script or a call to another host works without it
and fails in production.

## The zip

Build, then zip the content of the build folder, not the folder itself. `index.html` must be at the root
of the archive. Leave the source maps out unless you want them public.

<CodePanel title="Zip the build">
  <Fragment slot="javascript">
    ```bash
    npm run build
    cd dist && zip -r ../gloop-1.4.0.zip . -x '*.map' && cd ..
    unzip -l gloop-1.4.0.zip | head -5          # index.html first, at the root
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```bash
    pnpm build
    cd dist && zip -r ../gloop-1.4.0.zip . -x '*.map' && cd ..
    unzip -l gloop-1.4.0.zip | head -5          # index.html first, at the root
    ```

  </Fragment>
</CodePanel>

Limits: 200 MB for the zip, 500 MB unpacked, 10,000 files, 64 MB per file, no `..` in a path, no symbolic
link, no encrypted entry. The upload refuses a zip that breaks one and names the entry.

## Upload

In the console, open the game, Versions, "Upload a version". The version string follows semantic
versioning (`1.4.0`, `2.0.0-beta.1`) and is unique for the game. Pass the same string to `init({ version })`
in the build so events carry it. The browser uploads the zip straight to the files service; the console
then finalizes: the API unpacks the zip under `games/<slug>/<version>/`, checks `index.html`, counts files
and bytes. The version is a draft.

## Preview

"Preview" opens a signed link, valid one hour, in a new tab. It serves the draft from the preview host
under the same CSP as production, at the top level. The SDK is on its mock there: check loading, layout
and play. Saves and scores need the frame on filudi.com; the playground covers them.

## Request review

When the draft is right, press "Request review" on the version. Staff gets an email, plays the preview and
activates it. A pending game needs its approval first; the overview says which step is next. Once the
version is live, open the game on filudi.com and play it once: saves, the leaderboard and your statistics
now run against production.

## Next

[Game lifecycle on filudi.com](/docs/games/lifecycle/) explains what happened in that frame. The
[release checklist](/docs/games/release-checklist/) is the list to run before every zip.
