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.map(&:reverse).to_h
RANK_TO_INDEX =
('1'..'8').each_with_index.to_h
INDEX_TO_RANK =
RANK_TO_INDEX.map(&:reverse).to_h
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
# 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.map(&:reverse).to_h

  RANK_TO_INDEX = ('1'..'8').each_with_index.to_h
  INDEX_TO_RANK = RANK_TO_INDEX.map(&:reverse).to_h

  # 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
    @cells
  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 @cells[(arg1 * 16) + arg0] unless arg1.nil?

    @cells[(rank_of(arg0) * 16) + file_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)
    @cells[(rank_of(square) * 16) + file_of(square)] = piece
    self
  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

  # @return [PGN::Board] a copy of self. Copies the 128-cell 0x88 array;
  #   mutations to the copy do not affect the original.
  #
  def dup
    copy = PGN::Board.allocate
    copy.instance_variable_set(:@cells, @cells.dup)
    copy
  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

  private

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

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

Class Method Details

.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)


233
234
235
236
# File 'lib/pgn/board.rb', line 233

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



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

def at(arg0, arg1 = nil)
  return @cells[(arg1 * 16) + arg0] unless arg1.nil?

  @cells[(rank_of(arg0) * 16) + file_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



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

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)


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

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



153
154
155
# File 'lib/pgn/board.rb', line 153

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.



179
180
181
182
183
# File 'lib/pgn/board.rb', line 179

def dup
  copy = PGN::Board.allocate
  copy.instance_variable_set(:@cells, @cells.dup)
  copy
end

#index_for(file, rank) ⇒ Integer

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

Returns:

  • (Integer)

    idx = rank * 16 + file



200
201
202
# File 'lib/pgn/board.rb', line 200

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



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

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



170
171
172
173
174
# File 'lib/pgn/board.rb', line 170

def inspect
  squares.transpose.reverse.map do |row|
    row.map { |chr| UNICODE_PIECES[chr] }.join(' ')
  end.join("\n")
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



162
163
164
165
# File 'lib/pgn/board.rb', line 162

def position_for(coordinates)
  file, rank = coordinates
  INDEX_TO_FILE[file] + INDEX_TO_RANK[rank]
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)


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

def update(square, piece)
  @cells[(rank_of(square) * 16) + file_of(square)] = piece
  self
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)


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

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