rubykon 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +11 -0
  6. data/CHANGELOG.md +32 -0
  7. data/CODE_OF_CONDUCT.md +13 -0
  8. data/Gemfile +7 -0
  9. data/Guardfile +12 -0
  10. data/LICENSE +22 -0
  11. data/POSSIBLE_IMPROVEMENTS.md +25 -0
  12. data/README.md +36 -0
  13. data/Rakefile +6 -0
  14. data/benchmark/benchmark.sh +22 -0
  15. data/benchmark/full_playout.rb +17 -0
  16. data/benchmark/mcts_avg.rb +23 -0
  17. data/benchmark/playout.rb +15 -0
  18. data/benchmark/playout_micros.rb +188 -0
  19. data/benchmark/profiling/full_playout.rb +7 -0
  20. data/benchmark/profiling/mcts.rb +6 -0
  21. data/benchmark/results/HISTORY.md +541 -0
  22. data/benchmark/scoring.rb +20 -0
  23. data/benchmark/scoring_micros.rb +60 -0
  24. data/benchmark/support/benchmark-ips.rb +11 -0
  25. data/benchmark/support/benchmark-ips_shim.rb +143 -0
  26. data/benchmark/support/playout_help.rb +13 -0
  27. data/examples/mcts_laziness.rb +22 -0
  28. data/exe/rubykon +5 -0
  29. data/lib/benchmark/avg.rb +14 -0
  30. data/lib/benchmark/avg/benchmark_suite.rb +59 -0
  31. data/lib/benchmark/avg/job.rb +92 -0
  32. data/lib/mcts.rb +11 -0
  33. data/lib/mcts/examples/double_step.rb +68 -0
  34. data/lib/mcts/mcts.rb +13 -0
  35. data/lib/mcts/node.rb +88 -0
  36. data/lib/mcts/playout.rb +22 -0
  37. data/lib/mcts/root.rb +49 -0
  38. data/lib/rubykon.rb +13 -0
  39. data/lib/rubykon/board.rb +188 -0
  40. data/lib/rubykon/cli.rb +122 -0
  41. data/lib/rubykon/exceptions/exceptions.rb +1 -0
  42. data/lib/rubykon/exceptions/illegal_move_exception.rb +4 -0
  43. data/lib/rubykon/eye_detector.rb +27 -0
  44. data/lib/rubykon/game.rb +115 -0
  45. data/lib/rubykon/game_scorer.rb +62 -0
  46. data/lib/rubykon/game_state.rb +93 -0
  47. data/lib/rubykon/group.rb +99 -0
  48. data/lib/rubykon/group_tracker.rb +144 -0
  49. data/lib/rubykon/gtp_coordinate_converter.rb +25 -0
  50. data/lib/rubykon/move_validator.rb +55 -0
  51. data/lib/rubykon/version.rb +3 -0
  52. data/rubykon.gemspec +21 -0
  53. metadata +97 -0
@@ -0,0 +1,11 @@
1
+ require_relative 'mcts/node'
2
+ require_relative 'mcts/root'
3
+ require_relative 'mcts/playout'
4
+ require_relative 'mcts/mcts'
5
+
6
+ module MCTS
7
+ UCT_BIAS_FACTOR = 2
8
+ DEFAULT_PLAYOUTS = 1_000
9
+ end
10
+
11
+
@@ -0,0 +1,68 @@
1
+ module MCTS
2
+ module Examples
3
+ class DoubleStep
4
+
5
+ FINAL_POSITION = 6
6
+ MAX_STEP = 2
7
+
8
+ attr_reader :positions
9
+
10
+ def initialize(positions = init_positions, n = 0)
11
+ @positions = positions
12
+ @move_count = n
13
+ end
14
+
15
+ def finished?
16
+ @positions.any? {|_color, position| position >= FINAL_POSITION }
17
+ end
18
+
19
+ def generate_move
20
+ rand(MAX_STEP) + 1
21
+ end
22
+
23
+ def set_move(move)
24
+ steps = move
25
+ @positions[next_turn_color] += steps
26
+ @move_count += 1
27
+ end
28
+
29
+ def dup
30
+ self.class.new @positions.dup, @move_count
31
+ end
32
+
33
+ def won?(color)
34
+ fail "Game not finished" unless finished?
35
+ @positions[color] > @positions[other_color(color)]
36
+ end
37
+
38
+ def all_valid_moves
39
+ if finished?
40
+ []
41
+ else
42
+ [1, 2]
43
+ end
44
+ end
45
+
46
+ def last_turn_color
47
+ @move_count.odd? ? :black : :white
48
+ end
49
+
50
+ private
51
+ def next_turn_color
52
+ @move_count.even? ? :black : :white
53
+ end
54
+
55
+ def init_positions
56
+ {black: 0, white: 0}
57
+ end
58
+
59
+ def other_color(color)
60
+ if color == :black
61
+ :white
62
+ else
63
+ :black
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,13 @@
1
+ module MCTS
2
+ class MCTS
3
+ def start(game_state, playouts = DEFAULT_PLAYOUTS)
4
+ root = Root.new(game_state)
5
+
6
+ playouts.times do |i|
7
+ root.explore_tree
8
+ end
9
+ root
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,88 @@
1
+ module MCTS
2
+ class Node
3
+ attr_reader :parent, :move, :wins, :visits, :children, :game_state
4
+
5
+ def initialize(game_state, move, parent)
6
+ @parent = parent
7
+ @game_state = game_state
8
+ @move = move
9
+ @wins = 0.0
10
+ @visits = 0
11
+ @children = []
12
+ @untried_moves = game_state.all_valid_moves
13
+ @leaf = game_state.finished? || @untried_moves.empty?
14
+ end
15
+
16
+ def uct_value
17
+ win_percentage + UCT_BIAS_FACTOR * Math.sqrt(Math.log(parent.visits)/@visits)
18
+ end
19
+
20
+ def win_percentage
21
+ @wins/@visits
22
+ end
23
+
24
+ def root?
25
+ false
26
+ end
27
+
28
+ def leaf?
29
+ @leaf
30
+ end
31
+
32
+ def uct_select_child
33
+ children.max_by &:uct_value
34
+ end
35
+
36
+ # maybe get a maximum depth or soemthing in
37
+ def expand
38
+ move = @untried_moves.pop
39
+ create_child(move)
40
+ end
41
+
42
+ def rollout
43
+ playout = Playout.new(@game_state)
44
+ playout.play
45
+ end
46
+
47
+ def won
48
+ @visits += 1
49
+ @wins += 1
50
+ end
51
+
52
+ def lost
53
+ @visits += 1
54
+ end
55
+
56
+ def backpropagate(won)
57
+ node = self
58
+ node.update_won won
59
+ until node.root? do
60
+ won = !won # switching players perspective
61
+ node = node.parent
62
+ node.update_won(won)
63
+ end
64
+ end
65
+
66
+ def untried_moves?
67
+ !@untried_moves.empty?
68
+ end
69
+
70
+ def update_won(won)
71
+ if won
72
+ self.won
73
+ else
74
+ self.lost
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def create_child(move)
81
+ game_state = @game_state.dup
82
+ game_state.set_move(move)
83
+ child = Node.new game_state, move, self
84
+ @children << child
85
+ child
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,22 @@
1
+ module MCTS
2
+ class Playout
3
+
4
+ attr_reader :game_state
5
+
6
+ def initialize(game_state)
7
+ @game_state = game_state.dup
8
+ end
9
+
10
+ def play
11
+ my_color = @game_state.last_turn_color
12
+ playout
13
+ @game_state.won?(my_color)
14
+ end
15
+
16
+ def playout
17
+ until @game_state.finished?
18
+ @game_state.set_move @game_state.generate_move
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,49 @@
1
+ module MCTS
2
+ class Root < Node
3
+ def initialize(game_state)
4
+ super game_state, nil, nil
5
+ end
6
+
7
+ def root?
8
+ true
9
+ end
10
+
11
+ def best_child
12
+ children.max_by &:win_percentage
13
+ end
14
+
15
+ def best_move
16
+ best_child.move
17
+ end
18
+
19
+ def explore_tree
20
+ selected_node = select
21
+ playout_node = if selected_node.leaf?
22
+ selected_node
23
+ else
24
+ selected_node.expand
25
+ end
26
+ won = playout_node.rollout
27
+ playout_node.backpropagate(won)
28
+ end
29
+
30
+ def update_won(won)
31
+ # logic reversed as the node accumulates its children and has no move
32
+ # of its own
33
+ if won
34
+ self.lost
35
+ else
36
+ self.won
37
+ end
38
+ end
39
+
40
+ private
41
+ def select
42
+ node = self
43
+ until node.untried_moves? || node.leaf? do
44
+ node = node.uct_select_child
45
+ end
46
+ node
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'mcts'
2
+
3
+ require_relative 'rubykon/group'
4
+ require_relative 'rubykon/group_tracker'
5
+ require_relative 'rubykon/game'
6
+ require_relative 'rubykon/board'
7
+ require_relative 'rubykon/move_validator'
8
+ require_relative 'rubykon/eye_detector'
9
+ require_relative 'rubykon/game_scorer'
10
+ require_relative 'rubykon/exceptions/exceptions'
11
+ require_relative 'rubykon/game_state'
12
+ require_relative 'rubykon/gtp_coordinate_converter'
13
+ require_relative 'rubykon/cli'
@@ -0,0 +1,188 @@
1
+ # Board it acts a bit like a giant 2 dimensional array - but one based
2
+ # not zero based
3
+ module Rubykon
4
+ class Board
5
+ include Enumerable
6
+
7
+ BLACK = :black
8
+ WHITE = :white
9
+ EMPTY = nil
10
+
11
+ attr_reader :size, :board
12
+
13
+ # weird constructor for dup
14
+ def initialize(size, board = create_board(size))
15
+ @size = size
16
+ @board = board
17
+ end
18
+
19
+ def each
20
+ @board.each_with_index do |color, identifier|
21
+ yield identifier, color
22
+ end
23
+ end
24
+
25
+ def cutting_point_count
26
+ @board.size
27
+ end
28
+
29
+ def [](identifier)
30
+ @board[identifier]
31
+ end
32
+
33
+ def []=(identifier, color)
34
+ @board[identifier] = color
35
+ end
36
+
37
+ # this method is rather raw and explicit, it gets called a lot
38
+ def neighbours_of(identifier)
39
+ x = identifier % size
40
+ y = identifier / size
41
+ right = identifier + 1
42
+ below = identifier + @size
43
+ left = identifier - 1
44
+ above = identifier - @size
45
+ board_edge = @size - 1
46
+ not_on_x_edge = x > 0 && x < board_edge
47
+ not_on_y_edge = y > 0 && y < board_edge
48
+
49
+ if not_on_x_edge && not_on_y_edge
50
+ [[right, @board[right]], [below, @board[below]],
51
+ [left, @board[left]], [above, @board[above]]]
52
+ else
53
+ handle_edge_cases(x, y, above, below, left, right, board_edge,
54
+ not_on_x_edge, not_on_y_edge)
55
+ end
56
+ end
57
+
58
+ def neighbour_colors_of(identifier)
59
+ neighbours_of(identifier).map {|identifier, color| color}
60
+ end
61
+
62
+ def diagonal_colors_of(identifier)
63
+ diagonal_coordinates(identifier).inject([]) do |res, n_identifier|
64
+ res << self[n_identifier] if on_board?(n_identifier)
65
+ res
66
+ end
67
+ end
68
+
69
+ def on_edge?(identifier)
70
+ x, y = x_y_from identifier
71
+ x == 1 || x == size || y == 1 || y == size
72
+ end
73
+
74
+ def on_board?(identifier)
75
+ identifier >= 0 && identifier < @board.size
76
+ end
77
+
78
+ COLOR_TO_CHARACTER = {BLACK => ' X', WHITE => ' O', EMPTY => ' .'}
79
+ CHARACTER_TO_COLOR = COLOR_TO_CHARACTER.invert
80
+ LEGACY_CONVERSION = {'X' => ' X', 'O' => ' O', '-' => ' .'}
81
+ CHARS_PER_GLYPH = 2
82
+
83
+ def ==(other_board)
84
+ board == other_board.board
85
+ end
86
+
87
+ def to_s
88
+ @board.each_slice(@size).map do |row|
89
+ row_chars = row.map do |color|
90
+ COLOR_TO_CHARACTER.fetch(color)
91
+ end
92
+ row_chars.join
93
+ end.join("\n") << "\n"
94
+ end
95
+
96
+ def dup
97
+ self.class.new @size, @board.dup
98
+ end
99
+
100
+ MAKE_IT_OUT_OF_BOUNDS = 1000
101
+
102
+ def identifier_for(x, y)
103
+ return nil if x.nil? || y.nil?
104
+ x = MAKE_IT_OUT_OF_BOUNDS if x > @size || x < 1
105
+ (y - 1) * @size + (x - 1)
106
+ end
107
+
108
+ def x_y_from(identifier)
109
+ x = (identifier % (@size)) + 1
110
+ y = (identifier / (@size)) + 1
111
+ [x, y]
112
+ end
113
+
114
+ def self.from(string)
115
+ new_board = new(string.index("\n") / CHARS_PER_GLYPH)
116
+ each_move_from(string) do |index, color|
117
+ new_board[index] = color
118
+ end
119
+ new_board
120
+ end
121
+
122
+ def self.each_move_from(string)
123
+ glyphs = string.tr("\n", '').chars.each_slice(CHARS_PER_GLYPH).map(&:join)
124
+ relevant_glyphs = glyphs.select do |glyph|
125
+ CHARACTER_TO_COLOR.has_key?(glyph)
126
+ end
127
+ relevant_glyphs.each_with_index do |glyph, index|
128
+ yield index, CHARACTER_TO_COLOR.fetch(glyph)
129
+ end
130
+ end
131
+
132
+ def self.convert(old_board_string)
133
+ old_board_string.gsub /[XO-]/, LEGACY_CONVERSION
134
+ end
135
+
136
+ private
137
+
138
+ def create_board(size)
139
+ Array.new(size * size, EMPTY)
140
+ end
141
+
142
+ def handle_edge_cases(x, y, above, below, left, right, board_edge, not_on_x_edge, not_on_y_edge)
143
+ left_edge = x == 0
144
+ right_edge = x == board_edge
145
+ top_edge = y == 0
146
+ bottom_edge = y == board_edge
147
+ if left_edge && not_on_y_edge
148
+ [[right, @board[right]], [below, @board[below]],
149
+ [above, @board[above]]]
150
+ elsif right_edge && not_on_y_edge
151
+ [[below, @board[below]],
152
+ [left, @board[left]], [above, @board[above]]]
153
+ elsif top_edge && not_on_x_edge
154
+ [[right, @board[right]], [below, @board[below]],
155
+ [left, @board[left]]]
156
+ elsif bottom_edge && not_on_x_edge
157
+ [[right, @board[right]],
158
+ [left, @board[left]], [above, @board[above]]]
159
+ else
160
+ handle_corner_case(above, below, left, right, bottom_edge, left_edge, right_edge, top_edge)
161
+ end
162
+ end
163
+
164
+ def handle_corner_case(above, below, left, right, bottom_edge, left_edge, right_edge, top_edge)
165
+ if left_edge && top_edge
166
+ [[right, @board[right]], [below, @board[below]]]
167
+ elsif left_edge && bottom_edge
168
+ [[above, @board[above]], [right, @board[right]]]
169
+ elsif right_edge && top_edge
170
+ [[left, @board[left]], [below, @board[below]]]
171
+ elsif right_edge && bottom_edge
172
+ [[left, @board[left]], [above, @board[above]]]
173
+ end
174
+ end
175
+
176
+ def diagonal_coordinates(identifier)
177
+ x = identifier % size
178
+ if x == 0
179
+ [identifier + 1 - @size, identifier + 1 + @size]
180
+ elsif x == size - 1
181
+ [identifier - 1 - @size, identifier - 1 + @size]
182
+ else
183
+ [identifier - 1 - @size, identifier - 1 + @size,
184
+ identifier + 1 - @size, identifier + 1 + @size]
185
+ end
186
+ end
187
+ end
188
+ end