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,266 @@
1
+ module Faye
2
+ class Server
3
+ def initialize(options = {})
4
+ @options = options
5
+ @channels = Channel::Tree.new
6
+ @clients = {}
7
+ @namespace = Namespace.new
8
+ @security_policy = options[:security_policy]
9
+ end
10
+
11
+ # Notifies the server of stale connections that should be deleted
12
+ def update(message, client)
13
+ return unless message == :stale_client
14
+ destroy_client(client)
15
+ end
16
+
17
+ def client_ids
18
+ @clients.keys
19
+ end
20
+
21
+ def process(messages, transport_authentication = nil, local = false, &callback)
22
+ messages = [messages].flatten
23
+ processed, responses = 0, []
24
+
25
+ messages.each do |message|
26
+ handle(message, transport_authentication, local) do |reply|
27
+ reply = [reply].flatten
28
+ responses.concat(reply)
29
+ processed += 1
30
+ callback[responses] if processed == messages.size
31
+ end
32
+ end
33
+ end
34
+
35
+ def flush_connection(messages)
36
+ [messages].flatten.each do |message|
37
+ client = @clients[message['clientId']]
38
+ client.flush! if client
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def connection(id)
45
+ return @clients[id] if @clients.has_key?(id)
46
+ client = Connection.new(id, @options)
47
+ client.add_observer(self)
48
+ @clients[id] = client
49
+ end
50
+
51
+ def destroy_client(client)
52
+ client.disconnect!
53
+ client.delete_observer(self)
54
+ @namespace.release(client.id)
55
+ @clients.delete(client.id)
56
+ end
57
+
58
+ def handle(message, transport_authentication = nil, local = false, &callback)
59
+ client_id = message['clientId']
60
+ channel = message['channel']
61
+
62
+ allowed_to_publish = local || @security_policy.nil? || Channel.meta?(channel) || @security_policy.can_publish(transport_authentication, channel, message)
63
+
64
+ if !allowed_to_publish
65
+ response = { 'channel' => channel,
66
+ 'version' => BAYEUX_VERSION,
67
+ 'id' => message['id'],
68
+ 'error' => Error.channel_forbidden(channel),
69
+ 'successful' => false }
70
+ return callback[response]
71
+ end
72
+
73
+ @channels.glob(channel).each do |c|
74
+ c << message
75
+ end
76
+
77
+ if Channel.meta?(channel)
78
+ response = __send__(Channel.parse(channel)[1], message, transport_authentication, local)
79
+
80
+ client_id = response['clientId']
81
+ response['advice'] ||= {}
82
+ response['advice']['reconnect'] ||= @clients.has_key?(client_id) ? 'retry' : 'handshake'
83
+ response['advice']['interval'] ||= (Connection::INTERVAL * 1000).floor
84
+
85
+ return callback[response] unless response['channel'] == Channel::CONNECT and
86
+ response['successful'] == true
87
+
88
+ return connection(response['clientId']).connect(transport_authentication) do |events|
89
+ callback[[response] + events]
90
+ end
91
+ end
92
+
93
+ return callback[[]] if message['clientId'].nil? or Channel.service?(channel)
94
+
95
+ response = make_response(message)
96
+ response['successful'] = true
97
+ callback[response]
98
+ end
99
+
100
+ def make_response(message)
101
+ response = {}
102
+ %w[id clientId channel].each do |field|
103
+ if message[field]
104
+ response[field] = message[field]
105
+ end
106
+ end
107
+ response
108
+ end
109
+
110
+ # MUST contain * version
111
+ # * supportedConnectionTypes
112
+ # MAY contain * minimumVersion
113
+ # * ext
114
+ # * id
115
+ def handshake(message, transport_authentication, local = false)
116
+ response = make_response(message)
117
+ response['version'] = BAYEUX_VERSION
118
+
119
+ response['error'] = Error.parameter_missing('version') if message['version'].nil?
120
+
121
+ unless local
122
+ response['supportedConnectionTypes'] = CONNECTION_TYPES
123
+
124
+ client_conns = message['supportedConnectionTypes']
125
+
126
+ # This is a fix for the (non-spec following) cometd.js client
127
+ client_conns = client_conns.values if client_conns.is_a?(Hash)
128
+
129
+ if client_conns
130
+ common_conns = client_conns.select { |c| CONNECTION_TYPES.include?(c) }
131
+ response['error'] = Error.conntype_mismatch(*client_conns) if common_conns.empty?
132
+ else
133
+ response['error'] = Error.parameter_missing('supportedConnectionTypes')
134
+ end
135
+ end
136
+
137
+ response['successful'] = response['error'].nil?
138
+ return response unless response['successful']
139
+
140
+ client_id = @namespace.generate
141
+ response['clientId'] = connection(client_id).id
142
+ connection_listener = @options[:connection_listener]
143
+ if connection_listener
144
+ connection_listener.connected(client_id, transport_authentication)
145
+ end
146
+ response
147
+ end
148
+
149
+ # MUST contain * clientId
150
+ # * connectionType
151
+ # MAY contain * ext
152
+ # * id
153
+ def connect(message, transport_authentication, local = false)
154
+ response = make_response(message)
155
+
156
+ client_id = message['clientId']
157
+ client = client_id ? @clients[client_id] : nil
158
+
159
+ response['error'] = Error.client_unknown(client_id) if client.nil?
160
+ response['error'] = Error.parameter_missing('clientId') if client_id.nil?
161
+ response['error'] = Error.parameter_missing('connectionType') if message['connectionType'].nil?
162
+
163
+ response['successful'] = response['error'].nil?
164
+ response.delete('clientId') unless response['successful']
165
+ return response unless response['successful']
166
+
167
+ response['clientId'] = client.id
168
+ response
169
+ end
170
+
171
+ # MUST contain * clientId
172
+ # MAY contain * ext
173
+ # * id
174
+ def disconnect(message, transport_authentication, local = false)
175
+ response = make_response(message)
176
+
177
+ client_id = message['clientId']
178
+ client = client_id ? @clients[client_id] : nil
179
+
180
+ response['error'] = Error.client_unknown(client_id) if client.nil?
181
+ response['error'] = Error.parameter_missing('clientId') if client_id.nil?
182
+
183
+ response['successful'] = response['error'].nil?
184
+ response.delete('clientId') unless response['successful']
185
+ return response unless response['successful']
186
+
187
+ destroy_client(client)
188
+
189
+ response['clientId'] = client_id
190
+ response
191
+ end
192
+
193
+ # MUST contain * clientId
194
+ # * subscription
195
+ # MAY contain * ext
196
+ # * id
197
+ def subscribe(message, transport_authentication, local = false)
198
+ response = make_response(message)
199
+
200
+ client_id = message['clientId']
201
+ client = client_id ? @clients[client_id] : nil
202
+
203
+ subscription = message['subscription']
204
+ subscription = [subscription].flatten
205
+
206
+ response['error'] = Error.client_unknown(client_id) if client.nil?
207
+ response['error'] = Error.parameter_missing('clientId') if client_id.nil?
208
+ response['error'] = Error.parameter_missing('subscription') if message['subscription'].nil?
209
+
210
+ response['subscription'] = subscription.compact
211
+
212
+ subscription.each do |channel|
213
+ next if response['error']
214
+ response['error'] = Error.channel_forbidden(channel) unless local or Channel.subscribable?(channel)
215
+ response['error'] = Error.channel_invalid(channel) unless Channel.valid?(channel)
216
+ if @security_policy
217
+ response['error'] = Error.channel_forbidden(channel) unless @security_policy.can_subscribe(transport_authentication, channel, message)
218
+ end
219
+
220
+ next if response['error']
221
+ channel = @channels[channel] ||= Channel.new(channel)
222
+ client.subscribe(channel)
223
+ end
224
+
225
+ response['successful'] = response['error'].nil?
226
+ response
227
+ end
228
+
229
+ # MUST contain * clientId
230
+ # * subscription
231
+ # MAY contain * ext
232
+ # * id
233
+ def unsubscribe(message, transport_authentication, local = false)
234
+ response = make_response(message)
235
+
236
+ client_id = message['clientId']
237
+ client = client_id ? @clients[client_id] : nil
238
+
239
+ subscription = message['subscription']
240
+ subscription = [subscription].flatten
241
+
242
+ response['error'] = Error.client_unknown(client_id) if client.nil?
243
+ response['error'] = Error.parameter_missing('clientId') if client_id.nil?
244
+ response['error'] = Error.parameter_missing('subscription') if message['subscription'].nil?
245
+
246
+ response['subscription'] = subscription.compact
247
+
248
+ subscription.each do |channel|
249
+ next if response['error']
250
+
251
+ if not Channel.valid?(channel)
252
+ response['error'] = Error.channel_invalid(channel)
253
+ next
254
+ end
255
+
256
+ channel = @channels[channel]
257
+ client.unsubscribe(channel) if channel
258
+ end
259
+
260
+ response['successful'] = response['error'].nil?
261
+ response
262
+ end
263
+
264
+ end
265
+ end
266
+
@@ -0,0 +1,21 @@
1
+ module Faye
2
+ module Timeouts
3
+ def add_timeout(name, delay, &block)
4
+ @timeouts ||= {}
5
+ return if @timeouts.has_key?(name)
6
+ @timeouts[name] = EventMachine.add_timer(delay) do
7
+ @timeouts.delete(name)
8
+ block.call
9
+ end
10
+ end
11
+
12
+ def remove_timeout(name)
13
+ @timeouts ||= {}
14
+ timeout = @timeouts[name]
15
+ return if timeout.nil?
16
+ EventMachine.cancel_timer(timeout)
17
+ @timeouts.delete(name)
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,123 @@
1
+ #require 'em-http'
2
+ require 'json'
3
+ require 'uri'
4
+
5
+ module Faye
6
+
7
+ class Transport
8
+ def initialize(client, endpoint, cookie = nil)
9
+ @client = client
10
+ @endpoint = endpoint
11
+ @cookie = cookie
12
+ end
13
+
14
+ def connection_type
15
+ self.class.connection_type
16
+ end
17
+
18
+ def send(message, &block)
19
+ if message.is_a?(Hash) and not message.has_key?('id')
20
+ message['id'] = @client.namespace.generate
21
+ end
22
+
23
+ request(message) { |responses|
24
+ if block_given?
25
+ messages, deliverable = [], true
26
+ [responses].flatten.each do |response|
27
+
28
+ if message.is_a?(Hash) and response['id'] == message['id']
29
+ deliverable = false if block.call(response) == false
30
+ end
31
+
32
+ if response['advice']
33
+ @client.handle_advice(response['advice'])
34
+ end
35
+
36
+ if response['data'] and response['channel']
37
+ messages << response
38
+ end
39
+
40
+ end
41
+
42
+ @client.deliver_messages(messages) if deliverable
43
+ end
44
+ }
45
+ end
46
+
47
+ @transports = {}
48
+
49
+ class << self
50
+ attr_accessor :connection_type
51
+
52
+ def get(client, connection_types = nil)
53
+ endpoint = client.endpoint
54
+ connection_types ||= supported_connection_types
55
+
56
+ candidate_class = @transports.find do |type, klass|
57
+ connection_types.include?(type) and
58
+ klass.usable?(endpoint)
59
+ end
60
+
61
+ unless candidate_class
62
+ raise "Could not find a usable connection type for #{ endpoint }"
63
+ end
64
+
65
+ candidate_class.last.new(client, endpoint, client.cookie)
66
+ end
67
+
68
+ def register(type, klass)
69
+ @transports[type] = klass
70
+ klass.connection_type = type
71
+ end
72
+
73
+ def supported_connection_types
74
+ @transports.keys
75
+ end
76
+ end
77
+ end
78
+
79
+ class HttpTransport < Transport
80
+ def self.usable?(endpoint)
81
+ endpoint.is_a?(String)
82
+ end
83
+
84
+ def request(message, &block)
85
+ content = JSON.unparse(message)
86
+
87
+ url = URI.parse(@endpoint)
88
+ headers = {'Accept' => 'application/json', :host => url.host, :cookie => @cookie}
89
+ request = GoshrineBot::HttpClient.request(
90
+ :host => url.host,
91
+ :port => url.port,
92
+ :contenttype => 'application/json',
93
+ :request => url.path,
94
+ :verb => 'POST',
95
+ :content => content,
96
+ :custom_headers => headers)
97
+
98
+ request.callback do |response|
99
+ begin
100
+ block.call(JSON.parse(response[:content]))
101
+ rescue JSON::ParserError => e
102
+ puts "Bad JSON: #{response[:content].inspect}"
103
+ end
104
+ end
105
+ end
106
+ end
107
+ Transport.register 'long-polling', HttpTransport
108
+
109
+ class LocalTransport < Transport
110
+ def self.usable?(endpoint)
111
+ endpoint.is_a?(Server)
112
+ end
113
+
114
+ def request(message, &block)
115
+ @endpoint.process(message, nil, true) do |response|
116
+ block.call(response)
117
+ end
118
+ end
119
+ end
120
+ Transport.register 'in-process', LocalTransport
121
+
122
+ end
123
+
@@ -0,0 +1,36 @@
1
+ # This is a hacked up (and old) version of jcoglan's excellent Faye library.
2
+ # Hacks include adding cookie support (so we can use the cookies from a rails login),
3
+ # and avoiding use of em-http-request
4
+
5
+ require 'forwardable'
6
+ require 'observer'
7
+ require 'set'
8
+ require 'rubygems'
9
+ require 'eventmachine'
10
+
11
+ module Faye
12
+ VERSION = '0.3.2'
13
+
14
+ ROOT = File.expand_path(File.dirname(__FILE__))
15
+
16
+ BAYEUX_VERSION = '1.0'
17
+ ID_LENGTH = 128
18
+ JSONP_CALLBACK = 'jsonpcallback'
19
+ CONNECTION_TYPES = %w[long-polling callback-polling]
20
+
21
+ %w[ timeouts grammar namespace
22
+ server channel connection
23
+ error client transport
24
+ ].each do |lib|
25
+ require File.join(ROOT, 'faye', lib)
26
+ end
27
+
28
+ autoload :RackAdapter, File.join(ROOT, 'faye', 'rack_adapter')
29
+
30
+ def self.random(bitlength = ID_LENGTH)
31
+ field = 2 ** bitlength
32
+ strlen = bitlength / 4
33
+ ("%0#{strlen}s" % rand(field).to_s(16)).gsub(' ', '0')
34
+ end
35
+ end
36
+
@@ -0,0 +1,252 @@
1
+ module GoshrineBot
2
+ class GameInProgress
3
+ attr_accessor :state
4
+ attr_accessor :board_size
5
+ attr_accessor :proposed_by_id
6
+ attr_accessor :white_player_id
7
+ attr_accessor :black_player_id
8
+ attr_accessor :challenge_id
9
+ attr_accessor :token
10
+ attr_accessor :game_id
11
+ attr_accessor :gtp_client
12
+ attr_accessor :move_number
13
+ attr_accessor :turn
14
+ attr_accessor :komi
15
+ attr_accessor :moves
16
+ attr_accessor :handicap
17
+ attr_accessor :handicap_stones
18
+ attr_accessor :last_activity
19
+
20
+ def initialize(client)
21
+ @client = client
22
+ end
23
+
24
+ def play_message(m)
25
+ return unless m
26
+ #puts "Got game play message: #{m.inspect}"
27
+ case m["action"]
28
+ when 'user_arrive'
29
+ user_id = m["id"].to_i
30
+ puts "User arrived (#{token}): #{m["login"]}"
31
+ if state == 'new' && (user_id == black_player_id || user_id == white_player_id)
32
+ http = @client.http_post("/game/#{token}/attempt_start")
33
+ end
34
+ when 'game_started'
35
+ self.state = "in-play"
36
+ make_move
37
+ when 'updateBoard'
38
+ handle_move(m["data"])
39
+ #puts "Going to update board"
40
+ when 'resignedBy'
41
+ puts "Game resigned by #{m["data"]}"
42
+ stop_gtp_client
43
+ when 'updateForUndo'
44
+ # TODO - handle undos?
45
+ puts "Undo not currently supported"
46
+ when 'pleaseWait'
47
+ # ignore
48
+ when 'user_leave'
49
+ # ignore
50
+ when 'gameFinished'
51
+ stop_gtp_client
52
+ if m['data'].nil?
53
+ puts "Missing data: #{m.inspect}"
54
+ return
55
+ elsif m['data']['scoring_info'].nil?
56
+ puts "Missing scoring_info: #{m.inspect}"
57
+ return
58
+ end
59
+ score = m['data']['scoring_info']['score']
60
+ winner = score['black'] > score['white'] ? 'B' : 'W'
61
+ if my_color == winner
62
+ puts "I Won!"
63
+ else
64
+ puts "I Lost."
65
+ end
66
+ puts "Score: #{score.inspect}"
67
+ else
68
+ puts "Unhandled game play message #{m.inspect}"
69
+ end
70
+ end
71
+
72
+ def handle_move(m)
73
+ # {"captures"=>[], "move_number"=>1, "color"=>"b", "move"=>"ir", "black_seconds_left"=>1900, "white_seconds_left"=>1900, "turn_started_at"=>"Sat, 05 Dec 2009 00:44:19 -0600"}
74
+ pos = m["move"]
75
+ color = m["color"].upcase
76
+ if pos && color && color != my_color
77
+ pos = sgf_coord_to_gtp_coord(pos, board_size)
78
+ puts "Received move #{pos}"
79
+ res = gtp_client.play(color, pos)
80
+ res.callback {
81
+ self.moves << [color, pos]
82
+ self.move_number = m["move_number"].to_i
83
+ self.turn = m["color"] == "b" ? "w" : "b"
84
+ make_move
85
+ }
86
+ end
87
+ end
88
+
89
+ def idle_check
90
+ if @last_gtp_access && @last_gtp_access + 60 < Time.now
91
+ puts "Shutting down gtp client #{self.token} for idle."
92
+ stop_gtp_client
93
+ end
94
+ end
95
+
96
+ def private_message(m)
97
+ puts "Got private game message: #{m.inspect}"
98
+ case m["type"]
99
+ when 'undo_requested'
100
+ @client.http_post("/game/accept_undo/" + m["request_id"])
101
+ end
102
+ end
103
+
104
+ def handle_messages
105
+ msg = @queue.pop
106
+ args = msg[1..-1]
107
+ self.send(msg.first, *args)
108
+ end
109
+
110
+ def my_turn?
111
+ @client.my_user_id == players_turn
112
+ end
113
+
114
+ def started?
115
+ state != 'new'
116
+ end
117
+
118
+ def update_from_match_request(attrs)
119
+ self.state = attrs['state']
120
+ self.board_size = attrs['board_size'].to_i
121
+ self.proposed_by_id = attrs['proposed_by_id'].to_i
122
+ self.white_player_id = attrs['white_player_id'].to_i
123
+ self.black_player_id = attrs['black_player_id'].to_i
124
+ self.challenge_id = attrs['id'].to_i
125
+ self.handicap = attrs['handicap'].to_i
126
+ self.handicap_stones = attrs['handicap_stones']
127
+ self.turn = handicap > 1 ? 'w' : 'b'
128
+ self.move_number = 0
129
+ self.moves = []
130
+ end
131
+
132
+ def update_from_game_list(attrs)
133
+ #puts "attrs = #{attrs.inspect}"
134
+ self.state = attrs['state']
135
+ self.token = attrs['token']
136
+ self.game_id = attrs['id'].to_i
137
+ self.komi = attrs['komi'].to_f
138
+ self.white_player_id = attrs['white_player_id'].to_i
139
+ self.black_player_id = attrs['black_player_id'].to_i
140
+ self.turn = attrs['turn']
141
+ self.board_size = attrs['board']['size'].to_i
142
+ self.move_number = attrs['move_number'].to_i
143
+ self.handicap = attrs['handicap'].to_i rescue 0
144
+ self.handicap_stones = attrs['handicap_stones']
145
+ self.moves = []
146
+ move_colors = ["B", "W"]
147
+ handicap_offset = self.handicap > 0 ? 1 : 0
148
+ if attrs['moves']
149
+ attrs['moves'].each_with_index do |m,idx|
150
+ self.moves << [move_colors[(idx+handicap_offset) % 2], sgf_coord_to_gtp_coord(m, board_size)]
151
+ end
152
+ end
153
+
154
+ if state == 'new'
155
+ http = @client.http_post("/game/#{token}/attempt_start")
156
+ end
157
+
158
+ end
159
+
160
+ def my_color
161
+ @client.my_user_id == white_player_id ? "W" : "B"
162
+ end
163
+
164
+ def opponents_color
165
+ @client.my_user_id == white_player_id ? "B" : "W"
166
+ end
167
+
168
+ def make_move
169
+ return unless started? && my_turn?
170
+
171
+ res = gtp_client.genmove(turn)
172
+ res.callback { |response_move|
173
+ puts "Generated move: #{response_move} (#{token})"
174
+ self.move_number += 1
175
+ if response_move.upcase == 'PASS'
176
+ http = @client.http_post("/game/#{token}/pass")
177
+ elsif response_move.upcase == 'RESIGN'
178
+ http = @client.http_post("/game/#{token}/resign")
179
+ else
180
+ rgo_coord = gtp_coord_to_goshrine_coord(response_move.upcase, board_size)
181
+ http = @client.http_post("/game/#{token}/move/#{rgo_coord}")
182
+ end
183
+ http.callback {|response|
184
+ if response[:status] == 200
185
+ self.moves << [my_color, response_move]
186
+ else
187
+ puts "Could not make move: #{response.inspect}"
188
+ end
189
+ }
190
+ }
191
+ end
192
+
193
+ def start_engine
194
+ gtp = GtpStdioClient.new(@client.gtp_cmd_line, "gtp_#{token}.log")
195
+ gtp.boardsize(@board_size)
196
+ gtp.clear_board
197
+ #if @handicap > 1
198
+ # gtp.fixed_handicap(@handicap)
199
+ #end
200
+ @handicap_stones.each do |s|
201
+ puts "Placing handicap stone at #{s}"
202
+ gtp.play('B', sgf_coord_to_gtp_coord(s, board_size))
203
+ end
204
+ if @moves
205
+ @moves.each do |m|
206
+ #puts "Going to play #{m.first}, #{m.last}"
207
+ gtp.play(m.first, m.last)
208
+ end
209
+ end
210
+ gtp
211
+ end
212
+
213
+ def gtp_client
214
+ @last_gtp_access = Time.now
215
+ @gtp_client ||= start_engine
216
+ end
217
+
218
+ def stop_gtp_client
219
+ @gtp_client.close if @gtp_client
220
+ @gtp_client = nil
221
+ @last_gtp_access = nil
222
+ end
223
+
224
+ def players_turn
225
+ self.turn == 'b' ? black_player_id : white_player_id
226
+ end
227
+
228
+ def sgf_coord_to_gtp_coord(value, board_size)
229
+ return 'pass' if value.downcase == 'pass'
230
+ x = value[0].ord - 97
231
+ y = value[1].ord - 97
232
+ x += 1 if x >= 8
233
+ [x+65].pack('c') + (board_size - y).to_s
234
+ end
235
+
236
+ def goshrine_coord_to_gtp_coord(value, board_size)
237
+ x = value[0].ord - 97
238
+ y = value[1].ord - 97
239
+ x += 1 if x >= 8
240
+ [x+65].pack('c') + (board_size - y).to_s
241
+ end
242
+
243
+ def gtp_coord_to_goshrine_coord(value, board_size)
244
+ x = value[0].ord - 65
245
+ x -= 1 if x >= 8
246
+ y = board_size - (value[1..-1].to_i)
247
+ [x+97, y+97].pack('cc')
248
+ end
249
+
250
+
251
+ end
252
+ end