goshrine_bot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ # Provides a command line GTP interface
2
+ require 'timeout'
3
+
4
+ module GoshrineBot
5
+ module GtpProcess
6
+ def post_init
7
+ @command_id = 0
8
+ @results = Queue.new
9
+ @cmd_queue = Queue.new
10
+ @data = ""
11
+ end
12
+
13
+ def logfile=(logfile)
14
+ @logfile = logfile
15
+ end
16
+
17
+ def log(str)
18
+ File.open(@logfile, "a") do |f|
19
+ f.write "#{Time.now} - #{str}\n"
20
+ end
21
+ end
22
+
23
+ def send(command)
24
+ @command_id += 1
25
+ # if we're waiting on results, don't send data just yet; queue it.
26
+ if @results.size > 0
27
+ @cmd_queue.enq command
28
+ else
29
+ log "O: #{command.inspect}"
30
+ send_data("#{command}\n")
31
+ end
32
+ res = EM::DefaultDeferrable.new
33
+ @results.enq res
34
+ res
35
+ end
36
+
37
+ def receive_data(data)
38
+ log "I: #{data.inspect}"
39
+ @data += data
40
+ while (match_data = @data.match(/^([?=])(\d+)?\s(.*?)\n\n/m))
41
+ @data = @data[match_data[0].size..-1]
42
+
43
+ res = @results.deq
44
+ status = match_data[1]
45
+ if status == '?'
46
+ res.fail(match_data[3])
47
+ else
48
+ res.succeed(match_data[3])
49
+ end
50
+ if @cmd_queue.size > 0
51
+ command = @cmd_queue.deq
52
+ log "O: #{command.inspect}"
53
+ send_data("#{command}\n")
54
+ end
55
+ end
56
+ end
57
+
58
+ def unbind
59
+ log "gtp process exited with status: #{get_status.exitstatus.inspect}"
60
+ end
61
+ end
62
+
63
+ class GtpStdioClient
64
+ attr_accessor :command_id
65
+ attr_accessor :boardsize
66
+ attr_accessor :command_line
67
+
68
+ def initialize(command_line, logfile="gtp.log")
69
+ @command_line = command_line
70
+ puts "Opening #{command_line.inspect}"
71
+ @gtp = EM.popen(@command_line, GtpProcess, "args!")
72
+ @gtp.logfile = logfile
73
+ @logfile = logfile
74
+ log "Starting #{command_line.inspect}"
75
+ end
76
+
77
+ def log(str)
78
+ File.open(@logfile, "a") do |f|
79
+ f.write "#{Time.now} - #{str}\n"
80
+ end
81
+ end
82
+
83
+ def close
84
+ log "Closing #{command_line.inspect}"
85
+ @gtp.close_connection
86
+ end
87
+
88
+ def kill
89
+ log "Kill #{command_line.inspect}"
90
+ Process.kill 'TERM', @gtp.get_pid
91
+ end
92
+
93
+ def boardsize(size)
94
+ @boardsize = size
95
+ send("boardsize #{size}")
96
+ end
97
+
98
+ def play(color, move)
99
+ #puts "Going to play #{color} #{move}"
100
+ send("play #{color} #{move}")
101
+ end
102
+
103
+ def final_status_list(t)
104
+ Timeout::timeout(10) do
105
+ res = send("final_status_list #{t}")
106
+ res.split.map {|c| Position.create(@boardsize, c)}
107
+ end
108
+ end
109
+
110
+ def method_missing(methodname, *args)
111
+ args = args.map {|a| a.to_s}.join(" ")
112
+ send("#{methodname.to_s} #{args}")
113
+ end
114
+
115
+ private
116
+
117
+ def gtp_color(rgo_color)
118
+ if rgo_color == 'b'
119
+ "black"
120
+ elsif rgo_color == "w"
121
+ "white"
122
+ end
123
+ end
124
+
125
+ def send(command)
126
+ @gtp.send(command)
127
+ end
128
+ end
129
+ end
130
+
@@ -0,0 +1,288 @@
1
+ #--
2
+ #
3
+ # Author:: Francis Cianfrocca (gmail: blackhedd)
4
+ # Homepage:: http://rubyeventmachine.com
5
+ # Date:: 16 July 2006
6
+ #
7
+ # See EventMachine and EventMachine::Connection for documentation and
8
+ # usage examples.
9
+ #
10
+ #----------------------------------------------------------------------------
11
+ #
12
+ # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
13
+ # Gmail: blackhedd
14
+ #
15
+ # This program is free software; you can redistribute it and/or modify
16
+ # it under the terms of either: 1) the GNU General Public License
17
+ # as published by the Free Software Foundation; either version 2 of the
18
+ # License, or (at your option) any later version; or 2) Ruby's License.
19
+ #
20
+ # See the file COPYING for complete licensing information.
21
+ #
22
+ #---------------------------------------------------------------------------
23
+ #
24
+ #
25
+
26
+
27
+
28
+ module GoshrineBot
29
+
30
+ # === Usage
31
+ #
32
+ # EventMachine.run {
33
+ # http = EventMachine::Protocols::HttpClient.request(
34
+ # :host => server,
35
+ # :port => 80,
36
+ # :request => "/index.html",
37
+ # :query_string => "parm1=value1&parm2=value2"
38
+ # )
39
+ # http.callback {|response|
40
+ # puts response[:status]
41
+ # puts response[:headers]
42
+ # puts response[:content]
43
+ # }
44
+ # }
45
+ #--
46
+ # TODO:
47
+ # Add streaming so we can support enormous POSTs. Current max is 20meg.
48
+ # Timeout for connections that run too long or hang somewhere in the middle.
49
+ # Persistent connections (HTTP/1.1), may need a associated delegate object.
50
+ # DNS: Some way to cache DNS lookups for hostnames we connect to. Ruby's
51
+ # DNS lookups are unbelievably slow.
52
+ # HEAD requests.
53
+ # Chunked transfer encoding.
54
+ # Convenience methods for requests. get, post, url, etc.
55
+ # SSL.
56
+ # Handle status codes like 304, 100, etc.
57
+ # Refactor this code so that protocol errors all get handled one way (an exception?),
58
+ # instead of sprinkling set_deferred_status :failed calls everywhere.
59
+ class HttpClient < EventMachine::Connection
60
+ include EventMachine::Deferrable
61
+
62
+ MaxPostContentLength = 20 * 1024 * 1024
63
+
64
+ # === Arg list
65
+ # :host => 'ip/dns', :port => fixnum, :verb => 'GET', :request => 'path',
66
+ # :basic_auth => {:username => '', :password => ''}, :content => 'content',
67
+ # :contenttype => 'text/plain', :query_string => '', :host_header => '',
68
+ # :cookie => ''
69
+ def self.request( args = {} )
70
+ args[:port] ||= 80
71
+ EventMachine.connect( args[:host], args[:port], self ) {|c|
72
+ # According to the docs, we will get here AFTER post_init is called.
73
+ c.instance_eval {@args = args}
74
+ }
75
+ end
76
+
77
+ def post_init
78
+ @start_time = Time.now
79
+ @data = ""
80
+ @read_state = :base
81
+ end
82
+
83
+ # We send the request when we get a connection.
84
+ # AND, we set an instance variable to indicate we passed through here.
85
+ # That allows #unbind to know whether there was a successful connection.
86
+ # NB: This naive technique won't work when we have to support multiple
87
+ # requests on a single connection.
88
+ def connection_completed
89
+ @connected = true
90
+ send_request @args
91
+ end
92
+
93
+ def send_request args
94
+ args[:verb] ||= args[:method] # Support :method as an alternative to :verb.
95
+ args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?
96
+
97
+ verb = args[:verb].to_s.upcase
98
+ unless ["GET", "POST", "PUT", "DELETE", "HEAD"].include?(verb)
99
+ set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
100
+ return # NOTE THE EARLY RETURN, we're not sending any data.
101
+ end
102
+
103
+ request = args[:request] || "/"
104
+ unless request[0,1] == "/"
105
+ request = "/" + request
106
+ end
107
+
108
+ qs = args[:query_string] || ""
109
+ if qs.length > 0 and qs[0,1] != '?'
110
+ qs = "?" + qs
111
+ end
112
+
113
+ version = args[:version] || "1.1"
114
+
115
+ # Allow an override for the host header if it's not the connect-string.
116
+ host = args[:host_header] || args[:host] || "_"
117
+ # For now, ALWAYS tuck in the port string, although we may want to omit it if it's the default.
118
+ port = args[:port]
119
+
120
+ # POST items.
121
+ postcontenttype = args[:contenttype] || "application/octet-stream"
122
+ postcontent = args[:content] || ""
123
+ raise "oversized content in HTTP POST" if postcontent.length > MaxPostContentLength
124
+
125
+ # ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
126
+ # TODO: We ASSUME the caller wants to send a 1.1 request. May not be a good assumption.
127
+ req = [
128
+ "#{verb} #{request}#{qs} HTTP/#{version}",
129
+ "Host: #{host}:#{port}",
130
+ "User-agent: Ruby EventMachine",
131
+ ]
132
+
133
+ if verb == "POST" || verb == "PUT"
134
+ req << "Content-type: #{postcontenttype}"
135
+ req << "Content-length: #{postcontent.length}"
136
+ end
137
+
138
+ # TODO, this cookie handler assumes it's getting a single, semicolon-delimited string.
139
+ # Eventually we will want to deal intelligently with arrays and hashes.
140
+ if args[:cookie]
141
+ req << "Cookie: #{args[:cookie]}"
142
+ end
143
+
144
+ # Allow custom HTTP headers, e.g. SOAPAction
145
+ args[:custom_headers].each do |k,v|
146
+ req << "#{k}: #{v}"
147
+ end
148
+
149
+ # Basic-auth stanza contributed by Matt Murphy.
150
+ if args[:basic_auth]
151
+ basic_auth_string = ["#{args[:basic_auth][:username]}:#{args[:basic_auth][:password]}"].pack('m').strip.gsub(/\n/,'')
152
+ req << "Authorization: Basic #{basic_auth_string}"
153
+ end
154
+
155
+ req << ""
156
+ reqstring = req.map {|l| "#{l}\r\n"}.join
157
+ send_data reqstring
158
+
159
+ if verb == "POST" || verb == "PUT"
160
+ send_data postcontent
161
+ end
162
+ end
163
+
164
+
165
+ def receive_data data
166
+ while data and data.length > 0
167
+ case @read_state
168
+ when :base
169
+ # Perform any per-request initialization here and don't consume any data.
170
+ @data = ""
171
+ @headers = []
172
+ @content_length = nil # not zero
173
+ @content = ""
174
+ @status = nil
175
+ @read_state = :header
176
+ @connection_close = nil
177
+ when :header
178
+ ary = data.split( /\r?\n/m, 2 )
179
+ if ary.length == 2
180
+ data = ary.last
181
+ if ary.first == ""
182
+ if (@content_length and @content_length > 0) || @chunked || @connection_close
183
+ @read_state = :content
184
+ else
185
+ dispatch_response
186
+ @read_state = :base
187
+ end
188
+ else
189
+ @headers << ary.first
190
+ if @headers.length == 1
191
+ parse_response_line
192
+ elsif ary.first =~ /\Acontent-length:\s*/i
193
+ # Only take the FIRST content-length header that appears,
194
+ # which we can distinguish because @content_length is nil.
195
+ # TODO, it's actually a fatal error if there is more than one
196
+ # content-length header, because the caller is presumptively
197
+ # a bad guy. (There is an exploit that depends on multiple
198
+ # content-length headers.)
199
+ @content_length ||= $'.to_i
200
+ elsif ary.first =~ /\Aconnection:\s*close/i
201
+ @connection_close = true
202
+ elsif ary.first =~ /\Atransfer-encoding:\s*chunked/i
203
+ @chunked = true
204
+ end
205
+ end
206
+ else
207
+ @data << data
208
+ data = ""
209
+ end
210
+ when :content
211
+ if @chunked && @chunk_length
212
+ bytes_needed = @chunk_length - @chunk_read
213
+ new_data = data[0, bytes_needed]
214
+ @chunk_read += new_data.length
215
+ @content += new_data
216
+ data = data[bytes_needed..-1] || ""
217
+ if @chunk_length == @chunk_read && data[0,2] == "\r\n"
218
+ @chunk_length = nil
219
+ data = data[2..-1]
220
+ end
221
+ elsif @chunked
222
+ if (m = data.match(/\A(\S*)\r\n/m))
223
+ data = data[m[0].length..-1]
224
+ @chunk_length = m[1].to_i(16)
225
+ @chunk_read = 0
226
+ if @chunk_length == 0
227
+ dispatch_response
228
+ @read_state = :base
229
+ end
230
+ end
231
+ elsif @content_length
232
+ # If there was no content-length header, we have to wait until the connection
233
+ # closes. Everything we get until that point is content.
234
+ # TODO: Must impose a content-size limit, and also must implement chunking.
235
+ # Also, must support either temporary files for large content, or calling
236
+ # a content-consumer block supplied by the user.
237
+ bytes_needed = @content_length - @content.length
238
+ @content += data[0, bytes_needed]
239
+ data = data[bytes_needed..-1] || ""
240
+ if @content_length == @content.length
241
+ dispatch_response
242
+ @read_state = :base
243
+ end
244
+ else
245
+ @content << data
246
+ data = ""
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+
253
+ # We get called here when we have received an HTTP response line.
254
+ # It's an opportunity to throw an exception or trigger other exceptional
255
+ # handling.
256
+ def parse_response_line
257
+ if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
258
+ @status = $1.to_i
259
+ else
260
+ set_deferred_status :failed, {
261
+ :status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
262
+ }
263
+ close_connection
264
+ end
265
+ end
266
+ private :parse_response_line
267
+
268
+ def dispatch_response
269
+ @read_state = :base
270
+ set_deferred_status :succeeded, {
271
+ :content => @content,
272
+ :headers => @headers,
273
+ :status => @status
274
+ }
275
+ # TODO, we close the connection for now, but this is wrong for persistent clients.
276
+ close_connection
277
+ end
278
+
279
+ def unbind
280
+ if !@connected
281
+ set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
282
+ elsif (@read_state == :content and @content_length == nil)
283
+ dispatch_response
284
+ end
285
+ end
286
+ end
287
+
288
+ end
@@ -0,0 +1,91 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+ require 'erb'
4
+
5
+ module GoshrineBot
6
+ class Runner
7
+ attr_accessor :options
8
+
9
+ class << self
10
+ def run
11
+ self.new
12
+ end
13
+ end
14
+
15
+ def initialize
16
+ # defaults
17
+ self.options = {
18
+ :server_url => "http://goshrine.com/",
19
+ :gtp_cmd_line => "gnugo --mode gtp",
20
+ :debug => false,
21
+ :pid_path => "./goshrine_bot.pid",
22
+ :log_path => "./goshrine_bot.log",
23
+ }
24
+
25
+ cmd_line_options = parse_options
26
+
27
+ if !File.exists?(config_path)
28
+ puts "You must generate a config file."
29
+ exit
30
+ end
31
+
32
+ config = YAML::load(ERB.new(IO.read(config_path)).result)
33
+ #puts "config = #{config.inspect}"
34
+ bot_name = ARGV[0] || config.keys.first
35
+ #puts "bot_name = #{ARGV[0]}"
36
+ if config[bot_name].nil?
37
+ puts "No config found for #{bot_name.inspect}"
38
+ exit
39
+ end
40
+ options.merge!(config[bot_name])
41
+ #puts "Options = #{options.inspect}"
42
+ options[:bot_name] = bot_name
43
+
44
+ start
45
+ end
46
+
47
+ def config_path
48
+ options[:config_path] || "./goshrine_bot.yml"
49
+ end
50
+
51
+ def start
52
+ puts "Starting GoShrine bot client: #{options[:bot_name]}."
53
+
54
+ client = Client.new(options)
55
+ client.run
56
+ end
57
+
58
+ def parse_options
59
+ OptionParser.new do |opts|
60
+ opts.summary_width = 25
61
+ opts.banner = "GoShrineBot (1.0)\n\n",
62
+ "Usage: goshrine_bot [-c configfile] [bot_name]\n",
63
+ " goshrine_bot --help\n"
64
+
65
+ opts.separator ""
66
+ opts.separator ""; opts.separator "Configuration:"
67
+
68
+ opts.on("-c", "--config FILE", String, "Path to configuration file.", "(default: #{options[:config_path]})") do |v|
69
+ options[:config_path] = File.expand_path(v)
70
+ end
71
+
72
+ opts.separator ""; opts.separator "Miscellaneous:"
73
+
74
+ opts.on_tail("-?", "--help", "Display this usage information.") do
75
+ puts "#{opts}\n"
76
+ exit
77
+ end
78
+
79
+ end.parse!
80
+ options
81
+ end
82
+
83
+ private
84
+
85
+ def store_pid(pid)
86
+ FileUtils.mkdir_p(File.dirname(pid_path))
87
+ File.open(pid_path, 'w'){|f| f.write("#{pid}\n")}
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+ require 'eventmachine'
4
+
5
+ module GoshrineBot
6
+
7
+ VERSION = "0.1.0"
8
+
9
+ STDOUT.sync = true
10
+
11
+ ROOT = File.expand_path(File.dirname(__FILE__))
12
+
13
+ %w[ client
14
+ gtp_stdio_client
15
+ runner
16
+ game
17
+ core_ext/hash
18
+ faye
19
+ httpclient
20
+
21
+ ].each do |lib|
22
+ require File.join(ROOT, 'goshrine_bot', lib)
23
+ end
24
+
25
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: goshrine_bot
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Pete Schwamb
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-22 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :development
32
+ version_requirements: *id001
33
+ description: The GoShrine bot client is a library that allows you connect a local Go playing program that speaks GTP (like gnugo) to http://goshrine.com.
34
+ email:
35
+ - pete@schwamb.net
36
+ executables:
37
+ - goshrine_bot
38
+ extensions: []
39
+
40
+ extra_rdoc_files: []
41
+
42
+ files:
43
+ - History.txt
44
+ - Manifest.txt
45
+ - README.rdoc
46
+ - Rakefile
47
+ - TODO.txt
48
+ - bin/goshrine_bot
49
+ - goshrine_bot.gemspec
50
+ - goshrine_bot.yml
51
+ - lib/goshrine_bot.rb
52
+ - lib/goshrine_bot/client.rb
53
+ - lib/goshrine_bot/core_ext/hash.rb
54
+ - lib/goshrine_bot/faye.rb
55
+ - lib/goshrine_bot/faye/channel.rb
56
+ - lib/goshrine_bot/faye/client.rb
57
+ - lib/goshrine_bot/faye/connection.rb
58
+ - lib/goshrine_bot/faye/error.rb
59
+ - lib/goshrine_bot/faye/grammar.rb
60
+ - lib/goshrine_bot/faye/namespace.rb
61
+ - lib/goshrine_bot/faye/rack_adapter.rb
62
+ - lib/goshrine_bot/faye/server.rb
63
+ - lib/goshrine_bot/faye/timeouts.rb
64
+ - lib/goshrine_bot/faye/transport.rb
65
+ - lib/goshrine_bot/game.rb
66
+ - lib/goshrine_bot/gtp_stdio_client.rb
67
+ - lib/goshrine_bot/httpclient.rb
68
+ - lib/goshrine_bot/runner.rb
69
+ has_rdoc: true
70
+ homepage: http://github.com/ps2/goshrine_bot
71
+ licenses: []
72
+
73
+ post_install_message:
74
+ rdoc_options: []
75
+
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ segments:
84
+ - 0
85
+ version: "0"
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ~>
90
+ - !ruby/object:Gem::Version
91
+ segments:
92
+ - 1
93
+ - 3
94
+ - 6
95
+ version: 1.3.6
96
+ requirements: []
97
+
98
+ rubyforge_project:
99
+ rubygems_version: 1.3.7
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: A client to connect GTP go programs to GoShrine
103
+ test_files: []
104
+