# Friend challenges

_Host ≥ 1.2.0._ Players can challenge each other in your game — **high score**,
**fastest time**, or **win streak** — wagering Gamma Coins in escrow. The winner
takes the pot; ties and expiries refund. When your game is launched in challenge
mode the Gamma Games app injects the context; your job is to read it, report
progress, and submit one final result.

`gamma.challenges` lives under the main `gamma` object and is promise-first like
everything else:

```js
import {gamma} from '@swiftware/gamma-sdk';
// drop-in: global `gamma`
```

## Branch once at startup

Call `getActive()` after `ready()`. It returns `null` for a normal play session —
and on hosts older than 1.2.0 — so **the same build works everywhere**.

```js
await gamma.ready();
const challenge = await gamma.challenges.getActive();

if (challenge) {
  // {
  //   challengeId, type: 'high_score' | 'fastest_time' | 'level_streak',
  //   mode: 'live' | 'async', param, wagerCoins, deadline,
  //   myRole: 'challenger' | 'opponent',
  //   opponent: { displayName, avatar } | null
  // }
  startCompetitiveRun(challenge);
} else {
  startNormalGame();
}
```

## Report progress while playing

`submitScore(value)` reports the current progressive value — score, elapsed
milliseconds, or streak, matching the challenge type. It powers the live opponent
overlay and, for higher-wins types, the disconnect fallback. Call it as often as
you like; the host throttles.

```js
function onScoreChanged(currentScore) {
  gamma.challenges.submitScore(currentScore); // fire-and-forget
}
```

## Submit the final result

Call `complete(result)` **exactly once**, at game over. Set the key that matches
`challenge.type`:

| Challenge type | Result key | Example |
|---|---|---|
| `high_score` | `{ score }` | `{ score: 1240 }` |
| `fastest_time` | `{ timeMs }` | `{ timeMs: 53120 }` |
| `level_streak` | `{ streak }` | `{ streak: 9 }` |

```js
const res = await gamma.challenges.complete({score: finalScore});
// res.outcome: 'won' | 'lost' | 'tie' | 'pending_opponent'
// res.settled: true once both players have finished
// res.potCoins: coins won (winner only)
if (res.ok && res.settled) {
  showOutcome(res.outcome, res.potCoins);
}
```

## Rules that matter

- `getActive()` returns `null` for a normal session and on hosts &lt; 1.2.0 —
  always handle the non-challenge path so one build runs everywhere.
- `complete()` is accepted **once** per attempt; later calls return
  `already_completed`. Results are validated and settled server-side — never grant
  a payout client-side.
- In **live** mode, opponents race in real time: don't pause the run on your own
  ads or interstitials.
- `wagerCoins` is held in escrow by the host. Your game doesn't move coins for the
  wager — just play and report. (Use [`spendCoins`/`earnCoins`](./coins.md) only
  for your normal in-game economy.)

## Test outside the app

Seed the [dev mock](./dev-mock.md) to exercise the flow in a plain browser:

```js
gamma.configure({mock: {challenge: {type: 'high_score', wagerCoins: 100}}});
await gamma.ready();
const challenge = await gamma.challenges.getActive(); // mocked context
```

The mock pretends the opponent already finished and you won, so `complete()`
resolves with `{settled: true, outcome: 'won'}`. Don't ship `mock` enabled.

See also the full [API reference](../api-reference.md), [error codes](../error-codes.md),
and [App-only & offline](./app-only-and-offline.md).
