Class: PGN::Position

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

Overview

Position encapsulates all of the information necessary to completely understand a chess position. It can be turned into a FEN string or perform a move.

Constant Summary collapse

PLAYERS =
%i[white black].freeze
CASTLING =
%w[K Q k q].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(board, player, castling = CASTLING, en_passant = nil, halfmove = 0, fullmove = 1) ⇒ Position

Returns a new instance of Position.

Examples:

PGN::Position.new(
  PGN::Board.start,
  :white,
)

Parameters:

  • board (PGN::Board)

    the board for the position

  • player (Symbol)

    the player who moves next

  • castling (Array<String>) (defaults to: CASTLING)

    the castling moves that are still available

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

    the en passant square if applicable

  • halfmove (Integer) (defaults to: 0)

    the number of halfmoves since the last pawn move or capture

  • fullmove (Integer) (defaults to: 1)

    the number of fullmoves made so far



62
63
64
65
66
67
68
69
# File 'lib/pgn/position.rb', line 62

def initialize(board, player, castling = CASTLING, en_passant = nil, halfmove = 0, fullmove = 1)
  self.board      = board
  self.player     = player
  self.castling   = castling
  self.en_passant = en_passant
  self.halfmove   = halfmove
  self.fullmove   = fullmove
end

Instance Attribute Details

#boardPGN::Board

Returns the board for the position.

Returns:



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/pgn/position.rb', line 32

class Position
  PLAYERS  = %i[white black].freeze
  CASTLING = %w[K Q k q].freeze

  attr_accessor :board, :player, :castling, :en_passant, :halfmove, :fullmove

  # @return [PGN::Position] the starting position of a chess game
  #
  def self.start
    PGN::Position.new(
      PGN::Board.start,
      PLAYERS.first
    )
  end

  # @param board [PGN::Board] the board for the position
  # @param player [Symbol] the player who moves next
  # @param castling [Array<String>] the castling moves that are still
  #   available
  # @param en_passant [String, nil] the en passant square if applicable
  # @param halfmove [Integer] the number of halfmoves since the last pawn
  #   move or capture
  # @param fullmove [Integer] the number of fullmoves made so far
  #
  # @example
  #   PGN::Position.new(
  #     PGN::Board.start,
  #     :white,
  #   )
  #
  def initialize(board, player, castling = CASTLING, en_passant = nil, halfmove = 0, fullmove = 1)
    self.board      = board
    self.player     = player
    self.castling   = castling
    self.en_passant = en_passant
    self.halfmove   = halfmove
    self.fullmove   = fullmove
  end

  # @param str [String] the move to make in SAN
  # @return [PGN::Position] the resulting position
  #
  # @example
  #   queens_pawn = PGN::Position.start.move("d4")
  #
  def move(str)
    move       = PGN::Move.new(str, player)
    calculator = PGN::MoveCalculator.new(board, move)

    restrictions = calculator.castling_restrictions
    new_castling = restrictions.empty? ? castling : castling - restrictions
    new_halfmove = calculator.increment_halfmove? ? halfmove + 1 : 0
    new_fullmove = calculator.increment_fullmove? ? fullmove + 1 : fullmove
    no_move      = str == '--'

    PGN::Position.new(
      no_move ? board : calculator.result_board,
      next_player,
      new_castling,
      calculator.en_passant_square,
      new_halfmove,
      new_fullmove
    )
  end

  # @return [Symbol] the next player to move
  #
  def next_player
    player == :white ? :black : :white
  end

  # The perft node count at +depth+ from this position, computed by the
  # native bitboard engine via a FEN round-trip. Requires the compiled
  # native extension (the shipped gem); raises NameError if it is absent.
  #
  # @param depth [Integer] search depth, >= 0
  # @return [Integer]
  #
  def perft(depth)
    raise ArgumentError, 'depth must be a non-negative Integer' unless depth.is_a?(Integer) && depth >= 0

    PGN::Bitboard::Engine.new(to_fen.to_s).perft(depth)
  end

  # All legal moves from this position as sorted UCI strings
  # (e.g. "e2e4", "e1g1" for castling, "e7e8q" for promotion), computed
  # by the native bitboard engine via a FEN round-trip. Requires the
  # compiled native extension; raises NameError if it is absent.
  #
  # @return [Array<String>] sorted lexicographically
  #
  def legal_moves
    PGN::Bitboard::Engine.new(to_fen.to_s).legal_moves
  end

  def inspect
    "\n#{board.inspect}"
  end

  # @return [PGN::FEN] a {PGN::FEN} object representing the current position
  #
  def to_fen
    PGN::FEN.from_attributes(
      board: board,
      active: player == :white ? 'w' : 'b',
      castling: castling.join,
      en_passant: en_passant,
      halfmove: halfmove.to_s,
      fullmove: fullmove.to_s
    )
  end

  # Positions are equal when their board, side to move, castling rights,
  # and en-passant square match. Halfmove/fullmove counters are ignored
  # (matching threefold-repetition semantics).
  def eql?(other)
    other.is_a?(PGN::Position) &&
      player == other.player &&
      castling == other.castling &&
      en_passant == other.en_passant &&
      zobrist == other.zobrist &&
      board == other.board
  end

  alias == eql?

  def hash
    zobrist
  end

  # The Zobrist hash of the position. Computed lazily on first access and
  # cached, so the replay hot path (which never asks for the hash) pays
  # nothing; consumers like threefold-repetition checks pay one full seed.
  #
  # @return [Integer]
  def zobrist
    @zobrist ||= Zobrist.seed(board, player, castling, en_passant)
  end
end

#castlingArray<String>

Returns the castling moves that are still available.

Examples:

position.castling #=> ["K", "k", "q"]

Returns:

  • (Array<String>)

    the castling moves that are still available



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/pgn/position.rb', line 32

class Position
  PLAYERS  = %i[white black].freeze
  CASTLING = %w[K Q k q].freeze

  attr_accessor :board, :player, :castling, :en_passant, :halfmove, :fullmove

  # @return [PGN::Position] the starting position of a chess game
  #
  def self.start
    PGN::Position.new(
      PGN::Board.start,
      PLAYERS.first
    )
  end

  # @param board [PGN::Board] the board for the position
  # @param player [Symbol] the player who moves next
  # @param castling [Array<String>] the castling moves that are still
  #   available
  # @param en_passant [String, nil] the en passant square if applicable
  # @param halfmove [Integer] the number of halfmoves since the last pawn
  #   move or capture
  # @param fullmove [Integer] the number of fullmoves made so far
  #
  # @example
  #   PGN::Position.new(
  #     PGN::Board.start,
  #     :white,
  #   )
  #
  def initialize(board, player, castling = CASTLING, en_passant = nil, halfmove = 0, fullmove = 1)
    self.board      = board
    self.player     = player
    self.castling   = castling
    self.en_passant = en_passant
    self.halfmove   = halfmove
    self.fullmove   = fullmove
  end

  # @param str [String] the move to make in SAN
  # @return [PGN::Position] the resulting position
  #
  # @example
  #   queens_pawn = PGN::Position.start.move("d4")
  #
  def move(str)
    move       = PGN::Move.new(str, player)
    calculator = PGN::MoveCalculator.new(board, move)

    restrictions = calculator.castling_restrictions
    new_castling = restrictions.empty? ? castling : castling - restrictions
    new_halfmove = calculator.increment_halfmove? ? halfmove + 1 : 0
    new_fullmove = calculator.increment_fullmove? ? fullmove + 1 : fullmove
    no_move      = str == '--'

    PGN::Position.new(
      no_move ? board : calculator.result_board,
      next_player,
      new_castling,
      calculator.en_passant_square,
      new_halfmove,
      new_fullmove
    )
  end

  # @return [Symbol] the next player to move
  #
  def next_player
    player == :white ? :black : :white
  end

  # The perft node count at +depth+ from this position, computed by the
  # native bitboard engine via a FEN round-trip. Requires the compiled
  # native extension (the shipped gem); raises NameError if it is absent.
  #
  # @param depth [Integer] search depth, >= 0
  # @return [Integer]
  #
  def perft(depth)
    raise ArgumentError, 'depth must be a non-negative Integer' unless depth.is_a?(Integer) && depth >= 0

    PGN::Bitboard::Engine.new(to_fen.to_s).perft(depth)
  end

  # All legal moves from this position as sorted UCI strings
  # (e.g. "e2e4", "e1g1" for castling, "e7e8q" for promotion), computed
  # by the native bitboard engine via a FEN round-trip. Requires the
  # compiled native extension; raises NameError if it is absent.
  #
  # @return [Array<String>] sorted lexicographically
  #
  def legal_moves
    PGN::Bitboard::Engine.new(to_fen.to_s).legal_moves
  end

  def inspect
    "\n#{board.inspect}"
  end

  # @return [PGN::FEN] a {PGN::FEN} object representing the current position
  #
  def to_fen
    PGN::FEN.from_attributes(
      board: board,
      active: player == :white ? 'w' : 'b',
      castling: castling.join,
      en_passant: en_passant,
      halfmove: halfmove.to_s,
      fullmove: fullmove.to_s
    )
  end

  # Positions are equal when their board, side to move, castling rights,
  # and en-passant square match. Halfmove/fullmove counters are ignored
  # (matching threefold-repetition semantics).
  def eql?(other)
    other.is_a?(PGN::Position) &&
      player == other.player &&
      castling == other.castling &&
      en_passant == other.en_passant &&
      zobrist == other.zobrist &&
      board == other.board
  end

  alias == eql?

  def hash
    zobrist
  end

  # The Zobrist hash of the position. Computed lazily on first access and
  # cached, so the replay hot path (which never asks for the hash) pays
  # nothing; consumers like threefold-repetition checks pay one full seed.
  #
  # @return [Integer]
  def zobrist
    @zobrist ||= Zobrist.seed(board, player, castling, en_passant)
  end
end

#en_passantString

Returns the en passant square if applicable.

Returns:

  • (String)

    the en passant square if applicable



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/pgn/position.rb', line 32

class Position
  PLAYERS  = %i[white black].freeze
  CASTLING = %w[K Q k q].freeze

  attr_accessor :board, :player, :castling, :en_passant, :halfmove, :fullmove

  # @return [PGN::Position] the starting position of a chess game
  #
  def self.start
    PGN::Position.new(
      PGN::Board.start,
      PLAYERS.first
    )
  end

  # @param board [PGN::Board] the board for the position
  # @param player [Symbol] the player who moves next
  # @param castling [Array<String>] the castling moves that are still
  #   available
  # @param en_passant [String, nil] the en passant square if applicable
  # @param halfmove [Integer] the number of halfmoves since the last pawn
  #   move or capture
  # @param fullmove [Integer] the number of fullmoves made so far
  #
  # @example
  #   PGN::Position.new(
  #     PGN::Board.start,
  #     :white,
  #   )
  #
  def initialize(board, player, castling = CASTLING, en_passant = nil, halfmove = 0, fullmove = 1)
    self.board      = board
    self.player     = player
    self.castling   = castling
    self.en_passant = en_passant
    self.halfmove   = halfmove
    self.fullmove   = fullmove
  end

  # @param str [String] the move to make in SAN
  # @return [PGN::Position] the resulting position
  #
  # @example
  #   queens_pawn = PGN::Position.start.move("d4")
  #
  def move(str)
    move       = PGN::Move.new(str, player)
    calculator = PGN::MoveCalculator.new(board, move)

    restrictions = calculator.castling_restrictions
    new_castling = restrictions.empty? ? castling : castling - restrictions
    new_halfmove = calculator.increment_halfmove? ? halfmove + 1 : 0
    new_fullmove = calculator.increment_fullmove? ? fullmove + 1 : fullmove
    no_move      = str == '--'

    PGN::Position.new(
      no_move ? board : calculator.result_board,
      next_player,
      new_castling,
      calculator.en_passant_square,
      new_halfmove,
      new_fullmove
    )
  end

  # @return [Symbol] the next player to move
  #
  def next_player
    player == :white ? :black : :white
  end

  # The perft node count at +depth+ from this position, computed by the
  # native bitboard engine via a FEN round-trip. Requires the compiled
  # native extension (the shipped gem); raises NameError if it is absent.
  #
  # @param depth [Integer] search depth, >= 0
  # @return [Integer]
  #
  def perft(depth)
    raise ArgumentError, 'depth must be a non-negative Integer' unless depth.is_a?(Integer) && depth >= 0

    PGN::Bitboard::Engine.new(to_fen.to_s).perft(depth)
  end

  # All legal moves from this position as sorted UCI strings
  # (e.g. "e2e4", "e1g1" for castling, "e7e8q" for promotion), computed
  # by the native bitboard engine via a FEN round-trip. Requires the
  # compiled native extension; raises NameError if it is absent.
  #
  # @return [Array<String>] sorted lexicographically
  #
  def legal_moves
    PGN::Bitboard::Engine.new(to_fen.to_s).legal_moves
  end

  def inspect
    "\n#{board.inspect}"
  end

  # @return [PGN::FEN] a {PGN::FEN} object representing the current position
  #
  def to_fen
    PGN::FEN.from_attributes(
      board: board,
      active: player == :white ? 'w' : 'b',
      castling: castling.join,
      en_passant: en_passant,
      halfmove: halfmove.to_s,
      fullmove: fullmove.to_s
    )
  end

  # Positions are equal when their board, side to move, castling rights,
  # and en-passant square match. Halfmove/fullmove counters are ignored
  # (matching threefold-repetition semantics).
  def eql?(other)
    other.is_a?(PGN::Position) &&
      player == other.player &&
      castling == other.castling &&
      en_passant == other.en_passant &&
      zobrist == other.zobrist &&
      board == other.board
  end

  alias == eql?

  def hash
    zobrist
  end

  # The Zobrist hash of the position. Computed lazily on first access and
  # cached, so the replay hot path (which never asks for the hash) pays
  # nothing; consumers like threefold-repetition checks pay one full seed.
  #
  # @return [Integer]
  def zobrist
    @zobrist ||= Zobrist.seed(board, player, castling, en_passant)
  end
end

#fullmoveInteger

Returns the number of fullmoves made so far.

Returns:

  • (Integer)

    the number of fullmoves made so far



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/pgn/position.rb', line 32

class Position
  PLAYERS  = %i[white black].freeze
  CASTLING = %w[K Q k q].freeze

  attr_accessor :board, :player, :castling, :en_passant, :halfmove, :fullmove

  # @return [PGN::Position] the starting position of a chess game
  #
  def self.start
    PGN::Position.new(
      PGN::Board.start,
      PLAYERS.first
    )
  end

  # @param board [PGN::Board] the board for the position
  # @param player [Symbol] the player who moves next
  # @param castling [Array<String>] the castling moves that are still
  #   available
  # @param en_passant [String, nil] the en passant square if applicable
  # @param halfmove [Integer] the number of halfmoves since the last pawn
  #   move or capture
  # @param fullmove [Integer] the number of fullmoves made so far
  #
  # @example
  #   PGN::Position.new(
  #     PGN::Board.start,
  #     :white,
  #   )
  #
  def initialize(board, player, castling = CASTLING, en_passant = nil, halfmove = 0, fullmove = 1)
    self.board      = board
    self.player     = player
    self.castling   = castling
    self.en_passant = en_passant
    self.halfmove   = halfmove
    self.fullmove   = fullmove
  end

  # @param str [String] the move to make in SAN
  # @return [PGN::Position] the resulting position
  #
  # @example
  #   queens_pawn = PGN::Position.start.move("d4")
  #
  def move(str)
    move       = PGN::Move.new(str, player)
    calculator = PGN::MoveCalculator.new(board, move)

    restrictions = calculator.castling_restrictions
    new_castling = restrictions.empty? ? castling : castling - restrictions
    new_halfmove = calculator.increment_halfmove? ? halfmove + 1 : 0
    new_fullmove = calculator.increment_fullmove? ? fullmove + 1 : fullmove
    no_move      = str == '--'

    PGN::Position.new(
      no_move ? board : calculator.result_board,
      next_player,
      new_castling,
      calculator.en_passant_square,
      new_halfmove,
      new_fullmove
    )
  end

  # @return [Symbol] the next player to move
  #
  def next_player
    player == :white ? :black : :white
  end

  # The perft node count at +depth+ from this position, computed by the
  # native bitboard engine via a FEN round-trip. Requires the compiled
  # native extension (the shipped gem); raises NameError if it is absent.
  #
  # @param depth [Integer] search depth, >= 0
  # @return [Integer]
  #
  def perft(depth)
    raise ArgumentError, 'depth must be a non-negative Integer' unless depth.is_a?(Integer) && depth >= 0

    PGN::Bitboard::Engine.new(to_fen.to_s).perft(depth)
  end

  # All legal moves from this position as sorted UCI strings
  # (e.g. "e2e4", "e1g1" for castling, "e7e8q" for promotion), computed
  # by the native bitboard engine via a FEN round-trip. Requires the
  # compiled native extension; raises NameError if it is absent.
  #
  # @return [Array<String>] sorted lexicographically
  #
  def legal_moves
    PGN::Bitboard::Engine.new(to_fen.to_s).legal_moves
  end

  def inspect
    "\n#{board.inspect}"
  end

  # @return [PGN::FEN] a {PGN::FEN} object representing the current position
  #
  def to_fen
    PGN::FEN.from_attributes(
      board: board,
      active: player == :white ? 'w' : 'b',
      castling: castling.join,
      en_passant: en_passant,
      halfmove: halfmove.to_s,
      fullmove: fullmove.to_s
    )
  end

  # Positions are equal when their board, side to move, castling rights,
  # and en-passant square match. Halfmove/fullmove counters are ignored
  # (matching threefold-repetition semantics).
  def eql?(other)
    other.is_a?(PGN::Position) &&
      player == other.player &&
      castling == other.castling &&
      en_passant == other.en_passant &&
      zobrist == other.zobrist &&
      board == other.board
  end

  alias == eql?

  def hash
    zobrist
  end

  # The Zobrist hash of the position. Computed lazily on first access and
  # cached, so the replay hot path (which never asks for the hash) pays
  # nothing; consumers like threefold-repetition checks pay one full seed.
  #
  # @return [Integer]
  def zobrist
    @zobrist ||= Zobrist.seed(board, player, castling, en_passant)
  end
end

#halfmoveInteger

Returns the number of halfmoves since the last pawn move or capture.

Returns:

  • (Integer)

    the number of halfmoves since the last pawn move or capture



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/pgn/position.rb', line 32

class Position
  PLAYERS  = %i[white black].freeze
  CASTLING = %w[K Q k q].freeze

  attr_accessor :board, :player, :castling, :en_passant, :halfmove, :fullmove

  # @return [PGN::Position] the starting position of a chess game
  #
  def self.start
    PGN::Position.new(
      PGN::Board.start,
      PLAYERS.first
    )
  end

  # @param board [PGN::Board] the board for the position
  # @param player [Symbol] the player who moves next
  # @param castling [Array<String>] the castling moves that are still
  #   available
  # @param en_passant [String, nil] the en passant square if applicable
  # @param halfmove [Integer] the number of halfmoves since the last pawn
  #   move or capture
  # @param fullmove [Integer] the number of fullmoves made so far
  #
  # @example
  #   PGN::Position.new(
  #     PGN::Board.start,
  #     :white,
  #   )
  #
  def initialize(board, player, castling = CASTLING, en_passant = nil, halfmove = 0, fullmove = 1)
    self.board      = board
    self.player     = player
    self.castling   = castling
    self.en_passant = en_passant
    self.halfmove   = halfmove
    self.fullmove   = fullmove
  end

  # @param str [String] the move to make in SAN
  # @return [PGN::Position] the resulting position
  #
  # @example
  #   queens_pawn = PGN::Position.start.move("d4")
  #
  def move(str)
    move       = PGN::Move.new(str, player)
    calculator = PGN::MoveCalculator.new(board, move)

    restrictions = calculator.castling_restrictions
    new_castling = restrictions.empty? ? castling : castling - restrictions
    new_halfmove = calculator.increment_halfmove? ? halfmove + 1 : 0
    new_fullmove = calculator.increment_fullmove? ? fullmove + 1 : fullmove
    no_move      = str == '--'

    PGN::Position.new(
      no_move ? board : calculator.result_board,
      next_player,
      new_castling,
      calculator.en_passant_square,
      new_halfmove,
      new_fullmove
    )
  end

  # @return [Symbol] the next player to move
  #
  def next_player
    player == :white ? :black : :white
  end

  # The perft node count at +depth+ from this position, computed by the
  # native bitboard engine via a FEN round-trip. Requires the compiled
  # native extension (the shipped gem); raises NameError if it is absent.
  #
  # @param depth [Integer] search depth, >= 0
  # @return [Integer]
  #
  def perft(depth)
    raise ArgumentError, 'depth must be a non-negative Integer' unless depth.is_a?(Integer) && depth >= 0

    PGN::Bitboard::Engine.new(to_fen.to_s).perft(depth)
  end

  # All legal moves from this position as sorted UCI strings
  # (e.g. "e2e4", "e1g1" for castling, "e7e8q" for promotion), computed
  # by the native bitboard engine via a FEN round-trip. Requires the
  # compiled native extension; raises NameError if it is absent.
  #
  # @return [Array<String>] sorted lexicographically
  #
  def legal_moves
    PGN::Bitboard::Engine.new(to_fen.to_s).legal_moves
  end

  def inspect
    "\n#{board.inspect}"
  end

  # @return [PGN::FEN] a {PGN::FEN} object representing the current position
  #
  def to_fen
    PGN::FEN.from_attributes(
      board: board,
      active: player == :white ? 'w' : 'b',
      castling: castling.join,
      en_passant: en_passant,
      halfmove: halfmove.to_s,
      fullmove: fullmove.to_s
    )
  end

  # Positions are equal when their board, side to move, castling rights,
  # and en-passant square match. Halfmove/fullmove counters are ignored
  # (matching threefold-repetition semantics).
  def eql?(other)
    other.is_a?(PGN::Position) &&
      player == other.player &&
      castling == other.castling &&
      en_passant == other.en_passant &&
      zobrist == other.zobrist &&
      board == other.board
  end

  alias == eql?

  def hash
    zobrist
  end

  # The Zobrist hash of the position. Computed lazily on first access and
  # cached, so the replay hot path (which never asks for the hash) pays
  # nothing; consumers like threefold-repetition checks pay one full seed.
  #
  # @return [Integer]
  def zobrist
    @zobrist ||= Zobrist.seed(board, player, castling, en_passant)
  end
end

#playerSymbol

Returns the player who moves next.

Examples:

position.player #=> :white

Returns:

  • (Symbol)

    the player who moves next



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/pgn/position.rb', line 32

class Position
  PLAYERS  = %i[white black].freeze
  CASTLING = %w[K Q k q].freeze

  attr_accessor :board, :player, :castling, :en_passant, :halfmove, :fullmove

  # @return [PGN::Position] the starting position of a chess game
  #
  def self.start
    PGN::Position.new(
      PGN::Board.start,
      PLAYERS.first
    )
  end

  # @param board [PGN::Board] the board for the position
  # @param player [Symbol] the player who moves next
  # @param castling [Array<String>] the castling moves that are still
  #   available
  # @param en_passant [String, nil] the en passant square if applicable
  # @param halfmove [Integer] the number of halfmoves since the last pawn
  #   move or capture
  # @param fullmove [Integer] the number of fullmoves made so far
  #
  # @example
  #   PGN::Position.new(
  #     PGN::Board.start,
  #     :white,
  #   )
  #
  def initialize(board, player, castling = CASTLING, en_passant = nil, halfmove = 0, fullmove = 1)
    self.board      = board
    self.player     = player
    self.castling   = castling
    self.en_passant = en_passant
    self.halfmove   = halfmove
    self.fullmove   = fullmove
  end

  # @param str [String] the move to make in SAN
  # @return [PGN::Position] the resulting position
  #
  # @example
  #   queens_pawn = PGN::Position.start.move("d4")
  #
  def move(str)
    move       = PGN::Move.new(str, player)
    calculator = PGN::MoveCalculator.new(board, move)

    restrictions = calculator.castling_restrictions
    new_castling = restrictions.empty? ? castling : castling - restrictions
    new_halfmove = calculator.increment_halfmove? ? halfmove + 1 : 0
    new_fullmove = calculator.increment_fullmove? ? fullmove + 1 : fullmove
    no_move      = str == '--'

    PGN::Position.new(
      no_move ? board : calculator.result_board,
      next_player,
      new_castling,
      calculator.en_passant_square,
      new_halfmove,
      new_fullmove
    )
  end

  # @return [Symbol] the next player to move
  #
  def next_player
    player == :white ? :black : :white
  end

  # The perft node count at +depth+ from this position, computed by the
  # native bitboard engine via a FEN round-trip. Requires the compiled
  # native extension (the shipped gem); raises NameError if it is absent.
  #
  # @param depth [Integer] search depth, >= 0
  # @return [Integer]
  #
  def perft(depth)
    raise ArgumentError, 'depth must be a non-negative Integer' unless depth.is_a?(Integer) && depth >= 0

    PGN::Bitboard::Engine.new(to_fen.to_s).perft(depth)
  end

  # All legal moves from this position as sorted UCI strings
  # (e.g. "e2e4", "e1g1" for castling, "e7e8q" for promotion), computed
  # by the native bitboard engine via a FEN round-trip. Requires the
  # compiled native extension; raises NameError if it is absent.
  #
  # @return [Array<String>] sorted lexicographically
  #
  def legal_moves
    PGN::Bitboard::Engine.new(to_fen.to_s).legal_moves
  end

  def inspect
    "\n#{board.inspect}"
  end

  # @return [PGN::FEN] a {PGN::FEN} object representing the current position
  #
  def to_fen
    PGN::FEN.from_attributes(
      board: board,
      active: player == :white ? 'w' : 'b',
      castling: castling.join,
      en_passant: en_passant,
      halfmove: halfmove.to_s,
      fullmove: fullmove.to_s
    )
  end

  # Positions are equal when their board, side to move, castling rights,
  # and en-passant square match. Halfmove/fullmove counters are ignored
  # (matching threefold-repetition semantics).
  def eql?(other)
    other.is_a?(PGN::Position) &&
      player == other.player &&
      castling == other.castling &&
      en_passant == other.en_passant &&
      zobrist == other.zobrist &&
      board == other.board
  end

  alias == eql?

  def hash
    zobrist
  end

  # The Zobrist hash of the position. Computed lazily on first access and
  # cached, so the replay hot path (which never asks for the hash) pays
  # nothing; consumers like threefold-repetition checks pay one full seed.
  #
  # @return [Integer]
  def zobrist
    @zobrist ||= Zobrist.seed(board, player, castling, en_passant)
  end
end

Class Method Details

.startPGN::Position

Returns the starting position of a chess game.

Returns:



40
41
42
43
44
45
# File 'lib/pgn/position.rb', line 40

def self.start
  PGN::Position.new(
    PGN::Board.start,
    PLAYERS.first
  )
end

Instance Method Details

#eql?(other) ⇒ Boolean Also known as: ==

Positions are equal when their board, side to move, castling rights, and en-passant square match. Halfmove/fullmove counters are ignored (matching threefold-repetition semantics).

Returns:

  • (Boolean)


147
148
149
150
151
152
153
154
# File 'lib/pgn/position.rb', line 147

def eql?(other)
  other.is_a?(PGN::Position) &&
    player == other.player &&
    castling == other.castling &&
    en_passant == other.en_passant &&
    zobrist == other.zobrist &&
    board == other.board
end

#hashObject



158
159
160
# File 'lib/pgn/position.rb', line 158

def hash
  zobrist
end

#inspectObject



127
128
129
# File 'lib/pgn/position.rb', line 127

def inspect
  "\n#{board.inspect}"
end

All legal moves from this position as sorted UCI strings (e.g. "e2e4", "e1g1" for castling, "e7e8q" for promotion), computed by the native bitboard engine via a FEN round-trip. Requires the compiled native extension; raises NameError if it is absent.

Returns:

  • (Array<String>)

    sorted lexicographically



123
124
125
# File 'lib/pgn/position.rb', line 123

def legal_moves
  PGN::Bitboard::Engine.new(to_fen.to_s).legal_moves
end

#move(str) ⇒ PGN::Position

Returns the resulting position.

Examples:

queens_pawn = PGN::Position.start.move("d4")

Parameters:

  • str (String)

    the move to make in SAN

Returns:



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/pgn/position.rb', line 77

def move(str)
  move       = PGN::Move.new(str, player)
  calculator = PGN::MoveCalculator.new(board, move)

  restrictions = calculator.castling_restrictions
  new_castling = restrictions.empty? ? castling : castling - restrictions
  new_halfmove = calculator.increment_halfmove? ? halfmove + 1 : 0
  new_fullmove = calculator.increment_fullmove? ? fullmove + 1 : fullmove
  no_move      = str == '--'

  PGN::Position.new(
    no_move ? board : calculator.result_board,
    next_player,
    new_castling,
    calculator.en_passant_square,
    new_halfmove,
    new_fullmove
  )
end

#next_playerSymbol

Returns the next player to move.

Returns:

  • (Symbol)

    the next player to move



99
100
101
# File 'lib/pgn/position.rb', line 99

def next_player
  player == :white ? :black : :white
end

#perft(depth) ⇒ Integer

The perft node count at depth from this position, computed by the native bitboard engine via a FEN round-trip. Requires the compiled native extension (the shipped gem); raises NameError if it is absent.

Parameters:

  • depth (Integer)

    search depth, >= 0

Returns:

  • (Integer)

Raises:

  • (ArgumentError)


110
111
112
113
114
# File 'lib/pgn/position.rb', line 110

def perft(depth)
  raise ArgumentError, 'depth must be a non-negative Integer' unless depth.is_a?(Integer) && depth >= 0

  PGN::Bitboard::Engine.new(to_fen.to_s).perft(depth)
end

#to_fenPGN::FEN

Returns a FEN object representing the current position.

Returns:

  • (PGN::FEN)

    a FEN object representing the current position



133
134
135
136
137
138
139
140
141
142
# File 'lib/pgn/position.rb', line 133

def to_fen
  PGN::FEN.from_attributes(
    board: board,
    active: player == :white ? 'w' : 'b',
    castling: castling.join,
    en_passant: en_passant,
    halfmove: halfmove.to_s,
    fullmove: fullmove.to_s
  )
end

#zobristInteger

The Zobrist hash of the position. Computed lazily on first access and cached, so the replay hot path (which never asks for the hash) pays nothing; consumers like threefold-repetition checks pay one full seed.

Returns:

  • (Integer)


167
168
169
# File 'lib/pgn/position.rb', line 167

def zobrist
  @zobrist ||= Zobrist.seed(board, player, castling, en_passant)
end