Detailed references

v3 · Preview

Framework v3 is an opt-in preview available to everyone. New games use the stable framework by default.

The React client

The client draws the board and sends the seat's steps. It decides no rule: what the seat may do comes only from its offers. Exact hook signatures are in boardweaver/react reference.

The file

The entry is /src/frontend.tsx. A match cannot load without it.

  • Default export: the board component, which takes no props. The build fails without it.
  • GameOver (optional named export): the end screen. See Game over.

The client may import only these:

  • react (React 18): useState, useEffect, useLayoutEffect, useMemo, useCallback, useRef, useReducer, useContext, useId, useImperativeHandle, useSyncExternalStore, useTransition, useDeferredValue, useDebugValue, startTransition, createContext, forwardRef, memo, Fragment, Component, StrictMode, createElement, cloneElement, isValidElement and Children. The build refuses every other name, such as Suspense or lazy. For a portal, use Portal from boardweaver/ui.
  • react/jsx-runtime, which JSX uses.
  • boardweaver/react: the hooks and types in boardweaver/react reference.
  • boardweaver/motion, boardweaver/ui, boardweaver/dnd, zod and react-intl: animation, accessible controls, drag and drop, JSON parsing and the game's text. The names each one exports are in Client libraries.
  • boardweaver/game: the zod schema and type of each client type the game logic defines, in Client types.
  • The game's own .ts and .tsx files, by a relative path (./board) or a /src/ path (/src/board).
  • The game's own .json files, by their full path, as the JSON value they hold. The game logic can read a file in /src/data/ too: see Data files.

Any other import fails the commit with Cannot resolve import. That includes npm packages, react-dom, boardweaver/immer and CSS files. Style with the style prop or a <style> element.

The client has no network, no eval, no workers and no browser storage. It may load images from the game's art and fonts from Google Fonts. It plays the game's sounds with useSounds() (Sounds).

Drawing the board

Draw the board from useBoard(), the seat's view with its draft applied. Make only the offers of useOffers() clickable:

import { useMemo } from "react";
import { useAct, useOffers, useBoard, type Action } from "boardweaver/react";

export default function App() {
  const board = useBoard();
  const offers = useOffers();
  const act = useAct();

  const offerBySpace = useMemo(() => {
    const bySpace = new Map<string, Action>();
    for (const { action } of offers) {
      if (action.type === "SpaceClick") bySpace.set(action.spaceId, action);
    }
    return bySpace;
  }, [offers]);

  if (!board) return null;
  return (
    <div role="grid">
      {board.spaces.map((space) => {
        const offer = offerBySpace.get(space.spaceId);
        return (
          <button key={space.spaceId} disabled={!offer} onClick={() => offer && act(offer)}>
            {space.pieces?.[0]?.publicKind ?? ""}
          </button>
        );
      })}
    </div>
  );
}
  • The board is JSON in camelCase: spaces[].spaceId, pieces[].publicKind, publicState, metaData. Parse a map with the schema of its type from boardweaver/game, as Client types shows.
  • Pieces have no ids. Use the space id and the piece's index as React keys.
  • A piece click names the piece by its index on this board: { type: "PieceClick", spaceId, index }.
  • useOffers() is empty for a spectator, once the game is over, while a decision or undo is on its way, and while the board shows an earlier board (Other players' steps).

Taking a step

act(action) takes one step and answers at once:

type Means
applied A choice or cancel step joined the draft. The board already shows it.
sent The step ended the decision. The decision is on its way to Boardweaver.
failed Boardweaver did not take the step. reason.code says why.

act takes steps on the board of the render that created it. When the board has changed since that render, act fails as STALE. Call act once per event: after one call takes a step, a second call in the same handler fails. A call from a timer or after an await fails when the board changed in between.

To take several steps from one event, such as a form that collects several values, pass them as a list: act([first, second, third]). Each step applies to the board that the step before it left. Each joins the decision as its own step, and only the last may end it. When a step fails, the steps before it stay in the decision and Boardweaver takes none after it. The failed outcome's applied says how many steps joined.

Commands, confirm and cancel

Draw a control for each offer that is not a board click:

  • A Command offer: a button with the offer's label, written with useMessageText() (Failures).
  • A confirm offer: the button that ends the decision, such as "Take".
  • A cancel offer: a switch or take-back, such as a click on another staged card.
  • An offer with an input: a field. For Text, offer the suggestions and accept up to maxLen characters. For Integer, accept min to max in steps of step. Send the value in the command: act({ ...offer.action, value }).

useDraft() lists the draft's steps. Use it to show what the seat chose so far.

What the client must draw

Boardweaver draws a bar under the board: the menu, the controls for other players' steps, rules, sound, full screen and leave. It shrinks the area your client draws in to fit above the bar. On the board, Boardweaver draws only a card when the player returns and each step's log lines while steps play (Other players' steps). The client draws:

  • whose turn it is: board.activePlayerIds and useSelf()?.isActive;
  • names, avatars and which seats are bots: usePlayers();
  • scores during play: useScores();
  • the Undo button: useUndo();
  • what changed: log lines and movements from useLastChange();
  • decisions on their way: usePending();
  • failures: useFailures();
  • light and dark colors: useColorMode() and useGameTheme() (Design the theme).

Undo

import { FormattedMessage } from "react-intl";
import { useUndo } from "boardweaver/react";

export function UndoButton() {
  const undo = useUndo();
  return (
    <button onClick={() => undo.undo()} disabled={undo.available === "none"}>
      <FormattedMessage id="undo" />
    </button>
  );
}

available is draft (takes back the last draft step at once), server (asks Boardweaver to take back the last committed step) or none. The game logic decides which committed decisions the seat may take back (Changing the state).

Boardweaver also provides friendly takebacks around live matches. Keep useUndo for immediate undo without opponent approval.

Decisions on their way

usePending().pending is the one decision or undo on its way to Boardweaver. When its predicted is true, the board already shows the result. It is false for an undo, and for a decision whose last step Boardweaver must apply first, such as a step that reveals or draws randomness. Then show that the move is on its way. The board updates when Boardweaver answers.

Failures

useFailures().failures keeps every failed step, refused decision and dropped draft step until the client calls dismiss(id). Show each one and let the player dismiss it. When Boardweaver refuses a decision, its steps that still apply come back as the draft, and the player confirms again.

An offer's label and the reason of a GAME refusal are messages from your catalogs. A REFUSED, SERVER_REFUSED or DRAFT_DROPPED failure can carry that refusal. Write a message as text with the function that useMessageText() returns:

import { FormattedMessage } from "react-intl";
import { useFailures, useMessageText } from "boardweaver/react";

export function FailureList() {
  const { failures, dismiss } = useFailures();
  const text = useMessageText();
  return (
    <>
      {failures.map((failure) => {
        const refusal = "refusal" in failure.reason ? failure.reason.refusal : null;
        return (
          <p key={failure.id} role="alert">
            {refusal?.refused === "GAME" ? (
              text(refusal.reason)
            ) : (
              <FormattedMessage id="failure" values={{ code: failure.reason.code }} />
            )}{" "}
            <button onClick={() => dismiss(failure.id)}>
              <FormattedMessage id="failure.dismiss" />
            </button>
          </p>
        );
      })}
    </>
  );
}

What changed

useLastChange() gives the last change: its steps, each with actorPlayerId, lines and movements, or the undo that made it. Each line is a FormattedLine: the line's message (Changing the state) in the language the frame shows your game in. Its parts are typed in boardweaver/react reference. lineText joins a line's parts into one string:

const step = useLastChange().steps.at(-1);
const text = (step?.lines ?? []).map(lineText).join(" ");

Draw the parts yourself to style a player or a link. useDraft() gives the lines of the seat's open decision in the same form.

A coalesced change is a jump to a new board, such as a reconnect, a skip to now or an undo. Show its board without animating its steps. Movements describe only a step forward. To animate in either direction, including replay Back, use Directional animation.

Directional animation

useBoardTransition() describes the change from the board shown before to the current useBoard(). Its id grows each time the board on show changes, also on a return to an earlier board. fromId is the id of the board it changes from. While the board stays the same, the id stays the same. When React skipped the fromId board, or the component mounted after it, the hook returns snap with empty pieces.

Field Meaning
reason initial, change, playback, stepForward, stepBack, seek, skip, reconnect or undo.
direction forward, back, or null when the reason has no direction.
outcome animate when the recorded movements pair the pieces; otherwise snap, with empty pieces.
pieces Pairs of from and to endpoints. An endpoint is null when the piece is added or removed.
Endpoint spot { type: "At", spaceId, index } on that endpoint's board, or { type: "Hidden", spaceId }.
Endpoint piece What the viewer sees of the piece on that board. It is null in a hidden group: draw the game's card back.
  • Back and Forward combine every step inside one log row into one transition, the player's own decisions included. Each endpoint belongs to the board it is shown on.
  • Back from now also takes the player's draft, and a decision on its way to the server, back with the last row.
  • Two pieces with the same face stay apart by their recorded positions. A hidden group gives no single piece and no order. A piece that enters a hidden group and a piece that leaves it are two transitions.
  • First load, a seek, a skip to now and a reconnect snap. So do shuffles and every change Boardweaver cannot pair safely.
  • A gameplay Undo has reason undo. An Undo of a draft step plays its recorded movement back, with direction back. Any other Undo snaps.
  • Replay Back changes only the board on show. It does not ask for a gameplay Undo.

Optional animation helpers

Boardweaver chooses no art, path, easing, duration or stagger for an animation: the client does. Use one of these layers, or draw straight from useBoardTransition():

Layer Use
useTransitionEffect(duration, effect) Start a custom renderer with Boardweaver's timing and cancellation. Return its cleanup function.
TransitionProvider Measure registered anchors and give overlays, without choosing a renderer or a visual style.

For useTransitionEffect:

  • Pass the whole duration in milliseconds at 1x, with its delays and settling time.
  • The effect receives (transition, run). Pass every duration and delay through run.milliseconds(value), so it follows the playback speed.
  • Each run holds the board until its duration ends, it completes or Boardweaver cancels it. Call run.complete() to end it early. Other runs and the time in Step time still hold the board.
  • Return a cleanup that cancels every timer and animation and restores every visual your renderer owns. Boardweaver calls it on completion, a newer board, a step or seek, a speed change, a resize, a hidden tab, a reduced-motion change and unmount.
  • A late completion never ends a newer run. A cancelled run stays cancelled, and the next transition starts a new one.
  • Boardweaver skips the effect while the player prefers reduced motion or the tab is hidden. The time to read other players' steps still applies.

For measured overlays, wrap the board in TransitionProvider with anchor, duration and children:

  • anchor(endpoint) maps each endpoint to your own location key, or returns null to skip it.
  • Register each key with useAnimationAnchor(key), and put its ref on an element Boardweaver can measure. While hidden is true, hide that element's artwork but keep its size and place.
  • Register one group anchor for each hidden hand, also an empty one. A hidden endpoint has no index, so animate it to and from the group.
  • Register the places a piece passes through too, such as a selected-card slot. Other players' steps visit them during continuous playback. Forward combines a log row, so a test that only steps Forward from hand to board does not reach those anchors.

useTransitionOverlays() returns one overlay for each pair it measured: origin and destination rectangles in viewport CSS pixels, the paired piece, run, key, index, from and to. Use key as the overlay's React key. Convert the rectangles to your overlay layer's coordinates, and draw the endpoints with your own components.

  • A pair with a missing anchor gets no overlay, and its destination stays visible.
  • The provider measures every registered anchor on each animation frame, also while idle, and on each resize and scroll. When a destination anchor's size or place changes, the provider ends the run and its overlays.

Hexbloom's animations.tsx, transitions.ts and located-card.tsx (get_example with hexbloom) use this layer for card flights and flips. A canvas, CSS, Web Animations or Motion renderer can use useTransitionEffect without TransitionProvider.

Client structure

  • Derive what you draw from the board on show and the offers. Keep your animation choices apart from the transition's pairs.
  • Use one board geometry for drawing and for pointer hit tests. Keep container sizes fixed while cards move.
  • Test each viewer, each direction and an interrupted animation with Client checks.

Other players' steps

Boardweaver shows the steps of other players (people and bots alike) one at a time, in order, even when several arrive together. useLastChange() reports each one as Boardweaver shows it. Each step stays on the board long enough for the player to read its log lines and see its movements (Step time).

usePlayback().status says which board is on show:

  • now: the match as it stands. The seat acts only on this board.
  • playing or paused: an earlier board. That is a step the player has not seen, a step they went back to, or, on return, the board they last saw.

While the board is an earlier one, useBoard() shows it, useOffers() is empty, useDraft() has no steps and useMatch().isGameOver is false. An act or Undo fails as PLAYING. The draft comes back at now. useScores() and useMatch().status still describe the match as it stands (Scores).

When a player opens a match after other players moved, useMatch().ready is false until the first page of the missed steps arrives. Then the board shows the last board the player saw, with status paused. Boardweaver covers the board with a card: how many game log rows the player missed, Watch, Skip to now and Always skip to now in this match. Watch plays the missed steps. Skip to now jumps to now without playing them.

How Boardweaver plays the steps:

  • While the player watches, the bar has Back, Play or Pause, Forward, a speed of 0.5x, 1x, 2x or 4x, and Skip to now. It starts at 1x.
  • Play shows every step. Back and Forward move one game log row at a time: Forward to the end of the next row, past the steps that wrote no line, and Back to the end of the row before.
  • Each step's log lines show with their players' names in a bubble over the bottom center of the board.
  • Steps that arrive while the game tab is visible play on their own, with the same bubble.
  • Hiding the tab pauses playback on the board the player last saw. On return, the missed steps wait until the player chooses Watch or Skip to now, unless the player chose Always skip.

How the game log groups steps into rows:

  • At now, the bar offers Back and the game log, oldest first, with the rows the player missed marked.
  • Each step that writes lines is a row. A step that writes no line joins the row before it in the same decision, or the decision's first row when no earlier step writes a line.
  • A decision whose steps write no line is one row: the player's name and "made a move".
  • A tap on a row moves the board to that row's last step.

When an act or Undo fails as PLAYING, Boardweaver warns the player. Jump to current step shows the live board and does not retry the action. Keep viewing history closes the warning and keeps the earlier board. The warning also appears when the game draws its own playback controls.

Keep the bottom center of the board free of anything the player reads while steps play. Or draw your own controls: pass ownControls: true to usePlayback, and Boardweaver hides its card, its bubble and its step controls while that component is mounted. Then give the player pause, play, stepForward, stepBack, skipToNow and setSpeed. Show skipToNow whenever status is not now, so the player can always reach the board they act on:

import { FormattedMessage } from "react-intl";
import { usePlayback } from "boardweaver/react";

export function SkipToNow() {
  const playback = usePlayback({ ownControls: true });
  if (playback.status === "now") return null;
  return (
    <button onClick={playback.skipToNow}>
      <FormattedMessage id="playback.skipToNow" values={{ behind: playback.behind }} />
    </button>
  );
}
While reviewing moves, the match bar offers Back, Play or Pause, Forward, and Skip to now, with Rules and match actions alongside.
While reviewing moves, the match bar offers Back, Play or Pause, Forward, and Skip to now, with Rules and match actions alongside. Example data from the local development UI. Select the image to enlarge.

Step time

At 1x, Boardweaver shows each step of other players for at least this time:

The step has Time at 1x
log lines 800 ms, plus 55 ms for each character of their text, from 1500 ms to 5000 ms
movements and no log lines 700 ms, plus 100 ms for each movement after the first, up to 1500 ms
no log lines and no movements 250 ms

An undo stays for 600 ms. The speed divides each time: at 2x, a step of 1800 ms stays for 900 ms.

useTransitionEffect and TransitionProvider hold the board until their run ends (Optional animation helpers). When the client animates a change another way, such as with Motion from useLastChange(), call useStepDuration with the time your animation takes. Boardweaver shows each step for the longer of your time and the time in the table. Without it, the next step can start before your animation ends.

Give every step one time:

useStepDuration(1200);

Or compute the time from your animations. This client slides a piece for 450 ms and flips a piece for 300 ms. It adds 400 ms so the player sees the result:

import type { LastChange } from "boardweaver/react";

const SLIDE_MS = 450;
const FLIP_MS = 300;
const SETTLE_MS = 400;

export const stepTime = (change: LastChange) =>
  change.steps.reduce((total, step) => {
    const slides = step.movements.filter((movement) => movement.type === "Moved").length;
    const flips = step.movements.filter((movement) => movement.type === "Flipped").length;
    return total + slides * SLIDE_MS + flips * FLIP_MS + SETTLE_MS;
  }, 0);

Then call useStepDuration(stepTime) in the component that draws the board.

A log line's link (link(key, text), Changing the state) is a control in the game log: Boardweaver's panel, the replay page and <History />. useLogLinks hears its enter, leave and click events, each with the link's key. Each enter has one leave with the same key. A click on a link does not move the board. Show what the key names however the game likes, such as the card in a side panel. Clear it only on the leave of the key it shows:

const [shownCard, setShownCard] = useState<string | null>(null);
useLogLinks((event) => {
  if (event.type === "enter") setShownCard(event.key);
  if (event.type === "leave") setShownCard((shown) => (shown === event.key ? null : shown));
});

Every viewer reads every key, so make a key name only what every viewer may see.

The game log lists moves and links to earlier positions in the match.
The game log lists moves and links to earlier positions in the match. Example data from the local development UI. Select the image to enlarge.

The log in the game

Boardweaver draws the match's log in its own panel. To draw the log in the game instead, such as in a side panel, call useHistory({ ownPanel: true }) in the component that draws it. While that component is mounted, Boardweaver hides its panel. Its history button keeps its count of the moves the player has not seen, and opens and closes the game's log instead (open, setOpen).

import { FormattedMessage } from "react-intl";
import { History, useHistory } from "boardweaver/react";

export function LogSidebar() {
  const { open, setOpen } = useHistory({ ownPanel: true });
  if (!open) return null;
  return (
    <aside>
      <button onClick={() => setOpen(false)}>
        <FormattedMessage id="action.close" />
      </button>
      <History kindStyles={{ capture: { color: "crimson" } }} />
    </aside>
  );
}

<History /> draws the log as Boardweaver's panel does: each decision under its player, the rows the player missed marked, and the row on the board marked with a bar in the theme's colors.accent.500 along its leading edge, over a tint of that color. It takes the text color of where you place it. A tap on a row moves the board there, and its links reach useLogLinks. Boardweaver writes its own words in the log (a row for a move or an opening your game wrote no line for, an undo, the divider above the missed rows, and each row's age) in the language the frame shows your game in.

How <History /> scrolls:

  • It is its own scrolling element (overflowY: "auto" unless style sets overflowY or overflow), so it never scrolls the game's page.
  • To make it scroll, give it a maxHeight in style, or another bound on its height.
  • It opens with its newest row at the bottom. While the player stays at the bottom, it keeps each new row in view.
  • Each time another row becomes the row on the board, it scrolls that row into view. Rows that arrive while the board stays on its row never make it scroll.
  • It lays its rows out from the bottom up with display: "flex" and flexDirection: "column-reverse", so its style takes every CSS property but those two.
  • A wrapper that scrolls instead keeps the browser's own scrollbar. <History /> never scrolls the wrapper.

The log's own scrollbar is thin and in the log's text color. To change it, set scrollbarColor (the thumb's color, then the track's) or scrollbarWidth ("auto", "thin" or "none") in style. Each one replaces only its own default, so this scrollbar stays thin:

<History style={{ maxHeight: 320, scrollbarColor: "#b8862b transparent" }} />

Chrome and Edge ignore ::-webkit-scrollbar rules on an element whose scrollbarColor or scrollbarWidth is not "auto", and the elements inside <History /> take its scrollbarColor. To style the log's scrollbar with those rules in a <style> element, set both to "auto" in style.

To draw the log yourself, read useHistory().entries. Each line is a FormattedLine, as in What changed. A decision's steps hold every step, also a step that writes no line. endsLogRow is true on the last step of each of Boardweaver's rows, so a log that groups steps by it shows the same rows. While more is true, the last step read of the latest decision can have endsLogRow false: its row goes on in steps not read yet, so draw it as an open row.

Scores

useScores() is keyed by the player id as a string: scores[String(playerId)].publicPoints. Each seat sees every seat's publicPoints and only its own privatePoints. The game logic names no score columns: name each column in the client, with a message from your catalog, such as in GameOver.

useScores() always describes the match as it stands, also while the board shows an earlier board. Then compute progress from the board on show when it holds the data. Otherwise label the scores as current, or leave them out.

Layout

useViewport() gives the size of the area your client draws in. It changes when the player resizes the window, rotates a phone or enters full screen. Size the board from it, or choose a layout from it. The tic-tac-toe client sizes its cells from the width, less its padding and gaps:

import { useViewport } from "boardweaver/react";

const PADDING = 16;
const GAP = 6;

function useCellSize() {
  const { width } = useViewport();
  return Math.max(32, Math.min(88, Math.floor((width - 2 * PADDING - 2 * GAP) / 3)));
}
  • Keep a fixed place for controls, status, hands and private goals in every turn phase. Change what they hold without resizing the board.
  • Keep a seat's private information on screen during other players' turns while the view still holds it.
  • Scale the board to both the width and the height. Give the log its own bounded scrolling area.
  • Use wider side panels when the space allows. Test short landscape windows as well as narrow portrait ones.
  • Read names and avatars from usePlayers(). Draw a fallback for a player with no avatarUrl.
  • Use the dialog parts of boardweaver/ui (Client libraries) for focus and dismissal.

Text in the player's language

Write every word your client shows in a catalog, not in the code. A catalog is /src/i18n/<locale>.json: one JSON object that maps each message key to its message, in ICU MessageFormat. A translator can then translate it without reading your code. The tic-tac-toe client's catalog is /src/i18n/en.json (get_example with tic-tac-toe-rust-catalog):

{
  "status.yourMove": "Your move",
  "status.toMove": "{name} to move",
  "headline.wins": "{name} wins"
}

Show a message with react-intl, by its key, with the values its placeholders name:

import { FormattedMessage, useIntl } from "react-intl";

export function Status({ yourMove, name }: { yourMove: boolean; name: string }) {
  const intl = useIntl();
  return (
    <strong aria-live="polite">
      {yourMove ? <FormattedMessage id="status.yourMove" /> : intl.formatMessage({ id: "status.toMove" }, { name })}
    </strong>
  );
}
  • The frame chooses the language and gives your client its react-intl provider (Which locale a player sees).
  • Format numbers, dates and lists with react-intl too, such as <FormattedNumber value={score} />, so they follow the language.
  • The build checks every key you write as a literal. A <FormattedMessage id>, or an id in the first argument of formatMessage, defineMessage or defineMessages, that your default catalog lacks fails the build at its file and line. The build does not check a key you build at run time, such as `card.${face}`: it shows as the key when no catalog has it.
  • Text from the game logic comes from your catalogs too. Log lines, offer labels and refusal reasons are messages. The frame formats them in the language it shows your game in (Changing the state, Failures).

Which files and messages a catalog may hold, and the errors a bad one gets, are in Catalogs. To translate the whole game, follow Translate a game.

Images

Get the URL of uploaded art with useImage(key), where key is the art's key in the game's art library. The hook returns undefined when the game has no image for the key: draw a fallback then.

import { useImage } from "boardweaver/react";

export function CardBack() {
  const image = useImage("CardBack");
  return image ? <img src={image} alt="Card back" /> : <span>Card</span>;
}

When you rename an art key, change every useImage call that names it.

Sounds

Upload each sound with upload_sound, or with Upload sounds in the studio's Sounds view:

  • The file is MP3, WAV, OGG or M4A, at most 10 MiB.
  • The key is UpperCamelCase ASCII letters and digits, such as DiceRoll. The sound lives at /sounds/<Key>.
  • Uploading needs the Designer or Professional plan. Every player hears the game's sounds, whatever their plan.
  • An upload is a pending change. The studio preview plays it at once, and commit makes it permanent.
  • Upload the same key again to replace a sound. Move it with rename_file and remove it with delete_file, then change every key the client plays to match.
  • For a local file too large to pass inline, call create_upload with target { type: "sound", key }, its SHA-256 and its byte length. Then follow Upload local files to POST the raw file.

Play a sound by its key with useSounds(). Give each play a type: "music" for a soundtrack or an ambient loop, or "effect" for a sound that answers a step:

import { useEffect } from "react";
import { useLastChange, useSounds } from "boardweaver/react";

export function GameSounds() {
  const sounds = useSounds();
  const change = useLastChange();

  useEffect(() => {
    const theme = sounds.play("ThemeLoop", { type: "music", loop: true });
    return () => theme.stop();
  }, [sounds]);

  useEffect(() => {
    if (change.coalesced || change.direction === "back") return;
    if (change.steps.some((step) => step.movements.length > 0)) {
      sounds.play("PiecePlaced", { type: "effect" });
    }
  }, [change, sounds]);

  return null;
}
  • Play each step's sound from an effect on useLastChange(), so it plays as Boardweaver shows the step, for the seat's own steps and other players' alike. Skip a coalesced change and one whose direction is back.
  • Start music in an effect and stop it in the cleanup, so it ends with the component.
  • stopAll(type?) stops every playing sound, or every sound of one type.
  • volume runs from 0 to 1. iOS Safari ignores it.

Each player turns music and effects on and off, for every game or for one match, from the sound control on the bar. Boardweaver applies the choice: an effect played while effects are off does nothing, and music plays muted while music is off, so it keeps its place. Read useAudioSettings() only to show the player's choice in your own controls.

Browsers play no sound until the player presses or types in the game. Music started before that starts at the first press. Boardweaver drops an effect played before it.

Sound made with Web Audio ignores the player's settings, so a player who turned sound off still hears it. Upload a file and play it with useSounds() instead.

The Sounds page lists uploaded clips, their names, and playback controls.
The Sounds page lists uploaded clips, their names, and playback controls. Example data from the local development UI. Select the image to enlarge.
The match sound menu can override account preferences for just this match.
The match sound menu can override account preferences for just this match. Example data from the local development UI. Select the image to enlarge.

Game over

When the match is finished or locked and the board is at now (usePlayback().status), Boardweaver renders GameOver after the board, if the game exports it. The replay of a finished match never renders GameOver. Make it cover the board itself (for example position: "fixed", inset: 0) and offer the end controls:

import { FormattedMessage } from "react-intl";
import { useMatchEnd } from "boardweaver/react";

export function GameOver() {
  const end = useMatchEnd();
  return (
    <div role="dialog" style={{ position: "fixed", inset: 0 }}>
      {end.startsNextMatch && (
        <button onClick={end.nextMatch} disabled={end.nextMatchPending}>
          <FormattedMessage id={end.nextMatchPending ? "gameOver.starting" : "gameOver.next"} />
        </button>
      )}
      <button onClick={end.returnToGames}>
        <FormattedMessage id="gameOver.return" />
      </button>
    </div>
  );
}

Without a GameOver export, Boardweaver shows its own end screen with the results and the next-match and return buttons. Its score columns have no names, because the game logic names none: export GameOver to show what each column means. With a GameOver export, Boardweaver shows nothing else at the end, so GameOver must show the result. A match also ends without a winner when an error locks it or a player leaves it. Then usePlayers() marks who left with abandoned, and isGameOver can still be false. Write the end screen for those cases too, as the tic-tac-toe client's useHeadline does (get_example with tic-tac-toe-rust-client).

The default end screen shows results when a framework v3 game does not export GameOver. A seated player waits for the host to start the next match or returns to games.
The default end screen shows results when a framework v3 game does not export GameOver. A seated player waits for the host to start the next match or returns to games. Example data from the local development UI. Select the image to enlarge.

Spectators

A spectator's client renders the same component. useSelf() is null, useMatch().viewer is null, and useOffers() is empty. A spectator opens at now, and sees each player's steps one at a time, as a seated player does.

Need a hand with this?

Join our Discord