Open-source · MIT · v0.1

Write the rules.
Get the table.

Tablecraft turns one JavaScript module into a real 3D tabletop: physics dice, cards that fly and flip, hidden hands, multiplayer rooms, bots and an automated referee. Any board or card game — no game-engine experience needed.

npm install @table-craft/table-craft
Treasure Road — you vs. a bot · runs entirely in your browser⤢
1 filerules, setup, view
0 serversto prototype locally
Real physicsdice land on the server's roll
Bots + refereethousands of games in seconds

How it works

You describe the game. Tablecraft runs it, renders it and keeps everyone honest.

1

Write the rules

A game is a plain module: setup() deals, moves validate and change state, endIf() decides the winner. State is JSON; randomness is seeded and replayable.

moves: {
  roll(s, ctx) {
    if (cur(s).id !== ctx.playerId) return { error: 'Not your turn' };
    const r = ctx.roll(6, { id: 'die', by: ctx.playerId });
    s.pos[ctx.playerId] += r.face;
    return s;
  },
}
2

Describe the table

view() returns what this player may see: zones, cards, dice, tokens, a HUD. Opponents' cards simply have no face. Same ids move between zones — and animate.

view(s, { playerId }) {
  return { type: 'tabletop', scene: {
    zones: [{ id: 'market', layout: 'row', label: 'Market' }],
    objects: s.market.map((id) => ({
      id, kind: 'card', zone: 'market', enterFrom: 'deck',
      face: { title: CARDS[id].name, text: CARDS[id].text },
      click: { name: 'buy', args: { id } },
    })),
  }, hud: { log: s.log } };
}
3

Play anywhere

Run it in the browser against bots, or put it on a room server for real multiplayer. The client is identical; only the source of truth moves.

// local, no server
const match = createLocalMatch({ source, players: ['You', 'bot', 'bot'] });
// or online
const match = connectRoom({ io, url, room: 'AB12CD', name: 'Ada' });

<TablecraftGame client={match} />

Everything a tabletop needs

⚖️

Server-authoritative

Clients send intents; the rules run in a sandbox with per-call timeouts. No cheating by editing the page.

🎲

Physics that agrees

Dice are thrown with cannon-es and land on the face the server rolled. Tokens toss and stack; cards arc, tilt and flip.

🙈

Hidden information

view() is per player. Hands, decks and secret roles never reach the wrong browser.

🤖

Bots & referee

Export actions() and you get bots for free — plus a referee that plays hundreds of games and reports rule bugs.

🃏

Cards from data

Title, text, chips, corner badges and art. No art yet? A clean illustration is drawn for you until it exists.

🌍

Rooms you host

createRoomServer() on socket.io: lobby, seats, bots, sand timers, pacing. Your server, your data.

🎧

Sound & atmosphere

Procedural card, dice and coin sounds with room reverb, ambience and six lighting presets.

⚛️

React or not

<TablecraftGame>, <Tabletop> or mountGame(el) for any page. Themeable with CSS variables; English and Turkish built in.

✨

AI-ready

npx @table-craft/table-craft mcp lets Claude write a game, run the referee, audit every player's view and step through matches with you.

Drop it into your app

import { createLocalMatch } from '@table-craft/table-craft';
import { TablecraftGame } from '@table-craft/table-craft/react';
import '@table-craft/table-craft/style.css';
import source from './treasure-road.js?raw';

const match = createLocalMatch({ source, players: ['You', 'bot', 'bot'] });

export default function App() {
  return <TablecraftGame client={match} style={{ height: '100vh' }} />;
}
import { mountGame, createLocalMatch } from '@table-craft/table-craft/embed';
import '@table-craft/table-craft/style.css';

const source = await (await fetch('/games/treasure-road.js')).text();
mountGame(document.getElementById('table'), {
  client: createLocalMatch({ source, players: ['You', 'bot'] }),
  locale: 'en',
});
import fs from 'node:fs';
import { createServer } from 'node:http';
import { Server } from 'socket.io';
import { createRoomServer } from '@table-craft/table-craft/server';

const http = createServer();
const rooms = createRoomServer(new Server(http).of('/tablecraft'));
const code = rooms.createRoom({ source: fs.readFileSync('game.js', 'utf8') });

http.listen(3000, () => console.log('room', code));
// client: connectRoom({ io, url: 'http://localhost:3000', room: code, name })
import { simulate, summarize } from '@table-craft/table-craft/server';

const report = simulate(source, { games: 200 });
console.log(summarize(report));
// 200 games · 200 finished · 0 stuck · 0 hit the move cap · avg 18 moves
// …or every move actions() offered but move() rejected, stuck states,
//   invariant violations and broken scenes — with the game, move and args.

One source of truth

The rules run in exactly one place. Every player gets their own view; animations come from diffs, never from trust.

game.jssetup · moves · viewendIf · actions · bot HostMatch · sandbox · seeded RNGbots · pacing · sand timersroom server or your browser Ada's tableher hand, your card backs Bo's tablehis hand, her card backs Spectatoronly public information view + fx intents

Prototype tonight. Playtest tomorrow.

Tablecraft started inside an AI board-game studio, where hundreds of generated prototypes needed a table that was fair, readable and fun to watch. Now it is yours.