Class: PGN::MoveCalculator

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

Overview

MoveCalculator is responsible for computing all of the ways that a specific move changes the current position. This includes which squares on the board need to be updated, new castling restrictions, the en passant square and whether to update fullmove and halfmove counters.

Squares are addressed as 0x88 integer indices (see Board); this keeps the replay hot path free of [file, rank] coordinate arrays and square-name string allocations. The public #origin reader still returns an algebraic square string for API compatibility.

Constant Summary collapse

SLIDE =

0x88 ray-step offsets for sliding pieces. A step is a single integer add; off-board is (idx & 0x88) != 0, which also catches file wraparound.

{
  'b' => [-15, 15, -17, 17],
  'r' => [-1, 1, -16, 16],
  'q' => [-1, 1, -16, 16, -15, 15, -17, 17]
}.freeze
STEP =

0x88 single-step offsets for knight and king.

{
  'k' => [-1, 1, -16, 16, -15, 15, -17, 17],
  'n' => [33, 31, -31, -33, 18, 14, -14, -18]
}.freeze
PAWN_OFFSETS =

Possible pawn origins, expressed as offsets from the destination square (pawn moves are computed backwards from where the pawn landed).

{
  'P' => { capture: [-17, -15], normal: [-16], double: [-32] },
  'p' => { capture: [15, 17], normal: [16], double: [32] }
}.freeze
CASTLING =

The squares to update for each castling move, keyed by 0x88 index.

{
  'Q' => { 0 => nil, 2 => 'K', 3 => 'R', 4 => nil },
  'K' => { 4 => nil, 5 => 'R', 6 => 'K', 7 => nil },
  'q' => { 112 => nil, 114 => 'k', 115 => 'r', 116 => nil },
  'k' => { 116 => nil, 117 => 'r', 118 => 'k', 119 => nil }
}.freeze
A1 =

Corner-square 0x88 indices, used for castling-restriction bookkeeping (a rook leaving or being captured on a corner drops the matching right).

0
H1 =
7
A8 =
112
H8 =
119
ROOK_RESTRICTIONS =

rook-origin (0x88 index) -> castling restriction it drops.

{ A1 => 'Q', H1 => 'K', A8 => 'q', H8 => 'k' }.freeze
WHITE_CASTLE =

Castling-move characters by side, for the "castling occurs" restriction. Frozen so Array#include? does not allocate per call.

%w[K Q].freeze
BLACK_CASTLE =
%w[k q].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(board, move) ⇒ MoveCalculator

Returns a new instance of MoveCalculator.

Parameters:



77
78
79
80
81
# File 'lib/pgn/move_calculator.rb', line 77

def initialize(board, move)
  self.board = board
  self.move  = move
  @origin_idx = compute_origin
end

Instance Attribute Details

#boardPGN::Board

Returns the current board.

Returns:



20
21
22
23
24
25
26
27
28
29
30
31
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/pgn/move_calculator.rb', line 20

class MoveCalculator
  # 0x88 ray-step offsets for sliding pieces. A step is a single integer
  # add; off-board is `(idx & 0x88) != 0`, which also catches file wraparound.
  #
  SLIDE = {
    'b' => [-15, 15, -17, 17],
    'r' => [-1, 1, -16, 16],
    'q' => [-1, 1, -16, 16, -15, 15, -17, 17]
  }.freeze

  # 0x88 single-step offsets for knight and king.
  #
  STEP = {
    'k' => [-1, 1, -16, 16, -15, 15, -17, 17],
    'n' => [33, 31, -31, -33, 18, 14, -14, -18]
  }.freeze

  # Possible pawn origins, expressed as offsets from the destination square
  # (pawn moves are computed backwards from where the pawn landed).
  #
  PAWN_OFFSETS = {
    'P' => { capture: [-17, -15], normal: [-16], double: [-32] },
    'p' => { capture: [15, 17], normal: [16], double: [32] }
  }.freeze

  # The squares to update for each castling move, keyed by 0x88 index.
  #
  CASTLING = {
    'Q' => { 0 => nil, 2 => 'K', 3 => 'R', 4 => nil },
    'K' => { 4 => nil, 5 => 'R', 6 => 'K', 7 => nil },
    'q' => { 112 => nil, 114 => 'k', 115 => 'r', 116 => nil },
    'k' => { 116 => nil, 117 => 'r', 118 => 'k', 119 => nil }
  }.freeze

  # Corner-square 0x88 indices, used for castling-restriction bookkeeping
  # (a rook leaving or being captured on a corner drops the matching right).
  #
  A1 = 0
  H1 = 7
  A8 = 112
  H8 = 119

  # rook-origin (0x88 index) -> castling restriction it drops.
  #
  ROOK_RESTRICTIONS = { A1 => 'Q', H1 => 'K', A8 => 'q', H8 => 'k' }.freeze

  # Castling-move characters by side, for the "castling occurs" restriction.
  # Frozen so {Array#include?} does not allocate per call.
  #
  WHITE_CASTLE = %w[K Q].freeze
  BLACK_CASTLE = %w[k q].freeze

  attr_accessor :board, :move

  # @param board [PGN::Board] the current board
  # @param move [PGN::Move] the current move
  #
  def initialize(board, move)
    self.board = board
    self.move  = move
    @origin_idx = compute_origin
  end

  # @return [String, nil] the origin square in algebraic notation, for API
  #   compatibility. Internally the calculator works with the 0x88 index
  #   (see {#origin_idx}); this reader materialises the string on demand.
  #
  def origin
    return nil if @origin_idx.nil?

    board.position_for([@origin_idx & 0x0F, @origin_idx >> 4])
  end

  # @return [PGN::Board] the board after the move is made
  #
  def result_board
    new_board = board.dup
    new_board.apply!(changes)

    new_board
  end

  # @return [Array<String>] which castling moves are no longer available
  #
  def castling_restrictions
    restrict = []

    case move.piece
    when 'K'
      restrict << 'K' << 'Q'
    when 'k'
      restrict << 'k' << 'q'
    when 'R', 'r'
      restrict << ROOK_RESTRICTIONS[@origin_idx]
    end

    # when castling occurs
    if WHITE_CASTLE.include?(move.castle)
      restrict << 'K' << 'Q'
    elsif BLACK_CASTLE.include?(move.castle)
      restrict << 'k' << 'q'
    end

    # when a rook is taken
    dest = dest_idx
    restrict << 'Q' if dest == A1
    restrict << 'q' if dest == A8
    restrict << 'K' if dest == H1
    restrict << 'k' if dest == H8

    restrict.empty? ? restrict : restrict.compact.uniq
  end

  # @return [Boolean] whether to increment the halfmove clock
  #
  def increment_halfmove?
    !(move.capture || move.pawn?)
  end

  # @return [Boolean] whether to increment the fullmove counter
  #
  def increment_fullmove?
    move.black?
  end

  # @return [String, nil] the en passant square if applicable
  #
  def en_passant_square
    return nil if move.castle
    return nil unless move.pawn? && ((origin_rank - dest_rank).abs == 2)

    Board::INDEX_TO_FILE[origin_file] + (move.white? ? '3' : '6')
  end

  private

  # The integer-indexed changes to apply to the board. Keys are 0x88
  # indices, so no square-name strings are allocated on the hot path.
  #
  def changes
    changes = {}
    changes.merge!(CASTLING[move.castle]) if move.castle
    changes[@origin_idx] = nil
    changes[dest_idx] = move.piece
    changes[en_passant_capture] = nil
    changes[dest_idx] = move.promotion if move.promotion

    changes.reject! { |idx, _| idx.nil? }

    changes
  end

  # Using the current position and move, figure out where the piece
  # came from (as a 0x88 index).
  #
  def compute_origin
    return nil if move.castle

    possibilities = case move.piece
                    when 'B', 'R', 'Q', 'b', 'r', 'q' then direction_origins
                    when 'K', 'N', 'k', 'n' then move_origins
                    when 'P', 'p' then pawn_origins
                    else # don't care move, used in variations
                      return nil
                    end

    possibilities = disambiguate(possibilities) if possibilities.length > 1

    possibilities.first
  end

  # From the destination square, walk each slider direction until the first
  # occupied square. If that piece is the moving piece, the square it sits
  # on is a possible origin.
  #
  def direction_origins
    offsets = SLIDE[move.piece.downcase]
    dest    = dest_idx

    possibilities = []
    offsets.each do |off|
      square = first_piece(dest, off)
      possibilities << square if piece_at(square) == move.piece
    end

    possibilities
  end

  # From the destination square, apply each single-step offset. If the
  # target square is on the board and holds the moving piece, it is a
  # possible origin.
  #
  def move_origins(offsets = STEP[move.piece.downcase])
    dest = dest_idx

    possibilities = []
    offsets.each do |off|
      target = dest + off
      next unless (target & 0x88).zero? # rubocop:disable Style/BitwisePredicate

      possibilities << target if board.at_index(target) == move.piece
    end

    possibilities
  end

  # Computes the possible pawn origins based on the destination square
  # and whether or not the move is a capture.
  #
  def pawn_origins
    double = (dest_rank == 3 && move.white?) || (dest_rank == 4 && move.black?)

    pawn_moves = PAWN_OFFSETS[move.piece]
    offsets = move.capture ? pawn_moves[:capture] : pawn_moves[:normal]
    offsets += pawn_moves[:double] if double

    move_origins(offsets)
  end

  def disambiguate(possibilities)
    possibilities = disambiguate_san(possibilities)
    possibilities = disambiguate_pawns(possibilities)            if possibilities.length > 1
    possibilities = disambiguate_discovered_check(possibilities) if possibilities.length > 1

    possibilities
  end

  # Try to disambiguate based on the standard algebraic notation.
  #
  def disambiguate_san(possibilities)
    return possibilities unless move.disambiguation

    possibilities.select do |idx|
      board.position_for([idx & 0x0F, idx >> 4]).match(move.disambiguation)
    end
  end

  # A pawn can't move two spaces if there is a pawn in front of it. A
  # double-push origin sits on rank 2 (white) or 7 (black); reject those
  # candidates when more than one pawn could have reached the destination.
  #
  def disambiguate_pawns(possibilities)
    return possibilities unless move.piece.match?(/p/i) && !move.capture

    possibilities.reject { |idx| (idx >> 4) == 1 || (idx >> 4) == 6 }
  end

  # A piece can't move if it would result in a discovered check.
  #
  def disambiguate_discovered_check(possibilities)
    king_idx = king_position

    SLIDE.each do |attacking_piece, offsets|
      attacking_piece = attacking_piece.upcase if move.black?

      offsets.each do |off|
        square = first_piece(king_idx, off)
        next unless piece_at(square) == move.piece && possibilities.include?(square)

        next_square = first_piece(square, off)
        possibilities.reject! { |p| p == square } if piece_at(next_square) == attacking_piece
      end
    end

    possibilities
  end

  # Walks from `idx` in the 0x88 direction `off` until it reaches the edge
  # of the board or the first occupied square. Returns that square's 0x88
  # index, or nil if no piece was encountered before the edge.
  #
  def first_piece(idx, off)
    idx += off
    while (idx & 0x88).zero? # rubocop:disable Style/BitwisePredicate
      square = board.at_index(idx)
      return idx if square

      idx += off
    end
    nil
  end

  # Reads the piece at a 0x88 index, returning nil for an off-board (nil)
  # index. Keeps {#disambiguate_discovered_check} within the configured
  # complexity limits.
  #
  def piece_at(idx)
    idx && board.at_index(idx)
  end

  # If the move is a capture and there is no piece on the destination
  # square, it must be an en passant capture. The captured pawn sits on the
  # destination file and the moving pawn's origin rank.
  #
  def en_passant_capture
    return nil if move.castle
    return nil unless move.capture && board.at_index(dest_idx).nil?

    (origin_rank * 16) + (dest_idx & 0x0F)
  end

  def king_position
    king = move.white? ? 'K' : 'k'

    0.upto(7) do |rank|
      0.upto(7) do |file|
        idx = (rank * 16) + file
        return idx if board.at_index(idx) == king
      end
    end

    nil
  end

  # -- 0x88 index helpers --------------------------------------------------

  def dest_idx
    @dest_idx ||= move.destination && board.index_of(move.destination)
  end

  def origin_file
    @origin_idx & 0x0F
  end

  def origin_rank
    @origin_idx >> 4
  end

  def dest_rank
    dest_idx >> 4
  end
end

#movePGN::Move

Returns the current move.

Returns:



20
21
22
23
24
25
26
27
28
29
30
31
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/pgn/move_calculator.rb', line 20

class MoveCalculator
  # 0x88 ray-step offsets for sliding pieces. A step is a single integer
  # add; off-board is `(idx & 0x88) != 0`, which also catches file wraparound.
  #
  SLIDE = {
    'b' => [-15, 15, -17, 17],
    'r' => [-1, 1, -16, 16],
    'q' => [-1, 1, -16, 16, -15, 15, -17, 17]
  }.freeze

  # 0x88 single-step offsets for knight and king.
  #
  STEP = {
    'k' => [-1, 1, -16, 16, -15, 15, -17, 17],
    'n' => [33, 31, -31, -33, 18, 14, -14, -18]
  }.freeze

  # Possible pawn origins, expressed as offsets from the destination square
  # (pawn moves are computed backwards from where the pawn landed).
  #
  PAWN_OFFSETS = {
    'P' => { capture: [-17, -15], normal: [-16], double: [-32] },
    'p' => { capture: [15, 17], normal: [16], double: [32] }
  }.freeze

  # The squares to update for each castling move, keyed by 0x88 index.
  #
  CASTLING = {
    'Q' => { 0 => nil, 2 => 'K', 3 => 'R', 4 => nil },
    'K' => { 4 => nil, 5 => 'R', 6 => 'K', 7 => nil },
    'q' => { 112 => nil, 114 => 'k', 115 => 'r', 116 => nil },
    'k' => { 116 => nil, 117 => 'r', 118 => 'k', 119 => nil }
  }.freeze

  # Corner-square 0x88 indices, used for castling-restriction bookkeeping
  # (a rook leaving or being captured on a corner drops the matching right).
  #
  A1 = 0
  H1 = 7
  A8 = 112
  H8 = 119

  # rook-origin (0x88 index) -> castling restriction it drops.
  #
  ROOK_RESTRICTIONS = { A1 => 'Q', H1 => 'K', A8 => 'q', H8 => 'k' }.freeze

  # Castling-move characters by side, for the "castling occurs" restriction.
  # Frozen so {Array#include?} does not allocate per call.
  #
  WHITE_CASTLE = %w[K Q].freeze
  BLACK_CASTLE = %w[k q].freeze

  attr_accessor :board, :move

  # @param board [PGN::Board] the current board
  # @param move [PGN::Move] the current move
  #
  def initialize(board, move)
    self.board = board
    self.move  = move
    @origin_idx = compute_origin
  end

  # @return [String, nil] the origin square in algebraic notation, for API
  #   compatibility. Internally the calculator works with the 0x88 index
  #   (see {#origin_idx}); this reader materialises the string on demand.
  #
  def origin
    return nil if @origin_idx.nil?

    board.position_for([@origin_idx & 0x0F, @origin_idx >> 4])
  end

  # @return [PGN::Board] the board after the move is made
  #
  def result_board
    new_board = board.dup
    new_board.apply!(changes)

    new_board
  end

  # @return [Array<String>] which castling moves are no longer available
  #
  def castling_restrictions
    restrict = []

    case move.piece
    when 'K'
      restrict << 'K' << 'Q'
    when 'k'
      restrict << 'k' << 'q'
    when 'R', 'r'
      restrict << ROOK_RESTRICTIONS[@origin_idx]
    end

    # when castling occurs
    if WHITE_CASTLE.include?(move.castle)
      restrict << 'K' << 'Q'
    elsif BLACK_CASTLE.include?(move.castle)
      restrict << 'k' << 'q'
    end

    # when a rook is taken
    dest = dest_idx
    restrict << 'Q' if dest == A1
    restrict << 'q' if dest == A8
    restrict << 'K' if dest == H1
    restrict << 'k' if dest == H8

    restrict.empty? ? restrict : restrict.compact.uniq
  end

  # @return [Boolean] whether to increment the halfmove clock
  #
  def increment_halfmove?
    !(move.capture || move.pawn?)
  end

  # @return [Boolean] whether to increment the fullmove counter
  #
  def increment_fullmove?
    move.black?
  end

  # @return [String, nil] the en passant square if applicable
  #
  def en_passant_square
    return nil if move.castle
    return nil unless move.pawn? && ((origin_rank - dest_rank).abs == 2)

    Board::INDEX_TO_FILE[origin_file] + (move.white? ? '3' : '6')
  end

  private

  # The integer-indexed changes to apply to the board. Keys are 0x88
  # indices, so no square-name strings are allocated on the hot path.
  #
  def changes
    changes = {}
    changes.merge!(CASTLING[move.castle]) if move.castle
    changes[@origin_idx] = nil
    changes[dest_idx] = move.piece
    changes[en_passant_capture] = nil
    changes[dest_idx] = move.promotion if move.promotion

    changes.reject! { |idx, _| idx.nil? }

    changes
  end

  # Using the current position and move, figure out where the piece
  # came from (as a 0x88 index).
  #
  def compute_origin
    return nil if move.castle

    possibilities = case move.piece
                    when 'B', 'R', 'Q', 'b', 'r', 'q' then direction_origins
                    when 'K', 'N', 'k', 'n' then move_origins
                    when 'P', 'p' then pawn_origins
                    else # don't care move, used in variations
                      return nil
                    end

    possibilities = disambiguate(possibilities) if possibilities.length > 1

    possibilities.first
  end

  # From the destination square, walk each slider direction until the first
  # occupied square. If that piece is the moving piece, the square it sits
  # on is a possible origin.
  #
  def direction_origins
    offsets = SLIDE[move.piece.downcase]
    dest    = dest_idx

    possibilities = []
    offsets.each do |off|
      square = first_piece(dest, off)
      possibilities << square if piece_at(square) == move.piece
    end

    possibilities
  end

  # From the destination square, apply each single-step offset. If the
  # target square is on the board and holds the moving piece, it is a
  # possible origin.
  #
  def move_origins(offsets = STEP[move.piece.downcase])
    dest = dest_idx

    possibilities = []
    offsets.each do |off|
      target = dest + off
      next unless (target & 0x88).zero? # rubocop:disable Style/BitwisePredicate

      possibilities << target if board.at_index(target) == move.piece
    end

    possibilities
  end

  # Computes the possible pawn origins based on the destination square
  # and whether or not the move is a capture.
  #
  def pawn_origins
    double = (dest_rank == 3 && move.white?) || (dest_rank == 4 && move.black?)

    pawn_moves = PAWN_OFFSETS[move.piece]
    offsets = move.capture ? pawn_moves[:capture] : pawn_moves[:normal]
    offsets += pawn_moves[:double] if double

    move_origins(offsets)
  end

  def disambiguate(possibilities)
    possibilities = disambiguate_san(possibilities)
    possibilities = disambiguate_pawns(possibilities)            if possibilities.length > 1
    possibilities = disambiguate_discovered_check(possibilities) if possibilities.length > 1

    possibilities
  end

  # Try to disambiguate based on the standard algebraic notation.
  #
  def disambiguate_san(possibilities)
    return possibilities unless move.disambiguation

    possibilities.select do |idx|
      board.position_for([idx & 0x0F, idx >> 4]).match(move.disambiguation)
    end
  end

  # A pawn can't move two spaces if there is a pawn in front of it. A
  # double-push origin sits on rank 2 (white) or 7 (black); reject those
  # candidates when more than one pawn could have reached the destination.
  #
  def disambiguate_pawns(possibilities)
    return possibilities unless move.piece.match?(/p/i) && !move.capture

    possibilities.reject { |idx| (idx >> 4) == 1 || (idx >> 4) == 6 }
  end

  # A piece can't move if it would result in a discovered check.
  #
  def disambiguate_discovered_check(possibilities)
    king_idx = king_position

    SLIDE.each do |attacking_piece, offsets|
      attacking_piece = attacking_piece.upcase if move.black?

      offsets.each do |off|
        square = first_piece(king_idx, off)
        next unless piece_at(square) == move.piece && possibilities.include?(square)

        next_square = first_piece(square, off)
        possibilities.reject! { |p| p == square } if piece_at(next_square) == attacking_piece
      end
    end

    possibilities
  end

  # Walks from `idx` in the 0x88 direction `off` until it reaches the edge
  # of the board or the first occupied square. Returns that square's 0x88
  # index, or nil if no piece was encountered before the edge.
  #
  def first_piece(idx, off)
    idx += off
    while (idx & 0x88).zero? # rubocop:disable Style/BitwisePredicate
      square = board.at_index(idx)
      return idx if square

      idx += off
    end
    nil
  end

  # Reads the piece at a 0x88 index, returning nil for an off-board (nil)
  # index. Keeps {#disambiguate_discovered_check} within the configured
  # complexity limits.
  #
  def piece_at(idx)
    idx && board.at_index(idx)
  end

  # If the move is a capture and there is no piece on the destination
  # square, it must be an en passant capture. The captured pawn sits on the
  # destination file and the moving pawn's origin rank.
  #
  def en_passant_capture
    return nil if move.castle
    return nil unless move.capture && board.at_index(dest_idx).nil?

    (origin_rank * 16) + (dest_idx & 0x0F)
  end

  def king_position
    king = move.white? ? 'K' : 'k'

    0.upto(7) do |rank|
      0.upto(7) do |file|
        idx = (rank * 16) + file
        return idx if board.at_index(idx) == king
      end
    end

    nil
  end

  # -- 0x88 index helpers --------------------------------------------------

  def dest_idx
    @dest_idx ||= move.destination && board.index_of(move.destination)
  end

  def origin_file
    @origin_idx & 0x0F
  end

  def origin_rank
    @origin_idx >> 4
  end

  def dest_rank
    dest_idx >> 4
  end
end

Instance Method Details

#castling_restrictionsArray<String>

Returns which castling moves are no longer available.

Returns:

  • (Array<String>)

    which castling moves are no longer available



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
# File 'lib/pgn/move_calculator.rb', line 104

def castling_restrictions
  restrict = []

  case move.piece
  when 'K'
    restrict << 'K' << 'Q'
  when 'k'
    restrict << 'k' << 'q'
  when 'R', 'r'
    restrict << ROOK_RESTRICTIONS[@origin_idx]
  end

  # when castling occurs
  if WHITE_CASTLE.include?(move.castle)
    restrict << 'K' << 'Q'
  elsif BLACK_CASTLE.include?(move.castle)
    restrict << 'k' << 'q'
  end

  # when a rook is taken
  dest = dest_idx
  restrict << 'Q' if dest == A1
  restrict << 'q' if dest == A8
  restrict << 'K' if dest == H1
  restrict << 'k' if dest == H8

  restrict.empty? ? restrict : restrict.compact.uniq
end

#en_passant_squareString?

Returns the en passant square if applicable.

Returns:

  • (String, nil)

    the en passant square if applicable



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

def en_passant_square
  return nil if move.castle
  return nil unless move.pawn? && ((origin_rank - dest_rank).abs == 2)

  Board::INDEX_TO_FILE[origin_file] + (move.white? ? '3' : '6')
end

#increment_fullmove?Boolean

Returns whether to increment the fullmove counter.

Returns:

  • (Boolean)

    whether to increment the fullmove counter



141
142
143
# File 'lib/pgn/move_calculator.rb', line 141

def increment_fullmove?
  move.black?
end

#increment_halfmove?Boolean

Returns whether to increment the halfmove clock.

Returns:

  • (Boolean)

    whether to increment the halfmove clock



135
136
137
# File 'lib/pgn/move_calculator.rb', line 135

def increment_halfmove?
  !(move.capture || move.pawn?)
end

#originString?

Returns the origin square in algebraic notation, for API compatibility. Internally the calculator works with the 0x88 index (see #origin_idx); this reader materialises the string on demand.

Returns:

  • (String, nil)

    the origin square in algebraic notation, for API compatibility. Internally the calculator works with the 0x88 index (see #origin_idx); this reader materialises the string on demand.



87
88
89
90
91
# File 'lib/pgn/move_calculator.rb', line 87

def origin
  return nil if @origin_idx.nil?

  board.position_for([@origin_idx & 0x0F, @origin_idx >> 4])
end

#result_boardPGN::Board

Returns the board after the move is made.

Returns:

  • (PGN::Board)

    the board after the move is made



95
96
97
98
99
100
# File 'lib/pgn/move_calculator.rb', line 95

def result_board
  new_board = board.dup
  new_board.apply!(changes)

  new_board
end