uci 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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