Skip to main content
SAN (Standard Algebraic Notation) is the standard human-readable chess move notation. The chessops library provides functions to parse SAN moves and generate them from positions.

Parsing SAN

parseSan

Parses a SAN string into a Move object. Returns undefined if the move is illegal or invalid.
import { parseSan } from 'chessops/san';
import { Chess } from 'chessops/chess';

const pos = Chess.default();
const move = parseSan(pos, 'e4');

console.log(move); // { from: 12, to: 28 }
parseSan returns undefined for illegal moves. Always check the result before using it.

Supported Notation

The parser supports all standard SAN features: Normal moves:
parseSan(pos, 'e4');      // Pawn move
parseSan(pos, 'Nf3');     // Piece move
parseSan(pos, 'exd5');    // Pawn capture
parseSan(pos, 'Nxe5');    // Piece capture
parseSan(pos, 'e8=Q');    // Promotion
parseSan(pos, 'exd8=N+'); // Capture promotion with check
Castling:
parseSan(pos, 'O-O');     // Kingside castling
parseSan(pos, 'O-O-O');   // Queenside castling
Disambiguation:
parseSan(pos, 'Nbd7');    // File disambiguation
parseSan(pos, 'R1a3');    // Rank disambiguation
parseSan(pos, 'Qh4e1');   // Both file and rank
Crazyhouse drops:
parseSan(pos, 'N@f3');    // Knight drop
parseSan(pos, '@e4');     // Pawn drop (@ alone implies pawn)
parseSan(pos, 'Q@h7+');   // Drop with check
Check and checkmate symbols:
parseSan(pos, 'Qh5+');    // Check (symbol is optional)
parseSan(pos, 'Qxf7#');   // Checkmate (symbol is optional)
The + and # symbols are optional when parsing. The parser validates legality regardless of these annotations.

Example: Parsing a Game

import { parseSan } from 'chessops/san';
import { Chess } from 'chessops/chess';

const pos = Chess.default();
const moves = ['e4', 'e5', 'Nf3', 'Nc6', 'Bb5'];

for (const san of moves) {
  const move = parseSan(pos, san);
  if (!move) {
    console.error(`Illegal move: ${san}`);
    break;
  }
  pos.play(move);
}

console.log(pos.isCheck()); // false

Writing SAN

makeSan

Generates the SAN string for a move without modifying the position:
import { makeSan } from 'chessops/san';
import { Chess } from 'chessops/chess';
import { parseUci } from 'chessops';

const pos = Chess.default();
const move = parseUci('e2e4')!;
const san = makeSan(pos, move);

console.log(san); // 'e4'
console.log(pos.turn); // 'white' (position unchanged)

makeSanAndPlay

Generates the SAN string and plays the move on the position:
import { makeSanAndPlay } from 'chessops/san';
import { Chess } from 'chessops/chess';
import { parseUci } from 'chessops';

const pos = Chess.default();
const move = parseUci('e2e4')!;
const san = makeSanAndPlay(pos, move);

console.log(san); // 'e4'
console.log(pos.turn); // 'black' (position was modified)
makeSanAndPlay mutates the position. Use makeSan if you need to keep the original position.

makeSanVariation

Generates a full variation with move numbers:
import { makeSanVariation } from 'chessops/san';
import { Chess } from 'chessops/chess';
import { parseUci } from 'chessops';

const pos = Chess.default();
const variation = [
  parseUci('e2e4')!,
  parseUci('e7e5')!,
  parseUci('g1f3')!,
];

const san = makeSanVariation(pos, variation);
console.log(san); // '1. e4 e5 2. Nf3'
The position passed to makeSanVariation is cloned internally, so the original remains unchanged.

Starting from Black’s Move

import { makeSanVariation } from 'chessops/san';
import { Chess } from 'chessops/chess';
import { parseFen } from 'chessops/fen';
import { parseUci } from 'chessops';

const setup = parseFen('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1').unwrap();
const pos = Chess.fromSetup(setup).unwrap();

const variation = [parseUci('e7e5')!, parseUci('g1f3')!];
const san = makeSanVariation(pos, variation);

console.log(san); // '1... e5 2. Nf3'

Disambiguation

The library automatically handles all disambiguation cases:
import { makeSan } from 'chessops/san';
import { Chess } from 'chessops/chess';
import { parseFen } from 'chessops/fen';
import { parseUci } from 'chessops';

// Position with knights that can move to the same square
const setup = parseFen('N3k2N/8/8/3N4/N4N1N/2R5/1R6/4K3 w - -').unwrap();
const pos = Chess.fromSetup(setup).unwrap();

console.log(makeSan(pos, parseUci('a4b6')!)); // 'N4b6' (rank disambiguation)
console.log(makeSan(pos, parseUci('h8g6')!)); // 'N8g6' (rank disambiguation)
console.log(makeSan(pos, parseUci('h4g6')!)); // 'Nh4g6' (both needed)

Special Cases

Castling

import { makeSan } from 'chessops/san';
import { Chess } from 'chessops/chess';

const pos = Chess.default();
// After some preparation moves...

const kingsideCastle = { from: 4, to: 7 }; // King to h1 rook
console.log(makeSan(pos, kingsideCastle)); // 'O-O'

const queensideCastle = { from: 4, to: 0 }; // King to a1 rook
console.log(makeSan(pos, queensideCastle)); // 'O-O-O'
In chessops, castling moves specify the king moving to the rook’s square, not to its final position.

En Passant

import { makeSan } from 'chessops/san';
import { Chess } from 'chessops/chess';
import { parseFen } from 'chessops/fen';

const setup = parseFen('6bk/7b/8/3pP3/8/8/8/Q3K3 w - d6 0 2').unwrap();
const pos = Chess.fromSetup(setup).unwrap();

const epCapture = { from: 36, to: 43 }; // e5 to d6
const san = makeSan(pos, epCapture);

console.log(san); // 'exd6#'

Promotion

import { makeSan } from 'chessops/san';
import { Chess } from 'chessops/chess';
import { parseFen } from 'chessops/fen';

const setup = parseFen('7k/1p2Npbp/8/2P5/1P1r4/3b2QP/3q1pPK/2RB4 b - -').unwrap();
const pos = Chess.fromSetup(setup).unwrap();

const queenPromotion = { from: 13, to: 5, promotion: 'queen' };
console.log(makeSan(pos, queenPromotion)); // 'f1=Q'

const knightPromotion = { from: 13, to: 5, promotion: 'knight' };
console.log(makeSan(pos, knightPromotion)); // 'f1=N+'

Crazyhouse Drops

import { makeSan } from 'chessops/san';
import { Crazyhouse } from 'chessops/variant';

const pos = Crazyhouse.default();
// After some captures that give pieces in pocket...

const knightDrop = { role: 'knight', to: 21 }; // N@f3
const san = makeSan(pos, knightDrop);

console.log(san); // 'N@f3' or 'N@f3+' if it gives check

Check and Checkmate Annotation

The library automatically adds + for check and # for checkmate:
import { makeSanAndPlay } from 'chessops/san';
import { Chess } from 'chessops/chess';
import { parseFen } from 'chessops/fen';

const setup = parseFen('rnbqkbnr/pppp1ppp/8/4p3/5PPP/8/PPPPP3/RNBQKBNR b KQkq - 0 2').unwrap();
const pos = Chess.fromSetup(setup).unwrap();

const move = { from: 59, to: 31 }; // Qh4
const san = makeSanAndPlay(pos, move);

console.log(san); // 'Qh4#'
console.log(pos.isCheckmate()); // true

Null Moves

If a move is invalid (e.g., the piece doesn’t exist), the library returns --:
import { makeSan } from 'chessops/san';
import { Chess } from 'chessops/chess';

const pos = Chess.default();
const invalidMove = { from: 28, to: 36 }; // e4 to e5, but e4 is empty

const san = makeSan(pos, invalidMove);
console.log(san); // '--'

Complete Example: Converting UCI to SAN

import { parseSan, makeSan } from 'chessops/san';
import { Chess } from 'chessops/chess';
import { parseUci } from 'chessops';

function uciToSan(pos: Chess, uci: string): string | undefined {
  const move = parseUci(uci);
  if (!move) return undefined;
  
  return makeSan(pos, move);
}

const pos = Chess.default();
console.log(uciToSan(pos, 'e2e4')); // 'e4'
console.log(uciToSan(pos, 'g1f3')); // 'Nf3'

Complete Example: Parsing and Validating a Game

import { parseSan } from 'chessops/san';
import { Chess } from 'chessops/chess';

function playGame(moves: string[]): Chess | string {
  const pos = Chess.default();
  
  for (let i = 0; i < moves.length; i++) {
    const move = parseSan(pos, moves[i]);
    
    if (!move) {
      return `Illegal move at position ${i + 1}: ${moves[i]}`;
    }
    
    pos.play(move);
    
    if (pos.isCheckmate()) {
      return pos;
    }
  }
  
  return pos;
}

const result = playGame(['e4', 'e5', 'Qh5', 'Nc6', 'Bc4', 'Nf6', 'Qxf7#']);

if (typeof result === 'string') {
  console.error(result);
} else {
  console.log('Game completed');
  console.log('Checkmate:', result.isCheckmate());
}

Build docs developers (and LLMs) love