Class: Uci

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

Constant Summary collapse

VERSION =
"0.0.2"
RANKS =
{
  'a' => 0, 'b' => 1, 'c' => 2, 'd' => 3,
  'e' => 4, 'f' => 5, 'g' => 6, 'h' => 7
}
PIECES =
{
  'p' => :pawn,
  'r' => :rook,
  'n' => :knight,
  'b' => :bishop,
  'k' => :king,
  'q' => :queen
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Uci

make a new connection to a UCI engine

Options

Required options:

  • :engine_path - path to the engine executable

Optional options:

  • :debug - enable debugging messages - true /false

  • :name - name of the engine - string

  • :movetime - max amount of time the engine can “think” in ms - default 100

  • :options - hash to pass to the engine for configuration



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/uci.rb', line 37

def initialize(options = {})
  options = default_options.merge(options)
  require_keys!(options, [:engine_path, :movetime])
  @movetime = options[:movetime]

  set_debug(options)
  reset_board!
  set_startpos!

  check_engine(options)
  set_engine_name(options)
  open_engine_connection(options[:engine_path])
  set_engine_options(options[:options]) if !options[:options].nil?
  new_game!
end

Instance Attribute Details

#debugObject (readonly)

Returns the value of attribute debug.



8
9
10
# File 'lib/uci.rb', line 8

def debug
  @debug
end

#movesObject (readonly)

return the current movement log



174
175
176
# File 'lib/uci.rb', line 174

def moves
  @moves
end

#movetimeObject

Returns the value of attribute movetime.



9
10
11
# File 'lib/uci.rb', line 9

def movetime
  @movetime
end

Instance Method Details

#bestmoveObject

ask the chess engine what the “best move” is given the current state of the internal chess board. This does not actiually execute a move, it simply queries for and returns what the engine would consider to be the best option available.



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

def bestmove
  write_to_engine("go movetime #{@movetime}")
  until (move_string = read_from_engine).to_s.size > 1
    sleep(0.25)
  end
  if move_string =~ /^bestmove/
    if move_string =~ /^bestmove\sa1a1/ # fruit and rybka
      raise EngineResignError, "Engine Resigns. Check Mate? #{move_string}"
    elsif move_string =~ /^bestmove\sNULL/ # robbolita
      raise NoMoveError, "No more moves: #{move_string}"
    elsif move_string =~ /^bestmove\s\(none\)\s/ #stockfish
      raise NoMoveError, "No more moves: #{move_string}"
    elsif bestmove = move_string.match(/^bestmove\s([a-h][1-8][a-h][1-8])([a-z]{1}?)/)
      return bestmove[1..-1].join
    else
      raise UnknownBestmoveSyntax, "Engine returned a 'bestmove' that I don't understand: #{move_string}"
    end
  else
    raise ReturnStringError, "Expected return to begin with 'bestmove', but got '#{move_string}'"
  end
end

#board(empty_square_char = '.') ⇒ Object

ASCII-art representation of the current internal board.

Example

> puts board

ABCDEFGH

8 r.bqkbnr 7 pppppppp 6 n.…… 5 .….… 4 .P.….. 3 .….… 2 P.PPPPPP 1 RNBQKBNR



333
334
335
336
337
338
339
340
341
342
343
# File 'lib/uci.rb', line 333

def board(empty_square_char = '.')
  board_str = "  ABCDEFGH\n"
  (@board.size-1).downto(0).each do |rank_index|
    line = "#{rank_index+1} "
    @board[rank_index].each do |cell|
      line << (cell.nil? ? empty_square_char : cell)
    end
    board_str << line+"\n"
  end
  board_str
end

#clear_position(position) ⇒ Object

clear a position on the board, regardless of occupied state

Raises:



233
234
235
236
237
238
# File 'lib/uci.rb', line 233

def clear_position(position)
  raise BoardLockedError, "Board was set from FEN string" if @fen
  rank = RANKS[position.to_s.downcase.split('').first]
  file = position.downcase.split('').last.to_i-1
  @board[file][rank] = nil
end

#engine_nameObject

return the current engine name



347
348
349
# File 'lib/uci.rb', line 347

def engine_name
  @engine_name
end

#fenstringObject

return the state of the interal board in a FEN (Forsyth–Edwards Notation) string, SHORT format (no castling info, move, etc)



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
# File 'lib/uci.rb', line 287

def fenstring
  fen = []
  (@board.size-1).downto(0).each do |rank_index|
    rank = @board[rank_index]
    if rank.include?(nil)
      if rank.select{|r|r.nil?}.size == 8
        fen << 8
      else
        rank_str = ""
        empties = 0
        rank.each do |r|
          if r.nil?
            empties += 1
          else
            if empties > 0
              rank_str << empties.to_s
              empties = 0
            end
            rank_str << r
          end
        end
        rank_str << empties.to_s if empties > 0
        fen << rank_str
      end
    else
      fen << rank.join('')
    end
  end
  fen.join('/')
end

#get_piece(position) ⇒ Object

get the details of a piece at the current position raises NoPieceAtPositionError if position is unoccupied

returns array of [:piece, :player]

Example

> get_piece(“a2”)

> [:pawn, :white]



187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/uci.rb', line 187

def get_piece(position)
  rank = RANKS[position.to_s.downcase.split('').first]
  file = position.downcase.split('').last.to_i-1
  piece = @board[file][rank]
  if piece.nil?
    raise NoPieceAtPositionError, "No piece at #{position}!"
  end
  player = if piece =~ /^[A-Z]$/
    :white
  else
    :black
  end
  [piece_name(piece), player]
end

#go!Object

tell the engine what the current board layout it, get its best move AND execute that move on the current board.



114
115
116
117
# File 'lib/uci.rb', line 114

def go!
  send_position_to_engine
  move_piece(bestmove)
end

#move_piece(move_string) ⇒ Object

move a piece on the current interal board.

Attributes

  • move_string = algebraic standard notation of the chess move. Shorthand not allowed.

Simple movement: a2a3 Castling (king’s rook white): e1g1 Pawn promomition (to Queen): a7a8q

Note that there is minimal rule checking here, illegal moves will be executed.

Raises:



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
# File 'lib/uci.rb', line 129

def move_piece(move_string)
  raise BoardLockedError, "Board was set from FEN string" if @fen
  (move, extended) = *move_string.match(/^([a-h][1-8][a-h][1-8])([a-z]{1}?)$/)[1..2]

  start_pos = move.downcase.split('')[0..1].join
  end_pos = move.downcase.split('')[2..3].join
  (piece, player) = get_piece(start_pos)

  place_piece(player, piece, end_pos)
  clear_position(start_pos)

  if extended.to_s.size > 0
    if %w[q r b n].include?(extended)
      place = move.split('')[2..3].join
      p, player = get_piece(place)
      log("pawn promotion: #{p} #{player}")
      place_piece(player, piece_name(extended), place)
    else
      raise UnknownNotationExtensionError, "Unknown notation extension: #{move_string}"
    end
  end

  # detect castling
  if piece == :king
    start_rank = start_pos.split('')[1]
    start_file = start_pos.split('')[0].ord
    end_file = end_pos.split('')[0].ord
    if(start_file - end_file).abs > 1
      # assume the engine knows the rook is present
      if start_file < end_file # king's rook
        place_piece(player, :rook, "f#{start_rank}")
        clear_position("h#{start_rank}")
      elsif end_file < start_file # queen's rook
        place_piece(player, :rook, "d#{start_rank}")
        clear_position("a#{start_rank}")
      else
        raise "Unknown castling behviour!"
      end
    end
  end

  @moves << move_string
end

#new_game!Object

send “ucinewgame” to engine, reset interal board to standard starting layout



61
62
63
64
65
66
# File 'lib/uci.rb', line 61

def new_game!
  write_to_engine('ucinewgame')
  reset_board!
  set_startpos!
  @fen = nil
end

#new_game?Boolean

true if no moves have been recorded yet

Returns:

  • (Boolean)


69
70
71
# File 'lib/uci.rb', line 69

def new_game?
  moves.empty?
end

#piece_at?(position) ⇒ Boolean

returns a boolean if a position is occupied

Example

> piece_at?(“a2”)

> true

> piece_at?(“a3”)

> false

Returns:

  • (Boolean)


210
211
212
213
214
# File 'lib/uci.rb', line 210

def piece_at?(position)
  rank = RANKS[position.to_s.downcase.split('').first]
  file = position.downcase.split('').last.to_i-1
  !!@board[file][rank]
end

#piece_name(p) ⇒ Object

Returns the piece name OR the piece icon, depending on that was passes.

Example

> piece_name(:n)

> :knight

> piece_name(:queen)

> “q”



224
225
226
227
228
229
230
# File 'lib/uci.rb', line 224

def piece_name(p)
  if p.class.to_s == "Symbol"
    (p == :knight ? :night : p).to_s.downcase.split('').first
  else
    PIECES[p.downcase]
  end
end

#place_piece(player, piece, position) ⇒ Object

place a piece on the board, regardless of occupied state

Attributes

  • player - symbol: :black or :white

  • piece - symbol: :pawn, :rook, etc

  • position - a2, etc

Raises:



246
247
248
249
250
251
252
253
254
# File 'lib/uci.rb', line 246

def place_piece(player, piece, position)
  raise BoardLockedError, "Board was set from FEN string" if @fen
  rank_index = RANKS[position.downcase.split('').first]

  file_index = position.split('').last.to_i-1
  icon = (piece == :knight ? :night : piece).to_s.split('').first
  (player == :black ? icon.downcase! : icon.upcase!)
  @board[file_index][rank_index] = icon
end

#ready?Boolean

true if engine is ready, false if not yet ready

Returns:

  • (Boolean)


54
55
56
57
# File 'lib/uci.rb', line 54

def ready?
  write_to_engine('isready')
  read_from_engine == "readyok"
end

#send_position_to_engineObject

write board position information to the UCI engine, either the starting position + move log or the current FEN string, depending on how the board was set up.



102
103
104
105
106
107
108
109
110
# File 'lib/uci.rb', line 102

def send_position_to_engine
  if @fen
    write_to_engine("position fen #{@fen}")
  else
    position_str = "position startpos"
    position_str << " moves #{@moves.join(' ')}" unless @moves.empty?
    write_to_engine(position_str)
  end
end

#set_board(fen) ⇒ Object

set the board using Forsyth–Edwards Notation (FEN), LONG format including move, castling, etc.

Attributes

  • fen - rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1 (Please

see en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/uci.rb', line 262

def set_board(fen)
  # rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1
  fen_pattern = /^[a-zA-Z0-9\/]+\s[bw]\s[kqKQ-]+\s[a-h0-8-]+\s\d+\s\d+$/
  unless fen =~ fen_pattern
    raise FenFormatError, "Fenstring not correct: #{fen}. Expected to match #{fen_pattern}"
  end
  reset_board!
  fen.split(' ').first.split('/').reverse.each_with_index do |rank, rank_index|
    file_index = 0
    rank.split('').each do |file|
      if file.to_i > 0
        file_index += file.to_i
      else
        @board[rank_index][file_index] = file
        file_index += 1
      end
    end
  end
  new_game!
  @fen = fen
  send_position_to_engine
end