Poker, bluffing and social deduction, compiled end to end with XLA.
jit it, vmap it over thousands of tables, and collect hundreds of millions of samples per second.
BluffJAX is a suite of ten adversarial imperfect-information games for reinforcement learning research, written in pure JAX so the whole game runs on your accelerator. It has the well-studied benchmarks (Kuhn, Leduc and Texas Hold'em) and five games that have never been studied in RL before: Bluff, Kemps, multi-round Werewolf, 5-Card Draw and 7-Card Stud. Baselines are included as well: CFR, Deep CFR, PPO, PQN, NFSP variants and ReBeL.
aryamanreddi99.github.io/bluffjax: sit down at any of the ten tables and play against bots. There's nothing to install and no server. Each game's real BluffJAX reset, step_env and get_avail_actions are exported with jax.export to StableHLO and compiled to WebAssembly in your browser by whlo, so the rules you play by are the exact rules your agents train on. (The bots are simple heuristics, not trained policies. That part is up to you.)
![]() |
![]() |
| Pick a table | Bot 1 claims three 8s. Call bluff? |
- β‘ Fast. Game logic is written for XLA, so it jit-compiles, vectorizes and stays on the GPU. One RTX 6000 Ada reaches about 2Γ10βΈ samples/s, and four GPUs exceed 5Γ10βΈ.
- π Built around deception. Lying about your cards, signalling a partner under the noses of your opponents, hiding a role. These are the mechanics where today's RL baselines still struggle.
- π§© Simple.
make,reset,step,get_avail_actions. Episode resets happen insidestep, so a rollout is just alax.scan. - π Extensible. Every game subclasses a generic
AECEnvorParallelEnv, so adding a new one means writing the rules, not the plumbing. - π Reproducible. Baseline algorithms give you a fixed bar to measure against.
Click a game to play it.
![]() Kuhn Poker |
![]() Leduc Hold'em |
![]() Texas Limit Hold'em |
![]() Texas No-Limit Hold'em |
![]() 5-Card Draw |
![]() 7-Card Stud |
![]() Goofspiel |
![]() Werewolf |
![]() Bluff |
![]() Kemps |
| Game | ID | Players (n) | Obs. size | Actions | API | Metric |
|---|---|---|---|---|---|---|
| Kuhn Poker | kuhn_poker |
2 | 9 | 2 | AEC | exploitability |
| Leduc Hold'em | leduc_holdem |
2 | 24 | 3 | AEC | exploitability |
| Texas Limit Hold'em | texas_limit_holdem |
2β10 | 72+n | 4 | AEC | chips/hand |
| Texas No-Limit Hold'em | texas_nolimit_holdem |
2β10 | 74+n | 5 | AEC | chips/hand |
| 5-Card Draw π | five_card_draw |
2β10 | 67+7n | 37 | AEC | chips/hand |
| 7-Card Stud π | seven_card_stud |
2β10 | 76+262n | 4 | AEC | chips/hand |
| Goofspiel | goofspiel |
β₯2 | N(n+2)+n | N (13; num_cards) |
Parallel | win rate; exploitability for n = 2, N β€ 6 |
| Bluff π | bluff |
β₯2 | 270+54n | 13 | AEC | win rate |
| Werewolf π | werewolf |
β₯3, more humans than werewolves | 7+6n | n+1 | AEC | win rate |
| Kemps π | kemps |
4, 6, 8, β¦ | 104+nΒ·c | (170+n/2)Β·c | Parallel | win rate |
π not previously studied in RL. Sizes are for the default settings; c is the size of Kemps' communication channel (default 2).
The new games each test something different:
- 5-Card Draw: players swap cards with the house between betting rounds, so the deception moves from bet sizes to the draw.
- 7-Card Stud: hands are revealed card by card across several betting rounds, giving long horizons and dense public information.
- Bluff (a.k.a. Cheat, I Doubt It): race to empty your hand by playing cards face down. You may lie about them, but you must avoid getting caught over a long episode.
- Werewolf: the first JAX implementation of multi-round Werewolf with multiple roles (werewolves, seer, doctor, villagers).
- Kemps: teams of two try to collect four of a kind, then must signal their partner publicly without the opposing team noticing.
AEC vs. parallel. In turn-based (AEC) games, obs and action belong to the player to move. In simultaneous-move (parallel) games, both have a leading num_agents axis. In both cases reward is a vector with one entry per agent.
BluffJAX uses uv. It installs Python 3.12 and the exact dependency versions from uv.lock into .venv:
git clone https://github.com/AryamanReddi99/bluffjax.git
cd bluffjax
uv sync # CPU / Apple silicon
uv sync --extra cuda # or this, on Linux with an NVIDIA GPUThis is the example from the paper: 1,000 agent actions in 16 parallel environments, as a single jit-compiled function. Save it as example.py and run uv run python example.py.
import jax
import jax.numpy as jnp
import bluffjax
env = bluffjax.make("kuhn_poker") # Create an environment
rng = jax.random.key(seed=42)
rng, rng_reset = jax.random.split(rng)
rng_resets = jax.random.split(rng_reset, 16) # 16 parallel environments
state, obs = jax.vmap(env.reset)(rng_resets)
def get_action(rng, obs, avail):
# Your agent goes here. This one plays a uniformly random legal action.
return jax.random.categorical(rng, jnp.where(avail, 0.0, -1e9))
def step(carry, unused):
rng, state, obs = carry
rng, rng_act, rng_step = jax.random.split(rng, 3)
avail = jax.vmap(env.get_avail_actions)(state)
action = jax.vmap(get_action)(jax.random.split(rng_act, 16), obs, avail)
state, obs, rew, absorbing, done, info = jax.vmap(env.step)(
jax.random.split(rng_step, 16), state, action
)
return (rng, state, obs), rew
def step_scan(rng, state, obs):
return jax.lax.scan(step, (rng, state, obs), None, length=1000)
(_, final_state, final_obs), rew = jax.jit(step_scan)(rng, state, obs)The same code runs unchanged for all ten games. Swap the ID and go:
env.reset(rng)returns(state, obs).stateholds everything about the game: private hands, public cards, whose turn it is.env.step(rng, state, action)returns(state, obs, reward, absorbing, done, info). Whendoneis true, the returned state is already a fresh episode.absorbingmarks each agent that has reached an absorbing state (some players drop out early).doneis set when all agents have, or when the horizon is reached.env.get_avail_actions(state)returns the legal-action mask.
Games take keyword arguments to change the table size and difficulty:
print(bluffjax.available_envs())
env = bluffjax.make("bluff", num_agents=4, num_decks=2) # bigger table, double deck
env = bluffjax.make("texas_nolimit_holdem", num_agents=6, init_chips=200)
env = bluffjax.make("kemps", num_agents=6, comm_dim=4) # three teams, wider signal channelComing from another library? bluffjax/wrappers puts every game behind the JaxMARL and PettingZoo APIs. Install them with:
uv sync --extra wrappersJaxMARL. JaxMARLEnv is a JaxMARL MultiAgentEnv: observations, actions, rewards and dones are dicts keyed by agent, it is still pure JAX, and JaxMARL's own wrappers such as LogWrapper work on it.
import jax
import bluffjax
from bluffjax.wrappers import JaxMARLEnv
env = JaxMARLEnv(bluffjax.make("leduc_holdem"))
obs, state = env.reset(jax.random.key(0))
avail = env.get_avail_actions(state) # {"agent_0": mask, "agent_1": mask}
actions = {agent: avail[agent].argmax() for agent in env.agents}
obs, state, rewards, dones, infos = env.step(jax.random.key(1), state, actions)Turn-based games follow JaxMARL's Hanabi convention: every agent sends an action at every step, but only the action of the player to move is played. The others observe zeros and may only play the extra no-op (env.noop_action), so nobody sees another player's observation.
PettingZoo. PettingZooAECEnv wraps the turn-based games and PettingZooParallelEnv the simultaneous-move ones (also usable turn by turn through PettingZoo's parallel_to_aec). Observations are {"observation", "action_mask"} dicts of NumPy arrays, as in PettingZoo's classic games, and reset(seed=...) replays the same games for the same seed.
import numpy as np
import bluffjax
from bluffjax.wrappers import PettingZooAECEnv
env = PettingZooAECEnv(bluffjax.make("kuhn_poker"))
env.reset(seed=42)
for agent in env.agent_iter():
obs, reward, termination, truncation, info = env.last()
action = None if termination or truncation else np.flatnonzero(obs["action_mask"])[0]
env.step(action)Bluff's horizon cut-off shows up as a truncation: truncations in PettingZoo, infos["truncated"] in JaxMARL. bluffjax/wrappers/demo.py runs each wrapper with random legal actions: uv run --extra wrappers python bluffjax/wrappers/demo.py.
Throughput grows log-linearly with the number of parallel environments. At 10,000 environments every game produces at least 10β· samples/s on a single GPU, and the lightest exceed 10βΈ.
1000 random steps on an RTX 6000 Ada, mean of 10 seeds.
On the games other libraries share with BluffJAX, it matches or beats PGX, the GPU-based library. It is at least 10Γ faster than the CPU libraries (OpenSpiel, RLCard, PettingZoo) from 1,000 environments up, and it is faster even with no parallelism at all.
GPU: one RTX 6000 Ada. CPU: 16-core AMD Ryzen 9 with Python multiprocessing.
Scaling to multiple GPUs
Four GPUs give 2.3Γ the throughput on average (10,000 environments per GPU).
Training scripts live in bluffjax/examples/:
| CFR | Deep CFR | PPO | PQN | PPO-NFSP | PQN-NFSP | ReBeL | |
|---|---|---|---|---|---|---|---|
| Kuhn Poker | β | β | β | β | β | β | β |
| Leduc Hold'em | β | β | β | β | β | β | |
| Heads-up Limit / No-Limit Hold'em | β | β | β | ||||
| 5-Card Draw, 7-Card Stud, Werewolf, Bluff | β | β |
uv run --extra baselines python bluffjax/examples/kuhn/kuhn_cfr.py # tabular CFR
uv run --extra baselines python bluffjax/examples/kuhn/kuhn_ppo_nfsp.py # PPO-NFSP in self-play--extra baselines adds the training dependencies: distrax, optax, chex, hydra-core and wandb. Each training script reads its config_*.yaml through Hydra, so you can override any key on the command line. A script trains num_seeds seeds in parallel on one GPU with jax.vmap. Add wandb=true to log them to Weights & Biases, one run per seed, grouped by job. Set WANDB_MODE=offline to keep the runs local. Wherever you launch from, scripts that save checkpoints write them to checkpoints/<game>/, and Hydra writes its config and log to outputs/, both at the repo root and gitignored.
On the solved games, exploitability is measured exactly. The uniform random policy scores 0.4583 on Kuhn and 2.3736 on Leduc, matching OpenSpiel. Two-player Goofspiel with num_cards N β€ 6 also has an exact exploitability (in points, over every public state: 150 to 632,420 of them for N = 3 to 6), where uniform play scores (NΒ² β 1) / 6.
![]() |
![]() |
| Kuhn Poker | Leduc Hold'em (legend applies to both) |
Heads-up Texas Hold'em against a ReBeL opponent
![]() |
![]() |
| Limit | No-Limit |
@article{reddi2026bluffjax,
title = {BluffJAX: Adversarial Imperfect Information Games in JAX},
author = {Reddi, Aryaman and Peters, Jan and D'Eramo, Carlo},
journal = {arXiv preprint arXiv:2610.07686},
year = {2026}
}
















