npm package: @farcade/game-sdk
Description: The Farcade SDK enables game developers to integrate their HTML5 games with the Remix platform through a simple messaging interface.
Latest version: 0.3.0

# Farcade SDK

The Farcade SDK enables game developers to integrate their HTML5 games with the Remix platform through a simple messaging interface.

## Installation

Install the SDK using your preferred package manager:

```sh
npm install @farcade/game-sdk
# or 
bun install @farcade/game-sdk
# or
yarn add @farcade/game-sdk
# or
pnpm add @farcade/game-sdk
```

When you upload game code to Remix, a script tag to reference the SDK will automatically be added to your game code. The SDK will be accessible from `window.FarcadeSDK`.

## Example Implementation

```typescript
import type { GameState, Player, FarcadeSDK } from '@farcade/game-sdk'

class MyGame {
  sdk: FarcadeSDK
  player: Player
  gameState: GameState | null = null
  isMuted: boolean = true

  // some example values that correlate to items purchased by boosting
  canSaveGame: boolean = false
  hasSuperSkin: boolean = false

  constructor() {
    // attach the sdk to the Game class to we can reference it easily
    this.sdk = window.FarcadeSDK

    // Setup event listeners
    this.setupEventListeners()

    // Initialize your game
    this.initialize()
  }

  private async setupEventListeners() {
    // Listen for play again events
    this.sdk.onPlayAgain(() => {
      this.resetGame()
    })

    // Listen for mute state changes
    this.sdk.onToggleMute((data) => {
      this.isMuted = data.isMuted
    })

    // Listen for purchase completions (optional)
    this.sdk.onPurchaseComplete(() => {
      this.checkPurchasedItems()
    })
  }

  private async initialize() {
    // wait for the sdk to get game information from Remix
    await this.sdk.ready()

    // get information the game uses from the SDK
    this.player = this.sdk.player
    this.gameState = this.sdk.gameState

    // check which items the user has already purchased
    this.checkPurchasedItems()
    
    //... start the game logic
  }

  private checkPurchasedItems() {
    // check the identifier for each item to see if the user has purchased it

    if (this.sdk.hasItem('save-game')) {
      this.canSaveGame = true
    }

    if (this.sdk.hasItem('super-skin')) {
      this.hasSuperSkin = true
    }
  }

  private async purchaseItem(itemId: string) {
    // trigger the boost UI and then check for new items after
    await this.sdk.purchase({ item: itemId })
    this.checkPurchasedItems()
  }

  private gameOver(finalScore: number) {
    //
    this.sdk.singlePlayer.actions.gameOver({ score: finalScore })
  }

  private saveGame() {
    // return unless the user has purchased the 'save-game' item
    // see checkPurchasedItems
    if (!this.canSaveGame) return

    // push game state updates to save progress for the user
    const gameState: GameState = this.getCurrentGameState();
    this.sdk.singlePlayer.actions.saveGameState({ gameState })
  }

  private triggerHapticFeedback() {
    this.sdk.hapticFeedback()
  }
}
```


## API Reference

### Properties and Getters

#### `sdk.ready(): Promise<GameInfo>`

This function will return when the SDK has recieved all the game information from Remix. You should await the call to this function before getting info from the SDK (sdk.player, sdk.gameState, etc)

#### `sdk.isReady: boolean`

Check if the SDK has received game info and is ready. More often, you will just await the call to `sdk.ready()`, but you may use this value if you prefer.

#### `sdk.player: Player | undefined`

Get the current player object.

#### `sdk.players: Player[] | undefined`

Get the list of all players in the game. This is useful for multiplayer games. In multiplayer games, it is essential that you use the `players` array so you can report scores with the associated player ids and notify players it is there turn.

#### `sdk.gameState: GameState | null | undefined`

Get the current game state. This will be `null` when there is no existing GameState for this game and user.

#### `sdk.purchasedItems: string[]`

Get the list of item IDs that the current player has purchased. This is a read-only property that reflects the `purchasedItems` array from the current player's `GameInfo`.

#### `sdk.hasItem(item: string): boolean`

Check if the current player has purchased a specific item.

Parameters:
- `item`: The item identifier to check (string)

Returns:
- `true` if the player has purchased the item, `false` otherwise

#### `sdk.gameInfo: GameInfo | undefined`

Get the current `GameInfo` object. This is all the data passed into the game from Remix. It is recommended that you use the other functions to access the specific data you need instead of pulling data out of this large object.

#### `sdk.hapticFeedback()`

Call this method to trigger haptic feedback on supported devices. Use this for important game events like collisions, achievements, or other significant player interactions.

#### Best Practices for Haptic Feedback

- Use haptic feedback sparingly to avoid overwhelming the player
- Reserve haptic feedback for meaningful interactions and achievements
- Consider using it for:
  - Collisions with obstacles
  - Collecting power-ups
  - Completing levels
  - Achieving high scores
  - Important game events

#### `sdk.purchase({ item: string }) => Promise<{ success: boolean }>`

Call this function to initiate a purchase for an in-game item. This method sends a purchase request to the Remix platform and returns a promise that resolves with the purchase result. Make sure the `item` identifier matches an item you have added to your game using the [Remix](https://remix.gg) web app.

Parameters:
- `item`: The identifier of the item to purchase (string)

Returns:
- A promise that resolves with `{ success: boolean }` indicating whether the purchase was successful

The purchase flow works as follows:
1. Call `purchase({ item: 'item-id' })` to initiate the purchase
2. The platform handles the purchase transaction (payment processing, etc.)
3. The promise resolves with `{ success: true }` if the purchase succeeds, or `{ success: false }` if it fails
4. You can also listen for `purchase_complete` events using `sdk.onPurchaseComplete()` for additional handling

**Important**: The `purchasedItems` array in `GameInfo` will be automatically updated after a successful purchase, and you can check item ownership using `sdk.hasItem(item)` or `sdk.purchasedItems`.


### Single Player Actions

#### `sdk.singlePlayer.actions.gameOver({ score: number })`

Call this method when the game is over to report the final score.

Parameters:

- `score`: The player's final score (number)

#### `sdk.singlePlayer.actions.saveGameState({ gameState: Record<string, unknown> })`

Call this method when you wish to persist game state for the user. This will enable users to save their progress in a
game between play sessions.


### Multi-Player Actions

Muliplayer has the same actions as singlePlayer with 2 updates and 1 addition. It is important to note that YOU
MUST use the `gameInfo` data from the response to `ready()` in order to accurately report scores with player ids when the game ends.

#### `sdk.multiplayer.actions.gameOver({ scores: { playerId: string; score: number }[] })`

Call this method with the game is over to report the final scores.

Parameters:

- `scores`: The playerId and score for each player in the game.

#### `sdk.multiplayer.actions.saveGameState({ gameState: Record<string, unknown>; alertUserIds: string[] })`

Call this method when a player action should update the shared game state for all players. If your game has a concept of turns as most do, you must include the `alertUserIds` list with the id(s) of the player(s) that should be alerted it is their turn.

Parameters:

- `gameState`: A Record object containing your game state. The shape of the game state is open to the game creator's discretion.
- `alertUserIds`: A list of player ids to alert on this game state update. Typically this will be a list of length 1 containing the id of the player who is next to act, but different games may need to alert multiple players.

#### `sdk.multiplayer.actions.refuteGameState({ gameStateId: string })`

Call this method when your game client has determined an incoming game state is invalid. It is not strictly required that your game validates game state updates, but it should. Putting game state validation logic in your game client means that bad actors attempting to push unfair updates are invalidated by good actors running valid implementations of your game.

Parameters:

- `gameStateId`: The id from the `game_state_updated` event that the game client is refutting.

#### `sdk.multiplayer.actions.purchase({ item: string }) => Promise<{ success: boolean }>`

Same as the single player `purchase` method. Call this method to initiate a purchase for an in-game item in multiplayer games. See the single player purchase documentation above for details.


Here is an example function that handles update and refute game state for a Chess game:

```javascript
  handleGameStateUpdate({ gameState }) {
    if (!gameState) return;

    const { id, gameState: { moves } } = gameState;

    try {
      this.chess.reset();
      if (moves?.length > 0) {
        for (const move of moves) {
          this.chess.move(move);
        }
      }
      this.renderBoard();
      this.updateStatus();
      this.updateMoveHistory();
      this.updateCapturedPieces();
    } catch (error) {
      // game state is invalid, refuting this game state update
      this.sdk.multiplayer.actions.refuteGameState({ gameStateId: id });
    }
  }
```

### Events

The SDK provides both generic event listeners and specific typed listener methods. We recommend using the specific listener methods for better type safety and clarity.

#### Event Listeners

##### `sdk.onPlayAgain(callback: () => void)`

Register a callback function that is called when the player wants to play again.

##### `sdk.onToggleMute(callback: (data: { isMuted: boolean }) => void)`

Register a callback function that is called when receiving mute state changes.

Parameters:
- `callback`: Function that receives `{ isMuted: boolean }` indicating the current mute state

##### `sdk.onGameStateUpdated(callback: (data: { id: string, gameState: Record<string, unknown> } | null) => void)`

Register a callback function that is called when there is an update to the game state. This is essential for multiplayer games to receive state updates from other players.

Parameters:
- `callback`: Function that receives the game state update data, or `null` if the game state is cleared

##### `sdk.onGameInfo(callback: (data: GameInfo) => void)`

Register a callback function that is called when game information is received. This is typically called automatically when the game initializes, but you can listen for updates.

Parameters:
- `callback`: Function that receives the `GameInfo` object

##### `sdk.onPurchaseComplete(callback: (data: { success: boolean }) => void)`

Register a callback function that is called when a purchase completes. This can be used for additional handling beyond the promise returned by `purchase()`. It returns the same value as awaiting the call to `purchase()`, but sometimes users may boost your game without your game triggering the `purchase` function.

Parameters:
- `callback`: Function that receives `{ success: boolean }` indicating whether the purchase succeeded

## TypeScript Support

The SDK is written in TypeScript and includes full type definitions.


## Example Multiplayer Implementation

Multiplayer game development is not yet available to all users, but the SDK supports it. Watch the Remix Discord for announcements about
multiplayer game support.

```typescript
import type { FarcadeSDK, GameState, Player } from '@farcade/game-sdk'

class Game {
  sdk: FarcadeSDK
  gameState: GameState
  player: Player
  players: Player[]

  constructor() {
    this.sdk = window.FarcadeSDK

    // Initialize your game
    this.initialize()

    // Setup event listeners
    this.setupEventListeners()
  }

  private setupEventListeners() {
    // Listen for play again events
    this.sdk.onPlayAgain(() => {
      this.resetGame()
    })

    // Listen for mute state changes
    this.sdk.onToggleMute((data) => {
      this.setMuted(data.isMuted)
    })

    // Listen for game state updates (essential for multiplayer)
    this.sdk.onGameStateUpdated((data) => {
      const { id, gameState } = data
      const newGameStateIsValid = this.validateGameState(gameState);

      if (newGameStateIsValid) {
        this.gameState = gameState
      } else {
        sdk.multiplayer.actions.refuteGameState({ gameStateId: id });
      }
    })

    // Listen for purchase completions (optional)
    sdk.onPurchaseComplete((data) => {
      if (data.success) {
        this.handlePurchaseSuccess()
      }
    })
  }

  private async initialize() {
    await this.sdk.ready()

    this.gameState = this.sdk.gameState
    this.player = this.sdk.player
    this.players = this.sdk.players
  }

  private saveGameState(gameState: Record<string, unknown>, nextTurnPlayerId: string) {
    sdk.multiplayer.actions.saveGameState({
      gameState,
      alertUserIds: [nextTurnPlayerId],
    })
  }

  private gameOver(scores: { playerId: string, score: number }[]) {
    sdk.multiplayer.actions.gameOver({ scores })
  }

  private async purchaseItem(itemId: string) {
    try {
      const result = await sdk.multiplayer.actions.purchase({ item: itemId })
      if (result.success) {
        console.log(`Successfully purchased ${itemId}`)
        // Item is now available via sdk.hasItem(itemId) or sdk.purchasedItems
        // The purchasedItems array is automatically updated for all players
      } else {
        console.log(`Purchase failed for ${itemId}`)
      }
    } catch (error) {
      console.error('Purchase error:', error)
    }
  }
}
```

## License

MIT