Solving Senet
An Egyptian race game more than 4,600 years old, solved. Each of its 8.56 billion positions gets its chance of winning under perfect play, and a small network learns to play almost as well. You can play it at the end.
Senet is one of the oldest board games we know of. It first appears for certain in a painting in the tomb of Hesy-Re, an official of King Djoser, in the 27th century BC, and boards that may be Senet are older still.The dates are Peter Piccione’s. He puts Hesy-Re’s tomb at about 2686 BC, and the possible earlier boards, from burials at Abydos and Saqqara, at 3500–3100 BC. Tutankhamun was buried with it. It was played in Egypt for some three thousand years, until it died out around the start of the Christian era, and its rules were lost. What people play today are reconstructions, and the one used by most modern sets was published by the archaeologist Timothy Kendall in 1978.
This piece is about solving Kendall’s version. That means working out, for every position the game can reach, the chance that the player about to throw wins if both sides play perfectly from there on. There are 8,560,690,670 positions. Taken in the right order, a desktop computer values all of them in under three hours. The answer to the first question everyone asks is that White, who throws first, wins 50.2504% of games.
The table of values fills 34 GB, far too much for a web page. So I also trained a small neural network on it, which takes 0.8 MB and plays almost as well. You can play against it at the end.
The game
The board has thirty squares in three rows of ten. Both players race five pieces the same way round it, in an S: along the top row, back along the middle and along the bottom. White starts on the odd squares from 1 to 9 and Black on the even squares from 2 to 10.
Instead of dice there are four throwing sticks, light on one side and dark on the other. The number of light sides up is how far a piece moves, except that four dark sides count as 5. A throw of 1, 4 or 5 earns another throw.
Most of the other rules are about getting in each other’s way:
- A piece that lands on a lone enemy piece swaps places with it. Two enemy pieces side by side protect each other: .
- Three or more in a row form , which no piece may jump.
- Every piece has to stop on 26, the House of Beauty: .
- From 26, a 1 lands in 27, the House of Water, and to 15, the House of Rebirth, or to the nearest empty square below it.
- The last three squares are for leaving. only with exactly 3, 2 or 1, and can make no other move. From 26, a 5 bears a piece off directly.
- If no piece can move forward, one has to . If nothing can move at all, the turn passes, even after a 1, 4 or 5.
The first player to bear off all five pieces wins.Kendall gave each side seven pieces, as the oldest sets had. Most modern sets use five, as Egyptian sets did from the Eighteenth Dynasty on, and that is the game solved here. His description also leaves some questions open, and this version settles each one. Capture is by swapping, protection holds on every square, pieces on 28–30 move only by bearing off, and a throw with no legal move ends the turn. A different choice would give a different solution. Other reconstructions, such as R. C. Bell’s and Piccione’s own, are different games.
White to throw.
The sticks are what make Senet a game of chance, and they are not fair dice. Four sticks can fall in sixteen equally likely ways. Only one of those gives a 4, while six give a 2. The average throw is 2.31 squares, and three throws in eight earn another.
Solving a game of chance
With dice in the game, neither player can force a win, so every position has a chance of winning instead. Write for the chance that the player about to throw in position wins, if both sides play perfectly from then on. Solving the game means finding for every position.
The values are tied together. The player to throw gets throw with probability and picks the best legal move for it. Then they either throw again, after a 1, 4 or 5, or hand the sticks over. In the second case the opponent’s chance is of the new position seen from their side, so the mover’s chance is one minus that. Writing for what move is worth to the mover,
where
Here is the position after the move, from the point of view of whoever throws next. When there is no legal move, is one minus the value of the same board with the other player to throw.
The usual way to solve a game without dice is to work backwards from the end. First come the positions one move from a finished game, then two moves, and so on. That fails for Senet, because its positions lead round in circles. Captured pieces go backwards, the House of Water sends pieces back to 15, pieces are sometimes forced to retreat, and a turn with no legal move hands the same board straight back.
The smallest example has one piece each, yours on 30 and theirs on 29, with you to throw. A 1, with chance , takes your piece off and wins. Any other throw leaves you no move, so they throw, and they win with a 2, chance . If they miss, the same position comes back to you. Your chance therefore depends on itself, , so . That is worse than a coin toss, although you throw first and are further along. A piece on 30 gets off only with a 1, which comes up a quarter of the time, but one on 29 needs a 2, which comes up three times in eight.
So the values are the solution of one large system of equations. Each value depends on others and, through some chain of moves, on itself. The standard way to solve such a system is value iteration, which goes back to Lloyd Shapley’s 1953 paper on stochastic games. Start from any guess, then compute every value afresh from the equation, using the current guesses on the right-hand side, and repeat. After rounds, or sweeps, each value looks throws ahead, with whatever lies beyond still guessed. The game ends sooner or later, so the guesses matter less and less, and the values settle.
Your piece (the cone) on 24, theirs on 26, you to throw: a guess of you win 50.0%.
Figure 3 starts from the guess that every position is a coin toss. , only the positions where your piece can bear off on this throw have learned anything. After two, so have those where theirs can bear off on the next. , more than half the values have moved by a percentage point or more. , no value moves by more than , and the values agree with the database of the full solve to within .
The finished picture mostly says that whoever is further ahead is more likely to win, but the last squares break the pattern. Whatever square the other piece is on, the best place for yours is 29, which it leaves with a 2, the commonest throw. With yours on 29 and theirs on 1, you win 99.2%.
Eight and a half billion positions
A position is fixed by the squares each side occupies and by whose throw it is. A piece can stand on 29 squares, since one that lands in the House of Water is sent straight back. Each side can have from one to five pieces on the board. That makes 8,560,690,670 positions.
They fall into 25 layers by how many pieces each side has left (Figure 4). Bearing off is the one thing that can never be undone. So a layer leads only to itself, to its mirror image with the sides swapped, and to layers with one piece fewer. The solver therefore works through the layers from the fewest pieces up. It solves each group of layers by value iteration, with the smaller layers’ values already known.
8,560,690,670 positions in all; the corner with five pieces a side holds 59% of them.
Done naively, this would still be slow. The solver, written in Rust, makes it fast in four ways:
- It keeps the values of the group being solved in memory and updates them in place, so a value changed early in a sweep is used by the rest of the same sweep.
- It visits positions in order of how far their pieces have travelled, furthest first. Ordinary moves go forward, so most of a position’s successors are already up to date when it is reached.
- It updates each position together with its mirror image, the same board with the other player to throw. It solves the pair’s equations for forfeited turns exactly, instead of letting the value bounce between them.
- It runs on all 32 threads of an Intel i9-14900K at once.
The largest layer is stored at 24 bits a position instead of 32 so that it fits in memory, which peaked at 22.7 GB. The whole solve took 2 hours 56 minutes, two thirds of it on the five-against-five layer.
5v5: 5,047,562,520 positions, 49 sweeps, 1 h 58 min, the largest group, in ink. Pressing solve solves the endgames with up to two pieces a side in your browser and checks them against the database (41 KB).
The values are computed in floating point. A group’s iteration stops once a sweep changes no value by more than (or for the smaller groups), so every stored value is slightly off. How far off has been measured. An independent solver, written separately in Python with 64-bit numbers, was run until nothing changed by . It agrees with the database to within on all 3,917,900 positions with up to five pieces in total.
There is no independent check for the larger layers. The project estimates that no value in the table is off by more than about , but that is an estimate, not a proof. Either way it is far below anything a player could feel. Two moves would have to be within of each other for the perfect player to choose the worse one.
What the solution says
With perfect play, White wins 50.2504% of games. White throws first, but Black’s pieces start one square further along. Taken together, the two favour White by half a percentage point.
White’s first throw matters more than that. With a 2, the commonest throw, White has only one legal move, 9 to 11, because every other piece would land on one of its own. It is a poor move: White’s chance drops to 48.70%. With a 1, 3 or 5, the best move is always with the back piece, from square 1. It takes the Black piece it lands on and sends it back to square 1. With a 5 that move lifts White to 52.32%.
- 1–6, swap 52.32%
- 3–8, swap 50.70%
- 7–12 50.42%
- 9–14 50.20%
- 5–10, swap 49.34%
Two perfect players set against each other for 20,000 games took 198 throws a game on average. In 132 of them the player had a real choice between two or more moves, and 3.5 were forfeited. Of the moves played, 71% were ordinary, 14% were captures, 8% were forced backwards, 2% went into the Water and 4% bore a piece off.
Fortunes swing. In one game in five the eventual winner was at some point below a 25% chance, and in one in forty below 10%. But in none of the 20,000 games did a player come back from below 1%.
Players without the table
The table also measures other players exactly: for every decision a player makes, it says how much chance of winning the move gave away. Figure 7 adds this up over thousands of games for the usual kinds of game program.
A random mover throws away 32% a game. A hand-made evaluation, which weighs the race, protected pieces, the risk of capture and blockades, halves that. Searching one, two or three throws ahead helps. But the 3-throw search still gives away 8% a game, and the perfect player beats it 58% of the time.The head-to-head matches are played in pairs that replay the same throws with the colours swapped, which takes out much of the luck. The margin of error is ±0.3 percentage points for 100,000 games, and ±0.6 for the 25,000 of the slow 3-throw search.
A network that fits in a page
The database is the perfect player, but it takes 34 GB. To fit most of its strength into something small, I trained a neural network to imitate it: given a position, predict the database’s value.
The training data was 205 million positions, 71 million of them distinct, each labelled with its value from the database. Most came from games of the perfect player with some random moves mixed in, and 20 million were random positions. The network is a plain multilayer perceptron. Its 72 inputs say which squares each side occupies, how many pieces each has borne off and how far each still has to go. Three hidden layers of 512, 256 and 128 units lead to a single output, with 201,729 numbers in all. Training made twelve passes over the data, at about a minute a pass on one graphics card. On positions that never occur in its training data, its estimates are off by 0.08 percentage points on average.
201,729 weights and biases; trained on 205 million positions labelled by the database
An error of 0.08 points is small, but what matters is whether the network picks the right moves. Used directly, it chooses the move whose result it rates highest. That gives away 0.44% a game, eighteen times less than the 3-throw search. Letting it look one throw ahead before trusting its estimates brings that down to 0.16%. It then plays a perfect move in 92% of real decisions. Against the perfect player over 100,000 games it scored 49.93%, within the margin of error of an even match.
Figure 9 repeats one of the project’s tests in your browser. It takes 1,631 decisions from games of a perfect player that sometimes explored. None of their positions occurs in the training data. They were chosen so that the hard cases are well represented: moves into the Water, backward moves, blockades, bearing off and near-ties.
Pressing check downloads the network (807 KB) and 1,631 decisions with the database’s value of every move (127 KB), and has the network choose a move in each, alone and with a 1-throw search.
Play the network
Figure 10 is the network with its 1-throw search, the strongest player that runs without the database, running entirely in your browser.
Pressing new game downloads the network (807 KB), once.
Checking the answer
A table of eight and a half billion numbers can’t be checked by eye, so it was checked in other ways:
- Three implementations of the rules agree move for move on a million random positions. One is the solver’s own, in Rust. One was written in Python only from the written rules. The third is the browser engine this page’s code was ported from.
- The independent Python solver reproduces every value with up to five pieces in total to within .
- The equation was recomputed on 200,000 positions from every layer (every position of the smaller ones), and the table satisfies it to within .
- In 20,000 games between two perfect players, White won 50.18%, within the margin of error of the table’s 50.25%.
- The network gives the same outputs in Rust, in PyTorch and in the browser, to within a millionth.
Two of these checks run on this page: the endgame solve in Figure 5 and the held-out decisions in Figure 9.
What the table can’t say is whether this is the game the Egyptians played. Senet’s rules are lost, and Kendall’s are a careful guess, so this is the solution of one reconstruction. Others would have their own.
The project
The solver, the network and the checks quoted here are from my senet project. Every number comes from its records: the solve’s logs, its benchmark and strategy reports, and the network’s training log and manifest. The rules, the network’s forward pass and the endgame solver on this page are ports of the project’s code, tested against it before publishing. They make the same moves as its Rust engine on a million positions, give the same network outputs to within 2 × 10−8, and find the same endgame values as its database to within 1.1 × 10−7.
References
- P. A. Piccione, In search of the meaning of Senet, Archaeology 33, no. 4 (1980), 55–58: the game’s history, from its first appearance to its end.
- A.-E. Dunn-Vaturi, Board games from ancient Egypt and the Near East, Heilbrunn Timeline of Art History, The Metropolitan Museum of Art (2018): the board, the throwing sticks and the pieces.
- Game box of the scribe Merymaat, laid out for Senet, with its pieces. Abydos, Dynasty 18, reign of Thutmose III (about 1479–1425 BC). The Metropolitan Museum of Art, 01.4.1a.
- Spool-shaped game piece, early Dynasty 18 (about 1504–1447 BC), from the tomb of Neferkhawet at Thebes. The Metropolitan Museum of Art, 35.3.8.
- T. Kendall, Passing Through the Netherworld: The Meaning and Play of Senet, an Ancient Egyptian Funerary Game, Kirk Game Company, Belmont, Massachusetts (1978).
- C. Soubeyrand, The game of Senet, The Game Cabinet: summaries of Kendall’s rules and of R. C. Bell’s.
- R. C. Bell, Board and Table Games from Many Civilizations, revised edition, Dover (1979).
- L. S. Shapley, Stochastic games, Proceedings of the National Academy of Sciences 39 (1953), 1095–1100.
- R. Bellman, Dynamic Programming, Princeton University Press (1957).
Cite this
@misc{senet,
author = {Devin O'Keefe},
title = {Solving Senet},
year = {2026},
url = {https://devinokeefe.com/senet/}
}