go_gtp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ example/game.sgf
2
+ pkg
3
+
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.2-p0
data/README.markdown ADDED
@@ -0,0 +1,61 @@
1
+ The Go Text Protocol
2
+ ====================
3
+
4
+ This library wraps [GNU Go](http://www.gnu.org/software/gnugo/)'s version of the Go Text Protocol or GTP. It runs GNU Go in a separate process and communicates with the program over a pipe using the GTP protocol. This makes it easy to:
5
+
6
+ * Manage full games of Go
7
+ * Work with SGF files
8
+ * Analyze Go positions
9
+
10
+ Installing
11
+ ----------
12
+
13
+ This library is available as a gem, so you can install it with a command like:
14
+
15
+ gem install go_gtp
16
+
17
+ The above command may need super user privileges.
18
+
19
+ This library requires an install of GNU Go to communicate with. You will need to install that separately.
20
+
21
+ Examples
22
+ --------
23
+
24
+ This code would load an SGF file and show the current state of the game in that file:
25
+
26
+ require "go/gtp"
27
+
28
+ go = Go::GTP.run_gnugo
29
+ go.loadsgf("game.sgf") or abort "Failed to load file"
30
+ puts go.showboard
31
+ go.quit
32
+
33
+ This shows the two main types of GTP methods. Methods like `showboard()` return the indicated content. In this case, you actually get back a `Go::GTP::Board` object which can indexed into, or just converted into a `String` for display as it is used here.
34
+
35
+ Other methods, like `loadsgf()`, are just called for their side effects and they don't return anything. For these, you get a boolean result telling you if the call succeeded (`true`) or triggered an error (`false`). You can always check the `success?()` of either type of call after the fact and retrieve the `last_error()` when there is one, so these return values are just a convenience. As another convenience, these boolean methods can be called with Ruby's query syntax as well: `loadsgf?()`.
36
+
37
+ When working with a GNU Go process, it's a good idea to remember to call `quit()` so the pipe can be closed. One way to ensure that happens is to use the block form of `run_gnugo()` to have it done for you. Given that, the following example is another way to handle loading and displaying a game:
38
+
39
+ require "go/gtp"
40
+
41
+ Go::GTP.run_gnugo do |go|
42
+ go.loadsgf?("game.sgf") or abort "Failed to load file"
43
+ puts go.showboard
44
+ end # quit called automatically after this block
45
+
46
+ You can customize how GNU Go is invoked, by passing parameters to `run_gnugo()`. Probably the two most useful are the `:directory` where the executable lives and any `:arguments` you would like to pass it. For example:
47
+
48
+ require "go/gtp"
49
+
50
+ go = Go::GTP.run_gnugo( directory: "/usr/local/bin",
51
+ arguments: "--boardsize 9" )
52
+ # ...
53
+
54
+ Of course, you could also set the board size after the connection is open with `go.boardsize(9)`.
55
+
56
+ See the [example directory](http://github.com/JEG2/go_gtp/tree/master/example/) for more ideas about how to use this library.
57
+
58
+ GTP Commands
59
+ ------------
60
+
61
+ The method names are literally the command names right out of [the GTP documentation](http://www.gnu.org/software/gnugo/gnugo_19.html#SEC200). That's intended to make it easy to figure out what you can do with this library. Return values are Rubified into nice objects, when it makes sense to do so.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "rake"
2
+ require "spec/rake/spectask"
3
+ require "rake/gempackagetask"
4
+
5
+ task :default => :spec
6
+
7
+ Spec::Rake::SpecTask.new do |rspec|
8
+ rspec.warning = true
9
+ end
10
+
11
+ load(File.join(File.dirname(__FILE__), "go_gtp.gemspec"))
12
+ Rake::GemPackageTask.new(SPEC) do |package|
13
+ # do nothing: I just need a gem but this block is required
14
+ end
data/TODO ADDED
@@ -0,0 +1,4 @@
1
+ To Do
2
+
3
+ * Support the rest of the GTP commands GNU Go provides
4
+ * Add documentation
@@ -0,0 +1,10 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), *%w[.. lib])
2
+ require "go/gtp"
3
+
4
+ abort "USAGE: #{$PROGRAM_NAME} BOARD_SIZE_INT" unless ARGV.first =~ /\A\d+\z/
5
+
6
+ Go::GTP.run_gnugo do |go|
7
+ go.boardsize(ARGV.first) or abort "Invalid board size"
8
+ go.clear_board
9
+ go.printsgf(File.join(File.dirname(__FILE__), "game.sgf"))
10
+ end
@@ -0,0 +1,10 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), *%w[.. lib])
2
+ require "go/gtp"
3
+
4
+ GAME_FILE = File.join(File.dirname(__FILE__), "game.sgf")
5
+
6
+ Go::GTP.run_gnugo do |go|
7
+ color = go.loadsgf(GAME_FILE)
8
+ puts go.genmove(color)
9
+ go.printsgf(GAME_FILE)
10
+ end
@@ -0,0 +1,23 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), *%w[.. lib])
2
+ require "go/gtp"
3
+
4
+ abort "USAGE: #{$PROGRAM_NAME} BOARD_SIZE_INT" unless ARGV.first =~ /\A\d+\z/
5
+
6
+ # this script is handy for measuring performance
7
+
8
+ start = Time.now
9
+ colors = %w[black white].cycle
10
+ Go::GTP.run_gnugo do |go|
11
+ go.boardsize(ARGV.first) or abort "Invalid board size"
12
+ go.clear_board
13
+ 1.upto(19) do |row|
14
+ ("A".."T").each do |column|
15
+ next if column == "I"
16
+ move = "#{column}#{row}"
17
+ next if move == "T19" # illegal move
18
+ go.play(colors.next, move) or abort "Invalid move #{move}"
19
+ end
20
+ end
21
+ puts go.showboard
22
+ end
23
+ puts "Total time: #{Time.now - start} seconds"
@@ -0,0 +1,40 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), *%w[.. lib])
2
+ require "go/gtp"
3
+
4
+ class IllegalMoveError < RuntimeError; end
5
+
6
+ COLORS = %w[black white]
7
+ PLAYERS = Hash[COLORS.zip(%w[computer player].sample(2))]
8
+
9
+ begin
10
+ Go::GTP.run_gnugo(arguments: ARGV.empty? ? nil : ARGV) do |go|
11
+ COLORS.cycle do |color|
12
+ puts go.showboard
13
+ abort "Error: failed to show board" unless go.success?
14
+ move = nil
15
+ case PLAYERS[color]
16
+ when "player"
17
+ begin
18
+ print "Move for #{color}? "
19
+ move = $stdin.gets.to_s.strip
20
+ unless move =~ /\S/ and go.is_legal?(color, move)
21
+ raise IllegalMoveError, "Illegal move"
22
+ end
23
+ go.play?(color, move) or abort "Error: move failed"
24
+ rescue IllegalMoveError => error
25
+ puts error.message
26
+ retry
27
+ end
28
+ when "computer"
29
+ move = go.genmove(color)
30
+ abort "Error: failed to generate move" unless go.success?
31
+ puts "Move for #{color}: #{move}"
32
+ end
33
+ puts
34
+ break if go.over?
35
+ end
36
+ puts "Final score: #{go.final_score}"
37
+ end
38
+ rescue Errno::EPIPE
39
+ abort "Error: bad arguments"
40
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), *%w[.. lib])
2
+ require "go/gtp"
3
+
4
+ abort "USAGE: #{$PROGRAM_NAME} MOVE" unless ARGV.first =~ /\A[A-Z]\d+\z/i
5
+
6
+ GAME_FILE = File.join(File.dirname(__FILE__), "game.sgf")
7
+
8
+ Go::GTP.run_gnugo do |go|
9
+ color = go.loadsgf(GAME_FILE)
10
+ go.play(color, ARGV.first) or abort "Invalid move for #{color}"
11
+ go.printsgf(GAME_FILE)
12
+ end
@@ -0,0 +1,7 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), *%w[.. lib])
2
+ require "go/gtp"
3
+
4
+ Go::GTP.run_gnugo do |go|
5
+ go.loadsgf(File.join(File.dirname(__FILE__), "game.sgf"))
6
+ puts go.showboard
7
+ end
data/go_gtp.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ DIR = File.dirname(__FILE__)
2
+ LIB = File.join(DIR, *%w[lib go gtp.rb])
3
+ VERSION = open(LIB) { |lib|
4
+ lib.each { |line|
5
+ if v = line[/^\s*VERSION\s*=\s*(['"])(\d\.\d\.\d)\1/, 2]; break v end
6
+ }
7
+ }
8
+
9
+ SPEC = Gem::Specification.new do |s|
10
+ s.name = "go_gtp"
11
+ s.version = VERSION
12
+ s.platform = Gem::Platform::RUBY
13
+ s.authors = ["James Edward Gray II", "Ryan Bates"]
14
+ s.email = ["james@graysoftinc.com"]
15
+ s.homepage = "http://github.com/JEG2/go_gtp"
16
+ s.summary = "A wrapper for GNU Go's Go Text Protocol (GTP)."
17
+ s.description = <<-END_DESCRIPTION.gsub(/\s+/, " ").strip
18
+ This library runs GNU Go in a separate process and allows you to communicate
19
+ with it using the Go Text Protocol (GTP). This makes it easy to manage full
20
+ games of Go, work with SGF files, analyze Go positions, and more.
21
+ END_DESCRIPTION
22
+
23
+ s.required_rubygems_version = "~> 1.9.2"
24
+ s.required_rubygems_version = "~> 1.3.6"
25
+
26
+ s.add_development_dependency "rspec"
27
+
28
+ s.files = `git ls-files`.split("\n")
29
+ s.test_files = `git ls-files -- spec/*_spec.rb`.split("\n")
30
+ s.require_paths = %w[lib]
31
+ end
data/lib/go/gtp.rb ADDED
@@ -0,0 +1,485 @@
1
+ require "go/gtp/board"
2
+
3
+ module Go
4
+ class GTP
5
+ VERSION = "0.0.1"
6
+
7
+ def self.run_gnugo(options = { }, &commands)
8
+ directory = options.fetch(:directory, nil)
9
+ command = options.fetch(:command, "gnugo --mode gtp")
10
+ arguments = options.fetch(:arguments, nil)
11
+ redirections = options.fetch(:redirections, "2>&1")
12
+ path = [ File.join(*[directory, command].compact),
13
+ arguments,
14
+ redirections ].compact.join(" ")
15
+
16
+ new(IO.popen(path, "r+"), &commands)
17
+ end
18
+
19
+ def initialize(io)
20
+ @io = io
21
+ @id = 0
22
+ @last_error = nil
23
+
24
+ if block_given?
25
+ begin
26
+ yield self
27
+ ensure
28
+ quit
29
+ end
30
+ end
31
+ end
32
+
33
+ attr_reader :last_error
34
+
35
+ def success?
36
+ @last_error.nil?
37
+ end
38
+
39
+ def quit
40
+ send_command(:quit)
41
+ @io.close
42
+ success?
43
+ end
44
+ alias_method :quit?, :quit
45
+
46
+ def protocol_version
47
+ send_command(:protocol_version)
48
+ end
49
+
50
+ def name
51
+ send_command(:name)
52
+ end
53
+
54
+ def version
55
+ send_command(:version)
56
+ end
57
+
58
+ def boardsize(boardsize)
59
+ send_command(:boardsize, boardsize)
60
+ success?
61
+ end
62
+ alias_method :boardsize?, :boardsize
63
+
64
+ def query_boardsize
65
+ send_command(:query_boardsize)
66
+ end
67
+
68
+ def clear_board
69
+ send_command(:clear_board)
70
+ success?
71
+ end
72
+ alias_method :clear_board?, :clear_board
73
+
74
+ def orientation(orientation)
75
+ send_command(:orientation, orientation)
76
+ success?
77
+ end
78
+ alias_method :orientation?, :orientation
79
+
80
+ def query_orientation
81
+ send_command(:query_orientation)
82
+ end
83
+
84
+ def komi(komi)
85
+ send_command(:komi, komi)
86
+ success?
87
+ end
88
+ alias_method :komi?, :komi
89
+
90
+ def get_komi
91
+ send_command(:get_komi)
92
+ end
93
+
94
+ def play(color, vertex)
95
+ send_command(:play, color, vertex)
96
+ success?
97
+ end
98
+ alias_method :play?, :play
99
+
100
+ def replay(vertices)
101
+ colors = %w[black white].cycle
102
+ vertices.each do |vertex|
103
+ play(colors.next, vertex)
104
+ return success? unless success?
105
+ end
106
+ success?
107
+ end
108
+ alias_method :replay?, :replay
109
+
110
+ def fixed_handicap(number_of_stones)
111
+ extract_vertices(send_command(:fixed_handicap, number_of_stones))
112
+ end
113
+
114
+ def place_free_handicap(number_of_stones)
115
+ extract_vertices(send_command(:place_free_handicap, number_of_stones))
116
+ end
117
+
118
+ def set_free_handicap(*vertices)
119
+ send_command(:set_free_handicap, *vertices)
120
+ success?
121
+ end
122
+ alias_method :set_free_handicap?, :set_free_handicap
123
+
124
+ def get_handicap
125
+ send_command(:get_handicap)
126
+ end
127
+
128
+ def loadsgf(path, move_number_or_vertex = nil)
129
+ send_command(:loadsgf, *[path, move_number_or_vertex].compact)
130
+ end
131
+
132
+ def color(vertex)
133
+ extract_color(send_command(:color, vertex))
134
+ end
135
+
136
+ def list_stones(color)
137
+ extract_vertices(send_command(:list_stones, color))
138
+ end
139
+
140
+ def countlib(vertex)
141
+ extract_integer(send_command(:countlib, vertex))
142
+ end
143
+
144
+ def findlib(vertex)
145
+ extract_vertices(send_command(:findlib, vertex))
146
+ end
147
+
148
+ def accuratelib(color, vertex)
149
+ extract_vertices(send_command(:accuratelib, color, vertex))
150
+ end
151
+
152
+ def is_legal(color, vertex)
153
+ extract_boolean(send_command(:is_legal, color, vertex))
154
+ end
155
+ alias_method :is_legal?, :is_legal
156
+
157
+ def all_legal(color)
158
+ extract_vertices(send_command(:all_legal, color))
159
+ end
160
+
161
+ def captures(color)
162
+ extract_integer(send_command(:captures, color))
163
+ end
164
+
165
+ def last_move
166
+ extract_move(send_command(:last_move))
167
+ end
168
+
169
+ def move_history
170
+ extract_moves(send_command(:move_history))
171
+ end
172
+
173
+ def over?
174
+ last_two_moves = move_history.first(2)
175
+ Array(last_two_moves.first).last.to_s.upcase == "RESIGN" or
176
+ last_two_moves.map { |m| Array(m).last.to_s.upcase } == %w[PASS PASS]
177
+ end
178
+
179
+ def invariant_hash
180
+ send_command(:invariant_hash)
181
+ end
182
+
183
+ def invariant_hash_for_moves(color)
184
+ extract_moves(send_command(:invariant_hash_for_moves, color))
185
+ end
186
+
187
+ def trymove(color, vertex)
188
+ send_command(:trymove, color, vertex)
189
+ success?
190
+ end
191
+ alias_method :trymove?, :trymove
192
+
193
+ def tryko(color, vertex)
194
+ send_command(:tryko, color, vertex)
195
+ success?
196
+ end
197
+ alias_method :tryko?, :tryko
198
+
199
+ def popgo
200
+ send_command(:popgo)
201
+ success?
202
+ end
203
+ alias_method :popgo?, :popgo
204
+
205
+ def clear_cache
206
+ send_command(:clear_cache)
207
+ success?
208
+ end
209
+ alias_method :clear_cache?, :clear_cache
210
+
211
+ # ...
212
+
213
+ def increase_depths
214
+ send_command(:increase_depths)
215
+ success?
216
+ end
217
+ alias_method :increase_depths?, :increase_depths
218
+
219
+ def decrease_depths
220
+ send_command(:decrease_depths)
221
+ success?
222
+ end
223
+ alias_method :decrease_depths?, :decrease_depths
224
+
225
+ # ...
226
+
227
+ def unconditional_status(vertex)
228
+ send_command(:unconditional_status, vertex)
229
+ end
230
+
231
+ # ...
232
+
233
+ def genmove(color)
234
+ send_command(:genmove, color)
235
+ end
236
+
237
+ def reg_genmove(color)
238
+ send_command(:reg_genmove, color)
239
+ end
240
+
241
+ def gg_genmove(color, random_seed = nil)
242
+ send_command(:gg_genmove, *[color, random_seed].compact)
243
+ end
244
+
245
+ def restricted_genmove(color, *vertices)
246
+ send_command(:restricted_genmove, color, *vertices)
247
+ end
248
+
249
+ def kgs_genmove_cleanup(color)
250
+ send_command(:kgs_genmove_cleanup, color)
251
+ end
252
+
253
+ def level(level)
254
+ send_command(:level, level)
255
+ success?
256
+ end
257
+ alias_method :level?, :level
258
+
259
+ def undo
260
+ send_command(:undo)
261
+ success?
262
+ end
263
+ alias_method :undo?, :undo
264
+
265
+ def gg_undo(moves = nil)
266
+ send_command(:gg_undo, *[moves].compact)
267
+ success?
268
+ end
269
+ alias_method :gg_undo?, :gg_undo
270
+
271
+ def time_settings(main_time, byo_yomi_time, byo_yomi_stones)
272
+ send_command(:time_settings, main_time, byo_yomi_time, byo_yomi_stones)
273
+ success?
274
+ end
275
+ alias_method :time_settings?, :time_settings
276
+
277
+ def time_left(color, time, stones)
278
+ send_command(:time_left, color, time, stones)
279
+ success?
280
+ end
281
+ alias_method :time_left?, :time_left
282
+
283
+ def final_score(random_seed = nil)
284
+ send_command(:final_score, *[random_seed].compact)
285
+ end
286
+
287
+ def final_status(vertex, random_seed = nil)
288
+ send_command(:final_status, *[vertex, random_seed].compact)
289
+ end
290
+
291
+ def final_status_list(status, random_seed = nil)
292
+ extract_vertices( send_command( :final_status_list,
293
+ *[status, random_seed].compact ) )
294
+ end
295
+
296
+ def estimate_score
297
+ send_command(:estimate_score)
298
+ end
299
+
300
+ def experimental_score(color)
301
+ send_command(:experimental_score, color)
302
+ end
303
+
304
+ def reset_owl_node_counter
305
+ send_command(:reset_owl_node_counter)
306
+ success?
307
+ end
308
+ alias_method :reset_owl_node_counter?, :reset_owl_node_counter
309
+
310
+ def get_owl_node_counter
311
+ send_command(:reset_owl_node_counter)
312
+ end
313
+
314
+ def reset_reading_node_counter
315
+ send_command(:reset_reading_node_counter)
316
+ success?
317
+ end
318
+ alias_method :reset_reading_node_counter?, :reset_reading_node_counter
319
+
320
+ def get_reading_node_counter
321
+ send_command(:reset_reading_node_counter)
322
+ end
323
+
324
+ def reset_trymove_node_counter
325
+ send_command(:reset_trymove_node_counter)
326
+ success?
327
+ end
328
+ alias_method :reset_trymove_node_counter?, :reset_trymove_node_counter
329
+
330
+ def get_trymove_node_counter
331
+ send_command(:reset_trymove_node_counter)
332
+ end
333
+
334
+ def reset_connection_node_counter
335
+ send_command(:reset_connection_node_counter)
336
+ success?
337
+ end
338
+ alias_method :reset_connection_node_counter?, :reset_connection_node_counter
339
+
340
+ def get_connection_node_counter
341
+ send_command(:reset_connection_node_counter)
342
+ end
343
+
344
+ # ...
345
+
346
+ def cputime
347
+ send_command(:cputime)
348
+ end
349
+
350
+ def showboard
351
+ Board.new(send_command(:showboard))
352
+ end
353
+
354
+ # ...
355
+
356
+ def printsgf(path = nil)
357
+ if path
358
+ send_command(:printsgf, path)
359
+ success?
360
+ else
361
+ send_command(:printsgf)
362
+ end
363
+ end
364
+ alias_method :printsgf?, :printsgf
365
+
366
+ def tune_move_ordering(*move_ordering_parameters)
367
+ send_command(:tune_move_ordering, *move_ordering_parameters)
368
+ end
369
+
370
+ def echo(string)
371
+ send_command(:echo, string)
372
+ end
373
+
374
+ def echo_err(string)
375
+ send_command(:echo_err, string)
376
+ end
377
+
378
+ def help
379
+ extract_lines(send_command(:help))
380
+ end
381
+
382
+ def known_command(command)
383
+ extract_boolean(send_command(:known_command, command))
384
+ end
385
+ alias_method :known_command?, :known_command
386
+
387
+ def report_uncertainty(on_or_off)
388
+ send_command(:report_uncertainty, on_or_off)
389
+ end
390
+ alias_method :report_uncertainty?, :report_uncertainty
391
+
392
+ def get_random_seed
393
+ send_command(:get_random_seed)
394
+ end
395
+
396
+ def set_random_seed(random_seed)
397
+ send_command(:set_random_seed, random_seed)
398
+ success?
399
+ end
400
+ alias_method :set_random_seed?, :set_random_seed
401
+
402
+ def advance_random_seed(games)
403
+ send_command(:advance_random_seed, games)
404
+ success?
405
+ end
406
+ alias_method :advance_random_seed?, :advance_random_seed
407
+
408
+ # ...
409
+
410
+ def set_search_diamond(position)
411
+ send_command(:set_search_diamond, position)
412
+ success?
413
+ end
414
+ alias_method :set_search_diamond?, :set_search_diamond
415
+
416
+ def reset_search_mask
417
+ send_command(:reset_search_mask)
418
+ success?
419
+ end
420
+ alias_method :reset_search_mask?, :reset_search_mask
421
+
422
+ def limit_search(value)
423
+ send_command(:limit_search, value)
424
+ success?
425
+ end
426
+ alias_method :limit_search?, :limit_search
427
+
428
+ def set_search_limit(position)
429
+ send_command(:set_search_limit, position)
430
+ success?
431
+ end
432
+ alias_method :set_search_limit?, :set_search_limit
433
+
434
+ def draw_search_area
435
+ send_command(:draw_search_area)
436
+ end
437
+
438
+ private
439
+
440
+ def next_id
441
+ @id += 1
442
+ end
443
+
444
+ def send_command(command, *arguments)
445
+ @io.puts [next_id, command, *arguments].join(" ")
446
+ result = @io.take_while { |line| line != "\n" }.join
447
+ if result.sub!(/^=#{@id} */, "")
448
+ @last_error = nil
449
+ elsif result.sub!(/^\?#{@id} *(\S.*\S?).*/, "")
450
+ @last_error = $1
451
+ else
452
+ raise "Unexpected response format"
453
+ end
454
+ result.sub(/\A(?: *\n)+/, "").sub(/(?:\n *)+\z/, "")
455
+ end
456
+
457
+ def extract_vertices(response)
458
+ success? ? response.scan(/[A-Z]\d+/) : [ ]
459
+ end
460
+
461
+ def extract_color(response)
462
+ !success? || response == "empty" ? nil : response
463
+ end
464
+
465
+ def extract_move(response)
466
+ success? ? response.split : nil
467
+ end
468
+
469
+ def extract_moves(response)
470
+ success? ? response.lines.map { |line| line.strip.split } : nil
471
+ end
472
+
473
+ def extract_boolean(response)
474
+ success? ? response == "1" || response == "true" : nil
475
+ end
476
+
477
+ def extract_integer(response)
478
+ success? ? response.to_i : nil
479
+ end
480
+
481
+ def extract_lines(response)
482
+ success? ? response.lines.map { |line| line.strip } : nil
483
+ end
484
+ end
485
+ end
@@ -0,0 +1,44 @@
1
+ module Go
2
+ class GTP
3
+ class Board
4
+ STONES = {"X" => "black", "O" => "white"}
5
+
6
+ def initialize(board_string)
7
+ @string = board_string
8
+ @array = nil
9
+ end
10
+
11
+ def [](*args)
12
+ point = if args.size == 1 and args.first.is_a? Point
13
+ args.first
14
+ elsif args.size == 1 and
15
+ args.first =~ /\A([A-HJ-T])(\d{1,2})\z/i
16
+ Point.new(*args, board_size: size)
17
+ else
18
+ Point.new(*args)
19
+ end
20
+ to_a[point.y][point.x]
21
+ end
22
+
23
+ def captures(color)
24
+ @string[/#{Regexp.escape(color)}(?: \([XO])?\) has captured (\d+)/i, 1]
25
+ .to_i
26
+ end
27
+
28
+ def to_s
29
+ @string
30
+ end
31
+
32
+ def to_a
33
+ @array ||= @string.scan(/^\s*(\d+)((?:.[.+XO])+).\1\b/)
34
+ .map { |_, row| row.chars
35
+ .each_slice(2)
36
+ .map { |_, stone| STONES[stone] } }
37
+ end
38
+
39
+ def size
40
+ to_a.size
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,58 @@
1
+ module Go
2
+ class GTP
3
+ #
4
+ # This utility class manages points on a Go board using any one of three
5
+ # formats:
6
+ #
7
+ # * X and Y Array indices counting from the upper left hand corner
8
+ # * SGF letter pairs ("ac") counting from the upper left hand corner
9
+ # * GNU Go letter and number pairs ("A13") counting from the lower left hand
10
+ # corner
11
+ #
12
+ # There are two gotchas to stay aware of with these systems. First, the GNU
13
+ # Go format skips over I in columns, but the SGF format does not. Second,
14
+ # the GNU Go format relies on knowing the board size. A 19x19 size is
15
+ # assumed, but you can override this when creating from or converting to
16
+ # this format.
17
+ #
18
+ # Point instances can be initialized from any format and converted to any
19
+ # format.
20
+ #
21
+ class Point
22
+ BIG_A = "A".getbyte(0)
23
+ LITTLE_A = "a".getbyte(0)
24
+
25
+ def initialize(*args)
26
+ if args.size == 2 and args.all? { |n| n.is_a? Integer }
27
+ @x, @y = args
28
+ elsif (args.size == 1 or (args.size == 2 and args.last.is_a?(Hash))) and
29
+ args.first =~ /\A([A-HJ-T])(\d{1,2})\z/i
30
+ options = args.last.is_a?(Hash) ? args.pop : { }
31
+ letter = $1.upcase
32
+ @x = letter.getbyte(0) - BIG_A - (letter > "I" ? 1 : 0)
33
+ @y = options.fetch(:board_size, 19) - $2.to_i
34
+ elsif args.size == 1 and args.first =~ /\A([a-s])([a-s])\z/i
35
+ @x, @y = $~.captures.map { |l| l.downcase.getbyte(0) - LITTLE_A }
36
+ else
37
+ fail ArgumentError, "unrecognized point format"
38
+ end
39
+ end
40
+
41
+ attr_reader :x, :y
42
+
43
+ def to_indexes
44
+ [@x, @y]
45
+ end
46
+ alias_method :to_indices, :to_indexes
47
+
48
+ def to_sgf
49
+ [@x, @y].map { |n| (LITTLE_A + n).chr }.join
50
+ end
51
+
52
+ def to_gnugo(board_size = 19)
53
+ "#{(BIG_A + @x).chr}#{board_size - @y}"
54
+ end
55
+ alias_method :to_s, :to_gnugo
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,68 @@
1
+ require "go/gtp/board"
2
+
3
+ describe Go::GTP::Board do
4
+ before :all do
5
+ @string = <<-END_BOARD.gsub(/^ {4}/, "")
6
+ A B C D E F G H J
7
+ 9 . . . . . . . . . 9
8
+ 8 . . . . . . . . . 8
9
+ 7 . . X . . . + . . 7
10
+ 6 . . . . . . . . . 6
11
+ 5 . . . . + . . . . 5
12
+ 4 . . . . . . . . . 4
13
+ 3 . . + . . O + . . 3
14
+ 2 . . . . . . . . . 2 WHITE (O) has captured 10 stones
15
+ 1 . . . . . . . . . 1 BLACK (X) has captured 11 stones
16
+ A B C D E F G H J
17
+ END_BOARD
18
+ @board = Go::GTP::Board.new(@string)
19
+ end
20
+
21
+ it "should stringify to what it was created from" do
22
+ @board.to_s.should == @string
23
+ end
24
+
25
+ it "should be possible to get the board as an array of arrays" do
26
+ @board.to_a.should == [
27
+ [nil, nil, nil, nil, nil, nil, nil, nil, nil],
28
+ [nil, nil, nil, nil, nil, nil, nil, nil, nil],
29
+ [nil, nil, "black", nil, nil, nil, nil, nil, nil],
30
+ [nil, nil, nil, nil, nil, nil, nil, nil, nil],
31
+ [nil, nil, nil, nil, nil, nil, nil, nil, nil],
32
+ [nil, nil, nil, nil, nil, nil, nil, nil, nil],
33
+ [nil, nil, nil, nil, nil, "white", nil, nil, nil],
34
+ [nil, nil, nil, nil, nil, nil, nil, nil, nil],
35
+ [nil, nil, nil, nil, nil, nil, nil, nil, nil]
36
+ ]
37
+ end
38
+
39
+ it "should support indexing by coordinates" do
40
+ @board[0, 0].should be_nil
41
+ @board[2, 2].should satisfy { |color| color == "black" }
42
+ @board[5, 6].should satisfy { |color| color == "white" }
43
+ end
44
+
45
+ it "should support indexing by vertices" do
46
+ @board["A1"].should be_nil
47
+ @board["C7"].should satisfy { |color| color == "black" }
48
+ @board["F3"].should satisfy { |color| color == "white" }
49
+ end
50
+
51
+ it "should fail to index for any other combination" do
52
+ lambda { @board["A", "1"] }.should raise_error(ArgumentError)
53
+ end
54
+
55
+ it "should be able to count captures by color name" do
56
+ @board.captures("white").should satisfy { |stones| stones == 10 }
57
+ @board.captures("WHITE").should satisfy { |stones| stones == 10 }
58
+ @board.captures("black").should satisfy { |stones| stones == 11 }
59
+ @board.captures("BLACK").should satisfy { |stones| stones == 11 }
60
+ end
61
+
62
+ it "should be able to count captures by stone type" do
63
+ @board.captures("o").should satisfy { |stones| stones == 10 }
64
+ @board.captures("O").should satisfy { |stones| stones == 10 }
65
+ @board.captures("x").should satisfy { |stones| stones == 11 }
66
+ @board.captures("X").should satisfy { |stones| stones == 11 }
67
+ end
68
+ end
data/spec/gtp_spec.rb ADDED
@@ -0,0 +1,166 @@
1
+ require "stringio"
2
+
3
+ require "go/gtp"
4
+
5
+ class MockPipe
6
+ def initialize(input)
7
+ @input = StringIO.new(input)
8
+ @output = StringIO.new
9
+ @closed = false
10
+ end
11
+
12
+ attr_reader :input, :output
13
+
14
+ def puts(*args)
15
+ @output.puts(*args)
16
+ end
17
+
18
+ def close
19
+ @closed = true
20
+ end
21
+
22
+ def closed?
23
+ @closed
24
+ end
25
+
26
+ def method_missing(meth, *args, &blk)
27
+ @input.send(meth, *args, &blk)
28
+ end
29
+ end
30
+
31
+ describe Go::GTP do
32
+ def gtp(input = "=1", &commands)
33
+ @pipe = MockPipe.new(input)
34
+ return [Go::GTP.new(@pipe, &commands), @pipe.input, @pipe.output]
35
+ end
36
+
37
+ it "should send quit after running a provided block" do
38
+ ran = false
39
+ _, _, output = gtp do
40
+ ran = true
41
+ end
42
+ ran.should be(true)
43
+ output.string.should match(/\A\d+\s+quit\Z/)
44
+ end
45
+
46
+ context "when connected" do
47
+ before :each do
48
+ @go, @input, @output = gtp
49
+ end
50
+
51
+ def add_input(input)
52
+ @input << input
53
+ @input.rewind
54
+ end
55
+
56
+ it "should return an instance without sending commands without a block" do
57
+ @output.string.should be_empty
58
+ end
59
+
60
+ it "should report the success of the last command" do
61
+ @go.clear_board
62
+ @go.should be_success
63
+ end
64
+
65
+ it "should not report success if the last command failed" do
66
+ add_input("?1 error message")
67
+ @go.clear_board
68
+ @go.should_not be_success
69
+ end
70
+
71
+ it "should not have an error after a successful command" do
72
+ @go.clear_board.should be(true)
73
+ @go.last_error.should be_nil
74
+ end
75
+
76
+ it "should have an error after an unsuccessful command" do
77
+ add_input("?1 error message")
78
+ @go.clear_board.should be(false)
79
+ @go.last_error.should == "error message"
80
+ end
81
+
82
+ it "should close the IO on quit" do
83
+ @pipe.should_not be_closed
84
+ @go.quit
85
+ @pipe.should be_closed
86
+ end
87
+
88
+ it "should return output from commands that return data" do
89
+ add_input("=1 2.0")
90
+ @go.protocol_version.should == "2.0"
91
+ end
92
+
93
+ it "should return the success or failure of boolean operations" do
94
+ @go.boardsize(9).should == @go.success?
95
+ end
96
+
97
+ it "should support the query interface for boolean operations" do
98
+ @go.boardsize?(9).should == @go.success?
99
+ end
100
+
101
+ it "should return lists of vertices in an array" do
102
+ add_input("=1 A1 B2")
103
+ @go.fixed_handicap(2) == %w[A1 B1]
104
+ end
105
+
106
+ it "should return colors as black, white, or nil" do
107
+ add_input("=1 black\n\n=2 white\n\n=3 empty")
108
+ @go.color("A1").should satisfy { |stone| stone == "black" }
109
+ @go.color("B1").should satisfy { |stone| stone == "white" }
110
+ @go.color("C1").should be_nil
111
+ end
112
+
113
+ it "should return boolean results as true and false" do
114
+ add_input("=1 1\n\n=2 0")
115
+ @go.is_legal?("black", "A1").should be(true)
116
+ @go.is_legal?("black", "B1").should be(false)
117
+ end
118
+
119
+ it "should return a move in an array" do
120
+ add_input("=1 white A1")
121
+ @go.last_move.should == %w[white A1]
122
+ end
123
+
124
+ it "should return moves in an array of arrays" do
125
+ add_input("=1\nwhite A1\nblack B1")
126
+ @go.move_history.should == [%w[white A1], %w[black B1]]
127
+ end
128
+
129
+ it "should return board diagrams wrapped in an appropriate object" do
130
+ add_input("=1 board")
131
+ @go.showboard.should be_an_instance_of(Go::GTP::Board)
132
+ end
133
+
134
+ it "should support a dual interface for methods that can return data" do
135
+ add_input("=1 board\n\n=2")
136
+ @go.printsgf.should satisfy { |sgf| sgf == "board" }
137
+ @go.printsgf?("/path/to/file").should be(true)
138
+ end
139
+
140
+ it "should return line results in an array" do
141
+ add_input("=1 help\nknown_command")
142
+ @go.help.should == %w[help known_command]
143
+ end
144
+
145
+ it "should detect a resignation as a game over condition" do
146
+ add_input("=1 black RESIGN")
147
+ @go.should be_over
148
+ end
149
+
150
+ it "should detect two passes as a game over condition" do
151
+ add_input("=1 black PASS\nwhite PASS")
152
+ @go.should be_over
153
+ end
154
+
155
+ it "should not any other game over conditions" do
156
+ add_input("=1 black E4\nwhite PASS")
157
+ @go.should_not be_over
158
+ end
159
+
160
+ it "should allow you to replay a series of moves" do
161
+ add_input("=1\n\n=2\n\n=3\n\n=4 black E6")
162
+ @go.replay(%w[E4 E5 E6]).should be(true)
163
+ @go.last_move.should == %w[black E6]
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,75 @@
1
+ require "go/gtp/point"
2
+
3
+ describe Go::GTP::Point do
4
+ it "should initialize from an x, y integer index pair" do
5
+ lambda { Go::GTP::Point.new(0, 2) }.should_not raise_error(ArgumentError)
6
+ end
7
+
8
+ it "should initialize from a GNU Go letter and integer pair" do
9
+ lambda { Go::GTP::Point.new("A12") }.should_not raise_error(ArgumentError)
10
+ end
11
+
12
+ it "should initialize from an SGF letter pair" do
13
+ lambda { Go::GTP::Point.new("ad") }.should_not raise_error(ArgumentError)
14
+ end
15
+
16
+ it "should fail with an argmument error with any other arguments" do
17
+ lambda { Go::GTP::Point.new("junk") }.should raise_error(ArgumentError)
18
+ end
19
+
20
+ context "initialized from any format" do
21
+ before :all do
22
+ @points = [[0, 2], "A17", "ac"].map { |args| Go::GTP::Point.new(*args) }
23
+ end
24
+
25
+ it "should track its indices" do
26
+ @points.each do |point|
27
+ point.x.should satisfy { |x| x == 0 }
28
+ point.y.should satisfy { |y| y == 2 }
29
+ end
30
+ end
31
+
32
+ it "should convert to indices" do
33
+ @points.each do |point|
34
+ point.to_indexes.should satisfy { |xy| xy == [0, 2] }
35
+ point.to_indices.should satisfy { |xy| xy == [0, 2] }
36
+ end
37
+ end
38
+
39
+ it "should convert to SGF letters" do
40
+ @points.each do |point|
41
+ point.to_sgf.should == "ac"
42
+ end
43
+ end
44
+
45
+ it "should convert to GNU Go letters and numbers" do
46
+ @points.each do |point|
47
+ point.to_gnugo.should satisfy { |ln| ln == "A17" }
48
+ point.to_s.should satisfy { |ln| ln == "A17" }
49
+ end
50
+ end
51
+ end
52
+
53
+ context "in the GNU Go format" do
54
+ it "should default to assuming a 19x19 board on creation" do
55
+ Go::GTP::Point.new("A13").to_indices.should == [0, 6]
56
+ end
57
+
58
+ it "should allow you to override board size on creation" do
59
+ Go::GTP::Point.new("A13", board_size: 13).to_indices.should == [0, 0]
60
+ end
61
+
62
+ it "should default to assuming a 19x19 board on conversion" do
63
+ Go::GTP::Point.new(0, 6).to_gnugo.should == "A13"
64
+ end
65
+
66
+ it "should allow you to override board size on conversion" do
67
+ Go::GTP::Point.new(0, 0).to_gnugo(13).should == "A13"
68
+ end
69
+
70
+ it "should skip over the unused I column" do
71
+ Go::GTP::Point.new("H19").to_indices.should satisfy { |ln| ln == [7, 0] }
72
+ Go::GTP::Point.new("J19").to_indices.should satisfy { |ln| ln == [8, 0] }
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,75 @@
1
+ require "go/gtp"
2
+
3
+ describe Go::GTP, "when connecting to GNU Go" do
4
+ before :all do
5
+ Go::GTP::IO = mock
6
+ end
7
+
8
+ it "should return a connected instance" do
9
+ Go::GTP::IO.should_receive(:popen)
10
+ Go::GTP.run_gnugo.should be_an_instance_of(Go::GTP)
11
+ end
12
+
13
+ it "should open the connection in reading and writing mode" do
14
+ Go::GTP::IO.should_receive(:popen) do |_, mode|
15
+ mode.should match(/\A[rw]\+\z/)
16
+ end
17
+ Go::GTP.run_gnugo
18
+ end
19
+
20
+ it "should default to using no directory" do
21
+ Go::GTP::IO.should_receive(:popen) do |path, _|
22
+ path.should match(/\Agnugo\b/)
23
+ end
24
+ Go::GTP.run_gnugo
25
+ end
26
+
27
+ it "should allow the directory to be overriden" do
28
+ Go::GTP::IO.should_receive(:popen) do |path, _|
29
+ path.should match(%r{\A/usr/local/bin/gnugo\b})
30
+ end
31
+ Go::GTP.run_gnugo(directory: "/usr/local/bin")
32
+ end
33
+
34
+ it "should default to using gnugo in GTP mode" do
35
+ Go::GTP::IO.should_receive(:popen) do |path, _|
36
+ path.should match(/\Agnugo\b.*--mode\s+gtp\b/)
37
+ end
38
+ Go::GTP.run_gnugo
39
+ end
40
+
41
+ it "should allow the command to be overriden" do
42
+ Go::GTP::IO.should_receive(:popen) do |path, _|
43
+ path.should match(/\Amy_go\b/)
44
+ end
45
+ Go::GTP.run_gnugo(command: "my_go")
46
+ end
47
+
48
+ it "should default to using no arguments" do
49
+ Go::GTP::IO.should_receive(:popen) do |path, _|
50
+ path.should_not match(/--(?!mode\b)\w+/)
51
+ end
52
+ Go::GTP.run_gnugo
53
+ end
54
+
55
+ it "should allow the arguments to be overriden" do
56
+ Go::GTP::IO.should_receive(:popen) do |path, _|
57
+ path.should match(/--boardsize\s+9\b/)
58
+ end
59
+ Go::GTP.run_gnugo(arguments: "--boardsize 9")
60
+ end
61
+
62
+ it "should default to redirecting STDERR to STDOUT" do
63
+ Go::GTP::IO.should_receive(:popen) do |path, _|
64
+ path.should match(/2>&1\z/)
65
+ end
66
+ Go::GTP.run_gnugo
67
+ end
68
+
69
+ it "should allow the redirections to be overriden" do
70
+ Go::GTP::IO.should_receive(:popen) do |path, _|
71
+ path.should match(%r{2>/dev/null\z})
72
+ end
73
+ Go::GTP.run_gnugo(redirections: "2>/dev/null")
74
+ end
75
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: go_gtp
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - James Edward Gray II
13
+ - Ryan Bates
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-02 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ description: This library runs GNU Go in a separate process and allows you to communicate with it using the Go Text Protocol (GTP). This makes it easy to manage full games of Go, work with SGF files, analyze Go positions, and more.
35
+ email:
36
+ - james@graysoftinc.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - .gitignore
45
+ - .rvmrc
46
+ - README.markdown
47
+ - Rakefile
48
+ - TODO
49
+ - example/create_game.rb
50
+ - example/generate_move.rb
51
+ - example/play_all.rb
52
+ - example/play_go.rb
53
+ - example/play_move.rb
54
+ - example/show_board.rb
55
+ - go_gtp.gemspec
56
+ - lib/go/gtp.rb
57
+ - lib/go/gtp/board.rb
58
+ - lib/go/gtp/point.rb
59
+ - spec/board_spec.rb
60
+ - spec/gtp_spec.rb
61
+ - spec/point_spec.rb
62
+ - spec/run_gnugo_spec.rb
63
+ has_rdoc: true
64
+ homepage: http://github.com/JEG2/go_gtp
65
+ licenses: []
66
+
67
+ post_install_message:
68
+ rdoc_options: []
69
+
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ segments:
86
+ - 1
87
+ - 3
88
+ - 6
89
+ version: 1.3.6
90
+ requirements: []
91
+
92
+ rubyforge_project:
93
+ rubygems_version: 1.3.7
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: A wrapper for GNU Go's Go Text Protocol (GTP).
97
+ test_files:
98
+ - spec/board_spec.rb
99
+ - spec/gtp_spec.rb
100
+ - spec/point_spec.rb
101
+ - spec/run_gnugo_spec.rb