A deterministic, dependency-free no-limit Texas Hold'em state machine for Node.js.
The engine accepts serializable commands and returns a new authoritative state, domain events, or a typed error. It does not perform I/O, mutate input state, manage a database, or hide private cards for you unless you request a player-safe projection.
- Node.js 22 or newer
- Native ECMAScript modules
The published package contains compiled JavaScript and TypeScript declarations and has no runtime dependencies.
npm install @hivetech/poker-engineimport {
createShuffledDeck,
createTable,
getLegalActions,
transition,
} from "@hivetech/poker-engine";
let state = createTable({
smallBlind: 5,
bigBlind: 10,
minBuyIn: 1_000,
maxSeats: 6,
});
function dispatch(command) {
const result = transition(state, command);
if (!result.ok) {
console.error(result.error.code, result.error.message);
return false;
}
state = result.state;
for (const event of result.events) {
console.log(event);
}
return true;
}
dispatch({ type: "seat-player", playerId: "alice", stack: 1_000, seat: 0 });
dispatch({ type: "seat-player", playerId: "bob", stack: 1_000, seat: 1 });
dispatch({ type: "start-hand", deck: createShuffledDeck() });
const actor = state.hand.players.find(
(player) => player.seat === state.hand.currentActorSeat,
);
console.log(getLegalActions(state, actor.playerId));
dispatch({
type: "act",
playerId: actor.playerId,
action: { kind: "call" },
});transition leaves the input object untouched. A rejected command returns the exact input state:
type TransitionResult =
| { ok: true; state: TableState; events: readonly DomainEvent[] }
| { ok: false; state: TableState; error: EngineError };The TableCommand union supports:
seat-playerleave-playerset-sitting-outadd-chipsset-forced-betsstart-handact
Player actions are fold, check, call, bet-to, and raise-to. Bet and raise amounts always mean the player's final total commitment for the current street:
dispatch({
type: "act",
playerId: "alice",
action: { kind: "raise-to", amount: 60 },
});Use getLegalActions to obtain the exact call amount and permitted bet/raise range:
[
{ kind: "fold" },
{ kind: "call", amount: 20 },
{ kind: "raise-to", minAmount: 50, maxAmount: 980 },
];Normal invalid input—acting out of turn, checking into a bet, under-raising, or using an invalid stack—returns a stable EngineErrorCode. Corrupt or internally impossible state throws PokerEngineInvariantError.
start-hand requires a complete, unique 52-card deck. The first element is the next card dealt. This keeps the transition itself pure and makes replay exact.
Use createShuffledDeck() for a cryptographically shuffled Node.js deck, or supply a recorded/test deck:
dispatch({ type: "start-hand", deck: createShuffledDeck() });Commands—including each starting deck—are JSON-serializable. They can be replayed directly:
import { replayCommands } from "@hivetech/poker-engine";
const replay = replayCommands(tableConfig, commandLog);
if (!replay.ok) {
console.error(`Command ${replay.commandIndex} failed`, replay.error);
}The authoritative state is also JSON-serializable. It contains the deck and every hole card, so do not send it directly to untrusted clients.
Use projectTable and projectEvents at an application boundary:
import { projectEvents, projectTable } from "@hivetech/poker-engine";
const aliceView = projectTable(state, { kind: "player", playerId: "alice" });
const publicView = projectTable(state, { kind: "spectator" });
const aliceEvents = projectEvents(events, {
kind: "player",
playerId: "alice",
});Projections remove the deck and burned cards. During play, a player sees only their own hole cards and a spectator sees none. At showdown, non-folded hands are revealed; a hand won by folds remains hidden.
- Two through ten configurable seats, including sparse seating
- Correct heads-up button, blind, and action order
- Uniform ante or big-blind ante
- Partial forced bets and all-ins
- Minimum-bet and full-raise enforcement
- Short all-ins and cumulative action-reopening rules
- Main pots, nested side pots, unmatched-bet refunds, and folded-player eligibility
- Integer split-pot payouts, with odd chips awarded clockwise after the button
- Standard burns and automatic board runout when no decisions remain
- Waiting players, sitting out/in, mid-hand departure, and between-hand chip additions
- Dependency-free five-to-seven-card high-hand evaluation
The engine deliberately does not provide networking, persistence, bankrolls, timers, tournament blind schedules, straddles, run-it-twice, Omaha, or limit betting. A host application can change blinds and antes between hands with set-forced-bets.
Antes are posted before blinds. If a player's stack cannot cover a forced bet, the remaining stack is posted and the player is all-in.
Cards use compact rank and suit values:
import {
evaluateHand,
parseCard,
} from "@hivetech/poker-engine";
const result = evaluateHand(
"As Ks Qs Js Ts 2d 2c".split(" ").map(parseCard),
);
console.log(result.category); // "straight-flush"
console.log(result.tiebreak); // [14]Ranks are 2 through 9, T, J, Q, K, and A. Suits are c, d, h, and s.
npm ci
npm run typecheck
npm test
npm pack --dry-runThe test suite exhaustively verifies all 2,598,960 five-card combinations, exercises generated legal-action sequences, checks chip conservation and replay, and includes regressions for the original repository's payout, all-in, TypeScript, and packaging reports.
See MIGRATION.md when moving from @chevtek/poker-engine.