uci 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm use 1.9.3@uci
2
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in uci.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Matthew Nielsen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,238 @@
1
+ # Ruby UCI - A Universal Chess Interface for Ruby
2
+
3
+ The UCI gem allows for a much more ruby-like way of communicating with chess
4
+ engines that support the UCI protocol.
5
+
6
+ ## Installation
7
+
8
+ NOTE: No Chess engines are included. You must install an appropriate UCI-compatible engine first.
9
+
10
+ Standard installation applies. Either:
11
+
12
+ gem install uci
13
+
14
+ ..or, in a bundled project, add this line to your application's Gemfile:
15
+
16
+ gem 'uci'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ ## Example
23
+
24
+ ```ruby
25
+ require 'uci'
26
+
27
+ uci = Uci.new( :engine_path => '/usr/local/bin/stockfish' )
28
+
29
+ while !uci.ready? do
30
+ puts "Engine isn't ready yet, sleeping..."
31
+ sleep(1)
32
+ end
33
+
34
+ # this loop will make the engine play against itself.
35
+ loop do
36
+ puts "Move ##{uci.moves.size+1}."
37
+ puts uci.board # print ascii layout of current board.
38
+ uci.go!
39
+ end
40
+ ```
41
+
42
+ ## Docs
43
+
44
+ ### ::new(options = {})
45
+
46
+ make a new connection to a UCI engine
47
+
48
+ <pre>
49
+ Uci.new(
50
+ :engine_path => '/path/to/executable',
51
+ :debug => false, # true or false, default false
52
+ :name => "Name of engine", # optional
53
+ :movetime => 100, # max amount of time engine can "think" in ms - default 100
54
+ :options => { "Navalov Cache" => true } # optional configuration for engine
55
+ )
56
+ </pre>
57
+
58
+ ### #bestmove()
59
+
60
+ Ask the chess engine what the “best move” is given the current state of the
61
+ internal chess board. This does not actiually execute a move, it simply queries
62
+ for and returns what the engine would consider to be the best option available.
63
+
64
+ ### #board(empty_square_char = '.')
65
+
66
+ ASCII-art representation of the current internal board.
67
+
68
+ <pre>
69
+ > puts board
70
+ ABCDEFGH
71
+ 8 r.bqkbnr
72
+ 7 pppppppp
73
+ 6 n.......
74
+ 5 ........
75
+ 4 .P......
76
+ 3 ........
77
+ 2 P.PPPPPP
78
+ 1 RNBQKBNR
79
+ </pre>
80
+
81
+ ### clear_position(position)
82
+
83
+ Clear a position on the board, regardless of occupied state
84
+
85
+ ### engine_name()
86
+
87
+ Return the current engine name
88
+
89
+ ### fenstring()
90
+
91
+ Return the state of the interal board in a FEN (Forsyth–Edwards Notation)
92
+ string, SHORT format (no castling info, move, etc).
93
+
94
+ ### get_piece(position)
95
+
96
+ Get the details of a piece at the current position raises
97
+ NoPieceAtPositionError if position is unoccupied.
98
+ Returns array of [:piece, :player].
99
+
100
+ <pre>
101
+ > get_piece(“a2”)
102
+ > [:pawn, :white]
103
+ </pre>
104
+
105
+ ### go!()
106
+
107
+ Tell the engine what the current board layout it, get its best move AND
108
+ execute that move on the current board.
109
+
110
+ ### move_piece(move_string)
111
+
112
+ Move a piece on the current interal board. Will raise NoPieceAtPositionError
113
+ if source position is unoccupied. If destination is occipied, the occupying
114
+ piece will be removed from the game.
115
+
116
+ move_string is algebraic standard notation of the chess move. Shorthand is
117
+ not allowed.
118
+
119
+ <pre>
120
+ move_piece("a2a3") # Simple movement
121
+ move_piece("e1g1") # Castling (king’s rook white)
122
+ move_piece("a7a8q" # Pawn promomition (to Queen)
123
+ </pre>
124
+
125
+ Note that there is minimal rule checking here, illegal moves will be executed.
126
+
127
+ ### new_game!()
128
+
129
+ Send “ucinewgame” to engine, reset interal board to standard starting layout.
130
+
131
+ ### new_game?()
132
+ True if no moves have been recorded yet.
133
+
134
+ ### piece_at?(position)
135
+
136
+ returns a boolean if a position is occupied
137
+
138
+ <pre>
139
+ > piece_at?(“a2”)
140
+ > true
141
+ > piece_at?(“a3”)
142
+ > false
143
+ </pre>
144
+
145
+ ### piece_name(p)
146
+
147
+ Returns the piece name OR the piece icon, depending on that was passes.
148
+
149
+ <pre>
150
+ > piece_name(:n)
151
+ > :knight
152
+ > piece_name(:queen)
153
+ > “q”
154
+ </pre>
155
+
156
+ ### place_piece(player, piece, position)
157
+
158
+ Place a piece on the board, regardless of occupied state.
159
+
160
+ * player - symbol: :black or :white
161
+ * piece - symbol: :pawn, :rook, etc
162
+ * position - a2, etc
163
+
164
+ <pre>
165
+ place_piece(:black, :rook, "h1")
166
+ </pre>
167
+
168
+ ### ready?()
169
+
170
+ True if engine is ready, false if not yet ready.
171
+
172
+ ### send_position_to_engine()
173
+
174
+ Write board position information to the UCI engine, either the starting
175
+ position and move log or the current FEN string, depending on how the
176
+ board was set up.
177
+
178
+ This does not tell the engine to execute a move.
179
+
180
+ ### set_board(fen)
181
+
182
+ Set the board using Forsyth–Edwards Notation (FEN), LONG format including
183
+ current player, castling, etc.
184
+
185
+
186
+ * fen - rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1 (Please
187
+ see en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
188
+
189
+ ## Supported Engines
190
+
191
+ In theory it can support any UCI-compatible engine (except for conditions outlined in the 'caveats' section). It has been tested with:
192
+
193
+ * Stockfish (Jan 11 2013 Github source)
194
+ * Fruit 2.3.1 (Mac)
195
+
196
+ ## Caveats
197
+
198
+ #### No move checking
199
+
200
+ This gem assumes the engine knows what it's doing. If the gem wishes to place a illegal move it will be accepted.
201
+
202
+ #### Unix-style Line endings are assumed.
203
+
204
+ Current version assumes unix-style ("\n") line endings. That means running this under MS-DOS or Windows may barf.
205
+
206
+ #### Very limited command set.
207
+
208
+ Very few commands of the total UCI command set are currently supported. they are:
209
+
210
+ * Starting a new game
211
+ * Setting positions
212
+ * Getting best move
213
+ * Setting options
214
+
215
+ It DOES NOT _yet_ support:
216
+
217
+ * 'uci' command
218
+ * ponder mode / infinite mode
219
+ * ponderhit
220
+ * registrations
221
+
222
+ ## Known Issues
223
+
224
+ When connecting to more than one engine from the same code, there is a problem where their input streams get crosses. This is an issue with Open3, but for some reason turning "debug" on seems to mitigate it.
225
+
226
+ Open3 is supposed to work under in Ruby 1.9.x in Windows, but this is currently untested.
227
+
228
+ ## Contributing
229
+
230
+ Ruby UCI needs support for more features and to be tested with more chess
231
+ engines. To contribute to this project please fork the project and add/change
232
+ any new code inside of a new branch:
233
+
234
+ git checkout -b my-new-feature
235
+
236
+ Before committing and pushing code, please make sure all existing tests pass
237
+ and that all new code has tests. Once that is verified, please push the changes
238
+ and create a new pull request.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,482 @@
1
+ # The UCI gem allows for a much more ruby-like way of communicating with chess
2
+ # engines that support the UCI protocol.
3
+
4
+ require 'open3'
5
+ require 'io/wait'
6
+
7
+ class Uci
8
+ attr_reader :moves, :debug
9
+ attr_accessor :movetime
10
+ VERSION = "0.0.2"
11
+
12
+ RANKS = {
13
+ 'a' => 0, 'b' => 1, 'c' => 2, 'd' => 3,
14
+ 'e' => 4, 'f' => 5, 'g' => 6, 'h' => 7
15
+ }
16
+ PIECES = {
17
+ 'p' => :pawn,
18
+ 'r' => :rook,
19
+ 'n' => :knight,
20
+ 'b' => :bishop,
21
+ 'k' => :king,
22
+ 'q' => :queen
23
+ }
24
+
25
+ # make a new connection to a UCI engine
26
+ #
27
+ # ==== Options
28
+ #
29
+ # Required options:
30
+ # * :engine_path - path to the engine executable
31
+ #
32
+ # Optional options:
33
+ # * :debug - enable debugging messages - true /false
34
+ # * :name - name of the engine - string
35
+ # * :movetime - max amount of time the engine can "think" in ms - default 100
36
+ # * :options - hash to pass to the engine for configuration
37
+ def initialize(options = {})
38
+ options = default_options.merge(options)
39
+ require_keys!(options, [:engine_path, :movetime])
40
+ @movetime = options[:movetime]
41
+
42
+ set_debug(options)
43
+ reset_board!
44
+ set_startpos!
45
+
46
+ check_engine(options)
47
+ set_engine_name(options)
48
+ open_engine_connection(options[:engine_path])
49
+ set_engine_options(options[:options]) if !options[:options].nil?
50
+ new_game!
51
+ end
52
+
53
+ # true if engine is ready, false if not yet ready
54
+ def ready?
55
+ write_to_engine('isready')
56
+ read_from_engine == "readyok"
57
+ end
58
+
59
+ # send "ucinewgame" to engine, reset interal board to standard starting
60
+ # layout
61
+ def new_game!
62
+ write_to_engine('ucinewgame')
63
+ reset_board!
64
+ set_startpos!
65
+ @fen = nil
66
+ end
67
+
68
+ # true if no moves have been recorded yet
69
+ def new_game?
70
+ moves.empty?
71
+ end
72
+
73
+ # ask the chess engine what the "best move" is given the current state of
74
+ # the internal chess board. This does *not* actiually execute a move, it
75
+ # simply queries for and returns what the engine would consider to be the
76
+ # best option available.
77
+ def bestmove
78
+ write_to_engine("go movetime #{@movetime}")
79
+ until (move_string = read_from_engine).to_s.size > 1
80
+ sleep(0.25)
81
+ end
82
+ if move_string =~ /^bestmove/
83
+ if move_string =~ /^bestmove\sa1a1/ # fruit and rybka
84
+ raise EngineResignError, "Engine Resigns. Check Mate? #{move_string}"
85
+ elsif move_string =~ /^bestmove\sNULL/ # robbolita
86
+ raise NoMoveError, "No more moves: #{move_string}"
87
+ elsif move_string =~ /^bestmove\s\(none\)\s/ #stockfish
88
+ raise NoMoveError, "No more moves: #{move_string}"
89
+ elsif bestmove = move_string.match(/^bestmove\s([a-h][1-8][a-h][1-8])([a-z]{1}?)/)
90
+ return bestmove[1..-1].join
91
+ else
92
+ raise UnknownBestmoveSyntax, "Engine returned a 'bestmove' that I don't understand: #{move_string}"
93
+ end
94
+ else
95
+ raise ReturnStringError, "Expected return to begin with 'bestmove', but got '#{move_string}'"
96
+ end
97
+ end
98
+
99
+ # write board position information to the UCI engine, either the starting
100
+ # position + move log or the current FEN string, depending on how the board
101
+ # was set up.
102
+ def send_position_to_engine
103
+ if @fen
104
+ write_to_engine("position fen #{@fen}")
105
+ else
106
+ position_str = "position startpos"
107
+ position_str << " moves #{@moves.join(' ')}" unless @moves.empty?
108
+ write_to_engine(position_str)
109
+ end
110
+ end
111
+
112
+ # tell the engine what the current board layout it, get its best move AND
113
+ # execute that move on the current board.
114
+ def go!
115
+ send_position_to_engine
116
+ move_piece(bestmove)
117
+ end
118
+
119
+ # move a piece on the current interal board.
120
+ #
121
+ # ==== Attributes
122
+ # * move_string = algebraic standard notation of the chess move. Shorthand not allowed.
123
+ #
124
+ # Simple movement: a2a3
125
+ # Castling (king's rook white): e1g1
126
+ # Pawn promomition (to Queen): a7a8q
127
+ #
128
+ # Note that there is minimal rule checking here, illegal moves will be executed.
129
+ def move_piece(move_string)
130
+ raise BoardLockedError, "Board was set from FEN string" if @fen
131
+ (move, extended) = *move_string.match(/^([a-h][1-8][a-h][1-8])([a-z]{1}?)$/)[1..2]
132
+
133
+ start_pos = move.downcase.split('')[0..1].join
134
+ end_pos = move.downcase.split('')[2..3].join
135
+ (piece, player) = get_piece(start_pos)
136
+
137
+ place_piece(player, piece, end_pos)
138
+ clear_position(start_pos)
139
+
140
+ if extended.to_s.size > 0
141
+ if %w[q r b n].include?(extended)
142
+ place = move.split('')[2..3].join
143
+ p, player = get_piece(place)
144
+ log("pawn promotion: #{p} #{player}")
145
+ place_piece(player, piece_name(extended), place)
146
+ else
147
+ raise UnknownNotationExtensionError, "Unknown notation extension: #{move_string}"
148
+ end
149
+ end
150
+
151
+ # detect castling
152
+ if piece == :king
153
+ start_rank = start_pos.split('')[1]
154
+ start_file = start_pos.split('')[0].ord
155
+ end_file = end_pos.split('')[0].ord
156
+ if(start_file - end_file).abs > 1
157
+ # assume the engine knows the rook is present
158
+ if start_file < end_file # king's rook
159
+ place_piece(player, :rook, "f#{start_rank}")
160
+ clear_position("h#{start_rank}")
161
+ elsif end_file < start_file # queen's rook
162
+ place_piece(player, :rook, "d#{start_rank}")
163
+ clear_position("a#{start_rank}")
164
+ else
165
+ raise "Unknown castling behviour!"
166
+ end
167
+ end
168
+ end
169
+
170
+ @moves << move_string
171
+ end
172
+
173
+ # return the current movement log
174
+ def moves
175
+ @moves
176
+ end
177
+
178
+ # get the details of a piece at the current position
179
+ # raises NoPieceAtPositionError if position is unoccupied
180
+ #
181
+ # returns array of [:piece, :player]
182
+ #
183
+ # ==== Example
184
+ #
185
+ # > get_piece("a2")
186
+ # => [:pawn, :white]
187
+ def get_piece(position)
188
+ rank = RANKS[position.to_s.downcase.split('').first]
189
+ file = position.downcase.split('').last.to_i-1
190
+ piece = @board[file][rank]
191
+ if piece.nil?
192
+ raise NoPieceAtPositionError, "No piece at #{position}!"
193
+ end
194
+ player = if piece =~ /^[A-Z]$/
195
+ :white
196
+ else
197
+ :black
198
+ end
199
+ [piece_name(piece), player]
200
+ end
201
+
202
+ # returns a boolean if a position is occupied
203
+ #
204
+ # ==== Example
205
+ #
206
+ # > piece_at?("a2")
207
+ # => true
208
+ # > piece_at?("a3")
209
+ # => false
210
+ def piece_at?(position)
211
+ rank = RANKS[position.to_s.downcase.split('').first]
212
+ file = position.downcase.split('').last.to_i-1
213
+ !!@board[file][rank]
214
+ end
215
+
216
+ # Returns the piece name OR the piece icon, depending on that was passes.
217
+ #
218
+ # ==== Example
219
+ #
220
+ # > piece_name(:n)
221
+ # => :knight
222
+ # > piece_name(:queen)
223
+ # => "q"
224
+ def piece_name(p)
225
+ if p.class.to_s == "Symbol"
226
+ (p == :knight ? :night : p).to_s.downcase.split('').first
227
+ else
228
+ PIECES[p.downcase]
229
+ end
230
+ end
231
+
232
+ # clear a position on the board, regardless of occupied state
233
+ def clear_position(position)
234
+ raise BoardLockedError, "Board was set from FEN string" if @fen
235
+ rank = RANKS[position.to_s.downcase.split('').first]
236
+ file = position.downcase.split('').last.to_i-1
237
+ @board[file][rank] = nil
238
+ end
239
+
240
+ # place a piece on the board, regardless of occupied state
241
+ #
242
+ # ==== Attributes
243
+ # * player - symbol: :black or :white
244
+ # * piece - symbol: :pawn, :rook, etc
245
+ # * position - a2, etc
246
+ def place_piece(player, piece, position)
247
+ raise BoardLockedError, "Board was set from FEN string" if @fen
248
+ rank_index = RANKS[position.downcase.split('').first]
249
+
250
+ file_index = position.split('').last.to_i-1
251
+ icon = (piece == :knight ? :night : piece).to_s.split('').first
252
+ (player == :black ? icon.downcase! : icon.upcase!)
253
+ @board[file_index][rank_index] = icon
254
+ end
255
+
256
+ # set the board using Forsyth–Edwards Notation (FEN), *LONG* format including
257
+ # move, castling, etc.
258
+ #
259
+ # ==== Attributes
260
+ # * fen - rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1 (Please
261
+ # see http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
262
+ def set_board(fen)
263
+ # rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1
264
+ fen_pattern = /^[a-zA-Z0-9\/]+\s[bw]\s[kqKQ-]+\s[a-h0-8-]+\s\d+\s\d+$/
265
+ unless fen =~ fen_pattern
266
+ raise FenFormatError, "Fenstring not correct: #{fen}. Expected to match #{fen_pattern}"
267
+ end
268
+ reset_board!
269
+ fen.split(' ').first.split('/').reverse.each_with_index do |rank, rank_index|
270
+ file_index = 0
271
+ rank.split('').each do |file|
272
+ if file.to_i > 0
273
+ file_index += file.to_i
274
+ else
275
+ @board[rank_index][file_index] = file
276
+ file_index += 1
277
+ end
278
+ end
279
+ end
280
+ new_game!
281
+ @fen = fen
282
+ send_position_to_engine
283
+ end
284
+
285
+ # return the state of the interal board in a FEN (Forsyth–Edwards Notation)
286
+ # string, *SHORT* format (no castling info, move, etc)
287
+ def fenstring
288
+ fen = []
289
+ (@board.size-1).downto(0).each do |rank_index|
290
+ rank = @board[rank_index]
291
+ if rank.include?(nil)
292
+ if rank.select{|r|r.nil?}.size == 8
293
+ fen << 8
294
+ else
295
+ rank_str = ""
296
+ empties = 0
297
+ rank.each do |r|
298
+ if r.nil?
299
+ empties += 1
300
+ else
301
+ if empties > 0
302
+ rank_str << empties.to_s
303
+ empties = 0
304
+ end
305
+ rank_str << r
306
+ end
307
+ end
308
+ rank_str << empties.to_s if empties > 0
309
+ fen << rank_str
310
+ end
311
+ else
312
+ fen << rank.join('')
313
+ end
314
+ end
315
+ fen.join('/')
316
+ end
317
+
318
+ # ASCII-art representation of the current internal board.
319
+ #
320
+ # ==== Example
321
+ #
322
+ # > puts board
323
+ # ABCDEFGH
324
+ # 8 r.bqkbnr
325
+ # 7 pppppppp
326
+ # 6 n.......
327
+ # 5 ........
328
+ # 4 .P......
329
+ # 3 ........
330
+ # 2 P.PPPPPP
331
+ # 1 RNBQKBNR
332
+ #
333
+ def board(empty_square_char = '.')
334
+ board_str = " ABCDEFGH\n"
335
+ (@board.size-1).downto(0).each do |rank_index|
336
+ line = "#{rank_index+1} "
337
+ @board[rank_index].each do |cell|
338
+ line << (cell.nil? ? empty_square_char : cell)
339
+ end
340
+ board_str << line+"\n"
341
+ end
342
+ board_str
343
+ end
344
+
345
+
346
+ # return the current engine name
347
+ def engine_name
348
+ @engine_name
349
+ end
350
+
351
+ protected
352
+
353
+ def set_engine_options(options)
354
+ options.each do |k,v|
355
+ write_to_engine("setoption name #{k} value #{v}")
356
+ end
357
+ end
358
+
359
+ def write_to_engine(message, send_cr=true)
360
+ log("\twrite_to_engine")
361
+ log("\t\tME: \t'#{message}'")
362
+ if send_cr && message.split('').last != "\n"
363
+ @engine_stdin.puts message
364
+ else
365
+ @engine_stdin.print message
366
+ end
367
+ end
368
+
369
+ def read_from_engine(strip_cr=true)
370
+ log("\tread_from_engine") #XXX
371
+ response = ""
372
+ while @engine_stdout.ready?
373
+ unless (response = @engine_stdout.readline) =~ /^info/
374
+ log("\t\tENGINE:\t'#{response}'")
375
+ end
376
+ end
377
+ if strip_cr && response.split('').last == "\n"
378
+ response.chop
379
+ else
380
+ response
381
+ end
382
+ end
383
+
384
+ private
385
+
386
+ def reset_move_record!
387
+ @moves = []
388
+ end
389
+
390
+ def reset_board!
391
+ @board = []
392
+ 8.times do |x|
393
+ @board[x] ||= []
394
+ 8.times do |y|
395
+ @board[x] << nil
396
+ end
397
+ end
398
+ reset_move_record!
399
+ end
400
+
401
+ def set_startpos!
402
+ %w[a b c d e f g h].each do |f|
403
+ place_piece(:white, :pawn, "#{f}2")
404
+ place_piece(:black, :pawn, "#{f}7")
405
+ end
406
+
407
+ place_piece(:white, :rook, "a1")
408
+ place_piece(:white, :rook, "h1")
409
+ place_piece(:white, :night, "b1")
410
+ place_piece(:white, :night, "g1")
411
+ place_piece(:white, :bishop, "c1")
412
+ place_piece(:white, :bishop, "f1")
413
+ place_piece(:white, :king, "e1")
414
+ place_piece(:white, :queen, "d1")
415
+
416
+ place_piece(:black, :rook, "a8")
417
+ place_piece(:black, :rook, "h8")
418
+ place_piece(:black, :night, "b8")
419
+ place_piece(:black, :night, "g8")
420
+ place_piece(:black, :bishop, "c8")
421
+ place_piece(:black, :bishop, "f8")
422
+ place_piece(:black, :king, "e8")
423
+ place_piece(:black, :queen, "d8")
424
+ end
425
+
426
+ def check_engine(options)
427
+ unless File.exist?(options[:engine_path])
428
+ raise EngineNotFoundError, "Engine not found at #{options[:engine_path]}"
429
+ end
430
+ unless File.executable?(options[:engine_path])
431
+ raise EngineNotExecutableError, "Engine at #{options[:engine_path]} is not executable"
432
+ end
433
+ end
434
+
435
+ def set_debug(options)
436
+ @debug = !!options[:debug]
437
+ end
438
+
439
+ def log(message)
440
+ puts "DEBUG (#{engine_name}): #{message}" if @debug
441
+ end
442
+
443
+ def open_engine_connection(engine_path)
444
+ @engine_stdin, @engine_stdout = Open3.popen2e(engine_path)
445
+ end
446
+
447
+ def require_keys!(hash, *required_keys)
448
+ required_keys.flatten.each do |required_key|
449
+ if !hash.keys.include?(required_key)
450
+ key_string = (required_key.is_a?(Symbol) ? ":#{required_key}" : required_key )
451
+ raise MissingRequiredHashKeyError, "Hash key '#{key_string}' missing"
452
+ end
453
+ end
454
+ true
455
+ end
456
+
457
+ def set_engine_name(options)
458
+ if options[:name].to_s.size > 1
459
+ @engine_name = options[:name]
460
+ else
461
+ @engine_name = options[:engine_path].split('/').last
462
+ end
463
+ end
464
+
465
+ def default_options
466
+ { :movetime => 100 }
467
+ end
468
+ end
469
+
470
+ class UciError < StandardError; end
471
+ class MissingRequiredHashKeyError < StandardError; end
472
+ class EngineNotFoundError < UciError; end
473
+ class EngineNotExecutableError < UciError; end
474
+ class EngineNameMismatch < UciError; end
475
+ class ReturnStringError < UciError; end
476
+ class UnknownNotationExtensionError < UciError; end
477
+ class NoMoveError < UciError; end
478
+ class EngineResignError < NoMoveError; end
479
+ class NoPieceAtPositionError < UciError; end
480
+ class UnknownBestmoveSyntax < UciError; end
481
+ class FenFormatError < UciError; end
482
+ class BoardLockedError < UciError; end
@@ -0,0 +1,334 @@
1
+ require 'spec_helper'
2
+ require 'uci'
3
+
4
+ describe Uci do
5
+ before(:each) do
6
+ Uci.any_instance.stub(:check_engine)
7
+ Uci.any_instance.stub(:open_engine_connection)
8
+ Uci.any_instance.stub(:get_engine_name)
9
+ Uci.any_instance.stub(:new_game!)
10
+ end
11
+
12
+ subject do
13
+ Uci.new(
14
+ :engine_path => '/usr/bin/stockfish',
15
+ :debug => true
16
+ )
17
+ end
18
+
19
+ describe "#initialize" do
20
+ let(:valid_options) do
21
+ { :engine_path => 'xxx' }
22
+ end
23
+ it "should be an instance of Uci" do
24
+ subject.should be_a_kind_of Uci
25
+ end
26
+ it "should require :engine_path' in the options hash" do
27
+ lambda { Uci.new({}) }.should raise_exception
28
+ lambda { Uci.new(valid_options) }.should_not raise_exception
29
+ end
30
+ it "should set debug mode" do
31
+ uci = Uci.new(valid_options)
32
+ uci.debug.should be_false
33
+
34
+ uci = Uci.new(valid_options.merge( :debug => true ))
35
+ uci.debug.should be_true
36
+ end
37
+ end
38
+
39
+ describe "#ready?" do
40
+ before(:each) do
41
+ subject.stub!(:write_to_engine).with('isready')
42
+ end
43
+
44
+ context "engine is ready" do
45
+ it "should be true" do
46
+ subject.stub!(:read_from_engine).and_return('readyok')
47
+
48
+ subject.ready?.should be_true
49
+ end
50
+ end
51
+
52
+ context "engine is not ready" do
53
+ it "should be false" do
54
+ subject.stub!(:read_from_engine).and_return('no')
55
+
56
+ subject.ready?.should be_false
57
+ end
58
+ end
59
+ end
60
+
61
+ describe "new_game?" do
62
+ context "game is new" do
63
+ it "should be true" do
64
+ subject.stub(:moves).and_return([])
65
+ subject.new_game?.should be_true
66
+ end
67
+ end
68
+ context "game is not new" do
69
+ it "should be false" do
70
+ subject.stub(:moves).and_return(%w[ a2a3 ])
71
+ subject.new_game?.should be_false
72
+ end
73
+ end
74
+ end
75
+
76
+ describe "#board" do
77
+ it "should return an ascii-art version of the current board" do
78
+ # starting position
79
+ subject.board.should == " ABCDEFGH
80
+ 8 rnbqkbnr
81
+ 7 pppppppp
82
+ 6 ........
83
+ 5 ........
84
+ 4 ........
85
+ 3 ........
86
+ 2 PPPPPPPP
87
+ 1 RNBQKBNR
88
+ "
89
+
90
+ # moves
91
+ subject.move_piece('b2b4')
92
+ subject.move_piece('b8a6')
93
+ subject.board.should == " ABCDEFGH
94
+ 8 r.bqkbnr
95
+ 7 pppppppp
96
+ 6 n.......
97
+ 5 ........
98
+ 4 .P......
99
+ 3 ........
100
+ 2 P.PPPPPP
101
+ 1 RNBQKBNR
102
+ "
103
+ end
104
+ end
105
+
106
+ describe "#fenstring" do
107
+ it "should return a short fenstring of the current board" do
108
+ # starting position
109
+ subject.fenstring.should == "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
110
+
111
+ # moves
112
+ subject.move_piece('b2b4')
113
+ subject.move_piece('b8a6')
114
+ subject.fenstring.should == "r1bqkbnr/pppppppp/n7/8/1P6/8/P1PPPPPP/RNBQKBNR"
115
+ end
116
+ end
117
+
118
+ describe "#set_board" do
119
+ it "should set the board layout from a passed LONG fenstring" do
120
+ # given
121
+ subject.stub!( :send_position_to_engine )
122
+ subject.set_board("r1bqkbnr/pppppppp/n7/8/1P6/8/P1PPPPPP/RNBQKBNR b KQkq - 0 1")
123
+ # expect
124
+ subject.fenstring.should == "r1bqkbnr/pppppppp/n7/8/1P6/8/P1PPPPPP/RNBQKBNR"
125
+ end
126
+ it "should raise an error is the passed fen format is incorret" do
127
+ # try to use a short fen where we neeed a long fen
128
+ lambda { subject.set_board("r1bqkbnr/pppppppp/n7/8/1P6/8/P1PPPPPP/RNBQKBNR") }.should raise_exception FenFormatError
129
+ end
130
+ end
131
+
132
+ describe "#place_piece" do
133
+ it "should place a piece on the board" do
134
+ subject.place_piece(:white, :queen, "a3")
135
+ subject.get_piece("a3").should == [:queen, :white]
136
+
137
+ subject.place_piece(:black, :knight, "a3")
138
+ subject.get_piece("a3").should == [:knight, :black]
139
+ end
140
+ it "should raise an error if the board was set from a fen string" do
141
+ subject.stub!(:send_position_to_engine)
142
+ subject.set_board("r1bqkbnr/pppppppp/n7/8/1P6/8/P1PPPPPP/RNBQKBNR b KQkq - 0 1")
143
+ lambda { subject.place_piece(:black, :knight, "a3") }.should raise_exception BoardLockedError
144
+ end
145
+ end
146
+
147
+ describe "#clear_position" do
148
+ it "should clear a position on the board" do
149
+ # sanity
150
+ subject.get_piece("a1").should == [:rook, :white]
151
+ # given
152
+ subject.clear_position("a1")
153
+ # expect
154
+ subject.piece_at?("a1").should be_false
155
+ end
156
+ it "should raise an error if the board was set from a fen string" do
157
+ subject.stub!(:send_position_to_engine)
158
+ subject.set_board("r1bqkbnr/pppppppp/n7/8/1P6/8/P1PPPPPP/RNBQKBNR b KQkq - 0 1")
159
+ lambda { subject.clear_position("a1") }.should raise_exception BoardLockedError
160
+ end
161
+ end
162
+
163
+ describe "#piece_name" do
164
+ context "symbol name passed" do
165
+ it "should return the single letter symbol" do
166
+ subject.piece_name(:queen).should == "q"
167
+ subject.piece_name(:knight).should == "n"
168
+ end
169
+ end
170
+ context "single letter symbol passes" do
171
+ it "should return the symbiol name" do
172
+ subject.piece_name('n').should == :knight
173
+ subject.piece_name('k').should == :king
174
+ subject.piece_name('q').should == :queen
175
+ end
176
+ end
177
+ end
178
+
179
+ describe "#piece_at?" do
180
+ it "should be true if there is a piece at the position indicated" do
181
+ # assume startpos
182
+ subject.piece_at?("a1").should be_true
183
+ end
184
+ it "should be false if there is not a piece at the position indicated" do
185
+ # assume startpos
186
+ subject.piece_at?("a3").should be_false
187
+ end
188
+ end
189
+
190
+ describe "#get_piece" do
191
+ it "should return the information for a piece at a given position" do
192
+ # assume startpos
193
+ subject.get_piece('a1').should == [:rook, :white]
194
+ subject.get_piece('h8').should == [:rook, :black]
195
+ end
196
+ it "should raise an exception if there is no piece at the given position" do
197
+ lambda { subject.get_piece('a3') }.should raise_exception NoPieceAtPositionError
198
+ end
199
+ end
200
+
201
+ describe "#moves" do
202
+ it "should return the interal move list" do
203
+ # startpos
204
+ subject.moves.should == []
205
+
206
+ # add some moves
207
+ subject.move_piece('a2a3'); subject.move_piece('a7a5')
208
+ subject.moves.should == ['a2a3', 'a7a5']
209
+ end
210
+ end
211
+
212
+ describe "#move_piece" do
213
+ before(:each) do
214
+ # sanity
215
+ subject.piece_at?("a2").should be_true
216
+ subject.piece_at?("a3").should be_false
217
+ end
218
+ it "should raise an error if the board was set from a fen string" do
219
+ subject.stub!(:send_position_to_engine)
220
+ subject.set_board("r1bqkbnr/pppppppp/n7/8/1P6/8/P1PPPPPP/RNBQKBNR b KQkq - 0 1")
221
+ lambda { subject.move_piece("a2a3") }.should raise_exception BoardLockedError
222
+ end
223
+ it "should move pieces from one position to another" do
224
+ piece = subject.get_piece("a2")
225
+ subject.move_piece("a2a3")
226
+ piece.should == subject.get_piece("a3")
227
+ subject.piece_at?("a2").should be_false
228
+ end
229
+ it 'it should overwrite pieces if one is moved atop another' do
230
+ # note this is an illegal move
231
+ piece = subject.get_piece("a1")
232
+ subject.move_piece("a1a2")
233
+ piece.should == subject.get_piece("a2")
234
+ subject.piece_at?("a1").should be_false
235
+ end
236
+ it "should raise an exception if the source position has no piece" do
237
+ lambda { subject.move_piece("a3a4") }.should raise_exception NoPieceAtPositionError
238
+ end
239
+ it "should promote a pawn to a queen at the correct rank with the correct notation" do
240
+ subject.move_piece("a2a8q")
241
+ subject.get_piece("a8").should == [:queen, :white]
242
+ end
243
+ it "should promote a pawn to a rook at the correct rank with the correct notation" do
244
+ subject.move_piece("a2a8r")
245
+ subject.get_piece("a8").should == [:rook, :white]
246
+ end
247
+ it "should promote a pawn to a knight at the correct rank with the correct notation" do
248
+ subject.move_piece("a2a8n")
249
+ subject.get_piece("a8").should == [:knight, :white]
250
+ end
251
+ it "should promote a pawn to a bishop at the correct rank with the correct notation" do
252
+ subject.move_piece("a2a8b")
253
+ subject.get_piece("a8").should == [:bishop, :white]
254
+ end
255
+ it "should raise an exception if promotion to unallowed piece" do
256
+ lambda { subject.move_piece("a2a8k") }.should raise_exception UnknownNotationExtensionError
257
+ end
258
+ it "should properly understand castling, white king's rook" do
259
+ subject.move_piece("e1g1")
260
+ subject.get_piece("f1").should == [:rook, :white]
261
+ subject.get_piece("g1").should == [:king, :white]
262
+ end
263
+ it "should properly understand castling, white queens's rook" do
264
+ subject.move_piece("e1c1")
265
+ subject.get_piece("d1").should == [:rook, :white]
266
+ subject.get_piece("c1").should == [:king, :white]
267
+ end
268
+ it "should properly understand castling, black king's rook" do
269
+ subject.move_piece("e8g8")
270
+ subject.get_piece("f8").should == [:rook, :black]
271
+ subject.get_piece("g8").should == [:king, :black]
272
+ end
273
+ it "should properly understand castling, black queens's rook" do
274
+ subject.move_piece("e8c8")
275
+ subject.get_piece("d8").should == [:rook, :black]
276
+ subject.get_piece("c8").should == [:king, :black]
277
+ end
278
+
279
+ it "should append the move to the move log" do
280
+ subject.moves.should be_empty
281
+ subject.move_piece("a2a3")
282
+ subject.moves.should == ["a2a3"]
283
+ end
284
+ end
285
+
286
+ describe "#new_game!" do
287
+ it "should tell the engine a new game is set" do
288
+ pending
289
+ end
290
+ it "should reset the internal board" do
291
+ pending
292
+ end
293
+ it "should set the pieces in a startpos" do
294
+ pending
295
+ end
296
+ end
297
+
298
+ describe "#bestmove" do
299
+ it "should write the bestmove command to the engine" do
300
+ pending
301
+ end
302
+ it "should detect various forfeit notations" do
303
+ pending
304
+ end
305
+ it "should raise and exception if the bestmove notation is not understood" do
306
+ pending
307
+ end
308
+ it "shpould raise and exception if the returned command was not prefixed with 'bestmove'" do
309
+ pending
310
+ end
311
+ end
312
+
313
+ describe "#send_position_to_engine" do
314
+ context "board was set from fen" do
315
+ it "should send a 'position fen' command" do
316
+ pending
317
+ end
318
+ end
319
+ context "the board is set from startpos" do
320
+ it "should set a 'position startpo' command followed by the move log" do
321
+ pending
322
+ end
323
+ end
324
+ end
325
+
326
+ describe "#go!" do
327
+ it "should send the currentn position to the engine" do
328
+ pending
329
+ end
330
+ it "should update the current board with the result of 'bestmove'" do
331
+ pending
332
+ end
333
+ end
334
+ end
@@ -0,0 +1,23 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ require 'simplecov'
9
+ if ENV["COVERAGE"]
10
+ SimpleCov.start
11
+ end
12
+
13
+ RSpec.configure do |config|
14
+ config.treat_symbols_as_metadata_keys_with_true_values = true
15
+ config.run_all_when_everything_filtered = true
16
+ # config.filter_run :focus
17
+
18
+ # Run specs in random order to surface order dependencies. If you find an
19
+ # order dependency and want to debug it, you can fix the order by providing
20
+ # the seed, which is printed after each run.
21
+ # --seed 1234
22
+ config.order = 'random'
23
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'uci'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "uci"
8
+ gem.version = Uci::VERSION
9
+ gem.authors = ["Matthew Nielsen"]
10
+ gem.email = ["xunker@pyxidis.org"]
11
+ gem.description = %q{Ruby library for the Universal Chess Interface (UCI)}
12
+ gem.summary = %q{Ruby library for the Universal Chess Interface (UCI)}
13
+ gem.homepage = "https://github.com/xunker/uci"
14
+
15
+ gem.required_ruby_version = '>= 1.9.1'
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.has_rdoc = true
22
+ gem.add_development_dependency('simplecov', '0.7.1')
23
+ gem.add_development_dependency('rspec', '2.12.0')
24
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uci
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matthew Nielsen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: simplecov
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.7.1
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.7.1
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 2.12.0
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 2.12.0
46
+ description: Ruby library for the Universal Chess Interface (UCI)
47
+ email:
48
+ - xunker@pyxidis.org
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - .rspec
55
+ - .rvmrc
56
+ - Gemfile
57
+ - LICENSE.txt
58
+ - README.md
59
+ - Rakefile
60
+ - lib/uci.rb
61
+ - spec/lib/uci_spec.rb
62
+ - spec/spec_helper.rb
63
+ - uci.gemspec
64
+ homepage: https://github.com/xunker/uci
65
+ licenses: []
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: 1.9.1
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubyforge_project:
84
+ rubygems_version: 1.8.24
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: Ruby library for the Universal Chess Interface (UCI)
88
+ test_files:
89
+ - spec/lib/uci_spec.rb
90
+ - spec/spec_helper.rb