The client

v3 · Preview

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

Read the board

API

Hook Contract
useBoard() State | null: the seat's view with its draft and its decision on its way applied.
useMatch() { ready: boolean; status: "waiting" | "in_progress" | "finished" | "locked" | null; viewer: PlayerId | null; isGameOver: boolean; online: boolean }. ready is false until the match, the seat's saved draft and the first page of the steps the seat has not seen have arrived. isGameOver is false while the board is not at now.
useSelf() { playerId: PlayerId; isActive: boolean } | null. null for a spectator.
usePlayers() PlayerProfile[]: { id: number; name: string; avatarUrl?: string | null; bot?: { id: string; difficulty?: "easy" | "standard" | "hard" }; abandoned?: boolean }, in the order of the state's players. id is the seat's player id. name is the person's username or the bot's name in the player's language, and bot is set only on a bot's seat. abandoned is true on a seat whose player left the match (Game over).
useScores() Scores: Record<string, { publicPoints: number[]; privatePoints: number[] }>, keyed by player id as a string.

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).

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.

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