# Portals

What the same bundle does on CrazyGames, Poki and Playgama, host by host, and the options each one takes.

One bundle runs on filudi.com and on the three portals. At `init()` the SDK detects the host from the
portal's global or hostname, loads the portal's own script when the page does not have it, and maps every
call. Nothing portal-specific ships in your bundle. Each portal has its own listing process, outside Filudi;
this page covers what your game does once it runs there.

## Side by side

|                    | filudi.com               | CrazyGames                             | Poki                                        | Playgama                   |
| ------------------ | ------------------------ | -------------------------------------- | ------------------------------------------- | -------------------------- |
| Detection          | The handshake in a frame | `CrazyGames.SDK` or `*.crazygames.com` | `PokiSDK` or `*.poki.com`, `*.poki-gdn.com` | `window.bridge`            |
| Ads                | None yet                 | Rewarded and midgame                   | `rewardedBreak`, `commercialBreak`          | Rewarded and interstitial  |
| Saves              | The API                  | Data module, else `localStorage`       | `localStorage`                              | `bridge.storage`           |
| Scores             | Signed, to your boards   | `user.submitScore` with your key       | None                                        | `bridge.leaderboards`      |
| `top()` and `me()` | Work                     | `unavailable`                          | `unavailable`                               | Work with `in_game` boards |
| Player             | Guest or account         | The portal's user, else a random id    | Guest                                       | The portal's player        |
| Locale             | The site's               | `user.systemInfo.locale`               | `navigator.language`                        | `bridge.platform.language` |
| Events             | The API                  | Dropped unless `events: 'always'`      | Same                                        | Same                       |
| Extras             |                          | `sdk.host.crazygames`                  | `sdk.host.poki`                             | `sdk.host.playgama`        |

## CrazyGames

The SDK loads `https://sdk.crazygames.com/crazygames-sdk-v3.js` when the page does not have it, and falls
back to the mock when the script fails, takes longer than `timeoutMs` or reports the `disabled`
environment. `ready()` is `game.loadingStop`, the gameplay brackets are `gameplayStart` and
`gameplayStop`, `happyTime()` is `happytime`. Ads go through `ad.requestAd('rewarded' | 'midgame')`;
`ad.hasAdblock` turns `available()` off. The portal paces interstitials, so the SDK adds no interval there.

Saves go to the data module under one key, `filudi:save`, holding `{ v, d }`; with the module off they stay
in `localStorage`. Scores reach the portal board only with the key from the CrazyGames developer portal,
encrypted with AES-GCM; keep it in a build variable.

<CodePanel title="CrazyGames options">
  <Fragment slot="javascript">
    ```js
    await sdk.init({
      gameId: 'gloop',
      version: '1.4.0',
      crazygames: { leaderboardKey: import.meta.env.VITE_CG_LEADERBOARD_KEY ?? '' },
    });
    sdk.host.crazygames?.onMuteChange((muted) => audio.mute(muted));
    fullscreenButton.hidden = result.host === 'crazygames';   // the portal draws its own
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    const result = await sdk.init({
      gameId: 'gloop',
      version: '1.4.0',
      crazygames: { leaderboardKey: import.meta.env.VITE_CG_LEADERBOARD_KEY ?? '' },
    });
    sdk.host.crazygames?.onMuteChange((muted: boolean) => audio.mute(muted));
    fullscreenButton.hidden = result.host === 'crazygames';   // the portal draws its own
    ```

  </Fragment>
</CodePanel>

The portal's mute setting, invite links and game context are [host extras](/docs/sdk/host/). `device()`
reads `user.systemInfo.device.type`.

## Poki

The SDK loads `https://game-cdn.poki.com/scripts/v2/poki-sdk.js` when the page has no `window.PokiSDK`,
then calls `PokiSDK.init()`. A script that fails, takes longer than `timeoutMs`, or an `init()` that rejects
(Poki does when an ad blocker is on) puts the game on the mock with ads off. `ready()` is
`gameLoadingFinished`. Interstitials are `commercialBreak`, always `completed`: Poki says nothing about a
skip. Rewarded ads are `rewardedBreak`: `completed` on `true`, `skipped` on `false`.

Poki keeps no save, leaderboard or player: saves stay in `localStorage`, boards answer `unavailable`, the
player is a guest. `shareableURL(params)` and `getURLParam(key)` are the host extras.

<CodePanel title="Poki share link">
  <Fragment slot="javascript">
    ```js
    const link = (await sdk.host.poki?.shareableURL({ seed: today })) ?? location.href;
    const seed = sdk.host.poki?.getURLParam('seed') ?? new URLSearchParams(location.search).get('seed');
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    const link: string = (await sdk.host.poki?.shareableURL({ seed: today })) ?? location.href;
    const seed: string | null = sdk.host.poki?.getURLParam('seed') ?? new URLSearchParams(location.search).get('seed');
    ```

  </Fragment>
</CodePanel>

## Playgama

The adapter runs when the page has Playgama Bridge (`window.bridge`) and calls `bridge.initialize()` if the
page has not, within `timeoutMs`. Ads are `advertisement.showRewarded()` and `showInterstitial()`, read
through the state events: `opened` fires `start`, `rewarded` then `closed` is `completed`, `closed` alone is
`skipped`, `failed` is `no-fill`.

Saves go to `bridge.storage` under `filudi:save` as `{ v, d }`. Scores go to `bridge.leaderboards.setScore`
unless the board type is `not_available`, and `top()` reads `getEntries` when the type is `in_game`.
`visibility_state_changed` fires `pause` and `resume`; the locale is `bridge.platform.language`; `device()`
reads `bridge.device.type` (a TV falls back to the SDK's own guess). The audio setting is
`sdk.host.playgama?.onAudioChange(fn)`, with `enabled` the opposite of muted.

<CodePanel title="Playgama audio">
  <Fragment slot="javascript">
    ```js
    sdk.host.playgama?.onAudioChange((enabled) => audio.mute(!enabled));
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    sdk.host.playgama?.onAudioChange((enabled: boolean) => audio.mute(!enabled));
    ```

  </Fragment>
</CodePanel>

## Testing without the portals

The [playground](/docs/games/first-bundle/) installs each portal's global in the frame before your bundle
runs, with the shapes the adapters read, and lets you set the ad outcome, the environment, the data module,
the leaderboard type, the player and the locale. What it cannot do: show a real ad, decrypt a real
CrazyGames key (it checks the encryption against the key you paste), or stand for a portal's review. Run the
game on each host there before every release, as the [release checklist](/docs/games/release-checklist/)
says.

## Distribution

A game runs on filudi.com by default. The console's distribution switch opens it to approved third-party
portals on the Filudi catalog once it has a live version; the console shows which sites list it. That is a
different thing from CrazyGames, Poki and Playgama: those portals host your bundle themselves and you submit
it to them.
