Documentation

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.
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. A player leaving ends the match, so the match can be over while useMatch().isGameOver is still false.
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, and 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 index as React keys.
  • A piece click names the piece by its index in this board: { type: "PieceClick", spaceId, index }.
  • useOffers() is empty for a spectator, on another seat's turn, and while a decision is on its way.

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.

Spectators

A spectator's client renders the same component. useSelf() is null, useMatch().viewer is null, and there are no offers. A spectator starts 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