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