Class: PGN::Board

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

Overview

Board represents the squares of a chess board and the pieces on each square. It is responsible for translating between a human readable format (white queen's rook on the bottom left) and the obvious internal representation (white queen's rook is position [0,0]). It takes care of converting square names (e4) to actual locations, and can convert to unicode chess pieces for display purposes.

Constant Summary collapse

START =

The starting, internal representation of a chess board

[
  ['R', 'P', nil, nil, nil, nil, 'p', 'r'],
  ['N', 'P', nil, nil, nil, nil, 'p', 'n'],
  ['B', 'P', nil, nil, nil, nil, 'p', 'b'],
  ['Q', 'P', nil, nil, nil, nil, 'p', 'q'],
  ['K', 'P', nil, nil, nil, nil, 'p', 'k'],
  ['B', 'P', nil, nil, nil, nil, 'p', 'b'],
  ['N', 'P', nil, nil, nil, nil, 'p', 'n'],
  ['R', 'P', nil, nil, nil, nil, 'p', 'r']
].freeze
FILE_TO_INDEX =
('a'..'h').each_with_index.to_h
INDEX_TO_FILE =
FILE_TO_INDEX.invert
RANK_TO_INDEX =
('1'..'8').each_with_index.to_h
INDEX_TO_RANK =
RANK_TO_INDEX.invert
UNICODE_PIECES =

algebraic to unicode piece lookup

{
  'k' => "\u{265A}",
  'q' => "\u{265B}",
  'r' => "\u{265C}",
  'b' => "\u{265D}",
  'n' => "\u{265E}",
  'p' => "\u{265F}",
  'K' => "\u{2654}",
  'Q' => "\u{2655}",
  'R' => "\u{2656}",
  'B' => "\u{2657}",
  'N' => "\u{2658}",
  'P' => "\u{2659}",
  nil => '_'
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(squares) ⇒ Board

Returns a new instance of Board.

Examples:

PGN::Board.new(
  [
    ["R", "P", nil, nil, nil, nil, "p", "r"],
    ["N", "P", nil, nil, nil, nil, "p", "n"],
    ["B", "P", nil, nil, nil, nil, "p", "b"],
    ["Q", "P", nil, nil, nil, nil, "p", "q"],
    ["K", "P", nil, nil, nil, nil, "p", "k"],
    ["B", "P", nil, nil, nil, nil, "p", "b"],
    ["N", "P", nil, nil, nil, nil, "p", "n"],
    ["R", "P", nil, nil, nil, nil, "p", "r"],
  ]
)

Parameters:

  • squares (<Array<Array<String>>>)

    the squares of the board



86
87
88
# File 'lib/pgn/board.rb', line 86

def initialize(squares)
  self.squares = squares
end

Instance Attribute Details

#squaresArray<Array<String>>

Returns the board as a file-major 8x8 array (squares[rank]). Built on demand from the 0x88 array; equality with the START constant and other boards is preserved.

Returns:

  • (Array<Array<String>>)

    the board as a file-major 8x8 array (squares[rank]). Built on demand from the 0x88 array; equality with the START constant and other boards is preserved.



15
16
17
18
19
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
# File 'lib/pgn/board.rb', line 15

class Board
  # The starting, internal representation of a chess board
  #
  START = [
    ['R', 'P', nil, nil, nil, nil, 'p', 'r'],
    ['N', 'P', nil, nil, nil, nil, 'p', 'n'],
    ['B', 'P', nil, nil, nil, nil, 'p', 'b'],
    ['Q', 'P', nil, nil, nil, nil, 'p', 'q'],
    ['K', 'P', nil, nil, nil, nil, 'p', 'k'],
    ['B', 'P', nil, nil, nil, nil, 'p', 'b'],
    ['N', 'P', nil, nil, nil, nil, 'p', 'n'],
    ['R', 'P', nil, nil, nil, nil, 'p', 'r']
  ].freeze

  FILE_TO_INDEX = ('a'..'h').each_with_index.to_h
  INDEX_TO_FILE = FILE_TO_INDEX.invert

  RANK_TO_INDEX = ('1'..'8').each_with_index.to_h
  INDEX_TO_RANK = RANK_TO_INDEX.invert

  # algebraic to unicode piece lookup
  #
  UNICODE_PIECES = {
    'k' => "\u{265A}",
    'q' => "\u{265B}",
    'r' => "\u{265C}",
    'b' => "\u{265D}",
    'n' => "\u{265E}",
    'p' => "\u{265F}",
    'K' => "\u{2654}",
    'Q' => "\u{2655}",
    'R' => "\u{2656}",
    'B' => "\u{2657}",
    'N' => "\u{2658}",
    'P' => "\u{2659}",
    nil => '_'
  }.freeze

  # 0x88 board representation (see chess.js / the classic 0x88 move-generation
  # algorithm). A square is addressed by a single integer index
  # `rank * 16 + file`; the extra files/ranks make off-board detection a
  # single bitmask test -- `(idx & 0x88) != 0` -- which is faster than the
  # four-integer comparison a 0..7 bounds check needs, and lets ray
  # stepping be a single integer add. The public `squares` 8x8 API is built
  # from this array on demand (it is off the replay hot path), and the
  # MoveCalculator hot path works entirely in integer indices.
  #
  #   file = idx & 0x0F   (0..7)
  #   rank = idx >> 4      (0..7)

  # @return [PGN::Board] a board in the starting position
  #
  def self.start
    PGN::Board.new(START)
  end

  # @param squares [<Array<Array<String>>>] the squares of the board
  # @example
  #   PGN::Board.new(
  #     [
  #       ["R", "P", nil, nil, nil, nil, "p", "r"],
  #       ["N", "P", nil, nil, nil, nil, "p", "n"],
  #       ["B", "P", nil, nil, nil, nil, "p", "b"],
  #       ["Q", "P", nil, nil, nil, nil, "p", "q"],
  #       ["K", "P", nil, nil, nil, nil, "p", "k"],
  #       ["B", "P", nil, nil, nil, nil, "p", "b"],
  #       ["N", "P", nil, nil, nil, nil, "p", "n"],
  #       ["R", "P", nil, nil, nil, nil, "p", "r"],
  #     ]
  #   )
  #
  def initialize(squares)
    self.squares = squares
  end

  # @return [Array<Array<String>>] the board as a file-major 8x8 array
  #   (squares[file][rank]). Built on demand from the 0x88 array; equality
  #   with the START constant and other boards is preserved.
  #
  def squares
    (0..7).map { |f| (0..7).map { |r| @cells[(r * 16) + f] } }
  end

  def squares=(squares)
    @cells = Array.new(128)
    8.times do |f|
      8.times do |r|
        @cells[(r * 16) + f] = squares[f][r]
      end
    end
  end

  # @overload at(str)
  #   Looks up a piece based on the string representation of a square (e4)
  #   @param str [String] the square in algebraic notation
  # @overload at(file, rank)
  #   Looks up a piece based on zero-indexed coordinates (4, 3)
  #   @param file [Integer] the file the piece is on
  #   @param rank [Integer] the rank the piece is on
  # @return [String, nil] the piece on the square, or nil if it is
  #   empty
  # @example
  #   board.at(4,3)  #=> "P"
  #   board.at("e4") #=> "P"
  #
  def at(arg0, arg1 = nil)
    return at_index(index_for(arg0, arg1)) unless arg1.nil?

    at_index(index_of(arg0))
  end

  # @param changes [Hash<String, <String, nil>>] changes to make to the board
  # @return [self]
  # @example
  #   board.change!({"e2" => nil, "e4" => "P"})
  #
  def change!(changes)
    changes.each { |square, piece| update(square, piece) }
    self
  end

  # @param square [String] the square in algebraic notation
  # @param piece [String, nil] the piece to put on the square
  # @return [self]
  # @example
  #   board.update("e4", "P")
  #
  def update(square, piece)
    update_index(index_of(square), piece)
  end

  # @param position [String] the square in algebraic notation
  # @return [Array<Integer>] the coordinates of the square
  # @example
  #   board.coordinates_for("e4") #=> [4, 3]
  #
  def coordinates_for(position)
    [file_of(position), rank_of(position)]
  end

  # @param coordinates [Array<Integer>] the coordinates of the square
  # @return [String] the square in algebraic notation
  # @example
  #   board.position_for([4, 3]) #=> "e4"
  #
  def position_for(coordinates)
    file, rank = coordinates
    INDEX_TO_FILE[file] + INDEX_TO_RANK[rank]
  end

  # @return [String] the board in human readable format with unicode
  #   pieces
  #
  def inspect
    squares.transpose.reverse.map do |row|
      row.map { |chr| UNICODE_PIECES[chr] }.join(' ')
    end.join("\n")
  end

  # Build a {Board} directly from a 0x88 cell array, bypassing the 8x8
  # -> 0x88 conversion in {#initialize}. Used by {#dup} (which runs every
  # move) to skip the per-square rebuild; the cell array is already in the
  # canonical 128-cell layout.
  #
  # @param cells [Array<String, nil>] a 128-cell 0x88 array
  # @return [PGN::Board]
  #
  def self.from_cells(cells)
    board = allocate
    board.instance_variable_set(:@cells, cells)
    board
  end

  # @return [PGN::Board] a copy of self. Copies the 128-cell 0x88 array;
  #   mutations to the copy do not affect the original.
  #
  def dup
    self.class.from_cells(@cells.dup)
  end

  # -- 0x88 hot-path API (integer indices) ---------------------------------

  # The 0x88 index of an algebraic square name.
  #
  # @param square [String] e.g. "e4"
  # @return [Integer] idx = rank * 16 + file
  #
  def index_of(square)
    (rank_of(square) * 16) + file_of(square)
  end

  # The 0x88 index of zero-indexed file/rank coordinates.
  #
  # @return [Integer] idx = rank * 16 + file
  #
  def index_for(file, rank)
    (rank * 16) + file
  end

  # Looks up a piece by 0x88 index. The caller is responsible for having
  # already verified the index is on-board (`(idx & 0x88).zero?`); reading
  # an off-board index simply returns nil.
  #
  # @param idx [Integer] a 0x88 square index
  # @return [String, nil] the piece on that square
  #
  def at_index(idx)
    @cells[idx]
  end

  # Places a piece on a 0x88 index. Returns self.
  #
  # @param idx [Integer] a 0x88 square index
  # @param piece [String, nil]
  # @return [self]
  #
  def update_index(idx, piece)
    @cells[idx] = piece
    self
  end

  # Applies a batch of integer-indexed changes. The replay hot path uses
  # this so it never allocates square-name strings or `[file, rank]`
  # coordinate arrays.
  #
  # @param changes [Hash<Integer, <String, nil>>]
  # @return [self]
  #
  def apply!(changes)
    changes.each { |idx, piece| @cells[idx] = piece }
    self
  end

  # Whether a 0x88 index is on the board (see the class doc for the
  # bitmask this tests).
  #
  # @param idx [Integer] a 0x88 square index
  # @return [Boolean]
  #
  def on_board?(idx)
    (idx & 0x88).zero? # rubocop:disable Style/BitwisePredicate
  end

  # The algebraic square name of a 0x88 index.
  #
  # @param idx [Integer] a 0x88 square index
  # @return [String] e.g. "e4"
  #
  def square_name(idx)
    INDEX_TO_FILE[idx & 0x0F] + INDEX_TO_RANK[idx >> 4]
  end

  private

  def file_of(square)
    square.getbyte(0) - 97
  end

  def rank_of(square)
    square.getbyte(1) - 49
  end
end

Class Method Details

.from_cells(cells) ⇒ PGN::Board

Build a PGN::Board directly from a 0x88 cell array, bypassing the 8x8 -> 0x88 conversion in #initialize. Used by #dup (which runs every move) to skip the per-square rebuild; the cell array is already in the canonical 128-cell layout.

Parameters:

  • cells (Array<String, nil>)

    a 128-cell 0x88 array

Returns:



182
183
184
185
186
# File 'lib/pgn/board.rb', line 182

def self.from_cells(cells)
  board = allocate
  board.instance_variable_set(:@cells, cells)
  board
end

.startPGN::Board

Returns a board in the starting position.

Returns:

  • (PGN::Board)

    a board in the starting position



67
68
69
# File 'lib/pgn/board.rb', line 67

def self.start
  PGN::Board.new(START)
end

Instance Method Details

#apply!(changes) ⇒ self

Applies a batch of integer-indexed changes. The replay hot path uses this so it never allocates square-name strings or [file, rank] coordinate arrays.

Parameters:

  • changes (Hash<Integer, <String, nil>>)

Returns:

  • (self)


243
244
245
246
# File 'lib/pgn/board.rb', line 243

def apply!(changes)
  changes.each { |idx, piece| @cells[idx] = piece }
  self
end

#at(str) ⇒ String? #at(file, rank) ⇒ String?

Returns the piece on the square, or nil if it is empty.

Examples:

board.at(4,3)  #=> "P"
board.at("e4") #=> "P"

Overloads:

  • #at(str) ⇒ String?

    Looks up a piece based on the string representation of a square (e4)

    Parameters:

    • str (String)

      the square in algebraic notation

  • #at(file, rank) ⇒ String?

    Looks up a piece based on zero-indexed coordinates (4, 3)

    Parameters:

    • file (Integer)

      the file the piece is on

    • rank (Integer)

      the rank the piece is on

Returns:

  • (String, nil)

    the piece on the square, or nil if it is empty



120
121
122
123
124
# File 'lib/pgn/board.rb', line 120

def at(arg0, arg1 = nil)
  return at_index(index_for(arg0, arg1)) unless arg1.nil?

  at_index(index_of(arg0))
end

#at_index(idx) ⇒ String?

Looks up a piece by 0x88 index. The caller is responsible for having already verified the index is on-board ((idx & 0x88).zero?); reading an off-board index simply returns nil.

Parameters:

  • idx (Integer)

    a 0x88 square index

Returns:

  • (String, nil)

    the piece on that square



221
222
223
# File 'lib/pgn/board.rb', line 221

def at_index(idx)
  @cells[idx]
end

#change!(changes) ⇒ self

Examples:

board.change!({"e2" => nil, "e4" => "P"})

Parameters:

  • changes (Hash<String, <String, nil>>)

    changes to make to the board

Returns:

  • (self)


131
132
133
134
# File 'lib/pgn/board.rb', line 131

def change!(changes)
  changes.each { |square, piece| update(square, piece) }
  self
end

#coordinates_for(position) ⇒ Array<Integer>

Returns the coordinates of the square.

Examples:

board.coordinates_for("e4") #=> [4, 3]

Parameters:

  • position (String)

    the square in algebraic notation

Returns:

  • (Array<Integer>)

    the coordinates of the square



151
152
153
# File 'lib/pgn/board.rb', line 151

def coordinates_for(position)
  [file_of(position), rank_of(position)]
end

#dupPGN::Board

Returns a copy of self. Copies the 128-cell 0x88 array; mutations to the copy do not affect the original.

Returns:

  • (PGN::Board)

    a copy of self. Copies the 128-cell 0x88 array; mutations to the copy do not affect the original.



191
192
193
# File 'lib/pgn/board.rb', line 191

def dup
  self.class.from_cells(@cells.dup)
end

#index_for(file, rank) ⇒ Integer

The 0x88 index of zero-indexed file/rank coordinates.

Returns:

  • (Integer)

    idx = rank * 16 + file



210
211
212
# File 'lib/pgn/board.rb', line 210

def index_for(file, rank)
  (rank * 16) + file
end

#index_of(square) ⇒ Integer

The 0x88 index of an algebraic square name.

Parameters:

  • square (String)

    e.g. "e4"

Returns:

  • (Integer)

    idx = rank * 16 + file



202
203
204
# File 'lib/pgn/board.rb', line 202

def index_of(square)
  (rank_of(square) * 16) + file_of(square)
end

#inspectString

Returns the board in human readable format with unicode pieces.

Returns:

  • (String)

    the board in human readable format with unicode pieces



168
169
170
171
172
# File 'lib/pgn/board.rb', line 168

def inspect
  squares.transpose.reverse.map do |row|
    row.map { |chr| UNICODE_PIECES[chr] }.join(' ')
  end.join("\n")
end

#on_board?(idx) ⇒ Boolean

Whether a 0x88 index is on the board (see the class doc for the bitmask this tests).

Parameters:

  • idx (Integer)

    a 0x88 square index

Returns:

  • (Boolean)


254
255
256
# File 'lib/pgn/board.rb', line 254

def on_board?(idx)
  (idx & 0x88).zero? # rubocop:disable Style/BitwisePredicate
end

#position_for(coordinates) ⇒ String

Returns the square in algebraic notation.

Examples:

board.position_for([4, 3]) #=> "e4"

Parameters:

  • coordinates (Array<Integer>)

    the coordinates of the square

Returns:

  • (String)

    the square in algebraic notation



160
161
162
163
# File 'lib/pgn/board.rb', line 160

def position_for(coordinates)
  file, rank = coordinates
  INDEX_TO_FILE[file] + INDEX_TO_RANK[rank]
end

#square_name(idx) ⇒ String

The algebraic square name of a 0x88 index.

Parameters:

  • idx (Integer)

    a 0x88 square index

Returns:

  • (String)

    e.g. "e4"



263
264
265
# File 'lib/pgn/board.rb', line 263

def square_name(idx)
  INDEX_TO_FILE[idx & 0x0F] + INDEX_TO_RANK[idx >> 4]
end

#update(square, piece) ⇒ self

Examples:

board.update("e4", "P")

Parameters:

  • square (String)

    the square in algebraic notation

  • piece (String, nil)

    the piece to put on the square

Returns:

  • (self)


142
143
144
# File 'lib/pgn/board.rb', line 142

def update(square, piece)
  update_index(index_of(square), piece)
end

#update_index(idx, piece) ⇒ self

Places a piece on a 0x88 index. Returns self.

Parameters:

  • idx (Integer)

    a 0x88 square index

  • piece (String, nil)

Returns:

  • (self)


231
232
233
234
# File 'lib/pgn/board.rb', line 231

def update_index(idx, piece)
  @cells[idx] = piece
  self
end