rubykon 0.3.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.
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