This simple script generates all 29,274 possible Standard Algebraic Notation (SAN) strings for chess moves, with logic to avoid listing SAN strings that can never actually occur for geometric reasons.
If someone notices missing strings, or strings which are generated but can never occur, please open an issue!*
Check (+) and checkmate (#) symbols are omitted in san_strings.txt but included in san_strings_with_symbols.txt. It is fairly easy
to convince yourself that no special logic is required to determine which subset of all SAN moves could deliver check/mate: all moves can
deliver either check or checkmate at least via a discovery. Therefore, san_strings_with_symbols.txt (29,274 lines) is exactly three times the
length of san_strings.txt (9,758 lines) as it simply makes two additional copies of each SAN move, one appending + and one appending #.
SANs in both files are sorted by the key (len(san), san).
git clone https://github.com/jacksonthall22/SAN-strings.git && cd SAN-strings
pip install -r requirements.txt
python gen_san_strings.pyGenerating every possible SAN move in chess would be simple if not for discriminators, which occasionally must be inserted into the "standard" SAN move to disambiguate between multiple legal options.
If we have a piece, a from_square, and a to_square, how do we decide which (if any) discriminators
the SAN move might require? I'm glad you asked—this repo has the answer.
In a previous version of this code, special logic was used to manually handle all the edge cases for the different piece types. Unfortunately, this failed miserably, generating some syntactically-legal SAN moves that could never actually occur and failing to generate some that could. When I dug into the issues more deeply, I realized this problem is quite difficult to reason through.
Now, the code uses three much more logical and generalized algorithms that were designed to resemble how a human
might decide whether a particular from_square -> to_square move by a certain piece type could
require a rank, file, and/or full-square discriminator respectively, based on which other squares a piece of
the same type could come from when moving to to_square. Read on for the pseudocode, as well as some more
intuitive explanations.
Note that only knight, bishop, rook, and queen moves ever use discriminators. Pawns have their own special
move syntax and there is never more than one king per color. The details about how SAN strings are generated
for these piece types is omitted below because it is straightforward and comparatively uninteresting. However,
you can find the implementations in gen_san_strings.py at get_pawn_sans() and get_king_sans().
- Given
piece(must be aN,B,R, orQ) - For each
to_squarein the board:- Initialize an empty board
b - Place a
pieceonbatto_square - Get the
attacksbitboard forto_square, a 64-bit binary number representing squares thepiececould move to - For each
from_squareinattacks:- Define a bitboard
raywhose truthy bits start atto_squareand extend towardfrom_square, continuing past it until reaching a board edge - Remove all truthy bits in
rayfromattacks - Remove all truthy bits on the same file as
from_squarefromattacks - If any file in
attackshas any truthy bit, this move can require a file discriminator
- Define a bitboard
- Initialize an empty board
- Given
piece(must be aN,B,R, orQ) - For each
to_squarein the board:- Initialize an empty board
b - Place a
pieceonbatto_square - Get the
attacksbitboard forto_square, a 64-bit binary number representing squares thepiececould move to - For each
from_squareinattacks:- Define a bitboard
raywhose truthy bits start atto_squareand move towardfrom_square, continuing past it until reaching a board edge - Remove all truthy bits in
rayfromattacks - Remove all truthy bits not on the same file as
from_squarefromattacks - If any rank in
attackshas any truthy bit, this move can require a rank discriminator
- Define a bitboard
- Initialize an empty board
- Given
piece(must be aN,B,R, orQ) - For each
to_squarein the board:- Initialize an empty board
b - Place a
pieceonbatto_square - Get the
attacksbitboard forto_squarea 64-bit binary number representing squares thepiececould move to - For each
from_squareinattacks:- Define a bitboard
raywhose truthy bits start atto_squareand move towardfrom_square, continuing past it until reaching a board edge - Remove all truthy bits in
rayfromattacks - If there is any truthy bit in
from_square's rank and also infrom_square's file, this move can require a full-square discriminator
- Define a bitboard
- Initialize an empty board
To really understand the code in this repo, we need to understand the algorithm a human uses
to determine whether a move from a from_square to a to_square might require a file, rank,
and/or full-square discriminator.
First we should consider that if moving from from_square to to_square is a legal
move, then even if there is another piece of the same type and color on the ray which extends
from to_square to from_square and continues on to an edge of the board, moving that piece
to to_square would be illegal. If this piece falls between from_square and to_square,
then the original move would not be legal, so we have a contradiction. If it is past
from_square (on the extension of the ray between the squares that continues to the edge of
the board), then it is not legal because it cannot jump over the piece at from_square to
reach to_square.
This is important when considering disriminators because we are only interested in squares
from which another piece can legally move to to_square, and those which might create
a situation where a rank, file, or full-square discriminator is necessary.
With this in mind, the algorithm for determining whether we need a file discriminator is as follows:
- Take an empty board and place a
pieceonto_square, then get a bitboardattacksof all the squares it can move to. These may all be considered possiblefrom_squares. - Consider each
from_squareinattacks:- Assume the move from
from_squaretoto_squareis legal. Then we know that no otherpieceon the ray fromto_squaretofrom_squareis relevant because its move toto_squarewould be illegal. Therefore, we can subtract the bitmask of that ray fromattacksfor the next step, creating a bitboard representing all the other possible locations of apiecethat could legally move toto_squaregiven that apiececan legally move fromfrom_squaretoto_square. - We also know that any squares in this bitmask which fall on the same file as
from_squareare not relevant for determining whether a file discriminator might be required: if anotherpiecewere to occupy one of those squares, then its move toto_squarewould necessarily require a rank discriminator, not a file discriminator. Therefore we subtract the bitmask of all squares infrom_square's file from the bitboard in the previous step as well. - We now have a bitboard of all squares from which a
piececan legally move toto_square(given that the movepiecefromfrom_squaretoto_squareis legal) such that, if apiecereally were to occupy any one of those squares, it has potential to create a situation where a file discriminator is necessary. All that is left to do is check whether one or more files in this bitboard have any truthy bits. If so, then thefrom_squarefor this iteration can require a file discriminator.
- Assume the move from
The algorithm for determining whether we need a rank discriminator is similar, but has some differences which account for the fact that a file discriminator is preferred over a rank discriminator when both can disambiguate the move:
- Take an empty board and place a
pieceonto_square, then get a bitboardattacksof all the squares it can move to. These may all be considered possiblefrom_squares. - Consider each
from_squareinattacks:- Subtract the extended ray from
to_squaretowardsfrom_squarefromattacksby the same logic as above. - This time, we know that any squares that do not fall on the same file as
from_squareare not relevant for determining whether a rank discriminator might be required: if anotherpiecewere to occupy one of those squares, then its move toto_squarewould necessarily preference the file discriminator, not the rank discriminator. Therefore we use a logical AND between the bitboard from the previous step and the bitmask of all squares infrom_square's file. - By the same logic as above, all that is left to do is check whether one or more ranks
in this bitboard have any truthy bits. If so, then the
from_squarefor this iteration can require a rank discriminator.
- Subtract the extended ray from
Determining whether we need a full-square discriminator is actually the simplest:
- Take an empty board and place a
pieceonto_square, then get a bitboardattacksof all the squares it can move to. These may all be considered possiblefrom_squares. - Consider each
from_squareinattacks:- Subtract the extended ray from
to_squaretowardsfrom_squarefromattacksby the same logic as above. - A full-square discriminator is required when there is another
pieceonfrom_square's same file and another one on its same rank that can both move toto_square. Therefore, can use a logical AND betweenattacksand the bitmask of all squares infrom_square's file, then do the same for its rank, and if both of these have truthy bits, then thefrom_squarefor this iteration can require a full-square discriminator.
- Subtract the extended ray from