Class: PGN::Notation

Inherits:
Object
  • Object
show all
Defined in:
lib/pgn/notation.rb

Overview

Notation generates Standard Algebraic Notation (SAN) for a single move described in coordinate form -- an origin square, a destination square, and an optional promotion piece -- given the Position before the move is played.

Unlike Move, which parses an existing SAN string, Notation builds SAN from coordinates. This requires the kind of legality analysis the rest of the gem does not perform: attack detection, "does this move leave the mover's king in check", disambiguation among same-type pieces that can legally reach the destination, and check (+) / checkmate (#) suffix detection (the latter needing full legal-move generation for the side to move).

The board is addressed with the same 0x88 integer indices the rest of the gem uses (see Board); an index is on-board when (idx & 0x88).zero?.

Examples:

PGN::Notation.san(PGN::Position.start, 'g1', 'f3') #=> "Nf3"
PGN::Notation.san_from_fen(PGN::FEN::INITIAL, 'e2', 'e4') #=> "e4"

Constant Summary collapse

KNIGHT_OFFS =

0x88 single-step offsets for a knight (symmetric, so they double as attack deltas).

[33, 31, -31, -33, 18, 14, -14, -18].freeze
KING_OFFS =

0x88 single-step offsets for a king (symmetric).

[-1, 1, -16, 16, -15, 15, -17, 17].freeze
BISHOP_DIRS =
[-15, 15, -17, 17].freeze
ROOK_DIRS =
[-1, 1, -16, 16].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(position) ⇒ Notation

Returns a new instance of Notation.



57
58
59
60
61
62
63
# File 'lib/pgn/notation.rb', line 57

def initialize(position)
  @position = position
  @board = position.board
  @player = position.player
  @mover = (@player == :white ? 'w' : 'b')
  @enemy = (@player == :white ? 'b' : 'w')
end

Class Method Details

.san(position, from, to, promotion = nil) ⇒ String

Build SAN for a coordinate move from a Position.

Parameters:

  • position (PGN::Position)

    the position before the move

  • from (String)

    origin square in algebraic notation ("e2")

  • to (String)

    destination square in algebraic notation ("e4")

  • promotion (String, nil) (defaults to: nil)

    promotion piece letter ("q"/"Q"/"n"...); case-insensitive; SAN always emits an uppercase letter

Returns:

  • (String)

    the move in SAN, e.g. "Nf3", "exd5", "O-O", "Ra8#"



43
44
45
# File 'lib/pgn/notation.rb', line 43

def self.san(position, from, to, promotion = nil)
  new(position).san(from, to, promotion)
end

.san_from_fen(fen, from, to, promotion = nil) ⇒ String

Convenience: build SAN directly from a FEN string.

Parameters:

  • fen (String)

    the FEN of the position before the move

  • position (PGN::Position)

    the position before the move

  • from (String)

    origin square in algebraic notation ("e2")

  • to (String)

    destination square in algebraic notation ("e4")

  • promotion (String, nil) (defaults to: nil)

    promotion piece letter ("q"/"Q"/"n"...); case-insensitive; SAN always emits an uppercase letter

Returns:

  • (String)

    the move in SAN



53
54
55
# File 'lib/pgn/notation.rb', line 53

def self.san_from_fen(fen, from, to, promotion = nil)
  san(PGN::FEN.new(fen).to_position, from, to, promotion)
end

Instance Method Details

#san(from, to, promotion = nil) ⇒ String

Parameters:

  • position (PGN::Position)

    the position before the move

  • from (String)

    origin square in algebraic notation ("e2")

  • to (String)

    destination square in algebraic notation ("e4")

  • promotion (String, nil) (defaults to: nil)

    promotion piece letter ("q"/"Q"/"n"...); case-insensitive; SAN always emits an uppercase letter

Returns:

  • (String)

Raises:

  • (ArgumentError)


67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/pgn/notation.rb', line 67

def san(from, to, promotion = nil)
  from_idx = @board.index_of(from)
  to_idx = @board.index_of(to)
  piece = @board.at_index(from_idx)
  raise ArgumentError, "no piece on #{from}" if piece.nil?

  promotion = promotion.upcase if promotion

  castling = piece.upcase == 'K' && ((from_idx & 0x0F) - (to_idx & 0x0F)).abs == 2
  capture = castling ? false : capture?(piece, from_idx, to_idx)

  body =
    if castling
      to_idx < from_idx ? 'O-O-O' : 'O-O'
    elsif piece.upcase == 'P'
      pawn_body(from_idx, to_idx, capture, promotion)
    else
      piece_body(piece, from_idx, to_idx, capture)
    end

  body + suffix(from_idx, to_idx, piece, promotion, castling)
end