Source: https://oliviercouthaud.com/durak · Status: Prototype · 2026 · Tags: Python, PyTorch, Pygame, MCTS # Durak Durak, the Russian card game: a Python version I built with a search-based AI opponent (not public), and my ongoing work on teaching a neural network to play it by itself. Figure: Diagram. One round of Durak. The version I built plays this with two to four players. ## The problem I've played Durak, though not recently, and it made me curious whether an AI could beat my friends, my family or me. I haven't measured that yet, so what follows is what I built and what the experiments show so far. Durak hides information. You see your own hand, the table and the discard pile, and you know how many cards the other player holds, but not which. That rules out searching the whole game tree the way you would in chess. I wanted a computer opponent that plays properly under that uncertainty, and then I wanted to see whether a neural network could learn to do the same without being handed rules of thumb. The project went in three steps: a search player, a network cloned from my computer players (mostly the rule-based medium one), and then AlphaZero-style self-play loops where the network learns only from wins and losses. Around the AI is the game itself. The rules engine handles two to four players, there is a terminal version and a Pygame window (with an optional rule that lets the defender pass an attack on, called perevodnoy), and an advisor for playing with real cards: you type in your hand and the table and it tells you what to play. It is all Python, written over about a week in March 2026. The training only covers two-player games. ## How it works The search AI is Perfect Information Monte Carlo (PIMC). It samples possible opponent hands that are consistent with what it has seen, runs an alpha-beta search on each sampled world with a hand-written evaluation, and plays the move with the best average score. Before searching it ranks the moves with a heuristic and only searches the top three to five. The levels are easy (random legal moves), medium (greedy rules), hard (25 worlds, depth 4) and expert (300 worlds, depth 6). Once the deck is empty the hidden cards follow by elimination, so there is only one possible world and it searches four plies deeper instead. **What is Perfect Information Monte Carlo?** Search of the kind chess programs use needs to see every piece. Durak hides the other hand, so PIMC cheats in a controlled way: it deals the unseen cards out at random into a "world" that fits everything seen so far, searches that world as if it were fully visible, then repeats with a new deal and averages the results. Alpha-beta is the standard trick for skipping branches of that search that cannot change the answer. The blind spot is that inside each world it acts as if it knows the hidden cards, so it does not value hiding or revealing information. Figure: Diagram. The two ways the AI picks a move. The 235 features are bitmasks over the 36 cards (own hand, cards seen taken, table, discard, cards that could be in the opponent's hand) plus trump and size values. The network is a small MLP: 235 inputs, three hidden layers of 256, 256 and 128, and two heads on top. One head gives a score for each of 37 actions (the 36 cards, plus one for pass or take), the other gives a win probability. Click a block below to see what each part of the input holds. Figure: Diagram, drawn to scale. The table underneath says what each block holds. **What the 235 input numbers hold. The first six blocks have one slot per card in the 36-card deck.** | Block | Slots | What it holds | |---|---|---| | Own hand | 36 | The cards in my hand. | | Seen taken | 36 | Cards I have watched the opponent pick up from the table. | | Table attack | 36 | The attack cards on the table right now. | | Table defense | 36 | The cards that have been played to beat them. | | Discard pile | 36 | Cards that are already out of the game. | | Unseen cards | 36 | Every card that is not in my hand, on the table or in the discard pile: the opponent's hand plus whatever is left in the deck. Once the deck is empty this is exactly the opponent's hand. | | Trump suit | 4 | Which of the four suits is trump. | | Trump rank | 9 | The rank of the face-up trump card, from 6 to ace. | | Six single values | 6 | Deck size, the opponent's hand size, whether I am the attacker, whether it is the defend phase, whether the deck is empty, and whether I am the defender. | **What do the policy and value heads do?** The policy head says which move looks best: it outputs a score for each of the 37 possible actions, and illegal ones get ignored. The value head says how likely the player to move is to win from this position, as a number between 0 and 1. In AlphaZero-style search the first guides which moves are worth exploring, and the second stands in for playing a position out to the end. I trained the network to copy the existing players first. 547,764 of the 586,163 samples came from 10,000 games of the medium AI, and 38,399 from 500 games of a lighter hard setting (15 worlds, depth 3, not the 25 worlds and depth 4 described above), which generated games at 0.2 a second. The medium one managed about 700. The clone agreed with its teacher's move 94.2% of the time. That is agreement, not strength: it shows the network can imitate, not that it plays well. The self-play loop is the AlphaZero one. Games are played with Monte Carlo tree search using the network's priors, and each position is stored with the search's visit distribution and the final win or loss. A candidate network trains on those with a cross-entropy plus value loss, plays the current model, and replaces it only above 52% wins. **What is AlphaZero-style self-play?** The network plays against itself, but with a tree search on top that looks several moves ahead and uses the network's guesses to decide which branches to explore. How often the search visited each move becomes the training target, which is usually a better move than the network would have picked on its own. The network trains on that and on who won, the improved network searches better, and the loop repeats. Figure: Diagram. One generation of the training loop. The pit has two outcomes, and both lead to the next generation with whichever model was kept. The last two lines are a mismatch I think matters, covered below. ## Results The measurement so far is internal: each candidate against the model it came from, not yet against a fixed outside opponent. Across all the AlphaZero-style runs, 24 generations were promoted, all in two runs. I wrote a script that plays the network against the search AI, and it has not produced a result yet. I numbered the AlphaZero-style runs az, az2, az3 and so on up to az8, and az7 and az8 had several phases. **Candidate win rate against the current model, one row per training run. Greedy policy, no search, 100 games per generation (200 in az6 and az7), draws count half. The promotion threshold was 52% (55% in the first run). A further run, az2, stopped before it produced a result.** | Run | Start and data | Generations | Promoted | Candidate win rate | |---|---|---|---|---| | az | Started from the saved network; self-play, 50 sims | 10 | 0 | 22.5 to 38.0% | | az3, az4 | Random weights; self-play | 3 and 8 | 0 | 0 to 9% | | az5 | Random weights; games played by an earlier network cloned from the search AI | 35 | 13 | 36.0 to 92.0% | | az6 | Started from the saved network; self-play | 2 | 0 | 7.2 to 10.5% | | az7 phase 1 | Random weights; games played by the hard search AI | 40 | 11 | 36.0 to 58.8% | | az7 phase 2 | Phase 1 model; self-play | 12 | 0 | 9.2 to 17.2% | | az8 phase 3 | Started from the saved network; 70% self-play, 30% against the hard AI | 6 | 0 | 30.0 to 50.0% | Figure: Diagram. Every generation of the two longest runs. After the first few generations the marks scatter around 50%, which is what a coin-flip gate would produce. The table above has the numbers by run. The control below lets you pick a run, scrub through its generations and move the promotion threshold, and next to it is the arithmetic for two models of exactly equal strength. It needs JavaScript. ### Every generation of every run, and a gate you can move A 200-game match has a standard error near 3.5 points, so two equal models pass a 52% gate roughly a quarter to a third of the time: about 26% at 200 games, about 31% at 100, ignoring draws. az5 promoted 13 of 35 and az7 phase 1 promoted 11 of 40, against roughly 11 and 10 expected from coin flips. The first az5 generation (92%) was against a network with random weights, and the second (77%) against the one that had just replaced it, so those look real. The rest I read as the gate letting noise through, not steady improvement. **What is a standard error here?** If two equal players play 200 games, the number of wins wobbles around 100 the same way heads wobble over 200 coin flips. A typical wobble is about 7 games, which is 3.5 points of win rate. A gate at 52% is only 2 points above even, well inside that wobble, so it lets equal models through fairly often. ## What I ran into **Self-play has not yet beaten its starting model.** Every run that started from an already trained network and used mostly self-play data (az, az6, az7 phase 2 and az8) had no promotions over 30 generations. In az7 phase 2 the candidates won 9 to 17% of games against the model they started from. In az3 and az4 the trained candidates lost between 91 and 100 games out of 100 to the random-weight network they started from. The cause is still open. One suspect is the mismatch in the diagram above: inside the search, both the tree and the network's evaluations see the opponent's real hand, but the training examples I store show only the cards seen being taken plus the mask of cards that could still be in the opponent's hand (which equals the hand once the deck is empty). I haven't tested that. **GPU batching: no speed-up measured yet.** I wrote the batched search aiming for a 5 to 10 times speed-up. 300 games at 100 simulations took 264 to 549 seconds per generation on the GPU (264 to 306 for the first seven), against roughly 224 to 410 seconds on ten or twelve CPU workers. These were different runs and different models, so it is not a controlled comparison. **Pipeline issues along the way.** I hit a mistyped command line, a CPU/GPU device error in the evaluation step, and four process-pool crashes (az5 twice, az6, az7 phase 2). The input size changed more than once during the project, from 199 numbers to the 235 it has now, so the league rounds of the first cloning script repeatedly did not load their opponents, and some trained on only a few hundred samples. ## Where it stands The next piece is a fixed benchmark: the network against the PIMC player, over enough games to mean something. Beyond that sits the question I started with, whether either player can beat people I know, which is still open. After that, the obvious experiment is to search over sampled hidden hands during training, so the targets match what the network can see. Training is two-player only. The Pygame interface has the optional transfer rule with a simple AI rule for it, and the search and the network do not model transfers yet. Automated tests are still to be written. - **Stack**: Python, PyTorch, Pygame, trained on a laptop RTX 4070 - **Status**: Prototype - **Code**: Not public [← All projects](https://oliviercouthaud.com/projects)