kayabot 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/LICENSE +19 -0
- data/README.markdown +28 -0
- data/Rakefile +4 -0
- data/bin/kayabot +35 -0
- data/kayabot.gemspec +22 -0
- data/lib/gg.rb +118 -0
- data/lib/kayabot.rb +98 -0
- data/lib/node.rb +150 -0
- data/lib/sgf.rb +233 -0
- metadata +87 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Gabriel Benmergui
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
This is the official Gem to run a bot in Kaya.
|
2
|
+
|
3
|
+
Right now only gnubot is default and has to be installed.
|
4
|
+
|
5
|
+
Feel free to expand the gem to allow other bots with GTP interface, should work plug-play easily.
|
6
|
+
|
7
|
+
|
8
|
+
On Ubuntu:
|
9
|
+
sudo apt-get install gnugo
|
10
|
+
|
11
|
+
To install :
|
12
|
+
gem install kayabot
|
13
|
+
To run:
|
14
|
+
kayabot my_configuration.yaml
|
15
|
+
|
16
|
+
The configuration yaml must look like this:
|
17
|
+
|
18
|
+
url : "http://alpha.kaya.gs:9292"
|
19
|
+
user : "MyBot"
|
20
|
+
pass : "MyBot's password"
|
21
|
+
|
22
|
+
Ask for a bot account at info@kaya.gs to run it!
|
23
|
+
|
24
|
+
You can get a personalized bot account and if you run many games you can get a collaborator account.
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
|
data/Rakefile
ADDED
data/bin/kayabot
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.expand_path("../lib/sgf", File.dirname(__FILE__))
|
4
|
+
require 'rubygems'
|
5
|
+
require 'mechanize'
|
6
|
+
require 'json'
|
7
|
+
require 'yaml'
|
8
|
+
require File.expand_path("../lib/kayabot", File.dirname(__FILE__))
|
9
|
+
require File.expand_path("../lib/gg", File.dirname(__FILE__))
|
10
|
+
$0='kayabot'
|
11
|
+
|
12
|
+
|
13
|
+
@url = ""
|
14
|
+
|
15
|
+
if ARGV.length > 0
|
16
|
+
if ARGV.length == 1
|
17
|
+
config_file = ARGV.first
|
18
|
+
if File.exists?(config_file)
|
19
|
+
config = YAML.load_file(config_file)
|
20
|
+
if config["url"] && config["user"] && config["pass"]
|
21
|
+
k = KayaBot.new(config["url"], config["user"], config["pass"])
|
22
|
+
k.listener_loop
|
23
|
+
else
|
24
|
+
puts "Invalid config file."
|
25
|
+
end
|
26
|
+
else
|
27
|
+
puts "File does not exist."
|
28
|
+
end
|
29
|
+
elsif ARGV.length >= 3
|
30
|
+
k = KayaBot.new(ARGV[0], ARGV[1], ARGV[2])
|
31
|
+
k.listener_loop
|
32
|
+
end
|
33
|
+
else
|
34
|
+
puts "Usage:\n - kayabot <yaml_config_file>\n - kayabot <url> <bot_nickname> <password>"
|
35
|
+
end
|
data/kayabot.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "kayabot"
|
3
|
+
s.version = "0.1.0"
|
4
|
+
s.summary = "Interface to run Go Bots on Kaya"
|
5
|
+
s.description = "This is an interface to run GTP compliant Go Bots on Kaya."
|
6
|
+
s.authors = ["Gabriel Benmergui"]
|
7
|
+
s.email = ["gabriel.benmergui@kaya.gs"]
|
8
|
+
s.homepage = "http://github.com/conanbatt/OpenKaya"
|
9
|
+
|
10
|
+
s.executables.push("kayabot")
|
11
|
+
|
12
|
+
s.add_dependency("mechanize")
|
13
|
+
s.add_dependency("json")
|
14
|
+
|
15
|
+
s.files = Dir[
|
16
|
+
"README.markdown",
|
17
|
+
"LICENSE",
|
18
|
+
"Rakefile",
|
19
|
+
"lib/**/*.rb",
|
20
|
+
"*.gemspec",
|
21
|
+
]
|
22
|
+
end
|
data/lib/gg.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
class GTP
|
2
|
+
|
3
|
+
def self.run(bot_type, &command)
|
4
|
+
if bot_type == 'gnugo'
|
5
|
+
new(IO.popen("gnugo --mode gtp", "r+"), &command)
|
6
|
+
elsif bot_type == 'fuego'
|
7
|
+
raise "not supported yet"
|
8
|
+
# new(IO.popen("fuego --config #{RAILS_ROOT}/public/fuego.gtp", "r+"), &command)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(io)
|
13
|
+
@io = io
|
14
|
+
|
15
|
+
if block_given?
|
16
|
+
begin
|
17
|
+
yield self
|
18
|
+
ensure
|
19
|
+
quit
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def quit
|
25
|
+
send_command(:quit)
|
26
|
+
@io.close
|
27
|
+
end
|
28
|
+
|
29
|
+
def protocol_version
|
30
|
+
send_command(:protocol_version)
|
31
|
+
end
|
32
|
+
|
33
|
+
def genmove(color)
|
34
|
+
send_command(:genmove, color)
|
35
|
+
end
|
36
|
+
|
37
|
+
def loadsgf(path)
|
38
|
+
send_command(:loadsgf, path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def list_stones(color)
|
42
|
+
@io.puts [:list_stones, color].join(" ")
|
43
|
+
result = @io.take_while { |line| line != "\n" }.join
|
44
|
+
return result.sub(/^=\s/, "").sub(/\n/, "")
|
45
|
+
end
|
46
|
+
|
47
|
+
def send_command(command, *arguments)
|
48
|
+
@io.puts [command, *arguments].join(" ")
|
49
|
+
result = @io.take_while { |line| line != "\n" }.join
|
50
|
+
|
51
|
+
rc = result.scan(/^=\s[a-zA-Z]*[0-9]*$/)
|
52
|
+
if rc.first.nil?
|
53
|
+
return rc
|
54
|
+
else
|
55
|
+
return rc.first.sub(/^=\s/, "").sub(/\n/, "")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
SGF_FILE_PATH = "gnugo_games/"
|
61
|
+
|
62
|
+
def score_game(game_id, game_sgf)
|
63
|
+
re = nil
|
64
|
+
if not Dir.exists?(SGF_FILE_PATH)
|
65
|
+
Dir.mkdir(SGF_FILE_PATH)
|
66
|
+
end
|
67
|
+
filepath = SGF_FILE_PATH + "#{game_id}.sgf"
|
68
|
+
File.open(filepath, "w") do |f|
|
69
|
+
f.write game_sgf
|
70
|
+
end
|
71
|
+
|
72
|
+
IO.popen("gnugo --score aftermath #{filepath}") do |f|
|
73
|
+
re = f.read
|
74
|
+
end
|
75
|
+
|
76
|
+
File.delete(filepath)
|
77
|
+
return re
|
78
|
+
end
|
79
|
+
|
80
|
+
# return a move coordinates, such as "c17", or "PASS", or "resign"
|
81
|
+
def ai_move(game_id, game_sgf, color)
|
82
|
+
|
83
|
+
re = nil
|
84
|
+
if not Dir.exists?(SGF_FILE_PATH)
|
85
|
+
Dir.mkdir(SGF_FILE_PATH)
|
86
|
+
end
|
87
|
+
filepath = SGF_FILE_PATH + "#{game_id}.sgf"
|
88
|
+
File.open(filepath, "w") do |f|
|
89
|
+
f.write game_sgf
|
90
|
+
end
|
91
|
+
|
92
|
+
#why this?
|
93
|
+
if color == 'black' || color == 'white'
|
94
|
+
game_bot = 'gnugo'
|
95
|
+
end
|
96
|
+
GTP.run(game_bot) do |gtp|
|
97
|
+
gtp.loadsgf filepath
|
98
|
+
re = gtp.genmove color
|
99
|
+
end
|
100
|
+
p re
|
101
|
+
move = convert_move(re)
|
102
|
+
|
103
|
+
File.delete(filepath)
|
104
|
+
return move
|
105
|
+
end
|
106
|
+
|
107
|
+
#switches GTP move into sgf-like coordinate
|
108
|
+
def convert_move(move)
|
109
|
+
if move == "PASS"
|
110
|
+
return 'pass'
|
111
|
+
elsif move == "resign"
|
112
|
+
return 'resign'
|
113
|
+
else
|
114
|
+
alphabet = "ABCDEFGHIJKLMNOPQRS"
|
115
|
+
return alphabet["ABCDEFGHJKLMNOPQRST".index(move[0])].downcase + alphabet.reverse[(move[1].to_s + move[2].to_s).to_i - 1].downcase
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
data/lib/kayabot.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require File.expand_path("sgf", File.dirname(__FILE__))
|
2
|
+
require 'rubygems'
|
3
|
+
require 'mechanize'
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
$0='kayabot'
|
7
|
+
|
8
|
+
class KayaBot
|
9
|
+
|
10
|
+
PLAY_URL = "/bot/play"
|
11
|
+
OPEN_GAME_URL = "/bot/open_game"
|
12
|
+
RESIGN_URL = "/bot/resign"
|
13
|
+
SCORE_URL = "/bot/score"
|
14
|
+
|
15
|
+
attr_accessor :challenger, :status, :move
|
16
|
+
|
17
|
+
def initialize(server_url, user, pass)
|
18
|
+
@server_url = server_url
|
19
|
+
@user = user
|
20
|
+
@pass = pass
|
21
|
+
@agent = Mechanize.new
|
22
|
+
@status
|
23
|
+
@sgf
|
24
|
+
end
|
25
|
+
|
26
|
+
def game_configuration
|
27
|
+
{:title => "Come at me bro"}
|
28
|
+
end
|
29
|
+
|
30
|
+
def connect
|
31
|
+
page = @agent.post(@server_url+ "/session/create", {:id => @user, :password => @pass})
|
32
|
+
end
|
33
|
+
|
34
|
+
TIME_LAPSE = 4
|
35
|
+
|
36
|
+
def listener_loop
|
37
|
+
connect
|
38
|
+
while (true) do
|
39
|
+
fetch_and_parse_data
|
40
|
+
open_game if @status=="connected" || @status=="finished"
|
41
|
+
post_score if @status=="scoring"
|
42
|
+
post_move if @bots_turn && @status=="playing"
|
43
|
+
sleep TIME_LAPSE #lets not explode in requests
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def fetch_and_parse_data
|
48
|
+
page = @agent.get(@server_url + "/bot/status")
|
49
|
+
json = JSON.parse(page.body)
|
50
|
+
p json
|
51
|
+
@status = json["status"]
|
52
|
+
@move = json["moves"]
|
53
|
+
@bots_turn = json["bot_play?"]
|
54
|
+
@color = json["next"]
|
55
|
+
@master_node = json["sgf_master_node"]
|
56
|
+
end
|
57
|
+
|
58
|
+
def open_game
|
59
|
+
@agent.post(@server_url+ OPEN_GAME_URL, game_configuration)
|
60
|
+
end
|
61
|
+
def post_move
|
62
|
+
#TODO should insert master node . Need handi/komi
|
63
|
+
bot_move = ai_move("temp",sgf_content, @color)
|
64
|
+
if (bot_move == "resign")
|
65
|
+
resign
|
66
|
+
else
|
67
|
+
color_short = (@color=="black" ? "B" : "W")
|
68
|
+
bot_move = "" if bot_move == "pass"
|
69
|
+
bot_move = ";#{color_short}[#{bot_move}]#{color_short}L[#{(25*60) - (@move ? @move.count(";") : 1)}]"
|
70
|
+
@agent.post(@server_url+ PLAY_URL,
|
71
|
+
:move => bot_move)
|
72
|
+
#@sgf.add_move(bot_move)
|
73
|
+
@move = nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
def resign
|
77
|
+
@agent.post(@server_url+ RESIGN_URL, {:result => "resign"})
|
78
|
+
end
|
79
|
+
def post_score
|
80
|
+
result = score_game("temp", sgf_content)
|
81
|
+
p result
|
82
|
+
@agent.post(@server_url+ SCORE_URL, {:score => parse_result_from_bot(result)})
|
83
|
+
end
|
84
|
+
|
85
|
+
#Black wins by 61.5 points
|
86
|
+
def parse_result_from_bot(result)
|
87
|
+
color = result[0]
|
88
|
+
points = result.match(/\d{0,3}\.\d/)[0]
|
89
|
+
return "#{color}+#{points}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def sgf_content
|
93
|
+
@sgf = SGF.new
|
94
|
+
@sgf.add_move(@move) if @move
|
95
|
+
return "(#{@master_node}#{@sgf.move_list})"
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
data/lib/node.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
class Node
|
2
|
+
|
3
|
+
attr_reader :node_text, :children, :parent
|
4
|
+
|
5
|
+
def initialize(parent,node_text= "")
|
6
|
+
validate_node_format(node_text)
|
7
|
+
@node_text = node_text
|
8
|
+
@comments = []
|
9
|
+
@children = []
|
10
|
+
@parent = parent
|
11
|
+
@parent.add_child(self) if parent
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_child(node)
|
15
|
+
@children.push(node)
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_move_list
|
19
|
+
children_to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def children_to_s(with_comments=false)
|
23
|
+
branches = ""
|
24
|
+
if @children.size > 1
|
25
|
+
@children[1..-1].each {|node| branches += "(#{node.children_to_s(with_comments)})"}
|
26
|
+
end
|
27
|
+
children_text =""
|
28
|
+
if @children.first
|
29
|
+
children_text = @children.first.children_to_s(with_comments)
|
30
|
+
end
|
31
|
+
comment_node = with_comments ? comments : ""
|
32
|
+
node_text + comment_node + branches + children_text
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
children_to_s(true)
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_comment(comment)
|
40
|
+
@comments << (comment + "\n")
|
41
|
+
end
|
42
|
+
def comments
|
43
|
+
@comments.empty? ? "" : "C[#{@comments.join.gsub("]","\\]").gsub(")","\\)")}]"
|
44
|
+
end
|
45
|
+
|
46
|
+
def color
|
47
|
+
@node_text[1]
|
48
|
+
end
|
49
|
+
|
50
|
+
def x
|
51
|
+
@node_text[3] unless pass_node?
|
52
|
+
end
|
53
|
+
def y
|
54
|
+
@node_text[4] unless pass_node?
|
55
|
+
end
|
56
|
+
def coordinate
|
57
|
+
x+y
|
58
|
+
end
|
59
|
+
def pass_node?
|
60
|
+
@node_text.match(/[BW]\[\]/)
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_node_format(node)
|
64
|
+
valid = node.match(/;[BW]\[(|[a-z][a-z])\]/)
|
65
|
+
if node.include?("BL") || node.include?("WL")
|
66
|
+
valid = valid && node.match(/[BW]L\[\d{0,6}.\d{3}\]/)
|
67
|
+
end
|
68
|
+
raise "#{node} is invalid node format" unless valid
|
69
|
+
end
|
70
|
+
|
71
|
+
def time_left
|
72
|
+
@node_text.match(/[#{color}]L\[\d{0,6}.\d{3}\]/).to_s[3..-2].to_f
|
73
|
+
end
|
74
|
+
def time_left=(time_left)
|
75
|
+
@node_text.gsub!(/[#{color}]L\[\d{0,6}.\d{3}\]/, "#{color}L[%.3f]" % [time_left])
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
class ConfigNode
|
82
|
+
|
83
|
+
attr_accessor :node_text, :children
|
84
|
+
|
85
|
+
def initialize(property="")
|
86
|
+
@node_text = property.dup
|
87
|
+
write_property(:file_format,4)
|
88
|
+
handicap = property(:handicap)
|
89
|
+
@comments = []
|
90
|
+
@children = []
|
91
|
+
end
|
92
|
+
|
93
|
+
def parent
|
94
|
+
#stub
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_child(node)
|
98
|
+
@children.push(node)
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_node_format
|
102
|
+
return true
|
103
|
+
end
|
104
|
+
|
105
|
+
def add_comment(comment)
|
106
|
+
@comments << (comment + "\n")
|
107
|
+
end
|
108
|
+
def comments
|
109
|
+
@comments.empty? ? "" : "C[#{@comments.join.gsub("]","\\]").gsub(")","\\)")}]"
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_s
|
113
|
+
children_text = @children.first.to_s
|
114
|
+
";"+node_text + comments + children_text
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_move_list
|
118
|
+
children_text = @children.first.to_move_list
|
119
|
+
";"+node_text + children_text
|
120
|
+
end
|
121
|
+
|
122
|
+
METALABELS= {:black_rank => "BR", :white_rank => "WR",:white_player => "PW", :black_player => "PB",
|
123
|
+
:komi => "KM", :date => "DT", :result => "RE",
|
124
|
+
:file_format => "FF", :black_country => "BC",
|
125
|
+
:white_country => "WC", :event => "EV", :source => "SO",
|
126
|
+
:encoding => "CA", :size => "SZ", :rules => "RU", :time_set => "OT",:handicap => "HA"}
|
127
|
+
|
128
|
+
def property(symbol)
|
129
|
+
return node_text if symbol == :all
|
130
|
+
dup = node_text.dup
|
131
|
+
dup.slice!(/.*#{METALABELS[symbol]}\[/)
|
132
|
+
return nil if dup.length == node_text.length #means it wasnt found
|
133
|
+
dup.slice!(/\].*/)
|
134
|
+
return dup
|
135
|
+
end
|
136
|
+
|
137
|
+
def write_property(symbol, value)
|
138
|
+
return unless value
|
139
|
+
raise "Invalid property #{symbol}" unless METALABELS[symbol]
|
140
|
+
node = "#{METALABELS[symbol]}[#{value}]"
|
141
|
+
@node_text.gsub!(/#{METALABELS[symbol]}\[\w*\]/, "") #in case it already had it
|
142
|
+
@node_text = node + @node_text
|
143
|
+
size = property(:size)
|
144
|
+
#a little hackish to insert the AB node only
|
145
|
+
@node_text += SGF.handi_node(property(:size),value)[5..-1] if(size &&
|
146
|
+
symbol == :handicap)
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
data/lib/sgf.rb
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
require File.expand_path("node", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class SGF
|
4
|
+
|
5
|
+
BLACK = "B"
|
6
|
+
WHITE = "W"
|
7
|
+
attr_accessor :move_list, :comment_buffer,:property, :focus
|
8
|
+
|
9
|
+
def initialize(moves="", properties={})
|
10
|
+
moves ||= ""
|
11
|
+
@config = ConfigNode.new
|
12
|
+
@focus = @config
|
13
|
+
nodify_move_list(moves) unless moves.empty?
|
14
|
+
@comment_buffer = ""
|
15
|
+
@size = properties[:size]
|
16
|
+
properties.keys.each {|k| @config.write_property(k,properties[k]) }
|
17
|
+
@config.write_property(:handicap, properties[:handicap])
|
18
|
+
end
|
19
|
+
|
20
|
+
def last_two_moves_are_pass?
|
21
|
+
if @focus && @focus.parent
|
22
|
+
return @focus.pass_node? && @focus.parent.pass_node?
|
23
|
+
end
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def nodify_move_list(moves)
|
28
|
+
moves.split(";").each do |txt|
|
29
|
+
add_move(";"+txt) unless txt.empty?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_move(node) #TODO objetify node
|
34
|
+
@focus = Node.new(@focus,node)
|
35
|
+
move_list
|
36
|
+
end
|
37
|
+
|
38
|
+
def last_play_color
|
39
|
+
@focus != @config && @focus.color
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_comment(comment)
|
43
|
+
if @focus
|
44
|
+
@focus.add_comment(comment)
|
45
|
+
else
|
46
|
+
@config.add_comment(comment)
|
47
|
+
end
|
48
|
+
move_list
|
49
|
+
end
|
50
|
+
#takes a hash and inputs the contents into the nodes
|
51
|
+
def parse_comments!(comments)
|
52
|
+
comments.each do |key, value|
|
53
|
+
if (key.to_i == 0)
|
54
|
+
value.each {|v| @config.add_comment(hash_to_comment(v))}
|
55
|
+
next
|
56
|
+
end
|
57
|
+
value.each{ |v| move_by_number(key.to_i-1) && move_by_number(key.to_i-1).add_comment(hash_to_comment(v))}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def hash_to_comment(hash)
|
62
|
+
raise "invalid hash" unless hash["user"] && hash["rank"] && hash["message"]
|
63
|
+
"#{hash["user"]}#{hash["rank"]}: #{hash["message"]}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def move_list
|
67
|
+
@config.children.first.to_move_list unless @config.children.empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
def move_list_with_comments
|
71
|
+
@config.children.first.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
def move_by_number(index)
|
75
|
+
index = index.to_i
|
76
|
+
return if (index < 0)
|
77
|
+
node = @config.children.first
|
78
|
+
while(index >0)
|
79
|
+
node = node.children.first
|
80
|
+
index -= 1
|
81
|
+
end
|
82
|
+
node
|
83
|
+
end
|
84
|
+
|
85
|
+
#light validation to make sure the input is not totally bs. only makes sure the coordinate is in the board
|
86
|
+
def validate_coordinate(x, y)
|
87
|
+
lower_boundary = 97
|
88
|
+
upper_boundary = 97+ @size.to_i
|
89
|
+
valid_y_axis = y.bytes.first <= upper_boundary && y.bytes.first >= lower_boundary
|
90
|
+
valid_x_axis = x.bytes.first <= upper_boundary && x.bytes.first >= lower_boundary
|
91
|
+
throw "Invalid coordinate #{x},#{y}" unless valid_y_axis && valid_x_axis
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate_node_format(node)
|
95
|
+
valid = node.match(/;[BW]\[(|[a-z][a-z])\]/)
|
96
|
+
if node.include?("BL") || node.include?("WL")
|
97
|
+
valid = valid && node.match(/[BW]L\[\d{0,6}.\d{3}\]/)
|
98
|
+
end
|
99
|
+
raise "#{node} is invalid node format" unless valid
|
100
|
+
end
|
101
|
+
|
102
|
+
def load_file(filename)
|
103
|
+
sgf =""
|
104
|
+
File.open(filename, 'r') do |file|
|
105
|
+
while (line = file.gets)
|
106
|
+
sgf += line
|
107
|
+
end
|
108
|
+
load_from_string(sgf)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
def load_from_string(input)
|
112
|
+
properties= input.split(";")[1]
|
113
|
+
@focus = @config = ConfigNode.new(properties) #will process this later
|
114
|
+
nodify_move_list(input.gsub(properties, "").chomp[2..-2])
|
115
|
+
end
|
116
|
+
|
117
|
+
def properties
|
118
|
+
@config.to_s
|
119
|
+
end
|
120
|
+
|
121
|
+
def properties=(arg)
|
122
|
+
@config.node_text = arg
|
123
|
+
end
|
124
|
+
|
125
|
+
def property(symbol)
|
126
|
+
@config.property(symbol)
|
127
|
+
end
|
128
|
+
|
129
|
+
def write_property(symbol, value)
|
130
|
+
@config.write_property(symbol, value)
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_s
|
134
|
+
"(#{@config.to_s})"
|
135
|
+
end
|
136
|
+
|
137
|
+
def time_left(player)
|
138
|
+
raise "Invalid input #{player}. W or B expected" unless player == "B" || player == "W"
|
139
|
+
ln = last_node_by_player(player)
|
140
|
+
ln && ln.time_left
|
141
|
+
end
|
142
|
+
|
143
|
+
def add_time(player,time)
|
144
|
+
ln = last_node_by_player(player)
|
145
|
+
ln.time_left= (ln.time_left + time)
|
146
|
+
end
|
147
|
+
|
148
|
+
def last_node_by_player(player)
|
149
|
+
return if @focus == @config
|
150
|
+
if @focus.color == player
|
151
|
+
return @focus
|
152
|
+
elsif @focus.parent
|
153
|
+
@focus.parent
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def undo
|
158
|
+
to_del = @focus
|
159
|
+
@focus = @focus.parent
|
160
|
+
@focus.children.delete(to_del)
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.handi_node(size,handicap)
|
164
|
+
case size.to_i
|
165
|
+
|
166
|
+
when 19
|
167
|
+
case handicap
|
168
|
+
when 2
|
169
|
+
return "HA[2]AB[dd][pp]"
|
170
|
+
when 3
|
171
|
+
return "HA[3]AB[dd][dp][pd]"
|
172
|
+
when 4
|
173
|
+
return "HA[4]AB[dd][pd][dp][pp]"
|
174
|
+
when 5
|
175
|
+
return "HA[5]AB[dd][pd][dp][pp][jj]"
|
176
|
+
when 6
|
177
|
+
return "HA[6]AB[dd][pd][dp][pp][dj][pj]"
|
178
|
+
when 7
|
179
|
+
return "HA[7]AB[dd][pd][dp][pp][dj][pj][jj]"
|
180
|
+
when 8
|
181
|
+
return "HA[8]AB[dd][jd][pd][dj][pj][dp][jp][pp]"
|
182
|
+
when 9
|
183
|
+
return "HA[9]AB[dd][jd][pd][dj][jj][pj][dp][jp][pp]"
|
184
|
+
else
|
185
|
+
raise "Invalid handicap setting #{handicap}"
|
186
|
+
end
|
187
|
+
when 13
|
188
|
+
case handicap
|
189
|
+
when 2
|
190
|
+
return "HA[2]AB[dd][jj]"
|
191
|
+
when 3
|
192
|
+
return "HA[3]AB[dd][dj][jd]"
|
193
|
+
when 4
|
194
|
+
return "HA[4]AB[dd][jd][dj][jj]"
|
195
|
+
when 5
|
196
|
+
return "HA[5]AB[dd][jd][dj][gg][jj]"
|
197
|
+
when 6
|
198
|
+
return "HA[6]AB[dd][jd][dj][jj][dg][jg]"
|
199
|
+
when 7
|
200
|
+
return "HA[7]AB[dd][jd][dj][jj][dg][jg][gg]"
|
201
|
+
when 8
|
202
|
+
return "HA[8]AB[dd][jd][dj][gj][jj][jg][gd][dg]"
|
203
|
+
when 9
|
204
|
+
return "HA[9]AB[dd][jd][dj][gj][jj][jg][gg][gd][dg]"
|
205
|
+
else
|
206
|
+
raise "Invalid handicap setting #{handicap}"
|
207
|
+
end
|
208
|
+
when 9
|
209
|
+
case handicap
|
210
|
+
when 2
|
211
|
+
return "HA[2]AB[cc][gg]"
|
212
|
+
when 3
|
213
|
+
return "HA[3]AB[cc][cg][gg]"
|
214
|
+
when 4
|
215
|
+
return "HA[4]AB[cc][gg][cg][gc]"
|
216
|
+
when 5
|
217
|
+
return "HA[5]AB[cc][gg][cg][gc][ee]"
|
218
|
+
when 6
|
219
|
+
return "HA[6]AB[cc][gg][cg][gc][ee][ge]"
|
220
|
+
when 7
|
221
|
+
return "HA[7]AB[cc][gg][cg][gc][ee][ge][ee]"
|
222
|
+
when 8
|
223
|
+
return "HA[8]AB[cc][gc][cg][gg][ce][ge][ec][eg]"
|
224
|
+
when 9
|
225
|
+
return "HA[9]AB[cc][gc][cg][gg][ce][ge][ec][ee][eg]"
|
226
|
+
else
|
227
|
+
raise "Invalid handicap setting #{handicap}"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
raise "Invalid handicap setting Size: #{size} and handicap #{handicap}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kayabot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Gabriel Benmergui
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-25 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mechanize
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: json
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: This is an interface to run GTP compliant Go Bots on Kaya.
|
47
|
+
email:
|
48
|
+
- gabriel.benmergui@kaya.gs
|
49
|
+
executables:
|
50
|
+
- kayabot
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- README.markdown
|
55
|
+
- LICENSE
|
56
|
+
- Rakefile
|
57
|
+
- lib/kayabot.rb
|
58
|
+
- lib/node.rb
|
59
|
+
- lib/sgf.rb
|
60
|
+
- lib/gg.rb
|
61
|
+
- kayabot.gemspec
|
62
|
+
- bin/kayabot
|
63
|
+
homepage: http://github.com/conanbatt/OpenKaya
|
64
|
+
licenses: []
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.8.23
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: Interface to run Go Bots on Kaya
|
87
|
+
test_files: []
|