Belote AI

Paused

Game-playing agents for Belote on OpenSpiel, built as a ladder of measured rungs: hand-built rules, Monte Carlo search, networks that replace the search, and a language model at the table.

Started
July 24, 2026
Phases
9 of 10 done
Repository
GitHub ↗

Highlights

Overview

Browser front end mid-deal: the current trick, the player's six-card hand, and the agent's evaluation of each legal card as a bar chart, with J of hearts best at +32.2 points per deal
Playing the strongest agent in a browser. Before you choose, the Monte Carlo agent shows what each of your legal cards is worth, in points per deal.

Game-playing agents for Belote, a four-player French trick-taking card game, built on OpenSpiel. The project is a ladder: each rung asks one question, answers it with a measured comparison, and hands the result to the next, from fixed rules through Monte Carlo search to neural networks and a local language model.

The strongest card play contains no network. One step of Monte Carlo policy improvement over a hand-written rule is the largest gain on the ladder, and the networks buy speed rather than strength: a search-free agent ties that search at 148x the speed. Building the game’s resampler also surfaced a seeding bug in OpenSpiel’s own ISMCTS, fixed upstream in open_spiel#1584.

Problem

  • Belote is not an OpenSpiel game. There is no engine to build agents on, and “Belote” is many rule sets: this is classic à la retourne, with a take-or-pass auction, not Coinchée or Contrée.
  • One deal swings by roughly ±162 points, and not like a bell curve. The all-or-nothing contract piles margins up at the extremes: one dealer orientation alone has a standard deviation of 123 points a deal, so “I won fifty deals” carries almost no information.
  • The hands are hidden. A search agent has to reason over worlds consistent with what its seat actually knows, never over the real deal.
  • Bidding and card play confound each other. A margin between two agents that differ in both halves can be attributed to neither, and a search agent that searches every action silently bids by search too.
  • A language model is too slow for a match. At 7.7 to 16.7 seconds a decision, the thousands of deals a 5-point comparison needs are out of reach.

Approach

Every claim of the form A beats B is a paired match: each deal is played twice with the dealer swapped, the margin carries a 95% confidence interval, and the deal count is solved in advance to detect 5 points a deal at 80% power. An interval containing zero is reported as a tie, which is a bound (smaller than about five points), not a claim of equality. A comparison meant to isolate card play pins bidding, and a guard verifies it rather than trusting the configuration.

Three instruments carry that rule to players of very different cost: matches for whole agents, decision grading for a player too slow to play hundreds of deals, and a judge that screens a card-play change too small for a match, as a paired difference at the same stored positions. Every rung writes its configuration, rows and findings to results/ladder/, so each number on this page traces to a record in the repo.

What I built

The agents the ladder converged on:

AgentRoleResultCost
BidNet + Monte CarloStrongest agent+5.50 over Monte Carlo with rule bidding~120 ms/deal
BidNet + PlayNetFastest strong agentLevel with Monte Carlo, -0.63 [-2.06, +0.81]~0.9 ms/deal
Monte Carlo, 32 worldsStrongest card play+38.68 over the best rule, win_or_dump~120 ms/deal
BidNetBidding+5.59 over the best rule, hand_strength 62+

The gameDoneBelote as an OpenSpiel game, its scoring and resampler checked against the public API.

GoalImplement Belote as an OpenSpiel game whose rules, scoring and resampling behave exactly as the agents built on it assume.

python_belote lives in a fork of OpenSpiel: chance nodes deal the cards, the framework enforces imperfect information, and one flat action space of 38 actions covers the auction and card play. The fork carries 42 rule tests; the ladder’s own first rung checks the contract with the agents instead: returns zero-sum over 200 deals, trick points exactly 162 or 252, and resampled worlds that keep the actor’s hand, never resurrect a played card and respect every inferred void.

Unit of play

Chose
One deal is one game, returning the signed point margin
Instead of
A race to 501 or 1000 across deals
Why
Every result becomes a mean of independent per-deal margins, and the game is zero-sum by construction.

Declarations

Chose
Belote-rebelote only, granted automatically
Instead of
Announcement actions, plus tierce, cinquante, cent and carré
Why
Keeps the deck worth a fixed 162 (252 on a capot), which every confidence interval depends on, without adding a bluffing action.

Dealer

Chose
A game parameter that rotates on each redeal
Instead of
A fixed first seat
Why
It is what makes dealer-swapped paired matches possible, the most important measurement device in the project.
MeasurementDonePairing removes 28% more noise than averaging, and a true null reads as a tie.

GoalBuild the instrument before any agent worth pointing it at, and prove it on cases where the right answer is known.

Who deals decides who bids first, which is worth real points, so an unpaired comparison partly measures seat luck. The fix borrows duplicate bridge’s trick:

One seeded shuffle

the identical cards are dealt both times

play it twice, dealer rotated one seat

Dealer at seat 0

one team bids first: this orientation's margin

Dealer at seat 1

the other team bids first: the mirror margin

average the two margins

Paired margin

seat-order advantage cancels out

The rung measures the instrument with bots too simple to have opinions:

CheckResult
Spread, one dealer orientationsd 123.0
Spread expected from averaging two draws alonesd 87.0
Spread, dealer-swapped pairsd 63.0, 28% tighter than averaging
Random vs random, a known null-0.04 [-3.52, +3.45] over 1,199 deals: tie
Sized to detect 5 points a deal1,153 deals, achieved half-width 3.54
Bidding guard, always-take vs always-passcaught at deal 0
Hand-built rulesDoneRules beat random by +12.6 a deal, and counting trumps beats counting points by +4.7.

GoalFind the strongest player that can be written by hand, with bidding and card play tuned separately and in the right order.

The first agent splits into two independent halves, a bid rule and a card-play rule, so every later comparison can freeze one and vary the other. That split paid off immediately: the whole agent beats random by +12.55, but isolated, bidding carries +20.86 while playing the highest-value card loses to random card play by 3.84.

Card play was ranked first, bidding pinned, against highest_value:

win_or_dump
+45.26 pts/deal[+41.92, +48.60]
help_partner
+40.98 pts/deal[+37.66, +44.30]
conserve_trump_on_lead
+34.48 pts/deal[+31.16, +37.81]
lowest_value
+17.54 pts/deal[+14.00, +21.08]
random card play
+1.85 pts/deal[-1.58, +5.29]

Only then was the bid threshold tuned, against win_or_dump rather than against random. A second bid rule, hand_strength, adds trump length, honours and voids to raw card points, and beats the tuned points rule by +4.73 [+1.28, +8.19].

Monte CarloDoneOne step of policy improvement over the best rule, no training: +38.7 a deal.

GoalTest whether one step of policy improvement over the best rule beats that rule, with no network, no tree and no training.

At each real choice the agent samples complete deals consistent with what its seat knows, plays every legal card out in each under win_or_dump, and keeps the best average. Two details do most of the work:

  • Common random numbers. Every candidate card is scored on the same worlds, the pairing trick moved inside the agent.
  • Forced decisions are skipped. About a third of card-play decisions have one legal card.

Against win_or_dump, bidding pinned on both sides:

4 worlds (14.5 ms/deal)
+26.03 pts/deal[+22.52, +29.54]
8 worlds (28.9 ms/deal)
+27.44 pts/deal[+23.95, +30.94]
16 worlds (56.3 ms/deal)
+33.44 pts/deal[+29.91, +36.97]
32 worlds (114.2 ms/deal)
+38.68 pts/deal[+35.18, +42.19]

Four worlds already capture 67% of the 32-world margin, so most of the gain is the method. But every smaller budget also loses to 32 head to head, so 32 worlds is a floor, not an optimum, at 385x the rule’s cost.

PlayNetDone86% of Monte Carlo's margin at 1/152 of the cost; no lever closes the last 5.17 points.

GoalAmortize the Monte Carlo agent into a network fast enough to play against, and find out what limits the copy.

MCPlayBot.evaluate() already values every legal card, so 25,000 played deals yield 846,086 labels for free. PlayNet predicts each card’s advantage over the average legal card, on a trump-relative encoding, with the split grouped by deal because ~34 decisions share each deal’s outcome.

Input (145)

132 for the position, 13 for the candidate card

Hidden 1 (256)

Linear, ReLU

Hidden 2 (128)

Linear, ReLU

Output (1)

the card's advantage, in points

Chosen over 128 × 64 by held-out R² (+0.411 against +0.405). Capacity is not the limit.

It beats win_or_dump by +33.81 against the search’s +39.12, at 0.79 ms a deal against 120, and loses to the search head to head, -5.17 [-6.59, -3.74]. Every lever on that gap was measured:

LeverResult
128 label worlds instead of 32+0.018 R²
Twice the data+1.14 in play, tie
Who played each card-0.76 in play, tie
A listwise ranking loss-0.08 [-0.63, +0.46], tie
As the search’s rollout policy+1.48, tie, at 1.4x the cost
A value net as the search’s leaf evaluator-4.03 at depth 8, -23.29 at depth 0
BidNet and the search-free agentDoneBidNet out-bids the rule by +5.59; with PlayNet it ties Monte Carlo at 148x the speed.

GoalReplace the hand-written bid rule with a network, then assemble both networks into an agent with no search at run time.

The auction has no teacher, so each bid’s value is measured by playing it out. Every legal bid is cloned and played to the end with PlayNet, in 24 re-dealt worlds consistent with the bidder’s hand, on the same worlds for every bid. Resampling cuts label noise within a trump-length bucket from 147.2 to 46.5, and the labels read the way a player would expect:

Round-1 trump lengthTake minus pass
0-2-54.9
3+28.1
4++77.2
BidNet vs rule, PlayNet playing (4,695 deals)
+5.59 pts/deal[+4.18, +6.99]
BidNet vs rule, Monte Carlo playing (6,000 deals)
+5.50 pts/deal[+4.05, +6.96]
BidNet + PlayNet vs Monte Carlo agent (6,000 deals)
-0.63 pts/deal[-2.06, +0.81]

BidNet is also less selective than the rule (0.20 redeals a deal against 0.55), so its margin is not bought by declining to play.

Decision gradingDoneOne decision becomes one measurement: 411 positions rank every player in match order.

GoalMeasure a player one decision at a time, so that a player too slow or too costly for a match lands on a scale that already means something.

Monte Carlo at 128 worlds values every legal card at a position, so a choice is scored exactly: cost = best card’s value - chosen card’s value. 411 decisions from 40 deals, each stored with the action history that rebuilds the state, grade every player already on the ladder:

PlayerPoints given up per decisionPicks a best card
Random legal card8.2542.6%
win_or_dump4.0161.6%
PlayNet2.6971.3%
Monte Carlo, 32 worlds0.9283.2%
The teacher, 128 worlds0.4886.1%

A player can also state claims, checked at three levels: public (“my partner is winning”), hidden (“an opponent holds the ace”) and future (“this ace takes the trick”). The checker is tested against the hands it must not look at, and agrees on all 120 positions.

A language model at the tableDoneA local 12B model gives up more per decision than a random card, and loses by 35.9 a deal.

GoalPut a local language model on the same scale as every other player: first graded one decision at a time, then over whole deals.

gemma4:12b, served by Ollama, sees only what its seat can see and answers with a card, a reason and any claims, under a schema that deliberately does not enumerate the legal cards. On 150 graded decisions it gives up 9.28 points per decision against 6.13 for a random legal card, and thinking first does not help.

A LangGraph pipeline then hands it the counts it got wrong (trumps out, master cards, what beats the trick) and splits the decision into four steps:

One callFour steps
Points given up per decision9.159.23
“My side wins this trick”, correct62.1%78.7%
Model calls / seconds per decision1 / 7.73 / 24.2

It describes the position better and chooses no better: what limits it is judgement, not information. Over 24 whole deals against Monte Carlo it loses -35.92 [-52.54, -19.29] a deal.

Playing the agentDoneA person plays BidNet + Monte Carlo in a browser, then reviews and replays every decision.

GoalPut a person at the table against the strongest agent, the only opponent from outside the project that any agent here faces.

The browser page is a view over the real python_belote state served from Python, not a JavaScript copy of the rules, so a deal played there is the same game the matches measure. A terminal version plays the same agent, and both keep a running record of the human’s margin, with its interval.

Post-deal review showing the contract verdict, all four dealt hands, and an eight-trick grid of who played which card, with trick winners outlined
After the deal: the verdict, every hand as dealt, and every trick. The deal can be saved to a file and reopened later.
Card-by-card replay at trick 3, showing each seat's remaining hand and the evaluation of the J of hearts decision, identical to the live evaluation
The replay steps through the deal a card at a time, with each seat's remaining hand and the evaluation made at that moment: the same +32.2 shown live, not recomputed.
Larger models and a new harnessPlannedTarget: a language model that gives up less per decision than a random legal card.

GoalFind out whether what holds the language model back is its size or the way it is asked, measured on the same 150 graded decisions and then in whole deals.

The 12B model reads the board well and chooses badly, and handing it more facts changed nothing, so the next steps test judgement rather than information. The bar is fixed in advance: 6.13 points given up per decision, a random legal card on the same positions.

  • Larger models. gemma4:26b and the frontier models, graded first, then in whole deals. That includes whether a stronger model forms beliefs about the hidden hands; the 12B model made 3 reads beyond what the play proves in 150 decisions.
  • A harness aimed at the choice. The model proposes a few candidate cards for Monte Carlo to check, or plays through a tool-using agent loop, instead of receiving more facts.
  • Bidding by the model. A rule bids for it today. Take-or-pass on five cards is a smaller test of judgement, and decision grading extends to it.

Tech stack

What each piece of the stack is actually doing, and where to look for more:

Languages

  • Python

    The agents, the training, the measurement layer and both front ends.

AI/ML

  • OpenSpiel

    Belote is python_belote in a fork of OpenSpiel, with a C++ twin verified identical over 40 seeded deals. The game's resampler is what every search agent stands on.

  • PyTorch

    PlayNet (145 → 256 → 128 → 1) and BidNet (81 → 128 → 64 → 1), trained on Monte Carlo labels with train/validation splits grouped by deal.

  • LangGraph

    The four-step pipeline (facts computed in code, then infer, evaluate, choose) that tests whether decomposing a decision helps the language model.

  • Ollama

    Serves gemma4:12b locally for the language-model rungs; the MLX build runs 1.6x faster than GGUF for the same weights.

Tooling

  • pytest

    Statistics, the game-contract checks and the front ends, alongside the fork's own 42 rule tests.

  • uv

    Dependency management, with the OpenSpiel fork installed as an editable path dependency.

Future improvements

The game, the hand-built player and the networks are settled; the language model’s next steps are the planned phase above. Beyond those, these are the known gaps, and the repo’s docs list each part’s measured dead ends alongside them:

  • Worlds sampled from a model of the hidden hands. Monte Carlo draws the opponents’ hands uniformly from every deal consistent with the play and ignores what the auction and discards say. A network predicting who holds which card would make a stronger teacher without changing anything downstream; Monte Carlo that sees the real deal bounds what it can buy, and is not yet measured.
  • Partner signalling. No agent here signals on purpose, so tactical plays like an appel (calling for a suit through a discard) or an impasse (a finesse) are neither produced nor measured.