Too Many Dice SDK
TypeScript SDK for integrating TooManyDice into your online game. Control a shared dice-rolling room from your game server.
Installation
npm install too-many-dice
Quick Start
import { TooManyDiceRoom } from "too-many-dice";
// 1. Create a room
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 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();
// 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.
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 |
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 |
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) |
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[]>
DiceResult
| Field | Type | Description |
|---|---|---|
diceId |
string |
The die's ID |
value |
number |
The rolled value |
dieType |
DieType |
The die type |
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.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
await room.enableSwipeGestures(true); // re-enable
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) {
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 = [];
const handle = await room.sendCallbackForm({
targetPlayer: room.players[0],
fields: [
{
field: new SliderForm("str", "Strength", 1, 20, 1),
onChange: (value) => { currentStrength = value; },
},
{
field: new MultiSelectForm("perks", "Perks", [
"Shield", "Haste", "Berserk",
]),
onChange: (value) => { selectedPerks = value; },
},
],
buttons: [
{
label: "Confirm Build",
onClick: (playerId) => {
console.log(`${playerId} confirmed: str=${currentStrength}`);
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 |
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" |
Types Reference
import type {
DiceConfig,
DiceResult,
DieType,
TooManyDiceCallbacks,
CreateRoomOptions,
SubmitFormGroup,
CallbackField,
CallbackButton,
CallbackFormOptions,
CallbackFormHandle,
TmdForm,
DpadVisibility,
DpadDirectionConfig,
DpadFieldDef,
DpadFormOptions,
} from "too-many-dice";
DieType
type DieType = "d4" | "d6" | "d8" | "d10" | "d12" | "d20";
DiceConfig
interface DiceConfig {
id?: string;
type: DieType;
}
DiceResult
interface DiceResult {
diceId: string;
value: number;
dieType: DieType;
}
Complete Example
A turn-based game that creates a room, collects character info via forms, then runs dice rolls for each player.
import {
TooManyDiceRoom, TextForm, PickerForm, SliderForm,
} from "too-many-dice";
const HOST = "myapp.partykit.dev";
async function runGame() {
const characters = new Map();
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((resolve) => {
const interval = setInterval(() => {
if (room.players.length === 2) {
clearInterval(interval);
resolve();
}
}, 500);
});
await room.closeAccess();
// 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 submissions
await new Promise((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(`${player.name}'s turn — rolling d20...`);
const results = await room.roll(player);
console.log("Result:", results);
}
await room.destroy();
}
runGame().catch(console.error);