The client

v3 · Preview

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

Control playback

API

Hook Contract
usePlayback(options?) options is { ownControls?: boolean }. Returns { status: "now" | "playing" | "paused"; behind: number; more: boolean; fetching: boolean; canStepBack: boolean; speed: PlaybackSpeed; setSpeed; play; pause; stepForward; stepBack; skipToNow }, where PlaybackSpeed is 0.5 | 1 | 2 | 4 | 8. Boardweaver's step bar offers 0.5 to 4, and a finished match's replay page offers 1 to 8. behind counts the steps read after the one on the board. more is true when steps past those are not read yet. What ownControls hides: Other players' steps.
useStepDuration(duration) void. duration is milliseconds per change at 1x, a number or (change: LastChange) => number, while the component is mounted. When several components set one, the one mounted last applies. See Step time.

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.

Need a hand with this?

Join our Discord