npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

too-many-dice

v0.1.7

Published

TypeScript SDK for TooManyDice online game API

Readme

too-many-dice SDK

TypeScript SDK for integrating TooManyDice into your online game. Control a shared dice-rolling room from your game server: manage players, configure dice, trigger rolls, and push interactive UI forms to players.

Installation

npm install too-many-dice

Quick Start

import { TooManyDiceRoom } from "too-many-dice";

// 1. Create a room on your game server
const room = await TooManyDiceRoom.create("myapp.partykit.dev", {
  playerLimit: 4,
  diceConfig: [{ type: "d6" }, { type: "d6" }],
  callbacks: {
    onPlayerJoined: (player) => console.log(`${player.name} joined`),
    onPlayerLeft: (player) => console.log(`${player.name} left`),
    onResult: (results) => console.log("Roll results:", results),
  },
});

console.log("Room code:", room.roomCode); // Share this with players

// 2. Players open the TooManyDice app and enter the room code.

// 3. Trigger a roll once players are connected
const results = await room.roll();
console.log(results); // [{ diceId: "0", value: 4, dieType: "d6" }, { diceId: "1", value: 2, dieType: "d6" }]

// 4. Clean up
await room.destroy();

Concepts

Room Lifecycle

create() → share roomCode → players join via app → orchestrate gameplay → destroy()

Once created, the room is open for players to join using the roomCode. Use closeAccess() to stop new players from joining while the game is in progress, and openAccess() to re-open it.


API Reference

TooManyDiceRoom

TooManyDiceRoom.create(host, options?)

Creates a new room. Returns a TooManyDiceRoom with full owner privileges.

const room = await TooManyDiceRoom.create("myapp.partykit.dev", {
  playerLimit: 4,
  diceConfig: [{ id: "atk", type: "d20" }],
  callbacks: { onPlayerJoined, onPlayerLeft, onResult, onFormSubmit },
});

Options (CreateRoomOptions):

| Field | Type | Description | | ---------------------- | ----------------------- | -------------------------------------------------------------- | | playerLimit | number | Max number of players allowed to join | | diceConfig | DiceConfig[] | Initial dice configuration | | swipeGesturesEnabled | boolean | Allow players to swipe-throw individual dice (default: true) | | callbacks | TooManyDiceCallbacks | Event handlers (see Callbacks) |

Returns: Promise<TooManyDiceRoom>

Properties

| Property | Type | Description | | ------------- | ---------------------- | ----------------------------------------- | | roomCode | string | The code players enter in the app to join | | playerLimit | number \| null | Max players (null if not set) | | players | readonly TmdPlayer[] | Currently connected players |


Dice

room.setDice(diceConfig)

Reconfigure the dice at any time during a session.

await room.setDice([
  { id: "strength", type: "d20" },
  { id: "bonus", type: "d4" },
]);

DiceConfig:

| Field | Type | Description | | ------ | ------------------------------------------------- | ---------------------------------------------- | | type | "d4" \| "d6" \| "d8" \| "d10" \| "d12" \| "d20" | Die type | | id | string | Optional identifier (returned in roll results) |


Rolling

room.roll(player?)

Triggers a roll. Delegates 3D physics simulation to player (or the first connected player if omitted). Resolves with the results once the dice settle.

// First available player handles the physics
const results = await room.roll();

// Specific player handles the physics
const results = await room.roll(room.players[0]);

Returns: Promise<DiceResult[]>

Each DiceResult contains:

| Field | Type | Description | | --------- | --------- | ---------------------------------------------------------------- | | diceId | string | The die's ID | | value | number | The rolled value | | dieType | DieType | The die type ("d4", "d6", "d8", "d10", "d12", "d20") |

Throws if no players are connected, or times out after 30 seconds.


room.waitForRoll(player, timeoutMs?)

Waits for a specific player to manually roll (shake or tap in the app). The player initiates the roll themselves. Times out after timeoutMs milliseconds (default: 120,000 = 2 minutes). Also rejects if the socket closes or the server sends an error.

const results = await room.waitForRoll(room.players[0]);

// With custom timeout (60 seconds)
const results = await room.waitForRoll(room.players[0], 60000);

Returns: Promise<DiceResult[]>


Room Control

room.closeAccess()

Prevents new players from joining. Existing players stay connected.

await room.closeAccess();
room.openAccess()

Re-opens the room to new players after a closeAccess().

await room.openAccess();
room.enableSwipeGestures(enabled)

Controls whether players can swipe-throw individual dice. When disabled, players can only roll via the Roll button (or SDK-triggered rolls).

await room.enableSwipeGestures(false); // disable swipe
await room.enableSwipeGestures(true); // re-enable swipe
room.destroy()

Disconnects the WebSocket and cleans up all listeners. Call this when your game session ends. Safe to call multiple times.

await room.destroy();

Callbacks

Pass callbacks in CreateRoomOptions:

const room = await TooManyDiceRoom.create(host, {
  callbacks: {
    onPlayerJoined(player) {
      console.log(`${player.name} (${player.playerId}) joined`);
    },
    onPlayerLeft(player) {
      console.log(`${player.name} left`);
    },
    onResult(results) {
      // results: DiceResult[] — each has diceId, value, and dieType
      for (const r of results) {
        console.log(`Die ${r.diceId} (${r.dieType}): ${r.value}`);
      }
    },
    onFormSubmit({ formId, playerId, answers }) {
      console.log(`Player ${playerId} submitted form ${formId}:`, answers);
    },
  },
});

| Callback | Signature | Description | | ---------------- | ----------------------------------------------- | ----------------------------------- | | onPlayerJoined | (player: TmdPlayer) => void | Fired when a player connects | | onPlayerLeft | (player: TmdPlayer) => void | Fired when a player disconnects | | onResult | (results: DiceResult[]) => void | Fired when dice settle after a roll | | onFormSubmit | (data: { formId, playerId, answers }) => void | Fired when a player submits a form |


TmdPlayer

Represents a connected player.

| Property | Type | Description | | ---------- | -------- | --------------------------------- | | playerId | string | Unique identifier for this player | | name | string | Display name chosen by the player |


Forms

Forms are UI overlays pushed to a player's screen in the TooManyDice app. There are two form modes:

| Mode | API | Use case | | ------------------ | -------------------- | ----------------------------------------------------- | | Submit forms | sendSubmitForms() | Collect structured input; player taps a submit button | | Callback forms | sendCallbackForm() | React to live field changes and custom buttons |

A player can only have one form active at a time.


Submit Forms

Send one or more forms and receive results via the onFormSubmit callback.

import { TextForm, PickerForm, CheckboxForm } from "too-many-dice";

await room.sendSubmitForms([
  {
    formId: "character-setup",
    targetPlayer: room.players[0],
    fields: [
      new TextForm("name", "Character name", {
        placeholder: "Enter name",
        required: true,
      }),
      new PickerForm("class", "Class", ["Warrior", "Mage", "Rogue"]),
      new CheckboxForm("veteran", "Veteran player?"),
    ],
    submitButton: { label: "Confirm" },
  },
]);

// Results arrive via callbacks.onFormSubmit
// answers: { name: "Elara", class: "Mage", veteran: true }

To dismiss all forms:

await room.clearSubmitForms();

To show validation errors to a player:

await room.setFormErrors("character-setup", room.players[0], [
  "Name must be at least 3 characters",
]);

SubmitFormGroup:

| Field | Type | Description | | -------------- | ------------------- | ------------------------------------- | | formId | string | Unique identifier for this form | | targetPlayer | TmdPlayer | The player who sees this form | | fields | TmdForm[] | Array of form field instances | | submitButton | { label: string } | The submit button shown to the player |


Callback Forms

React to individual field changes and button clicks in real time.

import { SliderForm, MultiSelectForm } from "too-many-dice";

let currentStrength = 10;
let selectedPerks: unknown = [];

const handle = await room.sendCallbackForm({
  targetPlayer: room.players[0],
  fields: [
    {
      field: new SliderForm("str", "Strength", 1, 20, 1),
      onChange: (value) => {
        currentStrength = value as number;
      },
    },
    {
      field: new MultiSelectForm("perks", "Perks", [
        "Shield",
        "Haste",
        "Berserk",
      ]),
      onChange: (value) => {
        selectedPerks = value;
      },
    },
  ],
  buttons: [
    {
      label: "Confirm Build",
      onClick: (playerId) => {
        console.log(
          `${playerId} confirmed: str=${currentStrength}, perks=${selectedPerks}`,
        );
        handle.clear();
      },
    },
  ],
});

// Dismiss manually at any time:
await handle.clear();

CallbackFormOptions:

| Field | Type | Description | | -------------- | ------------------ | ------------------------------- | | targetPlayer | TmdPlayer | The player who sees this form | | fields | CallbackField[] | Fields with onChange handlers | | buttons | CallbackButton[] | Optional action buttons |

CallbackFormHandle:

| Member | Type | Description | | --------- | --------------------- | ----------------------------------------- | | formId | string | Auto-generated ID for this form | | clear() | () => Promise<void> | Dismiss the form and remove all listeners |


Form Field Types

All form fields take (id, label, ...args, options?).

CheckboxForm

A single boolean toggle.

new CheckboxForm("agree", "I agree to the rules");
new CheckboxForm("agree", "I agree to the rules", { required: true });

Submit value: boolean


TextForm

A free-text input field.

new TextForm("username", "Your name");
new TextForm("username", "Your name", {
  placeholder: "e.g. Gandalf",
  required: true,
});

Submit value: string


PickerForm

A single-selection dropdown or wheel picker.

new PickerForm("difficulty", "Difficulty", ["Easy", "Normal", "Hard"]);
new PickerForm("difficulty", "Difficulty", ["Easy", "Normal", "Hard"], {
  required: true,
});

Submit value: string (one of the provided options)


MultiSelectForm

Multiple selectable options.

new MultiSelectForm("skills", "Choose skills", ["Fireball", "Shield", "Haste"]);
new MultiSelectForm(
  "skills",
  "Choose skills",
  ["Fireball", "Shield", "Haste"],
  { required: true },
);

Submit value: string[]


SliderForm

A numeric range slider.

// (id, label, min, max, step, options?)
new SliderForm("hp", "Hit Points", 1, 100, 5);
new SliderForm("hp", "Hit Points", 1, 100, 5, { required: true });

Submit value: number


DpadForm

A directional pad control with four configurable directions.

new DpadForm("move", "Move Direction");
new DpadForm("move", "Move", {
  up: { visibility: "enabled" },
  down: { visibility: "disabled" },
  left: { visibility: "hidden" },
  required: true,
});

Submit value: string (the selected direction)

DpadDirectionConfig:

| Field | Type | Description | | ------------ | ---------------- | --------------------------------------- | | visibility | DpadVisibility | "enabled" \| "disabled" \| "hidden" |


Complete Example: Turn-Based Game

import {
  TooManyDiceRoom,
  TextForm,
  PickerForm,
  SliderForm,
} from "too-many-dice";

const HOST = "myapp.partykit.dev";

async function runGame() {
  const characters = new Map<string, Record<string, unknown>>();

  const room = await TooManyDiceRoom.create(HOST, {
    playerLimit: 2,
    callbacks: {
      onPlayerJoined: (p) =>
        console.log(`${p.name} joined (${room.players.length}/2)`),
      onFormSubmit: ({ playerId, answers }) => {
        characters.set(playerId, answers);
      },
    },
  });

  console.log("Share this code with players:", room.roomCode);

  // Wait for 2 players
  await new Promise<void>((resolve) => {
    const interval = setInterval(() => {
      if (room.players.length === 2) {
        clearInterval(interval);
        resolve();
      }
    }, 500);
  });

  await room.closeAccess(); // Lock the room

  // Send each player a character setup form
  await room.sendSubmitForms(
    room.players.map((p) => ({
      formId: `char-${p.playerId}`,
      targetPlayer: p,
      fields: [
        new TextForm("name", "Character name", { required: true }),
        new PickerForm("class", "Class", ["Warrior", "Mage", "Rogue"]),
        new SliderForm("level", "Starting level", 1, 10, 1),
      ],
      submitButton: { label: "Ready!" },
    })),
  );

  // Wait for both players to submit
  await new Promise<void>((resolve) => {
    const interval = setInterval(() => {
      if (characters.size === 2) {
        clearInterval(interval);
        resolve();
      }
    }, 200);
  });
  await room.clearSubmitForms();

  // Play turns
  await room.setDice([{ id: "atk", type: "d20" }]);

  for (const player of room.players) {
    console.log(`\n${player.name}'s turn — rolling d20...`);
    const results = await room.roll(player);
    console.log("Result:", results);
  }

  await room.destroy();
}

runGame().catch(console.error);

TypeScript Types

import type {
  DiceConfig,
  DiceResult,
  DieType,
  TooManyDiceCallbacks,
  CreateRoomOptions,
  SubmitFormGroup,
  CallbackField,
  CallbackButton,
  CallbackFormOptions,
  CallbackFormHandle,
  TmdForm,
  DpadVisibility,
  DpadDirectionConfig,
  DpadFieldDef,
  DpadFormOptions,
} from "too-many-dice";