goshrine_bot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+