Have more questions? Join our

The v3 React client

A v3 game's client is /src/frontend.tsx: one default export, a component taking no props. It imports from boardweaver/react, and is served the view of the state this player may see, so it can only draw what they are allowed to know.

import { useAvailableActions, useMatch, useSpace } from "boardweaver/react";

export default function App() {
  const match = useMatch();
  const hand = useSpace(`hand-${match.state.activePlayerIds[0]}`);
  const playable = new Set(
    useAvailableActions().flatMap(({ click }) =>
      click.pieceKey === undefined ? [] : [click.pieceKey]
    )
  );

  return (
    <ol>
      {hand.pieces.map((card) => (
        <li key={card.viewKey}>
          <button
            disabled={!playable.has(card.viewKey)}
            onClick={() => match.click({ type: "Click", pieceKey: card.viewKey })}
          >
            {card.privateKind ?? card.publicKind}
          </button>
        </li>
      ))}
    </ol>
  );
}

What a piece looks like here

A v3 piece has no id. It lives in its space's pieces, and the frame gives it a viewKey: use it as the React key, as the drag id, and to click the piece.

  • publicKind and publicState are always there.
  • privateKind and privateState are there only when this player may see them, so read them with ?? or a guard. A face-down card in someone else's hand simply has neither.
  • A viewKey is an identity consistent with what this player can see. It follows a piece while it looks the same to them, and while they can read what it is. Two cards a player cannot tell apart may swap keys, which is invisible because they render the same. Do not store one as a lasting name for a card.

The hooks

Hook What it gives
useMatch() The state this player sees, click, the pending queue, failures, online, the match lifecycle, undo and undoableActionId
useSelf() { playerId, isActive }
usePlayers() Every player, in seating order
useSpace(spaceId) One space and the pieces in it, in the order this player is shown them
useAvailableActions() Every legal move as a click, with its intent and the game's label
useButtons() The buttons among those moves
useScores(), useIsGameOver() The game's own answers for the state on screen
useLastChange() What changed in the last snapshot, for animation: piece viewKeys in pieceIds, plus spaceIds, playerIds, who acted, and whether it was one action's worth
useGameLog() The game log, in this player's own words
useImage(), useGameTheme(), useViewport(), useColorMode() The same as in v2

match.click takes one target: { pieceKey }, { spaceId } or { buttonId }.

What the client cannot see, and cannot predict

  • A hand, deck or bag is a Collection. The pieces this player cannot see arrive grouped after the ones they can, so hand.pieces.length is still the real count, but the order of the hidden ones says nothing. Render them as backs.
  • Clicking a card you cannot tell apart is decided by the server. It picks one of that group at random, so the frame does not predict the move: the board changes when the answer arrives.
  • Neither is a move that draws randomness. While either is in flight, a click on a piece is refused locally, with a failure explaining why, because the board on screen is missing that move's effect. Disable those affordances while match.pending is not empty if you want to avoid the refusal.

Undo

match.undoableActionId is the id of this player's own last move while they may take it back, and null otherwise: after someone else moves, while one of their clicks is pending, and for spectators. match.undo() takes it back. Nothing is predicted; the board changes when the server answers.

<button disabled={match.undoableActionId === null} onClick={() => match.undo()}>
  Undo
</button>

What is the same as v2

The UI and animation libraries (boardweaver/ui, boardweaver/motion, boardweaver/dnd, boardweaver/immer), images through useImage, the theme, and the rule that the client never decides the rules: it reads state and sends clicks, and the server decides what they mean. What is gone is useGameState and the def registry: a v3 client reads the state the hooks give it, and sizes and art are its own business.