Skip to content

Repository files navigation

@hivetech/poker-engine

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.

Requirements

  • Node.js 22 or newer
  • Native ECMAScript modules

The published package contains compiled JavaScript and TypeScript declarations and has no runtime dependencies.

Install

npm install @hivetech/poker-engine

Quick start

import {
  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 };

Commands

The TableCommand union supports:

  • seat-player
  • leave-player
  • set-sitting-out
  • add-chips
  • set-forced-bets
  • start-hand
  • act

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.

Deterministic dealing and replay

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.

Safe player and spectator views

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.

Table behavior

  • 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 and hand evaluation

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.

Development

npm ci
npm run typecheck
npm test
npm pack --dry-run

The 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.

License

MIT

About

No description, website, or topics provided.

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages