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 +0 -0
- data/Manifest.txt +0 -0
- data/README.rdoc +56 -0
- data/Rakefile +22 -0
- data/TODO.txt +2 -0
- data/bin/goshrine_bot +10 -0
- data/goshrine_bot.gemspec +33 -0
- data/goshrine_bot.yml +5 -0
- data/lib/goshrine_bot/client.rb +180 -0
- data/lib/goshrine_bot/core_ext/hash.rb +53 -0
- data/lib/goshrine_bot/faye/channel.rb +143 -0
- data/lib/goshrine_bot/faye/client.rb +283 -0
- data/lib/goshrine_bot/faye/connection.rb +122 -0
- data/lib/goshrine_bot/faye/error.rb +44 -0
- data/lib/goshrine_bot/faye/grammar.rb +58 -0
- data/lib/goshrine_bot/faye/namespace.rb +20 -0
- data/lib/goshrine_bot/faye/rack_adapter.rb +115 -0
- data/lib/goshrine_bot/faye/server.rb +266 -0
- data/lib/goshrine_bot/faye/timeouts.rb +21 -0
- data/lib/goshrine_bot/faye/transport.rb +123 -0
- data/lib/goshrine_bot/faye.rb +36 -0
- data/lib/goshrine_bot/game.rb +252 -0
- data/lib/goshrine_bot/gtp_stdio_client.rb +130 -0
- data/lib/goshrine_bot/httpclient.rb +288 -0
- data/lib/goshrine_bot/runner.rb +91 -0
- data/lib/goshrine_bot.rb +25 -0
- metadata +104 -0
@@ -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
|