Documentation

The client

v2 · Stable

End-of-game controls

When the match ends

match.status is the platform's answer to "is this match still being played". Only "in_progress" accepts moves : while it is anything else every click fails locally with INVALID_ACTION and is never sent, because the server refuses it too.

That is a different question from useIsGameOver(), which is your isGameOver rule. A match reaches "finished" when the rules end it or when a seated player abandons it, so a match can be over with useIsGameOver() still false:

if (match.status !== "in_progress") {
  const quitters = match.abandonedPlayerIds;
  return quitters.length > 0 ? (
    <MatchEnded reason="abandoned" playerIds={quitters} />
  ) : (
    <Results scores={scores} />
  );
}

By default the platform paints its own "Match ended" panel over your board once this leaves "in_progress", so you get a correct ending for free. If you want to render it yourself, set rendersMatchEnd on the game : either via update_game, or from the studio's Code view by opening /manifest.json, which is a form over the same field. The platform then leaves the surface entirely to you, which means you must handle every non-"in_progress" status, or players are stranded on a board that looks playable and isn't.

Getting players out of a finished match

The panel you suppressed carried the only two ways out of a match, so a game that owns the ending has to offer them itself:

<button onClick={() => match.returnToGames()}>Return to games</button>;
{
  match.isHost ? (
    <button
      disabled={match.nextMatchPending}
      onClick={() => match.requestNextMatch()}
    >
      {match.nextMatchPending ? "Starting…" : "Next match"}
    </button>
  ) : (
    <p>Waiting for the host to start a new match…</p>
  );
}

returnToGames() leaves the match for the play screen.

requestNextMatch() starts a rematch: a new match on the same game and version. Only the host may ask (match.isHost); the server refuses it from anyone else, and refuses it at all until the match is finished.

A match at an event table has no host. There match.isHost is true for every seated player, and requestNextMatch() takes that player back to the table to sit down for its next game, rather than creating a rematch for everyone. Gate your button on match.isHost as above and it does the right thing in both cases.

Both calls are ignored while match.status is still "in_progress", So wire them to the same branch that renders your ending, not to a button that's live mid-play.

Creating a rematch is a round trip, so match.nextMatchPending is true from your request until the new match exists or the attempt fails. Render it: without it your button looks inert for that beat and players click it again. On success everyone is navigated into the new match, so the flag going back to false is what you see when it failed, and the platform has already shown the player why.

Both are requests to the platform, not things your frontend does. Your game runs in a sandboxed frame with no top-level navigation, and self-navigating trips containment and locks the match, so these two calls are the whole set of exits available to you.

In the studio preview and the simulator there is no match behind the frame. match.isHost reads false there and both calls do nothing.

The end-of-match panel shows results and lets players start another match or return to games.
The end-of-match panel shows results and lets players start another match or return to games. Example data from the local development UI. Select the image to enlarge.

Need a hand with this?

Join our Discord