Card Isomorphism — Free 24× Speedup

Why suits don't matter (until they do) and how poker solvers exploit the symmetry. Back to index
Suit permutations4! = 24
Distinct preflop hands1,326 → 169
Distinct flop boards22,100 → 1,755
DeepStack training cost saved~24×

Section 1 · The big idea

Suits are interchangeable except for flush considerations

You hold Ah Kh. The flop comes 9h 7h 2h — you flopped a flush. Now imagine the identical sequence with all hearts replaced by spades:

Hand A Ah Kh vs board 9h 7h 2h

Hand B As Ks vs board 9s 7s 2s

These play strategically identically. The same equity, the same optimal bet sizes, the same EV. Only the labels differ. So when a poker solver computes the equilibrium for hand A, it gets the answer for hand B for free.

Card isomorphism is the technique of recognizing this symmetry and computing only one canonical representative per equivalence class. For HUNL, this is roughly a 24× compute reduction with zero accuracy loss.

Section 2 · Equivalence classes — visualized

The same hand under all 4 suit assignments

Below: As Kh on flop 9d 7c 2s — and three of its suit-permuted equivalents. All four configurations have identical equity and optimal play.

Original
AsKh | 9d7c2s
Permutation 1
AhKs | 9c7d2h
Permutation 2
AdKc | 9s7h2d
Permutation 3
AcKd | 9h7s2c

There are 24 total suit permutations of this configuration — 4! ways to relabel {♠ ♥ ♦ ♣}. Every permutation has the same equity vs every other hand, the same equilibrium strategy, the same EV. A solver that exploits this only needs to solve one of the 24.

Section 3 · Try it — interactive canonicalizer

Pick a hand + board. See the canonical form and equivalence class size.

Your hand & board
Hole cards -- --
Flop -- -- --
Turn --
River --
Click a slot above, then pick a card. Click a filled slot to clear it.
Canonical form & equivalence class
Your input
— pick cards to begin
Canonical form
Equivalence class size
All equivalent forms
What you're seeing. The canonical form is the lex-smallest representation under any permutation of the 4 suits. Two different inputs that map to the same canonical form are guaranteed to play identically (same equity, same strategy, same EV). The "class size" is how many specific suit-assignments exist for a configuration with this exact rank-and-suit-pattern structure.
Try this. Pick As Ks on a Qs Js Ts board (suited to the same suit). Class size is much smaller (4) because the "3 hole cards on this board can't be the same suit" constraint is broken by some permutations. Now try As Kh on Qd Jc Tc (rainbow → max diversity) — class size hits the maximum 24.

Section 4 · The compute savings

What this means for poker solvers

ScopeWithout isomorphismWith isomorphismSpeedup
Distinct preflop hand types 1,326 169 ~7.8×
Distinct flop boards (3 cards from 50) 22,100 1,755 ~12.6×
Distinct (hand, flop) pairs ~25M ~1.3M ~19×
Distinct turn boards (4 cards from 49) ~270K ~16K ~17×
Distinct river boards (5 cards from 48) ~2.6M ~123K ~21×
Full HUNL game tree (effective) ~10160 states ~10156–10158 ~24×

The 24× factor isn't constant — it's the upper bound (when all 4 suits are used distinguishably). Flushy and paired boards have smaller equivalence classes, so you save less. Across a representative HUNL workload, the average is roughly 18–22×.

Section 5 · When isomorphism breaks down

Not every "looks similar" situation is actually isomorphic

Iso only works when the suit relabeling preserves all strategic structure. The classic gotcha: flush-relevant relationships break when relabeling.

Two configurations that look similar but are NOT isomorphic
Config A Ah Kh on board 9h 7h 2s — flush draw alive
Config B Ah Ks on board 9h 7h 2s — no flush draw

Config A's hero has 4 hearts → drawing to a flush. Config B's hero has only 1 heart → no flush draw. These play very differently on subsequent streets, so they're not in the same equivalence class.

The interactive canonicalizer above handles this correctly — try toggling between them and watch the class size change.

Section 6 · How it's implemented

Canonicalize, hash, deduplicate

The standard recipe in any production solver:

// 1. Pick a canonical ordering of suits
function canonicalize(cards) {
  let best = null;
  // Try all 24 suit permutations
  for (const perm of allSuitPermutations()) {
    const transformed = cards.map(c => ({
      rank: c.rank,
      suit: perm[c.suit]
    }));
    const repr = stringify(transformed);
    if (best === null || repr < best) best = repr;
  }
  return best;
}

// 2. Cache solver results by canonical key
const cache = new Map();
function solveSpot(cards) {
  const key = canonicalize(cards);
  if (cache.has(key)) return cache.get(key);
  const result = runCFR(cards);  // expensive — runs once per canonical class
  cache.set(key, result);
  return result;
}

// 3. When user asks about non-canonical input, look up by canonical form,
//    then apply the inverse permutation to the strategy

That's it. Most production poker solvers (postflop-solver, OpenSpiel's universal_poker, libratus open-source forks) ship this as library code — you fork it, you don't write it from scratch.

The subtlety. "Apply the inverse permutation" matters because the user wants to see strategies in their hand's suits, not the canonical ones. The canonicalizer must remember the permutation that mapped input → canonical, so the solver's output can be relabeled back when displayed.

Section 7 · Why DeepStack training needs this

The difference between feasible and infeasible

DeepStack's training pipeline generates 10 million turn examples, each requiring one CFR solve. Without isomorphism:

With isomorphism applied at every layer (hand canonicalization, board canonicalization, range vector indexing):

Bottom line. Card isomorphism isn't an optimization — it's the optimization that turns DeepStack-style training from "won't fit in any compute budget" to "feasible on a small GPU cluster." Every production poker solver uses it. Your runtime solver should too.