Back to Draft concepts

Chess encoding

Encoding chess moves, positions, games, and game graphs using as few bits as reasonably possible while keeping positions readable and reconstructible.

We consider the problem of encoding moves, positions, games, and trees or graphs of chess games, focusing first on storage efficiency and secondarily on simplicity and read speed.

Some considerations follow.

Games as sequences of moves

A game can be encoded as the sequence of its individual positions. It is also possible, however, to store only the list of individual moves, so that the game and the positions that compose it can be reconstructed. An efficient move encoding becomes an efficient game encoding.

Move Encoding

Move encoding with origin and destination

We can define a move as the piece to move, or perhaps better its square, and the destination square. This still holds for castling: castling can be encoded by moving the king two squares to its destination, making the rook's follow-up move implicit. Selecting one of the 64 squares, identified by 6 bits, as the origin and one as the destination identifies any move in 12 bits.

A second important observation is that many moves are illegal; in particular, the king will always have many illegal moves. These can be used to represent states that are more particular than a standard move, especially resignation or a draw.

For example, a draw can be represented by one king capturing the other. Resignation can instead be represented by moving the king to an inaccessible corner, using the color of the destination square to indicate the resigning player, since such an illegal corner will always exist.

Another creative use of illegal moves is pawn promotion: we can indicate only the destination file, and use ranks 1, 2, 3, 4 for White, and 8, 7, 6, 5 for Black, to indicate promotion respectively to queen, rook, bishop, knight.

The encoding can be made more efficient if we know whose turn it is: list the side-to-move pieces in lexicographic order, select one with 4 bits, at most 16 pieces, and use 6 bits for the destination square.

This identifies a move in 10 bits, or 11 if the player must also be specified, and therefore encodes a game in 10 bits per half-move: a 40-move game would require 100 bytes.

Move encoding by enumeration

A lower weight can be achieved if, instead of using 4 bits to identify a piece, we dynamically allocate only as many bits as are needed to identify one of the pieces on the board that has legal moves. For example, if only 8 pieces can move, only the first 3 bits identify the piece, giving a 9-bit move. A forced move weighs 7 bits.

Continuing in this direction, we can also order lexicographically the possible moves that a piece can make after it has been identified, allocating the necessary bits dynamically, adding 3 moves for draw and resignations, and adding moves for the different pawn promotions. The maximum number of moves for a piece is 27, for a centralized queen, so at most 5 bits are needed.

A move, knowing whose turn it is, is therefore represented by at most 9 bits: 4 to choose the piece, 5 to move it. In the minimum case, a forced move, no bits are needed to identify the piece, and only 2 bits are used to make the move or choose a draw or resignation.

These two tricks reduce the storage used by the encoding, but they require reading the current position in order to decode the selected piece, whose possible moves must be calculated unless pieces with no legal moves are excluded, and the move that was made.

Following this path, we can compute all possible moves in a position, list them lexicographically after listing the pieces lexicographically, and add draw and resignations. We can then allocate only the bits needed to cover the available moves. A storage improvement can be achieved only on average if we introduce the idea that some moves are more plausible than others, or for games with an encoding that is not move-by-move.

Encoding geometrically admissible moves [June 13]

A middle path would be to consider only the moves that are geometrically admissible according to the piece type and its position on the board, ignoring the other pieces in play and the chess rules.

For example, a queen has at most 27 positions it can move to if it is centralized. This is simpler than computing the legal moves, which requires a chess engine and considerations about obstructions by the other pieces on the board, as well as checks.

We could therefore choose the piece to move with at most 4 bits, and then the geometrically sensible move with at most 5 bits, obtaining a reasonably clean encoding up to 9 bits.

We can also enumerate all geometrically plausible actions by listing the pieces in lexicographic order, and then, for each piece, enumerating its geometrically plausible moves according to type and position, treating resignations and draws as extra actions.

In this way we obtain an encoding that does not require a complete chess engine for encoding and decoding, and that in most cases can compress the move into 1 byte or less, requiring 9 bits only in rare cases: those in which there are more than 255 geometrically plausible moves, or 252 if space is left for special moves, essentially cases with many promoted queens.

Position Encoding

Combinatorial encoding of a legal position

We can encode a legal position as follows.

  • 1 bit allocated to determine the side to move.
  • 6 bits + 6 bits for the kings' positions.

Then we observe that a player has 15 other pieces, of 6 types: pawn, knight, bishop, rook, queen, or captured piece, in various configurations, also accounting for pawn promotion. To exploit the fungibility of equivalent pieces, we use bits to determine the configuration with the combinatorial stars-and-bars technique: ****|***|***|*||****. The total number of combinations is 20!/(15!*5!), and requires 14 bits per player to be represented, with standard decoding algorithms.

For each uncaptured piece, we can identify the position with 6 bits, selecting pieces in the category order chosen in the previous step. Since pieces of the same type are fungible, however, we can improve efficiency by allocating only the bits needed to identify the n-tuple among the remaining available squares, decoding it with standard algorithms such as the combinadic. In fact, we can also compress this sequence of numbers with a mixed-radix method to save bits for each tuple.

Finally, we can allocate the bits needed to identify which plausible castling rights are available, up to 4 bits, and which plausible en-passant states are available, up to 3 bits to cover up to 6 cases.

The optimal case, with only 2 kings, uses 1 + 6 + 6 + 14 + 14 = 41 bits. According to ChatGPT: worst case 189 bits, starting case 175 bits.

If a fixed size is desired, one can use 1 + 4 + 3 bits for turn, castling, and en passant; 14 + 14 for the piece configurations; 6 + 6 for the kings' positions; and 6*30 for the positions of the pieces, for a total of 228. Alternatively, allocate 189 bits and waste the unnecessary ones at the tail.

Encoding illegal positions

For positions with illegal piece combinations, we can apply a similar strategy. Assuming there are still 2 kings, we can determine the piece combination for both players as a choice of 62 pieces among 11 types: 5 white pieces, 5 black pieces, and captured pieces. Then we can determine the positions of the n-tuples and add bits for turn, castling, and en passant. According to ChatGPT: worst case 249 bits, starting case 186 bits.

Second take on encoding pseudo-legal positions [June 13]

We have effectively split the problem of encoding a position into metadata encoding plus board-state encoding.

For the metadata: turn, castling, and en passant, we have seen that 1 to 8 bits of information are enough for the encoding.

For the board state, the method discussed above is conceptually divided into determining the types of pieces on the board, and placing them while exploiting fungibility through the combinadic.

In fact, there is no reason to place the kings immediately: it is logically and practically better to relegate this to the later phase in which all positions are identified.

Most of the optimization we can still obtain remains in the phase that determines the piece types: the stars-and-bars method allows illegal configurations, such as 15 queens. If we consider that each player has at most eight pawns, and that compared with the starting count of 2 rooks, 2 bishops, 2 knights, and 1 queen, extra pieces can exist only if at least that many pawns are missing due to promotion, far fewer legal configurations remain.

We could enumerate only those configurations, at worst with precomputed lookup tables, although it would be nice to find a clean encoding for this tranche, saving bits.

Then, during the piece-placement phase, a further useful optimization is to place the pawns immediately, considering that only 48 squares can contain pawns, not the first and last ranks.

Other optimizations are possible, such as mixed radix between metadata, type configuration, and placement, and by noticing that some piece configurations are impossible. For example, pawns on a2, a3, and b2 are impossible; doubled pawns without captures are impossible.

With enough optimizations of this kind, the starting case should reach about 153 bits, the best case 27 bits, and the worst case around 185 bits, potentially also guaranteeing 23 bytes or less per position.

Implementation (41/175/189 algorithm)

Download chess-encoding-demo.js