Skip to content
AryamanReddi99Public

About

No description, website, or topics provided.

Resources

Stars

17 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

176 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

BluffJAX

Adversarial imperfect-information games in JAX.

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.

Play the demo arXiv 2610.07686 Python 3.12+ JAX 0.7.2

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.

πŸƒ Play it in your browser

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

The demo lobby with all ten games A hand of Bluff in the demo: call bluff or believe it
Pick a table Bot 1 claims three 8s. Call bluff?

✨ Why BluffJAX?

BluffJAX design criteria

  • ⚑ 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 inside step, so a rollout is just a lax.scan.
  • πŸ›  Extensible. Every game subclasses a generic AECEnv or ParallelEnv, so adding a new one means writing the rules, not the plumbing.
  • πŸ” Reproducible. Baseline algorithms give you a fixed bar to measure against.

🎲 The games

Click a game to play it.

Kuhn Poker
Kuhn Poker
Leduc Hold'em
Leduc Hold'em
Texas Limit Hold'em
Texas Limit Hold'em
Texas No-Limit Hold'em
Texas No-Limit Hold'em
5-Card Draw
5-Card Draw
7-Card Stud
7-Card Stud
Goofspiel
Goofspiel
Werewolf
Werewolf
Bluff
Bluff
Kemps
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.

πŸš€ Quick start

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 GPU

This 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). state holds everything about the game: private hands, public cards, whose turn it is.
  • env.step(rng, state, action) returns (state, obs, reward, absorbing, done, info). When done is true, the returned state is already a fresh episode.
  • absorbing marks each agent that has reached an absorbing state (some players drop out early). done is 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 channel

πŸ”Œ Wrappers

Coming from another library? bluffjax/wrappers puts every game behind the JaxMARL and PettingZoo APIs. Install them with:

uv sync --extra wrappers

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

⚑ Speed

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

Samples per second and peak memory against number of parallel environments, for all ten games
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.

Throughput of BluffJAX, PGX, OpenSpiel, RLCard and PettingZoo on Kuhn, Leduc, Limit and No-Limit Hold'em
GPU: one RTX 6000 Ada. CPU: 16-core AMD Ryzen 9 with Python multiprocessing.

Scaling to multiple GPUs

Throughput on one vs four RTX 6000 Ada GPUs
Four GPUs give 2.3Γ— the throughput on average (10,000 environments per GPU).

πŸ“ˆ Baselines

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.

Exploitability on Kuhn Poker Exploitability on Leduc Hold'em
Kuhn Poker Leduc Hold'em (legend applies to both)
Heads-up Texas Hold'em against a ReBeL opponent
Chips per hand in Limit Hold'em Chips per hand in No-Limit Hold'em
Limit No-Limit
Chips per hand against a ReBeL opponent trained for 5Γ—10⁢ samples. Each algorithm trains in self-play for 10⁷ samples; 10 seeds; dashed line = break-even.

πŸ“ Citation

@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}
}

About

No description, website, or topics provided.

Resources

Stars

17 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages