uci 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rvmrc +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +238 -0
- data/Rakefile +1 -0
- data/lib/uci.rb +482 -0
- data/spec/lib/uci_spec.rb +334 -0
- data/spec/spec_helper.rb +23 -0
- data/uci.gemspec +24 -0
- metadata +90 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/uci.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/uci.gemspec
ADDED
@@ -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
|