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.
data/History.txt ADDED
File without changes
data/Manifest.txt ADDED
File without changes
data/README.rdoc ADDED
@@ -0,0 +1,56 @@
1
+ = GoShrine Bot Client
2
+
3
+ * http://goshrine.com
4
+ * http://github.com/ps2/goshrine_bot
5
+
6
+ The GoShrine bot client is a library that allows you connect a local Go playing program that speaks GTP (like gnugo) to goshrine.com
7
+
8
+ The GTP protocol is documented here: http://www.lysator.liu.se/~gunnar/gtp/
9
+
10
+ == INSTALL:
11
+
12
+ * gem install goshrine_bot
13
+
14
+ == USE:
15
+
16
+ * Create a normal account on goshrine for your bot.
17
+ * Email feedback@goshrine.com with the account name, and request that it be changed to a bot account.
18
+ * Edit goshrine_bot.yml with your account info.
19
+ * Create a database.yml file in the following format:
20
+
21
+ botname:
22
+ :login: yourbotlogin
23
+ :password: yourbotpassword
24
+ :gtp_cmd_line: gnugo --mode gtp --level 1
25
+
26
+ * Run goshrine_bot botname
27
+
28
+ == REQUIREMENTS:
29
+
30
+ * json
31
+ * eventmachine
32
+
33
+ == LICENSE:
34
+
35
+ (The MIT License)
36
+
37
+ Copyright (c) 2010 Pete Schwamb
38
+
39
+ Permission is hereby granted, free of charge, to any person obtaining
40
+ a copy of this software and associated documentation files (the
41
+ 'Software'), to deal in the Software without restriction, including
42
+ without limitation the rights to use, copy, modify, merge, publish,
43
+ distribute, sublicense, and/or sell copies of the Software, and to
44
+ permit persons to whom the Software is furnished to do so, subject to
45
+ the following conditions:
46
+
47
+ The above copyright notice and this permission notice shall be
48
+ included in all copies or substantial portions of the Software.
49
+
50
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
51
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
52
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
53
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
54
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
55
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
56
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ # Available options:
2
+ #
3
+ # rake test - Runs all test cases.
4
+ # rake package - Runs test cases and builds packages for distribution.
5
+ # rake rdoc - Builds API documentation in doc dir.
6
+
7
+ require 'rake'
8
+ require 'rspec/core/rake_task'
9
+ require 'rake/gempackagetask'
10
+
11
+ task :default => :spec
12
+
13
+ RSpec::Core::RakeTask.new do |rspec|
14
+ #rspec.ruby_opts="-w"
15
+ end
16
+
17
+ load(File.join(File.dirname(__FILE__), "goshrine_bot.gemspec"))
18
+
19
+ Rake::GemPackageTask.new(SPEC) do |package|
20
+ # do nothing: I just need a gem but this block is required
21
+ end
22
+
data/TODO.txt ADDED
@@ -0,0 +1,2 @@
1
+
2
+ * Shutdown idle engines (to be restarted later) to save on resources.
data/bin/goshrine_bot ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'goshrine_bot')
4
+ EventMachine.run {
5
+ EM.error_handler{ |e|
6
+ puts "Error raised during event loop: #{e.message} " + e.backtrace.join("\n")
7
+ }
8
+ GoshrineBot::Runner.run
9
+ }
10
+
@@ -0,0 +1,33 @@
1
+ DIR = File.dirname(__FILE__)
2
+ LIB = File.join(DIR, *%w[lib goshrine_bot.rb])
3
+ VERSION = open(LIB) { |lib|
4
+ lib.each { |line|
5
+ if v = line[/^\s*VERSION\s*=\s*(['"])(\d\.\d\.\d)\1/, 2]
6
+ break v
7
+ end
8
+ }
9
+ }
10
+
11
+ SPEC = Gem::Specification.new do |s|
12
+ s.name = "goshrine_bot"
13
+ s.version = VERSION
14
+ s.platform = Gem::Platform::RUBY
15
+ s.authors = ["Pete Schwamb"]
16
+ s.email = ["pete@schwamb.net"]
17
+ s.homepage = "http://github.com/ps2/goshrine_bot"
18
+ s.summary = "A client to connect GTP go programs to GoShrine"
19
+ s.description = <<-END_DESCRIPTION.gsub(/\s+/, " ").strip
20
+ 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.
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.executables = ['goshrine_bot']
29
+
30
+ s.files = `git ls-files`.split("\n")
31
+ s.test_files = `git ls-files -- spec/*_spec.rb`.split("\n")
32
+ s.require_paths = %w[lib]
33
+ end
data/goshrine_bot.yml ADDED
@@ -0,0 +1,5 @@
1
+ botname:
2
+ :login: yourbotlogin
3
+ :password: yourbotpass
4
+ :gtp_cmd_line: gnugo --mode gtp --level 1
5
+
@@ -0,0 +1,180 @@
1
+
2
+ module GoshrineBot
3
+
4
+ class Client
5
+
6
+ class << self
7
+ end
8
+
9
+ def initialize(options)
10
+ @base_url = URI::parse(options[:server_url])
11
+ @options = options
12
+ @password = options[:password]
13
+ @login = options[:login]
14
+ @gtp_cmd_line = options[:gtp_cmd_line]
15
+ @connected = false
16
+ @cookie = ""
17
+ @games = {}
18
+ end
19
+
20
+ def gtp_cmd_line
21
+ @gtp_cmd_line
22
+ end
23
+
24
+ def my_user_id
25
+ @my_user_id
26
+ end
27
+
28
+ def parse_cookie(headers)
29
+ cookies = []
30
+ headers.each do |h|
31
+ if h.match(/^Set-Cookie: /)
32
+ pieces = h[12..-1].split("=")
33
+ cookies << "#{pieces[0]}=#{pieces[1].split(";").first}";
34
+ end
35
+ end
36
+ @cookie = cookies.join("; ") + ';'
37
+ end
38
+
39
+ def login(&blk)
40
+ # login
41
+ http = http_post('/sessions/create', {'login' => @login, 'password' => @password})
42
+ http.callback {|response|
43
+ #puts "login returned: #{response[:status]}"
44
+ if response[:status].to_i == 401
45
+ puts "Invalid Login or Password"
46
+ EventMachine::stop
47
+ else
48
+ parse_cookie(response[:headers])
49
+ user = JSON.parse(response[:content])
50
+ @queue_id = user['queue_id']
51
+ @my_user_id = user['id']
52
+ if user['user_type'] != 'bot'
53
+ puts "Account #{user['login']} is not registered as a robot!"
54
+ EventMachine::stop
55
+ else
56
+ puts "Login successful"
57
+ blk.call
58
+ end
59
+ end
60
+ }
61
+ http.errback {|response|
62
+ puts "Login failed (network issue?)";
63
+ }
64
+ end
65
+
66
+ def http_post(url, data = nil)
67
+ headers = {'Accept' => 'application/json', :cookie => @cookie}
68
+ if data.is_a? Hash
69
+ data = data.to_params
70
+ contenttype = "application/x-www-form-urlencoded"
71
+ else
72
+ contenttype = "application/octet-stream"
73
+ end
74
+ #puts "Posting #{data}"
75
+ GoshrineBot::HttpClient.request(
76
+ :host => @base_url.host,
77
+ :port => @base_url.port,
78
+ :contenttype => contenttype,
79
+ :request => url,
80
+ :verb => 'POST',
81
+ :content => data,
82
+ :custom_headers => headers)
83
+ end
84
+
85
+ def http_get(url)
86
+ headers = {'Accept' => 'application/json', :cookie => @cookie}
87
+ GoshrineBot::HttpClient.request(
88
+ :host => @base_url.host,
89
+ :port => @base_url.port,
90
+ :request => url,
91
+ :custom_headers => headers)
92
+ end
93
+
94
+ def subscribe
95
+ @client.subscribe("/user/private/" + @queue_id) do |m|
96
+ msg_type = m["type"]
97
+ case msg_type
98
+ when 'match_requested'
99
+ handle_match_request(m["match_request"])
100
+ when 'match_accepted'
101
+ handle_match_accept(m["game_token"])
102
+ else
103
+ puts "Unsupported private message type: #{msg_type}"
104
+ end
105
+ #@msg_queue_in.push(doc)
106
+ end
107
+ @client.subscribe("/room/1") do |m|
108
+ #@msg_queue_in.push(doc)
109
+ end
110
+ end
111
+
112
+ def run
113
+ login {
114
+ @client = Faye::Client.new((@base_url + '/events').to_s, :timeout => 120, :cookie => @cookie)
115
+ subscribe
116
+ load_existing_games {
117
+ EM::add_periodic_timer( 60 ) {
118
+ @games.each do |token, game|
119
+ game.idle_check
120
+ end
121
+ }
122
+ }
123
+ }
124
+ end
125
+
126
+ def load_existing_games(&blk)
127
+ http = http_get('/game/active')
128
+ http.callback {|response|
129
+ #puts "Got #{response.inspect}"
130
+ parse_cookie(response[:headers])
131
+ games = JSON.parse(response[:content])
132
+ puts "#{games.count} game(s) in progress"
133
+ games.each do |game_attrs|
134
+ game = GameInProgress.new(self)
135
+ game.update_from_game_list(game_attrs)
136
+ if game.move_number != game.moves.size
137
+ puts "Only #{game.moves.size} available! Expected #{game.move_number} in game #{game.token}"
138
+ else
139
+ add_game(game)
140
+ end
141
+ end
142
+ blk.call
143
+ }
144
+ end
145
+
146
+ def add_game(game)
147
+ @games[game.game_id] = game
148
+ game.make_move # does nothing if its not our turn, or the game is not started
149
+ @client.subscribe("/game/private/" + game.token + '/' + @queue_id) do |m|
150
+ game.private_message(m)
151
+ end
152
+ @client.subscribe("/game/play/" + game.token) do |m|
153
+ game.play_message(m)
154
+ end
155
+ end
156
+
157
+ def handle_match_accept(token)
158
+ game = GameInProgress.new(self)
159
+ http = http_get("/g/#{token}")
160
+ http.callback {|response|
161
+ attrs = JSON.parse(response[:content])
162
+ game.update_from_game_list(attrs)
163
+ add_game(game)
164
+ }
165
+ end
166
+
167
+ def handle_match_request(request)
168
+ game = GameInProgress.new(self)
169
+ game.update_from_match_request(request)
170
+ http = http_get("/match/accept?id=#{game.challenge_id}")
171
+ http.callback {|response|
172
+ attrs = JSON.parse(response[:content])
173
+ game.update_from_game_list(attrs)
174
+ add_game(game)
175
+ }
176
+ end
177
+
178
+ end
179
+ end
180
+
@@ -0,0 +1,53 @@
1
+ class Hash
2
+ # Stolen partially from Merb : http://noobkit.com/show/ruby/gems/development/merb/hash/to_params.html
3
+ # Convert this hash to a query string:
4
+ #
5
+ # { :name => "Bob",
6
+ # :address => {
7
+ # :street => '111 Ruby Ave.',
8
+ # :city => 'Ruby Central',
9
+ # :phones => ['111-111-1111', '222-222-2222']
10
+ # }
11
+ # }.to_params
12
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones]=111-111-1111222-222-2222&address[street]=111 Ruby Ave."
13
+ #
14
+ def to_params
15
+ params = ''
16
+ stack = []
17
+
18
+ each do |k, v|
19
+ if v.is_a?(Hash)
20
+ stack << [k,v]
21
+ elsif v.is_a?(Array)
22
+ stack << [k,Hash.from_array(v)]
23
+ else
24
+ params << "#{k}=#{v}&"
25
+ end
26
+ end
27
+
28
+ stack.each do |parent, hash|
29
+ hash.each do |k, v|
30
+ if v.is_a?(Hash)
31
+ stack << ["#{parent}[#{k}]", v]
32
+ else
33
+ params << "#{parent}[#{k}]=#{v}&"
34
+ end
35
+ end
36
+ end
37
+
38
+ params.chop! # trailing &
39
+ params
40
+ end
41
+
42
+ ##
43
+ # Builds a hash from an array with keys as array indices.
44
+ def self.from_array(array = [])
45
+ h = Hash.new
46
+ array.size.times do |t|
47
+ h[t] = array[t]
48
+ end
49
+ h
50
+ end
51
+
52
+ end
53
+
@@ -0,0 +1,143 @@
1
+ module Faye
2
+ class Channel
3
+
4
+ include Observable
5
+ attr_reader :name
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ end
10
+
11
+ def <<(message)
12
+ changed(true)
13
+ notify_observers(:message, message)
14
+ end
15
+
16
+ HANDSHAKE = '/meta/handshake'
17
+ CONNECT = '/meta/connect'
18
+ SUBSCRIBE = '/meta/subscribe'
19
+ UNSUBSCRIBE = '/meta/unsubscribe'
20
+ DISCONNECT = '/meta/disconnect'
21
+
22
+ META = :meta
23
+ SERVICE = :service
24
+
25
+ class << self
26
+ def valid?(name)
27
+ Grammar::CHANNEL_NAME =~ name or
28
+ Grammar::CHANNEL_PATTERN =~ name
29
+ end
30
+
31
+ def parse(name)
32
+ return nil unless valid?(name)
33
+ name.split('/')[1..-1].map { |s| s.to_sym }
34
+ end
35
+
36
+ def meta?(name)
37
+ segments = parse(name)
38
+ segments ? (segments.first == META) : nil
39
+ end
40
+
41
+ def service?(name)
42
+ segments = parse(name)
43
+ segments ? (segments.first == SERVICE) : nil
44
+ end
45
+
46
+ def subscribable?(name)
47
+ return nil unless valid?(name)
48
+ not meta?(name) and not service?(name)
49
+ end
50
+ end
51
+
52
+ class Tree
53
+ include Enumerable
54
+ attr_accessor :value
55
+
56
+ def initialize(value = nil)
57
+ @value = value
58
+ @children = {}
59
+ end
60
+
61
+ # Remove channels that have no subscribers
62
+ def delete_unobserved_channels
63
+ total_observers = 0
64
+ if @value
65
+ total_observers += @value.count_observers
66
+ @value = nil if @value.count_observers == 0
67
+ end
68
+
69
+ @children.delete_if { |key, subtree|
70
+ num_child_observers = subtree.delete_unobserved_channels
71
+ total_observers += num_child_observers
72
+ num_child_observers == 0
73
+ }
74
+ total_observers
75
+ end
76
+
77
+ def each_child
78
+ @children.each { |key, subtree| yield(key, subtree) }
79
+ end
80
+
81
+ def each(prefix = [], &block)
82
+ each_child { |path, subtree| subtree.each(prefix + [path], &block) }
83
+ yield(prefix, @value) unless @value.nil?
84
+ end
85
+
86
+ def keys
87
+ map { |key, value| '/' + key * '/' }
88
+ end
89
+
90
+ def [](name)
91
+ subtree = traverse(name)
92
+ subtree ? subtree.value : nil
93
+ end
94
+
95
+ def []=(name, value)
96
+ subtree = traverse(name, true)
97
+ subtree.value = value unless subtree.nil?
98
+ end
99
+
100
+ def traverse(path, create_if_absent = false)
101
+ path = Channel.parse(path) if String === path
102
+
103
+ return nil if path.nil?
104
+ return self if path.empty?
105
+
106
+ subtree = @children[path.first]
107
+ return nil if subtree.nil? and not create_if_absent
108
+ subtree = @children[path.first] = self.class.new if subtree.nil?
109
+
110
+ subtree.traverse(path[1..-1], create_if_absent)
111
+ end
112
+
113
+ def glob(path = [])
114
+ path = Channel.parse(path) if String === path
115
+
116
+ return [] if path.nil?
117
+ return @value.nil? ? [] : [@value] if path.empty?
118
+
119
+ if path == [:*]
120
+ return @children.inject([]) do |list, (key, subtree)|
121
+ list << subtree.value unless subtree.value.nil?
122
+ list
123
+ end
124
+ end
125
+
126
+ if path == [:**]
127
+ list = map { |key, value| value }
128
+ list.pop unless @value.nil?
129
+ return list
130
+ end
131
+
132
+ list = @children.values_at(path.first, :*).
133
+ compact.
134
+ map { |t| t.glob(path[1..-1]) }
135
+
136
+ list << @children[:**].value if @children[:**]
137
+ list.flatten
138
+ end
139
+ end
140
+
141
+ end
142
+ end
143
+