User Guide

How to play. Everything happens on a single page — the table is rendered by index.php and updates itself automatically.

Register & log in

  1. Click an empty seat or the Sit prompt to open the auth dialog.
  2. Use the Register tab: choose a username (3–16 chars), password, country and avatar.
  3. New accounts start with the configured starting stack (default 1000 chips).
  4. Returning players use the Login tab. Use the Logout button (top bar) when done.
Register / login dialog

Sitting at a table

Sit out, away, and removal

Changing the blinds

Any seated player can change the table blinds by clicking the ⚙️ Blinds button in the top bar (visible only when logged in). The new blinds apply to new hands only — the current hand finishes with the old blinds. The change affects all players at that table.

The admin can take this away with POKER_BLINDS_BY_PLAYERS = false: the button then disappears, and a stale page that still shows it gets Changing the blinds is disabled. The admin observer can always change the blinds. See Admin & Settings.

The felt adapts to the number of players

The seats are not tied to fixed table positions: the players who are actually seated share the ring evenly, so two players sit opposite each other, three form a triangle, four a diamond, and so on. Your own seat is always the bottom centre and the order runs clockwise. The chip piles, the $ amounts and the D / SB / BB discs are all derived from the seat position, so they follow the same layout automatically. A Sit button is never drawn on top of a player: when the table is nearly full the players keep the classic nine slots, which looks like a live table with one empty chair.

Adding bots (for local testing)

Registered players can click 🤖 Add bot to seat a bot opponent in an empty seat. Bots use preflop GTO ranges and Monte Carlo equity, so a table is never empty. Two switches decide whether that button exists: POKER_BOTS_ALLOWED (bots may sit at the table at all) and POKER_BOTS_ADD_BY_PLAYERS (players, not just the admin, may seat them). Both are true by default; see Configuration or the Bots tab of Admin & Settings.

Bot personalities

Every bot draws a personality when it sits down, from its own id, and keeps it until it leaves the table. Nobody chooses it for them and it is not shown on the felt — you read it from how they play.

StyleHow it playsHow to handle it
TAG
40%
Balanced and aggressive: enters with good hands and raises when it does. The solid default; the hardest to exploit.
Rock
25%
Tight and passive: fewer hands, smaller opens, folds more often. A Rock raising usually has a real hand — steal its blinds instead.
LAG
25%
Loose and aggressive: more hands, more raises and more bluffs. Wait for a real hand and let it bet into you.
Station
10%
Loose and passive: plays too many hands and mostly calls. Never bluff it; bet only for value.

The difference is measurable: first-in raises come at about 18% for a Rock, 24% for a TAG and 34% for a LAG, while a Station calls a single open roughly three times as often as a TAG.

Thinking time follows the style: a LAG answers quickly, a Rock or a Station takes longer, any bot waits a little more when the decision is close to the price (call or fold at the pot odds) and snaps when there is nothing to decide (a free check). Everything stays under 4.5 seconds, and around four bots in ten also buy a bigger Monte-Carlo sample on borderline spots. At a two-player table every bot widens up and attacks more, as heads-up poker requires.

One switch turns all of it off: POKER_BOT_STYLES_ENABLED in Configuration (or the Bots tab of Admin & Settings). With it off every bot plays the identical baseline engine again — same ranges, same sizing, same fixed 1.5–2.8 s wait.

Removing bots

To remove a bot from the table, click on its seat. A confirmation modal appears; confirm and the bot leaves when the current hand ends (it folds immediately, then its seat is freed).

Multiple rooms

Each room has its own chat, hand history, and table state. To join a specific room, open index.php#RoomName in your browser.

Watching as a spectator

If you don't log in, you can still open index.php and watch the table. Spectators see the same table, chat, and animations — but cannot sit, add bots, change blinds, or write in chat. You can pick a theme from the top bar; the choice is stored in a first-party cookie so it survives a reload.

Disconnect, timeouts, and auto-rebuy

All-in showdown

When two or more players are all-in, all hole cards are revealed and a live win probability % is displayed above each seat. The board cards are dealt one by one with a short pause between streets. Under the pot you see the name of your current best hand (e.g. "Pair of Sevens") updated on every street.

At showdown the winning hand is named the way a live room does it — with the cards that broke the tie, called the kickers: Pair of Sevens, Ace kicker, Ace high, King kicker, Two pair, Kings and Sevens, Ace kicker. A kicker can be any rank (also a small card: Pair of Twos, Five kicker). Flush, straight, full house and straight flush keep their classic names, because there the whole five cards decide the hand. The same wording is used on the felt, in the game log and in the hand replay.

When somebody goes all-in for less than the others bet, the centre of the table shows the pots separately: a MAIN stack with its amount and, next to it, one or more SIDE stacks — each with its own chips and its own total. The main pot is the one every player in the hand paid into (limited by the shortest all-in); the side pots hold the rest and only the players who covered them can win them. A plain bet/raise difference (nobody all-in) is not a side pot: it stays one single stack. The result line lists the winners with their amounts, e.g. 🏆 Bot Radu wins $1803 · poker wins $31986.

While an all-in run-out is in progress the betting is closed, so no action buttons are shown — the action bar only says ⏳ ALL-IN — running out the board…. If you are all-in yourself it says ALL-IN — waiting for result., and a player with no chips left simply cannot act (the server rejects it with “You are all-in”). Only the players who still have chips and are not all-in keep their FOLD / CHECK / CALL / RAISE buttons.

Rabbit Hunt (fold-out reveal)

When a hand ends early because everybody folded and only one player is left, the table shows the cards that would have come next — the classic "rabbit hunt" from real card rooms. The cards fly out of the deck on the table, flip over one by one and stay visible for about 3.5 seconds before they fade out. It is a visual extra only: the pot is already awarded and the result never changes.

Hand ended onCards revealed
Preflop (everybody folded before the flop)5 — the would-be flop, turn and river
Flop2 — the would-be turn and river
Turn1 — the would-be river
RiverNothing — there are no cards left to come

Hand strength meter

Under your hand name on the felt (below the board) there is a thin hand-strength bar: a live 0–100% reading of how strong your current hand is against the players still in the pot. It is calculated in the browser (Monte Carlo against random hands, cached until something changes), fills red below 35%, green above 65%, and hides itself when you are not in the hand, after you fold, between hands and during an all-in showdown — there the win-% badges above the seats take over. Turn the whole feature off with POKER_HAND_METER_ENABLED (Configuration).

Playing a hand

Playing a hand — action buttons, slider and presets

Pre-actions

Before it is your turn you can queue an action so the game keeps flowing: Check/Fold or Call any. The pre-action is applied automatically when the action reaches you.

Fair deal & card randomness

The deck is created and shuffled on the server, in PHP, once per hand. The shuffle uses PHP's cryptographic random number generator random_int() — the same class of generator Node.js exposes as crypto.randomInt() — driven by a Fisher–Yates (Durstenfeld) loop:

function deck_new(): array {
  // the 52 cards, in a fixed order
}

function shuffle_arr(array &$a): void {
  for ($i = count($a) - 1; $i > 0; $i--) {
    $j = random_int(0, $i);          // CSPRNG, not rand()/mt_rand()
    if ($j !== $i) { $t = $a[$i]; $a[$i] = $a[$j]; $a[$j] = $t; }
  }
}

How to verify it yourself

The distribution was measured with a standalone CLI script — a developer tool that is not shipped with the package — which copies the two functions above verbatim and shuffles a full deck 200,000 times. From your own copy, outside the web root:

php rng_test.php 200000

The production code contains no test hooks and no way to force a deck. Randomness is also not configurable — it is always random_int(), see Configuration.

Chat & emoji

Seated players can talk in the Chat tab of the side panel (emoji button included). Messages are moderated by the banned-word list and rate-limited to prevent flooding.

Profile (avatar, country, theme)

Open the Profile dialog from the top bar to change your avatar, country flag and theme. The choice is stored in your account and applied on your next page load. Guests (spectators) can pick a theme from the top bar; it is kept in a first-party cookie.

Hand history & replay

Use the 📜 Hands tab to list your recent hands. Select one to replay it street by street, including showdown equity for all-in situations.

Hand history replay with board and equity

Table info (top-left)

In the top-left corner of the table a small badge shows how many players are seated (👤 3 online) and — only while at least one bot is seated — how many of them are bots (🤖 2 bots). It is public: the winner, the other players and spectators all see the same numbers, and it updates on every hand. Switch the whole badge off with POKER_TABLE_INFO_ENABLED in Configuration.

Mobile

The layout is responsive and a dedicated mobile stylesheet loads on small screens. The side panel (chat / game info) collapses, and the action buttons move to the bottom for thumb access.

Sound

Game sounds (cards, chips, fold/call/raise) play on first user interaction, as required by browsers. Mute the tab or the device to silence them.

Sounds

The top bar has a 🔊 Sound ON / 🔇 Sound OFF toggle. The choice is saved in your browser (localStorage) and survives reloads. All sounds play only after your first interaction with the page, as required by modern browsers.

Game log & hand list

The side panel on the right has three tabs:

On mobile, use the 💬 tab on the left side of the screen to open/close the panel.

Admin: observer, kick and bans

Open admin.php with the admin password (configured as a bcrypt hash in $ADMIN_HASH at the top of the file; the password itself is never stored). It renders the real table — same cards, chips, dealer button and animations — with every player's hole cards visible. The top bar links to ⚙️ Settings (?cfg=1, the config editor — documented in Admin & Settings) and back to the game.

Players panel

On the right side of the observer page, the Players panel lists the human players at the current room, each with their IP and stack. Bots are not listed there — in the observer, click a bot's seat on the table and confirm, exactly as a seated player does. If no hand is running the bot leaves immediately; if a hand is running it plays that hand to the end and its seat is freed the moment the hand is over, without being dealt into the next one. The player list refreshes when the page opens, on ⟳, and every 15 seconds.

Bans list

Below the player list, the Bans section shows the active bans, newest first, limited to the 10 most recent — the header reads latest 10 when more exist. Each row is one ban: username · IP, the date and time it was issued, and a live countdown (2d 04h 22m 07s; permanent bans read permanently).

What a banned player sees

The table stays loaded, but every request is refused server-side and the server's message — the remaining time and the reason — is shown large in the middle of the screen, for example Banned for 9m 58s on one line and Reason: abusive chat on the next. Closing the page, reloading or registering another account from the same address does not help while the IP is banned. When the timer reaches zero the ban expires and the table starts responding again, with no reload needed.

Bans and kick deadlines are kept in data/bans.json — no database. Note that an IP ban also covers everyone sharing that address (home, office, mobile carrier), and that changing IP bypasses the IP part while the username ban stays in force.